package foo.bar;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Observable;
import java.util.Observer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class can check whether another copy of application is already running and send corresponding events to
 * subscribers. Check is based on opening a socket and listening to new connections.
 * <p>
 * Notification can be synchronous or asynchronous. <code>syncCheck</code> method provides synchronous notification,
 * asynchronous notification uses standard <code>Observer</code> - <code>Observable</code> mechanism.
 * 
 * @author Leonid Bogdanov
 * @version 1.1, 02.08.09
 * @see java.util.Observable
 * @see java.util.Observer
 */
public class InstanceGuard extends Observable {

    /**
     * Represents different types of events that <code>InstanceGuard</code> can raise. 
     */
    public static enum Event {
        /**
         * Raised when running instance of application was detected
         */
        ALREADY_RUNNING,
        /**
         * Raised when current application was the first instance
         * or another instance had tried to start
         */
        ACTIVATE_CURRENT,
        /**
         * Raised when it wasn't possible to discover application state
         * due to some reasons (e.g. firewall blocked socket communication)
         */
        UNKNOWN_STATE
    }

    private static final class InstanceGuardHolder {
        private static final InstanceGuard INSTANCE = new InstanceGuard();
    }

    protected static final Logger LOG = LoggerFactory.getLogger(InstanceGuard.class);

    private static final int[]  PORTS = {10000, 10001, 10002};
    private static final byte   HELLO = 1;
    private static final byte   BYE   = -1;

    private          Object       lock         = new Object();
    private          Thread       thread;
    private volatile ServerSocket serverSocket;
    private volatile boolean      keepGuarding = true;

    /**
     * Returns <code>InstanceGuard</code> singleton instance 
     * 
     * @return <code>InstanceGuard</code> instance
     */
    public static InstanceGuard getInstance() {
        return InstanceGuardHolder.INSTANCE;
    }

    /**
     * Starts guarding application from running another copy; sends corresponding events to subscribers
     * <p>
     * Calling this method more than ones has no effect
     */
    public synchronized void startGuard() {
        if (!isGuarding()) {
            thread = new Thread(new Runnable() {

                public void run() {
                    LOG.info("Instance guard is starting...");
                    try {
                        for (int port : PORTS) {
                            try {
                                serverSocket = new ServerSocket(port, 5);
                            } catch (BindException be) {
                                Socket socket = null;
                                try {
                                    socket           = new Socket(InetAddress.getLocalHost(), port);
                                    OutputStream out = socket.getOutputStream();
                                    InputStream  in  = socket.getInputStream();
                                    out.write(HELLO);
                                    out.flush();
                                    if (BYE == (byte) in.read()) {
                                        raiseEvent(Event.ALREADY_RUNNING);
                                        return;
                                    }
                                    continue;
                                } finally {
                                    if (socket != null) {
                                        socket.close();
                                    }
                                }
                            }
                            raiseEvent(Event.ACTIVATE_CURRENT);
                            while (keepGuarding) {
                                Socket socket = null;
                                try {
                                    socket           = serverSocket.accept();
                                    OutputStream out = socket.getOutputStream();
                                    InputStream  in  = socket.getInputStream();
                                    if (HELLO == (byte) in.read()) {
                                        out.write(BYE);
                                        out.flush();
                                        raiseEvent(Event.ACTIVATE_CURRENT);
                                    }
                                } finally {
                                    if (socket != null) {
                                        socket.close();
                                    }
                                }
                            }
                            return;
                        }
                        LOG.info("Instance guard couldn't start because ports {} were in use", PORTS);
                    } catch (SocketException se) {
                        LOG.trace("Socket error", se);
                    } catch (IOException ioe) {
                        LOG.debug("I/O error in guard's thread", ioe);
                    } finally {
                        if (serverSocket != null) {
                            try {
                                serverSocket.close();
                            } catch (IOException ioe) {
                                LOG.debug("Can't close server socket", ioe);
                            }
                        }
                        LOG.info("Instance guard is stopping...");
                    }
                }

            });
            thread.start();
        }
    }

    /**
     * Stops guarding application from duplicate copies
     * <p>
     * Calling this method more than ones has no effect
     */
    public synchronized void stopGuard() {
        if (isGuarding()) {
            keepGuarding = false;
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException ioe) {
                    LOG.debug("Can't close server socket", ioe);
                }
            }
        }
    }

    /**
     * Returns current state of instance guard
     * 
     * @return <code>true</code> if instance guard is guarding application; <code>false</code> otherwise
     */
    public boolean isGuarding() {
        return (thread == null) ? false : thread.isAlive();
    }

    /**
     * Performs synchronous check, i.d. blocks calling thread till result is received<br> 
     * but no more than 500 ms
     * 
     * @param observer observer to add for future asynchronous notifications; can be <code>null</code>
     * @return corresponding event
     * @see InstanceGuard#syncCheck(Observer, long)
     */
    public Event syncCheck(Observer observer) {
        return syncCheck(observer, 500);
    }

    /**
     * Performs synchronous check, i.d. blocks calling thread till result is received<br>
     * Maximum wait time can be specified through <code>millis</code> parameter
     *  
     * @param observer observer to add for future asynchronous notifications; can be <code>null</code>
     * @param millis maximum wait time; 0 means to wait forever
     * @return corresponding event
     */
    public Event syncCheck(Observer observer, long millis) {
        synchronized (lock) {
            final class EventHolder {
                private Event event = Event.UNKNOWN_STATE;
            }
            final EventHolder eventHolder   = new EventHolder();
            Observer          innerObserver = new Observer() {

                public void update(Observable o, Object arg) {
                    eventHolder.event = (Event) arg;
                }

            };
            addObserver(innerObserver);
            startGuard();
            try {
                thread.join(millis);
            } catch (InterruptedException ie) {
                LOG.debug("Guard's thread was interrupted", ie);
            }
            deleteObserver(innerObserver);
            if (isGuarding() && observer != null) {
                addObserver(observer);
            }
            if (countObservers() == 0) {
                stopGuard();
            }
            return eventHolder.event;
        }
    }

    protected InstanceGuard() {}

    protected void raiseEvent(Event event) {
        setChanged();
        notifyObservers(event);
    }

}

