/**
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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.
 *
 * muCommander 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.commons.file.connection;

import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

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

import com.mucommander.commons.file.Credentials;
import com.mucommander.commons.file.FileURL;


/**
 * @see com.mucommander.commons.file.connection.ConnectionHandler
 * @author Maxence Bernard
 */
public class ConnectionPool implements Runnable {
    private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class);

    /** Singleton instance */
    private static ConnectionPool instance = new ConnectionPool();

    /** List of registered ConnectionHandler */
    private final static List<ConnectionHandler> connectionHandlers = new ArrayList<ConnectionHandler>();

    /** The thread that monitors connections, null if there currently is no registered ConnectionHandler */
    private static Thread monitorThread;

    /** Controls how of often the thread monitor checks connections */
    private final static int MONITOR_SLEEP_PERIOD = 1000;

    /** Maximum number of simultaneous connections per realm/credentials combo */
    private final static int MAX_CONNECTIONS_PER_REALM = 4;


    public static ConnectionHandler getConnectionHandler(ConnectionHandlerFactory connectionHandlerFactory, FileURL url, boolean acquireLock) throws InterruptedIOException {
        FileURL realm = url.getRealm();

        while(true) {
            synchronized(connectionHandlers) {      // Ensures that monitor thread is not currently changing the list while we access it
                Credentials urlCredentials = url.getCredentials();
                int matchingConnHandlers = 0;

                // Try and find an appropriate existing ConnectionHandler
                for(ConnectionHandler connHandler : connectionHandlers) {
                	// ConnectionHandler must match the realm and credentials and must not be locked
                	if(connHandler.equals(realm, urlCredentials)) {
                		matchingConnHandlers++;
                		synchronized(connHandler) {     // Ensures that lock remains unchanged while we access/update it
                			if(!connHandler.isLocked()) {
                				// Try to acquire lock if a lock was requested
                				if(!acquireLock || connHandler.acquireLock()) {
                					LOGGER.info("returning ConnectionHandler {}, realm = {}", connHandler, realm);

                					// Update last activity timestamp to now
                					connHandler.updateLastActivityTimestamp();

                					return connHandler;
                				}
                			}
                		}
                	}
                    
                    if(matchingConnHandlers==MAX_CONNECTIONS_PER_REALM) {
                        LOGGER.info("Maximum number of connection per realm reached, waiting for one to be removed or released...");
                        try {
                            // Wait for a ConnectionHandler to be released or removed from the pool
                            connectionHandlers.wait();      // relinquishes the lock on connectionHandlers
                            break;
                        }
                        catch(InterruptedException e) {
                            LOGGER.info("Interrupted while waiting on a connection for {}", url, e);
                            throw new InterruptedIOException();
                        }
                    }
                }

                if(matchingConnHandlers==MAX_CONNECTIONS_PER_REALM)
                    continue;

                // No suitable ConnectionHandler found, create a new one
                ConnectionHandler connHandler = connectionHandlerFactory.createConnectionHandler(url);

                // Acquire lock if a lock was requested
                if(acquireLock)
                    connHandler.acquireLock();

                LOGGER.info("adding new ConnectionHandler {}, realm = {}", connHandler, connHandler.getRealm());

                // Insert new ConnectionHandler at first position as if it has more chances to be accessed again soon
                connectionHandlers.add(0, connHandler);

                // Start monitor thread if it is not currently running (if there previously was no registered ConnectionHandler)
                if(monitorThread==null) {
                    LOGGER.info("starting monitor thread");
                    monitorThread = new Thread(instance);
                    monitorThread.start();
                }

                // Update last activity timestamp to now
                connHandler.updateLastActivityTimestamp();

                return connHandler;
            }
        }
    }


    /**
     * Returns a list of registered ConnectionHandler instances. As the name of this method implies, the returned
     * list is only a snapshot and will not reflect the modifications that are made after this method has been called.
     * The Vector is a cloned one and thus can be safely modified. 
     *
     * @return a list of registered ConnectionHandler instances
     */
    public static List<ConnectionHandler> getConnectionHandlersSnapshot() {
    	synchronized (connectionHandlers) {
    		return new ArrayList<ConnectionHandler>(connectionHandlers);
    	}
    }
    
    /**
     * Called by {@link ConnectionHandler#releaseLock()} to notify the <code>ConnectionHandler</code> that a
     * <code>ConnectionHandler</code> has been released.
     */
    static void notifyConnectionHandlerLockReleased() {
        synchronized(connectionHandlers) {
            // Notify any thread waiting for a ConnectionHandler to be released
            connectionHandlers.notify();
        }
    }

    /**
     * Monitors connections and periodically:
     * <ul>
     *   <li>keeps connections alive
     *   <li>closes and removes connections that have expired
     * </ul>
     */
    public void run() {

        while(monitorThread!=null) {        // Thread will be interrupted by CloseConnectionThread if there are no more ConnectionHandler
            long now = System.currentTimeMillis();

            synchronized(connectionHandlers) {      // Ensures that getConnectionHandler is not currently changing the list while we access it
                for (Iterator<ConnectionHandler> it = connectionHandlers.iterator(); it.hasNext(); ) {
                	ConnectionHandler connHandler = it.next();
                    synchronized(connHandler) {     // Ensures that no one is trying to acquire a lock on the connection while we access it 
                        if(!connHandler.isLocked()) {   // Do not touch ConnectionHandler if it is currently locked

                            // Remove ConnectionHandler instance from the list of registered ConnectionHandler
                            // if it is not connected
                            if(!connHandler.isConnected()) {
                                LOGGER.info("Removing unconnected ConnectionHandler {}", connHandler);

                                it.remove();
                                // Notify any thread waiting for a ConnectionHandler to be released
                                connectionHandlers.notify();

                                continue;       // Skips close on inactivity and keep alive checks
                            }

                            long lastUsed = connHandler.getLastActivityTimestamp();

                            // If time-to-live has been reached without any connection activity, remove ConnectionHandler
                            // from the list of registered ConnectionHandler and close the connection in a separate thread
                            long closePeriod = connHandler.getCloseOnInactivityPeriod();
                            if(closePeriod!=-1 && now-lastUsed>closePeriod*1000) {
                                LOGGER.info("Removing timed-out ConnectionHandler {}",connHandler);

                                it.remove();
                                // Notify any thread waiting for a ConnectionHandler to be released
                                connectionHandlers.notify();

                                // Close connection in a separate thread as it could lock this thread
                                new CloseConnectionThread(connHandler).start();

                                continue;       // Skips keep alive check
                            }

                            // If keep-alive period has been reached without any connection activity or a keep alive,
                            // keep connection alive in a separate thread
                            long keepAlivePeriod = connHandler.getKeepAlivePeriod(); 
                            if(keepAlivePeriod!=-1 && now-Math.max(lastUsed, connHandler.getLastKeepAliveTimestamp())>keepAlivePeriod*1000) {
                                // Update last keep alive timestamp to now
                                connHandler.updateLastKeepAliveTimestamp();

                                // Keep connection alive in a separate thread as it could lock this thread
                                new KeepAliveConnectionThread(connHandler).start();
                            }
                        }
                    }
                }

                // Stop monitor thread if there are no more ConnectionHandler
                if(connectionHandlers.isEmpty()) {
                    LOGGER.info("No more ConnectionHandler, stopping monitor thread");
                    monitorThread = null;
                }
            }

            // Sleep for MONITOR_SLEEP_PERIOD milliseconds, minus the processing time of this loop
            try {
                Thread.sleep(Math.max(0, MONITOR_SLEEP_PERIOD-(System.currentTimeMillis()-now)));
            }
            catch(InterruptedException e) {
                // Will loop again
            }
        }
    }


    /**
     * Closes a specified ConnectionHandler's connection in a separate thread and removes the ConnectionHandler from
     * the list of registered ConnectionHandler instances.
     */
    private static class CloseConnectionThread extends Thread {

        private ConnectionHandler connHandler;

        private CloseConnectionThread(ConnectionHandler connHandler) {
            this.connHandler = connHandler;
        }

        @Override
        public void run() {
            // Try to close connection, only if it is connected
            if (connHandler.isConnected()) {
                LOGGER.info("Closing connection held by {}", connHandler);
                connHandler.closeConnection();
            }
        }
    }


    /**
     * Keeps alive a specified ConnectionHandler's connection in a separate thread. If the connection is not currently
     * active, {@link com.mucommander.commons.file.connection.ConnectionHandler#keepAlive()} will not be called.
     */
    private static class KeepAliveConnectionThread extends Thread {

        private final ConnectionHandler connHandler;

        private KeepAliveConnectionThread(ConnectionHandler connHandler) {
            this.connHandler = connHandler;
        }

        @Override
        public void run() {
            LOGGER.info("keeping connection alive: {}", connHandler);

            synchronized(connHandler) {
                // Ensures that lock was not grabbed in the meantime
                if(connHandler.isLocked())
                    return;

                // Keep alive connection, only if it is connected
                if(connHandler.isConnected())
                    connHandler.keepAlive();
            }
        }
    }

}
