/*
 * Mojito Distributed Hash Table (Mojito DHT)
 * Copyright (C) 2006-2007 LimeWire LLC
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
 
package org.limewire.mojito.io;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;

import org.limewire.mojito.KUID;
import org.limewire.mojito.StatusCode;
import org.limewire.mojito.db.DHTValue;
import org.limewire.mojito.db.DHTValueEntity;
import org.limewire.mojito.db.DHTValueType;
import org.limewire.mojito.messages.MessageID;
import org.limewire.mojito.messages.DHTMessage.OpCode;
import org.limewire.mojito.messages.StatsRequest.StatisticType;
import org.limewire.mojito.messages.StoreResponse.StoreStatusCode;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.Vendor;
import org.limewire.mojito.routing.Version;
import org.limewire.security.AddressSecurityToken;
import org.limewire.security.SecurityToken;


/**
 * The MessageOutputStream class writes a DHTMessage (serializes)
 * to a given OutputStream.
 * 
 * <b>NOTE</b>: This class is specific to Mojito's Gnutella backed
 * Message format. You may or may not be able to use parts of this
 * class for alternative message formats!
 */
public class MessageOutputStream extends DataOutputStream {
    
    public MessageOutputStream(OutputStream out) {
        super(out);
    }
	
    /**
     * Writes the given KUID to the OutputStream
     */
    public void writeKUID(KUID kuid) throws IOException {
        if (kuid == null) {
            throw new NullPointerException("KUID cannot be null");
        }
        
        kuid.write(this);
    }
    
    /**
     * Writes the given MessageID to the OutputStream
     */
    public void writeMessageID(MessageID messageId) throws IOException {
        if (messageId == null) {
            throw new NullPointerException("MessageID cannot be null");
        }
        
        messageId.write(this);
    }
    
    /**
     * Writes the given BigInteger to the OutputStream
     */
    public void writeDHTSize(BigInteger estimatedSize) throws IOException {
        byte[] data = estimatedSize.toByteArray();
        if (data.length > KUID.LENGTH) { // Can't be more than 2**160 bit
            throw new IOException("Illegal length: " + data.length + "/" + estimatedSize);
        }
        writeByte(data.length);
        write(data, 0, data.length);
    }
    
    /**
     * Writes the given DHTValue to the OutputStream
     */
    public void writeDHTValueEntity(DHTValueEntity entity) throws IOException {
        writeContact(entity.getCreator());
        entity.getPrimaryKey().write(this);
        writeDHTValue(entity.getValue());
    }
    
    /**
     * 
     */
    private void writeDHTValue(DHTValue value) throws IOException {
        writeDHTValueType(value.getValueType());
        writeVersion(value.getVersion());
        byte[] data = value.getValue();
        writeShort(data.length);
        write(data, 0, data.length);
    }
    
    /**
     * Writes the given Collection of KUIDs to the OutputStream
     */
    public void writeKUIDs(Collection<KUID> keys) throws IOException {
        writeCollectionSize(keys);
        for (KUID k : keys) {
            k.write(this);
        }
    }
    
    /**
     * Writes the given Collection of DHTValues to the OutputStream
     */
    public void writeDHTValueEntities(Collection<? extends DHTValueEntity> values) throws IOException {
        writeCollectionSize(values);
        for(DHTValueEntity entity : values) {
            writeDHTValueEntity(entity);
        }
    }
    
    /**
     * Writes the given Signature to the OutputStream
     */
    public void writeSignature(byte[] signature) throws IOException {
        if (signature != null && signature.length > 0) {
            writeByte(signature.length);
            write(signature, 0, signature.length);
        } else {
            writeByte(0);
        }
    }
    
    /**
     * Writes the given Contact to the OutputStream
     */
    public void writeContact(Contact node) throws IOException {
        writeVendor(node.getVendor());
        writeVersion(node.getVersion());
        writeKUID(node.getNodeID());
        writeSocketAddress(node.getContactAddress());
    }
    
    /**
     * Writes the given Collection of Contact to the OutputStream
     */
    public void writeContacts(Collection<? extends Contact> nodes) throws IOException {
        writeCollectionSize(nodes);
        for(Contact node : nodes) {
            writeContact(node);
        }
    }
    
    /**
     * Writes the given InetAddress to the OutputStream
     */
    public void writeInetAddress(InetAddress addr) throws IOException {
        byte[] address = addr.getAddress();
        writeByte(address.length);
        write(address, 0, address.length);
    }
    
    /**
     * Writes the given Port number to the OutputStream
     */
    public void writePort(int port) throws IOException {
        writeShort(port);
    }
    
    /**
     * Writes the given SocketAddress to the OutputStream
     */
    public void writeSocketAddress(SocketAddress addr) throws IOException {
        if (addr instanceof InetSocketAddress
                && !((InetSocketAddress)addr).isUnresolved()) {
            InetSocketAddress iaddr = (InetSocketAddress)addr;
            
            writeInetAddress(iaddr.getAddress());
            writePort(iaddr.getPort());
        } else {
            writeByte(0);
        }
    }
    
    /**
     * Writes the given AddressSecurityToken to the OutputStream
     */
    public void writeSecurityToken(SecurityToken securityToken) throws IOException {
        if (securityToken != null) {
            assert (securityToken instanceof AddressSecurityToken);
            byte[] qk = securityToken.getBytes();
            writeByte(qk.length);
            write(qk, 0, qk.length);
        } else {
            writeByte(0);
        }
    }
    
    /**
     * Writes an encoded Statistics payload to the OutputStream
     */
    public void writeStatistics(byte[] statistics) throws IOException {
        if (statistics != null) {
            writeShort(statistics.length);
            write(statistics);
        } else {
            writeShort(0);
        }
    }
    
    /**
     * Writes the given OpCode to the OutputStream
     */
    public void writeOpCode(OpCode opcode) throws IOException {
        writeByte(opcode.toByte());
    }
    
    /**
     * Writes the given Type to the OutputStream
     */
    public void writeStatisticType(StatisticType type) throws IOException {
        writeByte(type.toByte());
    }
    
    /**
     * Writes the given StatusCode to the OutputStream
     */
    public void writeStatusCode(StatusCode statusCode) throws IOException {
        writeShort(statusCode.shortValue());
        writeDHTString(statusCode.getDescription());
    }
    
    /**
     * Writes the given String to the OutputStream. This is different
     * from writeUTF(String) which writes the String in the so called
     * Modified-UTF format!
     */
    private void writeDHTString(String str) throws IOException {
        if (str == null) {
            writeShort(0);
        } else {
            byte[] b = str.getBytes("UTF-8");
            if (b.length > 0xFFFF) {
                throw new IOException("String is too big");
            }
            
            writeShort(b.length);
            write(b);
        }
    }
    
    /**
     * Writes the given Collection of StoreStatusCode(s) to the OutputStream
     */
    public void writeStoreStatusCodes(Collection<StoreStatusCode> statusCodes) throws IOException {
        writeCollectionSize(statusCodes);
        for (StoreStatusCode statusCode : statusCodes) {
            writeKUID(statusCode.getPrimaryKey());
            writeKUID(statusCode.getSecondaryKey());
            writeStatusCode(statusCode.getStatusCode());
        }
    }
    
    /**
     * Writes the size of the given Collection to the OutputStream
     */
    private void writeCollectionSize(Collection c) throws IOException {
        int size = c.size();
        if (size > 0xFF) {
            throw new IOException("Too many elements: " + size);
        }
        
        writeByte(size);
    }
    
    /**
     * Writes the DHTValueType to the OutputStream
     */
    public void writeDHTValueType(DHTValueType type) throws IOException {
        writeInt(type.toInt());
    }
    
    /**
     * Writes the Vendor to the OutputStream
     */
    public void writeVendor(Vendor vendor) throws IOException {
        writeInt(vendor.intValue());
    }
    
    /**
     * Writes the Version to the OutputStream
     */
    public void writeVersion(Version version) throws IOException {
        writeShort(version.shortValue());
    }
}
