/*FreeMind - A Program for creating and viewing Mindmaps
 *Copyright (C) 2000-2008  Joerg Mueller, Daniel Polansky, Christian Foltin and others.
 *
 *See COPYING for Details
 *
 *This program is free software; you can redistribute it and/or
 *modify it under the terms of the GNU General Public License
 *as published by the Free Software Foundation; either version 2
 *of the License, or (at your option) any later version.
 *
 *This program is distributed in the hope that it will be useful,
 *but WITHOUT ANY WARRANTY; without even the implied warranty of
 *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *GNU General Public License for more details.
 *
 *You should have received a copy of the GNU General Public License
 *along with this program; if not, write to the Free Software
 *Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Created on 28.12.2008
 */


package plugins.collaboration.socket;

import freemind.common.NumberProperty;
import freemind.common.StringProperty;
import freemind.controller.actions.generated.instance.CollaborationGoodbye;
import freemind.controller.actions.generated.instance.CollaborationUserInformation;
import freemind.extensions.DontSaveMarker;
import freemind.extensions.PermanentNodeHook;
import freemind.main.Resources;
import freemind.main.Tools;
import freemind.main.XMLElement;
import freemind.modes.MindMapNode;
import freemind.modes.mindmapmode.MindMapController;
import freemind.view.mindmapview.NodeView;

import javax.swing.*;
import java.io.IOException;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Vector;

/**
 * @author foltin
 */
public class MindMapMaster extends SocketBasics implements PermanentNodeHook,
        DontSaveMarker {

    /**
     *
     */
    public static final int SOCKET_TIMEOUT_IN_MILLIES = 500;
    MasterThread mListener = null;
    ServerSocket mServer;
    Vector mConnections = new Vector();
    protected boolean mLockEnabled = false;
    private String mLockMutex = "";
    private int mPort;
    private String mLockId;
    private long mLockedAt;
    private String mLockUserName;
    private boolean mMasterStarted;

    private class MasterThread extends TerminateableThread {

        private static final long TIME_BETWEEN_USER_INFORMATION_IN_MILLIES = 5000;
        private static final long TIME_FOR_ORPHANED_LOCK = 5000;
        private long mLastTimeUserInformationSent = 0;

        /**
         * @param pName
         */
        public MasterThread() {
            super("Master");
        }

        /*
         * (non-Javadoc)
         *
         * @see plugins.collaboration.socket.TerminateableThread#processAction()
         */
        public boolean processAction() throws Exception {
            try {
                logger.finest("Waiting for message");
                Socket client = mServer.accept();
                logger.info("Received new client.");
                client.setSoTimeout(SOCKET_TIMEOUT_IN_MILLIES);
                ServerCommunication c = new ServerCommunication(
                        MindMapMaster.this, client, getMindMapController());
                c.start();
                synchronized (mConnections) {
                    mConnections.addElement(c);
                }
            } catch (SocketTimeoutException e) {
            }
            final long now = System.currentTimeMillis();
            if (now - mLastTimeUserInformationSent > TIME_BETWEEN_USER_INFORMATION_IN_MILLIES) {
                mLastTimeUserInformationSent = now;
                CollaborationUserInformation userInfo = getMasterInformation();
                synchronized (mConnections) {
                    for (int i = 0; i < mConnections.size(); i++) {
                        try {
                            final ServerCommunication connection = (ServerCommunication) mConnections
                                    .elementAt(i);
                            /* to each server, the IP address is chosen that belongs to this connection.
							 * E.g. if the connection is routed over one of several network interfaces,
							 * the address of this interface is reported.
							 */
                            userInfo.setMasterIp(connection.getIpToSocket());
                            connection.send(userInfo);
                        } catch (Exception e) {
                            Resources.getInstance().logException(
                                    e);
                        }
                    }
                }
            }
            // timeout such that lock can't be held forever
            synchronized (mLockMutex) {
                if (mLockEnabled && now - mLockedAt > TIME_FOR_ORPHANED_LOCK) {
                    logger.warning("Release lock " + mLockId + " held by "
                            + mLockUserName);
                    clearLock();
                }
            }
            return true;
        }

    }

    public synchronized void removeConnection(ServerCommunication client) {
        synchronized (mConnections) {
            mConnections.remove(client);
        }
        // correct the map title, as we probably don't have clients anymore
        setTitle();
    }

    protected void setTitle() {
        getMindMapController().getController().setTitle();
    }

    public void startupMapHook() {
        super.startupMapHook();
        // Restart check, as the startup command is given, even if the mindmap
        // is changed via
        // the tab bar. So, this method must be idempotent...
        if (mMasterStarted) {
            // we were already here, so
            return;
        }
        MindMapController controller = getMindMapController();
        final StringProperty passwordProperty = new StringProperty(
                PASSWORD_DESCRIPTION, PASSWORD);
        final StringProperty passwordProperty2 = new StringProperty(
                PASSWORD_VERIFICATION_DESCRIPTION, PASSWORD_VERIFICATION);
        // StringProperty bindProperty = new StringProperty(
        // "IP address of the local machine, or 0.0.0.0 if ", "Host");
        final NumberProperty portProperty = getPortProperty();
        Vector controls = new Vector();
        controls.add(passwordProperty);
        controls.add(passwordProperty2);
        // controls.add(bindProperty);
        controls.add(portProperty);
        FormDialog dialog = new FormDialog(controller);
        dialog.setUp(controls, new FormDialogValidator() {
            public boolean isValid() {
                logger.finest("Output valid?");
                return Tools.safeEquals(passwordProperty.getValue(),
                        passwordProperty2.getValue());
            }
        });
        if (!dialog.isSuccess()) {
            switchMeOff();
            return;
        }
		/* Store port value in preferences. */
        setPortProperty(portProperty);
        mPassword = passwordProperty.getValue();
        // start server:
        logger.info("Start server...");
        mMasterStarted = true;
        try {
            mPort = getPortProperty().getIntValue();
            mServer = new ServerSocket(mPort);
            mServer.setSoTimeout(SOCKET_TIMEOUT_IN_MILLIES);
            mListener = new MasterThread();
            mListener.start();
        } catch (Exception e) {
            Resources.getInstance().logException(e);
            controller.getController().errorMessage(
                    Resources.getInstance().format(
                            SOCKET_CREATION_EXCEPTION_MESSAGE,
                            new Object[]{portProperty.getValue(),
                                    e.getMessage()}));
            switchMeOff();
            return;
        }
        registerFilter();
        logger.info("Starting server. Done.");
        setTitle();
    }

    public void switchMeOff() {
        final MindMapController mindMapController = getMindMapController();
        // this is not nice, but in the starting phase of the hook, it can't be switched off.
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                togglePermanentHook(mindMapController, MASTER_HOOK_LABEL);

            }
        });
    }

    public void loadFrom(XMLElement pChild) {
        // this plugin should not be saved.
    }

    public void save(XMLElement pXml) {
        // this plugin should not be saved.
        // nothing to do.
    }

    public void shutdownMapHook() {
        deregisterFilter();
        if (mListener != null) {
            signalEndOfSession();
            mListener.commitSuicide();
        }
        try {
            if (mServer != null) {
                mServer.close();
            }
        } catch (IOException e) {
            Resources.getInstance().logException(e);
        }
        mMasterStarted = false;
        super.shutdownMapHook();
    }

    /**
     *
     */
    private void signalEndOfSession() {
        CollaborationGoodbye goodbye = new CollaborationGoodbye();
        goodbye.setUserId(Tools.getUserName());
        synchronized (mConnections) {
            for (int i = 0; i < mConnections.size(); i++) {
                final ServerCommunication serverCommunication = (ServerCommunication) mConnections
                        .elementAt(i);
                try {
                    serverCommunication.send(goodbye);
                    serverCommunication.commitSuicide();
                    serverCommunication.close();
                } catch (IOException e) {
                    Resources.getInstance().logException(e);
                }
            }
            mConnections.clear();
        }
    }

    public void onAddChild(MindMapNode pAddedChildNode) {
    }

    public void onAddChildren(MindMapNode pAddedChild) {
    }

    public void onLostFocusNode(NodeView pNodeView) {
    }

    public void onNewChild(MindMapNode pNewChildNode) {
    }

    public void onRemoveChild(MindMapNode pOldChildNode) {
    }

    public void onRemoveChildren(MindMapNode pOldChildNode, MindMapNode pOldDad) {
    }

    public void onFocusNode(NodeView pNodeView) {
    }

    public void onUpdateChildrenHook(MindMapNode pUpdatedNode) {
    }

    public void onUpdateNodeHook() {
    }

    public void onViewCreatedHook(NodeView pNodeView) {
    }

    public void onViewRemovedHook(NodeView pNodeView) {
    }

    public Integer getRole() {
        return ROLE_MASTER;
    }

    /*
     * (non-Javadoc)
     *
     * @see plugins.collaboration.socket.SocketBasics#getPort()
     */
    public int getPort() {
        return mPort;
    }

    /*
     * (non-Javadoc)
     *
     * @see plugins.collaboration.socket.SocketBasics#lock()
     */
    protected String lock(String pUserName) throws UnableToGetLockException,
            InterruptedException {
        synchronized (mLockMutex) {
            if (mLockEnabled) {
                throw new UnableToGetLockException();
            }
            mLockEnabled = true;
            String lockId = "Lock_" + Math.random();
            mLockId = lockId;
            mLockedAt = System.currentTimeMillis();
            mLockUserName = pUserName;
            logger.info("New lock " + lockId + " by " + mLockUserName);
            return lockId;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * plugins.collaboration.socket.SocketBasics#sendCommand(java.lang.String,
     * java.lang.String, java.lang.String)
     */
    protected void broadcastCommand(String pDoAction, String pUndoAction,
                                    String pLockId) throws Exception {
        synchronized (mConnections) {
            for (int i = 0; i < mConnections.size(); i++) {
                ((ServerCommunication) mConnections.elementAt(i)).sendCommand(
                        pDoAction, pUndoAction, pLockId);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see plugins.collaboration.socket.SocketBasics#unlock()
     */
    protected void unlock() {
        synchronized (mLockMutex) {
            if (!mLockEnabled) {
                throw new IllegalStateException();
            }
            logger.fine("Release lock " + mLockId + " held by " + mLockUserName);
            clearLock();
        }
    }

    public void clearLock() {
        mLockEnabled = false;
        mLockId = "none";
        mLockUserName = null;
    }

    /*
     * (non-Javadoc)
     *
     * @see plugins.collaboration.socket.SocketBasics#shutdown()
     */
    public void shutdown() {
        // TODO Auto-generated method stub

    }

    public String getLockId() {
        synchronized (mLockMutex) {
            if (!mLockEnabled) {
                throw new IllegalStateException();
            }
            return mLockId;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see plugins.collaboration.socket.SocketBasics#getUsers()
     */
    public String getUsers() {
        StringBuffer users = new StringBuffer(Tools.getUserName());
        synchronized (mConnections) {
            for (int i = 0; i < mConnections.size(); i++) {
                users.append(',');
                users.append(' ');
                users.append(((ServerCommunication) mConnections.elementAt(i))
                        .getName());
            }
        }
        return users.toString();
    }

    public CollaborationUserInformation getMasterInformation() {
        CollaborationUserInformation userInfo = new CollaborationUserInformation();
        userInfo.setUserIds(getUsers());
        userInfo.setMasterHostname(Tools.getHostName());
        userInfo.setMasterPort(getPort());
        userInfo.setMasterIp(Tools.getHostIpAsString());
        return userInfo;
    }

    public void processUnfinishedLinks() {
    }

    /* (non-Javadoc)
     * @see freemind.extensions.PermanentNodeHook#saveHtml(java.io.Writer)
     */
    public void saveHtml(Writer pFileout) {
    }

}
