/*
 * This file is part of the Vamsas Client version 0.2. 
 * Copyright 2010 by Jim Procter, Iain Milne, Pierre Marguerite, 
 *  Andrew Waterhouse and Dominik Lindner.
 * 
 * Earlier versions have also been incorporated into Jalview version 2.4 
 * since 2008, and TOPALi version 2 since 2007.
 * 
 * The Vamsas Client is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * The Vamsas Client 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with the Vamsas Client.  If not, see <http://www.gnu.org/licenses/>.
 */
package uk.ac.vamsas.client.picking;

import java.net.*;
import java.util.*;
import java.util.logging.*;

import org.apache.commons.logging.Log;

/**
 * Concrete implementation of the IPickManager interface that uses sockets for
 * message communication. An instance of this class attempts to run the central
 * server for other clients; failing that, it attempts to connect to an existing
 * server instead.
 */
public class SocketManager implements IPickManager {
  private Log logger = org.apache.commons.logging.LogFactory
      .getLog(uk.ac.vamsas.client.picking.SocketManager.class);

  // Maintains a list of client communication objects - each object represents
  // a way of talking to either:
  // the server - if this is client side (and in which case, the list will only
  // contain one element
  // the other clients - if this is server side
  private LinkedList clients;

  private PickServer server;

  private IMessageHandler msgHandler;

  private boolean isRunning = true;

  /**
   * Constructs a new PickManager. This method will return immediately, while a
   * looping thread runs that attempts to run the server or connect to an
   * existing server.
   */
  public SocketManager() {
    // logger.setLevel(Level.OFF);

    server = new PickServer(this);
    clients = new LinkedList();

    new InitializeThread().start();
  }

  /**
   * Registers a message handler with the manager that allows the manager to
   * perform a method callback on that object whenever a message is received.
   * 
   * @param handler
   *          the message handler to register
   */
  public void registerMessageHandler(IMessageHandler handler) {
    msgHandler = handler;
  }

  /**
   * Attempts to establish a connection between two client endpoints. This
   * method is called in two ways: 1) by the server when it receives a remote
   * request (in which case the socket will already be established) and 2) by a
   * client that is attempting to connect *to* the server.
   * 
   * @param socket
   *          a socket endpoint for the connection
   * @return true if the connection is successfully, false otherwise
   */
  synchronized boolean addEndPoint(Socket socket) {
    PickEndPoint client = new PickEndPoint(this, socket);

    if (client.openConnection()) {
      clients.add(client);
      // logger.info("List now contains " + clients.size() + " client(s)");
      return true;
    }

    return false;
  }

  /**
   * Sends a message to other clients.
   * 
   * @param message
   *          the message to send
   */
  public void sendMessage(Message message) {
    forwardMessage(null, message);
  }

  /**
   * Forwards (or sends) a message. When the server (client A) receives a
   * message from client B, it must also forward it to clients C and D (etc),
   * but mustn't forward it *back* to client B.
   * 
   * @param origin
   *          the client endpoint that received the message (will be null if the
   *          message originates from this instance
   * @param message
   *          the message to send
   */
  private void forwardMessage(PickEndPoint origin, Message message) {
    for (int i = clients.size() - 1; i >= 0; i--) {
      try {
        PickEndPoint client = (PickEndPoint) clients.get(i);
        if (client != origin)
          client.send(message);
      } catch (Exception e) {
        System.out.println("FORWARD: " + e);
      }
    }
  }

  /**
   * Handles a received message. If the manager is running in server mode, then
   * it must ensure the message is also forwarded to the other clients.
   * 
   * @param origin
   *          the client endpoint that received the message
   * @message the message that was received
   */
  void processMessage(PickEndPoint origin, Message message) {
    if (server.isServer())
      forwardMessage(origin, message);

    if (msgHandler != null)
      msgHandler.handleMessage(message);
    // else
    // logger.info("No handler available to deal with incoming message");
  }

  /**
   * Removes a client connection from the list when its connection is no longer
   * valid.
   * 
   * @param client
   *          the client endpoint to remove
   */
  synchronized void removeEndPoint(PickEndPoint client) {
    clients.remove(client);
    // logger.info("List now contains " + clients.size() + " client(s)");

    // If there's no endpoints left, then we've lost all connections and
    // need to reinitialize - but only if we've not been told to stop
    if (clients.size() == 0 && isRunning)
      new InitializeThread().start();
  }

  /**
   * Thread extension class to handle the actual initialization
   */
  private class InitializeThread extends Thread {
    public void run() {
      logger.debug("Initializing connection...");
      boolean connected = false;

      // Loop until we can get a connection (one way or the other)
      while (!connected && isRunning) {
        // Sleep for a rnd time so we don't end up with all the VAMSAS
        // apps trying to initialize servers at the same time
        try {
          Thread.sleep((int) (10 * Math.random()));
        } catch (InterruptedException e) {
        }

        // Attempt to open the server port...
        if (server.isServer() || server.createServer())
          connected = true;

        // If it fails, then attempt to make a client connection...
        else if (addEndPoint(null))
          connected = true;
      }
      logger.debug("Completed initializing connection.");
    }
  }

  public void shutdown() {
    logger.debug("Shutting down socket manager.");
    if (server == null)
      throw new Error(
          "Client Implementation Error: shutdown() called on uninitialized SocketManager.");

    isRunning = false;

    if (server.isServer())
      server.terminate();

    while (clients.size() > 0) {
      logger.debug("Closing endpoint.");
      ((PickEndPoint) clients.getFirst()).terminate();
    }
    logger.debug("Shutdown of socketmanager completed.");
  }
}
