package org.limewire.lws.server;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.concurrent.ManagedThread;
import org.limewire.lws.server.LWSDispatcher;
import org.limewire.lws.server.LWSDispatcherSupport;
import org.limewire.lws.server.LWSServerUtil;
import org.limewire.lws.server.StringCallback;
import org.limewire.service.ErrorService;

/**
 * Base class for servers, both local and remote.
 */
public abstract class AbstractServer implements Runnable  {

    private static final Log LOG = LogFactory.getLog(AbstractServer.class);
    private static final byte[] NEWLINE = { (byte) '\r', (byte) '\n' };
    
    private final int port;
    private final String name;  
    private final AtomicBoolean hasShutDown = new AtomicBoolean(false);
    
    private boolean done = false;
    private LWSDispatcher dispatcher;
    private Thread runner;
    private ServerSocket serverSocket;

    // --------------------------------------------------------
    // Interface
    // --------------------------------------------------------

    public AbstractServer(int port, String name, LWSDispatcherSupport dispatcher) {
        this.port = port;
        this.name = name;
        this.dispatcher = dispatcher;
        LOG.debug(name + " on port " + port);
    }
    
    public AbstractServer(int port, String name) {
        this(port, name, null);        
    }
    
    public final Thread start() {
        Thread t = new ManagedThread(this);
        t.start();
        this.runner = t;
        return t;
    }

    /**
     * Main entry point.
     */
    public final void run() {
        noteRun();
        go();
    }
    
    public final LWSDispatcherSupport getDispatcher() {
        return (LWSDispatcherSupport)this.dispatcher;
    }

    public final void setDone(final boolean done) {
        this.done = done;
    }

    public final boolean isDone() {
        return this.done;
    }

    public final int getPort() {
        return port;
    }

    public final String getName() {
        return name;
    }
    
    public final String report(String error) {
        return LWSDispatcherSupport.report(error);
    }
    
    /**
     * Attempts to join this thread and then set Done to <code>true</code>.
     * 
     * @param millis milliseconds to wait for a join
     */
    public final void shutDown(final long millis) {
        if (hasShutDown.getAndSet(true)) return;
        setDone(true);
        stop(runner, millis);
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                handle(e, getClass() + " on port " + port);
            }
        }   
        getDispatcher().deauthenticate();
        runner = null;
    }    
    
    @Override
    public final String toString() {
        return name;
    }

    // --------------------------------------------------------
    // Interface to subclasses
    // --------------------------------------------------------
    
    protected abstract boolean sendIPToHandlers();

    protected final void handle(final Throwable t) {
        ErrorService.error(t);
    }

    protected final void handle(final Throwable t, final String msg) {
        ErrorService.error(t, msg);
    }
    
    /**
     * If subclasses can't pass a dispatcher in to the super constructor
     * because it needs to know about the server, you <b>must</b> call this
     * in the constructor.
     * 
     * @param dispatcher new {@link LWSDispatcherSupport}
     */
    protected final void setDispatcher(LWSDispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    /**
     * Set by {@link #start(AbstractServer)}.
     */
    protected final Thread getRunner() {
        return runner;
    }

    /**
     * Override this to know when we're starting.
     */
    protected void noteRun() {
    }

    // --------------------------------------------------------
    // Private
    // --------------------------------------------------------

    private void go() {
        int threadCounter = 0;
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            public void run() {
                shutDown();
            }
        }));       
        try {
            int tmpPort = port;
            for (; tmpPort < port+10; tmpPort++) {
                try {
                    serverSocket = new ServerSocket(tmpPort);
                } catch (IOException ignored) { }
                if (serverSocket != null) break;
            }
            while (!isDone()) {
                Socket s = serverSocket.accept();
                Worker ws = new Worker("lws worked #" + (threadCounter++));
                ws.setSocket(s, tmpPort);
                addNewThread(ws);
            }
        } catch (IOException e) {
            //
            // We are going to interupt this socket when we're
            // done, so don't complain about SocketExceptions
            //
            if (!(e instanceof SocketException)) handle(e, getClass() + " on port " + port);
            if (e instanceof java.net.BindException) setDone(true);
        } finally {
            shutDown();
        }
        shutDown();
    }

    private void addNewThread(Worker w) {
        Thread t = new ManagedThread(w, w.getName());
        t.start();
    }

    class Worker implements Runnable {

        final static int BUF_SIZE = 2048;

        /**
         * buffer to use for requests
         */
        private final byte[] buf = new byte[BUF_SIZE];

        /**
         * Socket to client we're handling
         */
        private Socket s;
        
        private final String name;
        Worker(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        synchronized void setSocket(Socket s, int port) {
            this.s = s;
            notify();
        }
        
        @Override
        public String toString() {
            return name;
        }

        public void run() {
            try {
                handleClient();
            } catch (IOException e) {
                LOG.debug("handleClient", e);
            }
        }
        
        @SuppressWarnings("deprecation")
        private void handleClient() throws IOException {
            InputStream is = new BufferedInputStream(s.getInputStream());
            final PrintStream ps = new PrintStream(s.getOutputStream(), true);
            /*
             * we will only block in read for this many milliseconds before we
             * fail with java.io.InterruptedIOException, at which point we will
             * abandon the connection.
             */
            /* zero out the buffer from last time */
            for (int i = 0; i < BUF_SIZE; i++) {
                buf[i] = 0;
            }
            try {
                /*
                 * We only support HTTP GET/HEAD, and don't support any fancy
                 * HTTP options, so we're only interested really in the first
                 * line.
                 */
                int nread = 0, r = 0;

                outerloop: while (nread < BUF_SIZE) {
                    r = is.read(buf, nread, BUF_SIZE - nread);
                    if (r == -1) {
                        /* EOF */
                        return;
                    }
                    int i = nread;
                    nread += r;
                    for (; i < nread; i++) {
                        if (buf[i] == (byte) '\n' || buf[i] == (byte) '\r') {
                            /* read one line */
                            break outerloop;
                        }
                    }
                }

                /* are we doing a GET or just a HEAD */
                /* beginning of file name */
                int index;
                if (buf[0] == (byte) 'G' && buf[1] == (byte) 'E'
                        && buf[2] == (byte) 'T' && buf[3] == (byte) ' ') {
                    index = 4;
                } else if (buf[0] == (byte) 'H' && buf[1] == (byte) 'E'
                        && buf[2] == (byte) 'A' && buf[3] == (byte) 'D'
                        && buf[4] == (byte) ' ') {
                    index = 5;
                } else {
                    /* we don't support this method */
                    ps.print("HTTP/1.1 " + HttpURLConnection.HTTP_BAD_METHOD + " unsupported method type: ");
                    ps.write(buf, 0, 5);
                    ps.write(NEWLINE);
                    ps.flush();
                    return;
                }

                int i = 0;
                /*
                 * find the file name, from: GET /foo/bar.html HTTP/1.0 extract
                 * "/foo/bar.html"
                 */
                for (i = index; i < nread; i++) {
                    if (buf[i] == (byte) ' ') {
                        break;
                    }
                }
                String request = (new String(buf, 0, index, i - index)).replace('/', File.separatorChar);
                if (request.startsWith(File.separator)) {
                    request = request.substring(1);
                }
                //
                // Only add the IP address if we're a remote server
                //
                if (sendIPToHandlers()) {
                    //
                    // We use Wicket-style arguments, because only remote servers want this
                    //
                    String ip = LWSServerUtil.getIPAddress(s.getInetAddress());
                    request += "/ip/" + ip;
                }
                getDispatcher().handle(request, ps, new StringCallback() {

                    public void process(String res) {
                        ps.print("HTTP/1.1 ");
                        ps.print(HttpURLConnection.HTTP_OK);
                        ps.print(" OK");
                        try {
                            ps.write(NEWLINE);
                            ps.print("Last-modified: ");
                            println(ps, LWSServerUtil.createCookieDate());
                            ps.print("Server: ");
                            ps.print(getName());
                            ps.write(NEWLINE);
                            ps.print("Date: " + new Date());
                            ps.write(NEWLINE);
                            ps.print("Content-length: ");
                            println(ps, String.valueOf(res.length()));
                            ps.write(NEWLINE);
                            ps.print(res);
                            ps.flush();
                        } catch (IOException e) {
                            ErrorService.error(e);
                        }
                    }
                    
                });

            } finally {
                s.close();
                is.close();
            }
        }

    }

    private void println(final PrintStream o, final String line)
            throws IOException {
        o.print(line);
        o.write(NEWLINE);
    }
    
    private void stop(final Thread t, final long millis) {
        if (t != null) {
            synchronized (t) {
                try {
                    t.join(millis);
                } catch (InterruptedException e) {
                    LOG.debug("trying to stop: " + t, e);
                }
            }
        }    
    }

    /**
     * Calls {@link #shutDown(long)} with <code>100</code>.
     */
    public final void shutDown() {
        shutDown(100);
    }

}
