package com.limegroup.gnutella.messages.vendor;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import com.limegroup.gnutella.ByteOrder;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.messages.FeatureSearchData;
import com.limegroup.gnutella.simpp.SimppManager;
import com.limegroup.gnutella.statistics.SentMessageStatHandler;
//import com.limegroup.gnutella.version.UpdateHandler;

/** 
 * The message that lets other know what capabilities you support.  Everytime 
 * you add a capability you should modify this class.
 *
 */
public final class CapabilitiesVM extends VendorMessage {

    /**
     * Bytes for advertising that we support a 'feature' search.
     * The value is 'WHAT' for legacy reasons, because 'what is new' 
     * was the first feature search supported.
     */
    static final byte[] FEATURE_SEARCH_BYTES = {(byte)87, (byte)72,
                                                      (byte)65, (byte)84};
    /**
     * The bytes for supporting SIMPP.  This used to be 'SIMP', but that
     * implementation was broken.  We now use 'IMPP' to advertise support.
     */
    private static final byte[] SIMPP_CAPABILITY_BYTES = {'I', 'M', 'P', 'P' };
    
    /**
     * The bytes for the LMUP message.
     */
    private static final byte[] LIME_UPDATE_BYTES = { 'L', 'M', 'U', 'P' };
    
    /**
     * The current version of this message.
     */
    public static final int VERSION = 0;

    /**
     * The capabilities supported.
     */
    private final Set _capabilitiesSupported = new HashSet();

    /**
     * The current instance of this CVM that this node will forward to others
     */
    private static CapabilitiesVM _instance;

    /**
     * Constructs a new CapabilitiesVM from data read off the network.
     */
    CapabilitiesVM(byte[] guid, byte ttl, byte hops, 
                   int version, byte[] payload) throws BadPacketException {
        super(guid, ttl, hops, F_NULL_VENDOR_ID, F_CAPABILITIES, version,
              payload);

        // populate the Set of supported messages....
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(getPayload());
            int vectorSize = ByteOrder.ushort2int(ByteOrder.leb2short(bais));
            // constructing the SMB will cause a BadPacketException if the
            // network data is invalid
            for (int i = 0; i < vectorSize; i++)
                _capabilitiesSupported.add(new SupportedMessageBlock(bais));
        } catch (IOException ioe) {
            ErrorService.error(ioe); // impossible.
        }
    }


    /**
     * Internal constructor for creating the sole instance of our 
     * CapabilitiesVM.
     */
    private CapabilitiesVM() {
        super(F_NULL_VENDOR_ID, F_CAPABILITIES, VERSION, derivePayload());
        addSupportedMessages(_capabilitiesSupported);
    }

    /**
     * Generates the default payload, using all our supported messages.
     */
    private static byte[] derivePayload() {
        Set hashSet = new HashSet();
        addSupportedMessages(hashSet);
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ByteOrder.short2leb((short)hashSet.size(), baos);
            for(Iterator i = hashSet.iterator(); i.hasNext(); ) {
                SupportedMessageBlock currSMP = (SupportedMessageBlock)i.next();
                currSMP.encode(baos);
            }
            return baos.toByteArray();
        } catch (IOException ioe) {
            ErrorService.error(ioe); // impossible.
            return null;
        }

    }

    // ADD NEW CAPABILITIES HERE AS YOU BUILD THEM....
    /**
     * Adds all supported capabilities to the given set.
     */
    private static void addSupportedMessages(Set hashSet) {
        SupportedMessageBlock smp = null;
        smp = new SupportedMessageBlock(FEATURE_SEARCH_BYTES, 
                                        FeatureSearchData.FEATURE_SEARCH_MAX_SELECTOR);
        hashSet.add(smp);
        
        smp = new SupportedMessageBlock(SIMPP_CAPABILITY_BYTES,
                                        SimppManager.instance().getVersion());
        hashSet.add(smp);
        
/* heavy
        smp = new SupportedMessageBlock(LIME_UPDATE_BYTES,
                                        UpdateHandler.instance().getLatestId());
        hashSet.add(smp);
*/
    }


    /** @return A CapabilitiesVM with the set of messages 
     *  this client supports.
     */
    public static CapabilitiesVM instance() {
        if (_instance == null)
            _instance = new CapabilitiesVM();
        return _instance;
    }


    /**
     * @return -1 if the ability isn't supported, else it returns the version 
     * of the message supported.
     */
    public int supportsCapability(byte[] capabilityName) {
        Iterator iter = _capabilitiesSupported.iterator();
        while (iter.hasNext()) {
            SupportedMessageBlock currSMP = (SupportedMessageBlock) iter.next();
            int version = currSMP.matches(capabilityName);
            if (version > -1)
                return version;
        }
        return -1;
    }


    /** @return 1 or higher if capability queries are supported.  the version
     *  number gives some indication about what exactly is a supported.  if no
     *  support, returns -1.
     */
    public int supportsFeatureQueries() {
        return supportsCapability(FEATURE_SEARCH_BYTES);
    }
    

    /** @return true if 'what is new' capability query feature is supported.
     */
    public boolean supportsWhatIsNew() {
        return FeatureSearchData.supportsWhatIsNew(
            supportsCapability(FEATURE_SEARCH_BYTES));
    }
    
    /**
     * Returns the current SIMPP version.
     */
    public int supportsSIMPP() {
        return supportsCapability(SIMPP_CAPABILITY_BYTES);
    }
    
    /**
     * Returns the current Update version.
     */
    public int supportsUpdate() {
        return supportsCapability(LIME_UPDATE_BYTES);
    }

    // override super
    public boolean equals(Object other) {
        if(other == this)
            return true;
        
        // two of these messages are the same if the support the same messages
        if (other instanceof CapabilitiesVM) {
            CapabilitiesVM vmp = (CapabilitiesVM) other;
            return _capabilitiesSupported.equals(vmp._capabilitiesSupported);
        }

        return false;
    }
    
    /**
     * Constructs a new instance for this node to advertise,
     * using the latest version numbers of supported messages.
     */
    public static void reconstructInstance() {
        //replace _instance with a newer one, which will be created with the
        //correct simppVersion, a new _capabilitiesSupported will be created
        _instance = new CapabilitiesVM();
    }

    
    // override super
    public int hashCode() {
        return 17*_capabilitiesSupported.hashCode();
    }
    

    /** Container for vector elements.
     */  
    static class SupportedMessageBlock {
        final byte[] _capabilityName;
        final int _version;
        final int _hashCode;
        
        public String toString() {
            return new String(_capabilityName) + "/" + _version;
        }

        public SupportedMessageBlock(byte[] capabilityName, int version) {
            _capabilityName = capabilityName;
            _version = version;
            _hashCode = computeHashCode(_capabilityName, _version);
        }

        /**
         * Constructs a new SupportedMessageBlock with data from the 
         * InputStream.  If not enough data is available,
         * throws BadPacketException.
         */
        public SupportedMessageBlock(InputStream encodedBlock)
          throws BadPacketException, IOException {
            if (encodedBlock.available() < 6)
                throw new BadPacketException("invalid block.");
            
            // first 4 bytes are capability name
            _capabilityName = new byte[4];
            encodedBlock.read(_capabilityName, 0, _capabilityName.length);

            _version = ByteOrder.ushort2int(ByteOrder.leb2short(encodedBlock));
            _hashCode = computeHashCode(_capabilityName, _version);

        }
        
        /**
         * Writes this capability (and version) to the OutputStream.
         */
        public void encode(OutputStream out) throws IOException {
            out.write(_capabilityName);
            ByteOrder.short2leb((short)_version, out);
        }

        /** @return 0 or more if this matches the message you are looking for.
         *  Otherwise returns -1;
         */
        public int matches(byte[] capabilityName) {
            if (Arrays.equals(_capabilityName, capabilityName))
                return _version;
            else 
                return -1;
        }

        public boolean equals(Object other) {
            if (other instanceof SupportedMessageBlock) {
                SupportedMessageBlock vmp = (SupportedMessageBlock) other;
                return ((_version == vmp._version) &&
                        (Arrays.equals(_capabilityName, vmp._capabilityName))
                        );
            }
            return false;
        }

        public int hashCode() {
            return _hashCode;
        }
        
        private static int computeHashCode(byte[] capabilityName, int version) {
            int hashCode = 0;
            hashCode += 37*version;
            for (int i = 0; i < capabilityName.length; i++)
                hashCode += (int) 37*capabilityName[i];
            return hashCode;
        }
    }

    /** Overridden purely for stats handling.
     */
    protected void writePayload(OutputStream out) throws IOException {
        super.writePayload(out);
        SentMessageStatHandler.TCP_MESSAGES_SUPPORTED.addMessage(this);
    }

    /** Overridden purely for stats handling.
     */
    public void recordDrop() {
        super.recordDrop();
    }

    public String toString() {
        return "{CapabilitiesVM:"+super.toString()+"; supporting: " + _capabilitiesSupported + "}";
    }

}



