package com.limegroup.gnutella;

import java.util.Arrays;
import java.util.Set;

import org.limewire.collection.BitNumbers;
import org.limewire.io.Connectable;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.IpPort;
import org.limewire.io.NetworkUtils;
import org.limewire.util.ByteUtils;

import com.limegroup.gnutella.http.HTTPConstants;
import com.limegroup.gnutella.uploader.HTTPHeaderUtils;

/**
 * Abstract class that does not store any values but provides default implementations
 * for methods that produce a common format.
 */
public abstract class AbstractPushEndpoint implements PushEndpoint {

    public byte[] toBytes(boolean includeTLS) {
        Set<? extends IpPort> proxies = getProxies();
        int payloadSize = getSizeBytes(proxies, includeTLS);
        IpPort addr = getValidExternalAddress();
        int FWTVersion = getFWTVersion();
        if (addr != null && FWTVersion > 0)
            payloadSize+=6;
        byte [] ret = new byte[payloadSize];
        toBytes(ret,0,proxies,addr,FWTVersion, includeTLS);
        return ret;
    }

    public void toBytes(byte[] where, int offset, boolean includeTLS) {
        toBytes(where, offset, getProxies(), getValidExternalAddress(),getFWTVersion(), includeTLS);
    }

    protected void toBytes(byte []where, int offset, Set<? extends IpPort> proxies,
            IpPort address, int FWTVersion, boolean includeTLS) {

        int neededSpace = getSizeBytes(proxies, includeTLS);
        if (address != null) { 
            if (FWTVersion > 0)
                neededSpace+=6;
        } else {
            FWTVersion = 0;
        }
        
        if (where.length-offset < neededSpace)
            throw new IllegalArgumentException ("target array too small");
        
        int featureIdx = offset;
        // store the number of proxies
        where[offset] = (byte)(Math.min(MAX_PROXIES,proxies.size()) 
                | getFeatures() 
                | FWTVersion << 3);
        
        // store the guid
        System.arraycopy(getClientGUID(),0,where,++offset,16);
        offset+=16;
        
        // if we know the external address, store that too
        // if its valid and not private and port is valid
        if (address != null && FWTVersion > 0) {
            byte [] addr = address.getInetAddress().getAddress();
            int port = address.getPort();
            
            System.arraycopy(addr,0,where,offset,4);
            offset+=4;
            ByteUtils.short2leb((short)port,where,offset);
            offset+=2;
        }
        
        // If we're including TLS, then add a byte for which proxies support it.
        
        int pptlsIdx = offset;
        int i=0;
        if(includeTLS) {
            // If any of the proxies are TLS-capable, increment the offset
            for(IpPort ppi : proxies) {
                if(i >= MAX_PROXIES)
                    break;
                
                if(ppi instanceof Connectable && ((Connectable)ppi).isTLSCapable()) {
                    offset++;
                    break;
                }
                
                i++;
            }
        }
        
        // store the push proxies
        i=0;
        for(IpPort ppi : proxies) {
            if(i >= MAX_PROXIES)
                break;
            
            byte [] addr = ppi.getInetAddress().getAddress();
            short port = (short)ppi.getPort();
            
            System.arraycopy(addr,0,where,offset,4);
            offset+=4;
            ByteUtils.short2leb(port,where,offset);
            offset+=2;
            i++;
        }
        
        // insert the tls indices & turn the feature on if TLS should be included
        BitNumbers bn = includeTLS ? HTTPHeaderUtils.getTLSIndices(proxies, (Math.min(proxies.size(), MAX_PROXIES))) : BitNumbers.EMPTY_BN;
        if(!bn.isEmpty()) {
            byte[] tlsIndexes = bn.toByteArray();
            assert tlsIndexes.length == 1;
            where[pptlsIdx] = tlsIndexes[0];
            where[featureIdx] |= PPTLS_BINARY;
        } else {
            where[featureIdx] &= ~PPTLS_BINARY; // make sure its not in the features!
        }
    }
    
    @Override
    public final boolean equals(Object obj) {
        if (obj instanceof PushEndpoint) {
            return Arrays.equals(getClientGUID(), ((PushEndpoint)obj).getClientGUID());
        }
        return false;
    }
    
    @Override
    public final int hashCode() {
        return new GUID(getClientGUID()).hashCode();
    }
    
    public String httpStringValue() {
        GUID guid = new GUID(getClientGUID());
        StringBuilder httpString =new StringBuilder(guid.toHexString()).append(";");
        
        //if version is not 0, append it to the http string
        int fwtVersion=getFWTVersion();
        if (fwtVersion!=0) {
            httpString.append(HTTPConstants.FW_TRANSFER)
                .append("/")
                .append(fwtVersion)
                .append(";");
        
            // append the external address of this endpoint if such exists
            // and is valid, non-private and if the port is valid as well.
            IpPort address = getValidExternalAddress();
            if (address!=null) {
                String addr = address.getAddress();
                int port = address.getPort();
                if (!addr.equals(RemoteFileDesc.BOGUS_IP) && 
                        NetworkUtils.isValidPort(port)){
                    httpString.append(port)
                    .append(":")
                    .append(addr)
                    .append(";");
                }
            }
        }
        
        Set<? extends IpPort> proxies = getProxies();
        if (!proxies.isEmpty()) {
            httpString.append(HTTPHeaderUtils.encodePushProxies(proxies, ";", PushEndpoint.MAX_PROXIES));
        } else {
            //trim the ; at the end
            httpString.deleteCharAt(httpString.length()-1);
        }

        return httpString.toString();
    }
    
    @Override
    public String toString() {
        String ret = "PE [FEATURES:"+getFeatures()+",\nFWT Version:"+getFWTVersion()+
        ",\nGUID:"+ new GUID(getClientGUID()) +", address: "+
        getAddress()+":"+getPort()+",\nproxies:{ "; 
        for (IpPort ppi : getProxies()) {
            ret = ret+ppi.getInetAddress()+":"+ppi.getPort()+"\n";
        }
        ret = ret+ "}]";
        return ret;
    }
    
    /**
     * @param proxies the set of proxies for this PE
     * @return how many bytes a PE will use when serialized.
     */
    public static int getSizeBytes(Set<? extends IpPort> proxies, boolean includeTLS) {
        boolean hasTLS = false;
        if(includeTLS) {
            int i = 0;
            for(IpPort ipp : proxies) {
                if(i >= MAX_PROXIES)
                    break;
                
                if(ipp instanceof ConnectableImpl && ((Connectable)ipp).isTLSCapable()) {
                    hasTLS = true;
                    break;
                }
                i++;
            }
        }
        return HEADER_SIZE + (hasTLS ? 1 : 0 ) + Math.min(proxies.size(),MAX_PROXIES) * PROXY_SIZE;
    }

}
