package net.i2p.sam;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by human in 2004 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't  make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.IOException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.channels.SocketChannel;
import java.util.Properties;

import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
import net.i2p.util.VersionComparator;

/**
 * SAM handler factory class.
 */
class SAMHandlerFactory {

    private static final String VERSION = "3.3";

    private static final int HELLO_TIMEOUT = 60*1000;

    /**
     * Return the right SAM handler depending on the protocol version
     * required by the client.
     *
     * @param s Socket attached to SAM client
     * @param i2cpProps config options for our i2cp connection
     * @throws SAMException if the connection handshake (HELLO message) was malformed
     * @return A SAM protocol handler, or null if the client closed before the handshake
     */
    public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps,
                                              SAMBridge parent) throws SAMException {
        String line;
        Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class);

        try {
            Socket sock = s.socket();
            sock.setKeepAlive(true);
            StringBuilder buf = new StringBuilder(128);
            ReadLine.readLine(sock, buf, HELLO_TIMEOUT);
            sock.setSoTimeout(0);
            line = buf.toString();
        } catch (SocketTimeoutException e) {
            throw new SAMException("Timeout waiting for HELLO VERSION", e);
        } catch (IOException e) {
            throw new SAMException("Error reading from socket", e);
        } catch (RuntimeException e) {
            throw new SAMException("Unexpected error", e);
        }
        if (log.shouldDebug())
            log.debug("New message received: [" + line + ']');

        // Message format: HELLO VERSION [MIN=v1] [MAX=v2]
        Properties props = SAMUtils.parseParams(line);
        if (!"HELLO".equals(props.remove(SAMUtils.COMMAND)) ||
            !"VERSION".equals(props.remove(SAMUtils.OPCODE))) {
            throw new SAMException("Must start with HELLO VERSION");
        }

        String minVer = props.getProperty("MIN");
        if (minVer == null) {
            //throw new SAMException("Missing MIN parameter in HELLO VERSION message");
            // MIN optional as of 0.9.14
            minVer = "1";
        }

        String maxVer = props.getProperty("MAX");
        if (maxVer == null) {
            //throw new SAMException("Missing MAX parameter in HELLO VERSION message");
            // MAX optional as of 0.9.14
            maxVer = "99.99";
        }

        String ver = chooseBestVersion(minVer, maxVer);

        if (ver == null) {
            SAMHandler.writeString("HELLO REPLY RESULT=NOVERSION\n", s);
            return null;
        }

        if (Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH))) {
            String user = props.getProperty("USER");
            String pw = props.getProperty("PASSWORD");
            if (user == null || pw == null) {
                if (user == null)
                    log.logAlways(Log.WARN, "SAM authentication failed");
                else
                    log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
                throw new SAMException("USER and PASSWORD required");
            }
            String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX);
            if (savedPW == null) {
                log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
                throw new SAMException("Authorization failed");
            }
            PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext());
            if (!pm.checkHash(savedPW, pw)) {
                log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
                throw new SAMException("Authorization failed");
            }
        }

        // Let's answer positively
        if (!SAMHandler.writeString("HELLO REPLY RESULT=OK VERSION=" + ver + "\n", s))
            throw new SAMException("Error writing to socket");       

        // ...and instantiate the right SAM handler
        int verMajor = getMajor(ver);
        int verMinor = getMinor(ver);
        SAMHandler handler;

        try {
            switch (verMajor) {
            case 1:
                handler = new SAMv1Handler(s, verMajor, verMinor, i2cpProps, parent);
                break;
            case 2:
                handler = new SAMv2Handler(s, verMajor, verMinor, i2cpProps, parent);
                break;
            case 3:
            	handler = new SAMv3Handler(s, verMajor, verMinor, i2cpProps, parent);
            	break;
            default:
                log.error("BUG! Trying to initialize the wrong SAM version!");
                throw new SAMException("BUG! (in handler instantiation)");
            }
        } catch (IOException e) {
            log.error("Error creating the handler for version "+verMajor, e);
            throw new SAMException("IOException caught during SAM handler instantiation");
        }
        return handler;
    }

    /*
     * @return "x.y" the best version we can use, or null on failure
     */
    private static String chooseBestVersion(String minVer, String maxVer) {
        if (VersionComparator.comp(VERSION, minVer) >= 0 &&
            VersionComparator.comp(VERSION, maxVer) <= 0)
            return VERSION;
        if (VersionComparator.comp("3.2", minVer) >= 0 &&
            VersionComparator.comp("3.2", maxVer) <= 0)
            return "3.2";
        if (VersionComparator.comp("3.1", minVer) >= 0 &&
            VersionComparator.comp("3.1", maxVer) <= 0)
            return "3.1";
        // in VersionComparator, "3" < "3.0" so
        // use comparisons carefully
        if (VersionComparator.comp("3.0", minVer) >= 0 &&
            VersionComparator.comp("3", maxVer) <= 0)
            return "3.0";
        if (VersionComparator.comp("2.0", minVer) >= 0 &&
            VersionComparator.comp("2", maxVer) <= 0)
            return "2.0";
        if (VersionComparator.comp("1.0", minVer) >= 0 &&
            VersionComparator.comp("1", maxVer) <= 0)
            return "1.0";
        return null;
    }

    /* Get the major protocol version from a string, or -1 */
    private static int getMajor(String ver) {
        if (ver == null)
            return -1;
        int dot = ver.indexOf('.');
        if (dot == 0)
            return -1;
        if (dot > 0)
            ver = ver.substring(0, dot);
        try {
            return Integer.parseInt(ver);
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    /* Get the minor protocol version from a string, or -1 */
    private static int getMinor(String ver) {
        if ( (ver == null) || (ver.indexOf('.') < 0) )
            return -1;
        try {
            String major = ver.substring(ver.indexOf('.') + 1);
            return Integer.parseInt(major);
        } catch (NumberFormatException e) {
            return -1;
        } catch (ArrayIndexOutOfBoundsException e) {
            return -1;
        }
    }
}
