package com.limegroup.gnutella.handshaking;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.Properties;

import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.http.HeaderSupport;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.StringSetting;
import com.limegroup.gnutella.util.NetworkUtils;

class HandshakeSupport extends HeaderSupport {

    /** Connection string. */
    private String GNUTELLA_CONNECT_06 = "GNUTELLA CONNECT/0.6";
    
    /** Valid connect line string. */
    private static final String GNUTELLA_06 = "GNUTELLA/0.6";
    
    /** Gnutella 0.6 accept connection string. */
    private static final String CONNECT = "CONNECT/";
    
    /** End of line for Gnutella 0.6 */
    private static final String CRLF = "\r\n";
    
    /** All headers we've read from the remote side. */
    private final Properties readHeaders;
    
    /** All headers we wrote to the remote side. */
    private final Properties writtenHeaders;
    
    /** The remote address to use when processing a sent header. */
    private final String remoteAddress;
    
    /** The connectLine used in the remote response. */
    private String remoteResponse;
    
    HandshakeSupport(String remoteAddress) {
        this.remoteAddress = remoteAddress;
        this.readHeaders = new Properties();
        this.writtenHeaders = new Properties();
    }

    /** Creates their response, based on the given connectLine. */
    HandshakeResponse createRemoteResponse(String connectLine) throws IOException {
        this.remoteResponse = connectLine.substring(GNUTELLA_06.length()).trim();
        return HandshakeResponse.createRemoteResponse(remoteResponse, readHeaders);
    }
    
    /** Appends the connect line to the given StringBuffer. */
    void appendConnectLine(StringBuffer sb) {
        sb.append(GNUTELLA_CONNECT_06).append(CRLF);
    }
    
    /** Appends a response (using the given response) to the given StringBuffer */
    void appendResponse(HandshakeResponse response, StringBuffer sb) {        
        sb.append(GNUTELLA_06).append(" ").append(response.getStatusLine()).append(CRLF);
        appendHeaders(response.props(), sb);
    }
    
    /**
     * Appends the given properties to the given StringBuffer.
     * 
     * @param props The headers to be sent. Note: null argument is acceptable,
     *  if no headers need to be sent the trailer will be sent.
     */
    void appendHeaders(Properties props, StringBuffer sb)  {
        if (props != null) {
            Enumeration names = props.propertyNames();
            while (names.hasMoreElements()) {
                String key = (String) names.nextElement();
                String value = props.getProperty(key);
                String toWrite = processKeyValueForWriting(key, value);
                sb.append(toWrite);
            }
        }
        sb.append(CRLF);
    }

    /** Determines if the given connect line is valid. */
    boolean isConnectLineValid(String s) {
        return s.startsWith(GNUTELLA_06);
    }

    /**
     * Returns true iff line ends with "CONNECT/N", where N is a number greater than or equal "0.6".
     */
    boolean notLessThan06(String line) {
        int i = line.indexOf(CONNECT);
        if (i < 0)
            return false;
        try {
            float f = Float.parseFloat(line.substring(i + CONNECT.length()));
            return f >= 0.6f;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    
    /** 
     * Process a single read header.
     * Returns true if this wasn't a blank line and more headers are expected,
     * returns false if this was a blank line and no more headers are expected.
     */
    public boolean processReadHeader(String line) {
        if(line.equals(""))
            return false;
        
        int i = line.indexOf(':');
        if (i > 0) {
            String key = line.substring(0, i);
            String value = line.substring(i + 1).trim();
            if (HeaderNames.REMOTE_IP.equals(key))
                changeAddress(value);
            readHeaders.put(key, value);
        }
        return true;
    }
    
    /**
     * Process a key/value pair for writing.
     * May change the value.
     * Returns what should be sent over the wire.
     */
    String processKeyValueForWriting(String key, String value) {
        // Ensure we put their remote-ip correctly.
        if (HeaderNames.REMOTE_IP.equals(key))
            value = remoteAddress;
        if (value == null)
            value = "";
        writtenHeaders.put(key, value);
        return key + ": " + value + CRLF;
    }

    /**
     * Determines if the address should be changed and changes it if
     * necessary.
     */
    private void changeAddress(final String v) {
        InetAddress ia = null;
        try {
            ia = InetAddress.getByName(v);
        } catch(UnknownHostException uhe) {
            return; // invalid.
        }
        
        // invalid or private, exit
        if(!NetworkUtils.isValidAddress(ia) ||
            NetworkUtils.isPrivateAddress(ia))
            return;
            
        // If we're forcing, change that if necessary.
        if( ConnectionSettings.FORCE_IP_ADDRESS.getValue() ) {
            StringSetting addr = ConnectionSettings.FORCED_IP_ADDRESS_STRING;
            if(!v.equals(addr.getValue())) {
                addr.setValue(v);
                RouterService.addressChanged();
            }
        }
        // Otherwise, if our current address is invalid, change.
        else if(!NetworkUtils.isValidAddress(RouterService.getAddress())) {
            // will auto-call addressChanged.
            RouterService.getAcceptor().setAddress(ia);
        }
        
        RouterService.getAcceptor().setExternalAddress(ia);
    }
    
    /** Returns the number of headers we've read so far. */
    public int getHeadersReadSize() {
        return readHeaders.size();
    }
    
    /** Constructs a HandshakeResponse object using the remote response data. */
    HandshakeResponse getReadHandshakeRemoteResponse() throws IOException {
        return HandshakeResponse.createRemoteResponse(remoteResponse, readHeaders);
    }

    /** Constructs a HandshakeResponse object wrapping the headers we've read. */
    HandshakeResponse getReadHandshakeResponse() {
        return HandshakeResponse.createResponse(readHeaders);
    }

    /** Constructs a HandshakeResponse object wrapping the headers we've written. */
    HandshakeResponse getWrittenHandshakeResponse() {
        return HandshakeResponse.createResponse(writtenHeaders);
    }

}
