/**
 * 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 com.mucommander.commons.file.AuthException;
import com.mucommander.commons.file.Credentials;
import com.mucommander.commons.file.FileURL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * ConnectionHandler is a an abstract class that provides the basic operations for to interact with a server: establish
 * the connection, keep it alive and close it.
 *
 * @see com.mucommander.commons.file.connection.ConnectionPool
 * @author Maxence Bernard
 */
public abstract class ConnectionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionHandler.class);

    /** URL of the server this ConnectionHandler connects to */
    protected FileURL realm;

    /** Credentials that are used to connect to the server */
    protected Credentials credentials;

    /** True if this ConnectionHandler is currently locked */
    protected boolean isLocked;

    /** Time at which the connection managed by this ConnectionHandler was last used */
    protected long lastActivityTimestamp;

    /** Time at which the connection managed by this ConnectionHandler was last kept alive */
    protected long lastKeepAliveTimestamp;

    /** Number of seconds of inactivity after which this ConnectionHandler's connection will be closed by ConnectionPool */
    protected long closeOnInactivityPeriod = DEFAULT_CLOSE_ON_INACTIVITY_PERIOD;

    /** Number of seconds of inactivity after which this ConnectionHandler's connection will be kept alive by ConnectionPool */
    protected long keepAlivePeriod = DEFAULT_KEEP_ALIVE_PERIOD;

    /** Default 'close on inactivity' period */
    private final static long DEFAULT_CLOSE_ON_INACTIVITY_PERIOD = 300;

    /** Default keep alive period (-1, keep alive disabled) */
    private final static long DEFAULT_KEEP_ALIVE_PERIOD = -1;


    /**
     * Creates a new ConnectionHandler for the given server URL using the Credentials included in the URL (potentially
     * <code>null</code>).
     *
     * @param serverURL URL of the server to connect to
     */
    public ConnectionHandler(FileURL serverURL) {
        realm = serverURL.getRealm();
        this.credentials = serverURL.getCredentials();
    }


    /**
     * Returns the URL of the server this ConnectionHandler connects to.
     *
     * @return the URL of the server this ConnectionHandler connects to
     */
    public FileURL getRealm() {
        return realm;
    }


    /**
     * Returns the Credentials that are used to connect to the server, <code>null</code> if no credentials are used.
     *
     * @return the Credentials that are used to connect to the server, <code>null</code> if no credentials are used
     */
    public Credentials getCredentials() {
        return credentials;
    }


    /**
     * Checks if the connection is currently active (as returned by {@link #isConnected()} and if it isn't, starts it
     * by calling {@link #startConnection()}. Returns true if the connection was properly started, false if the
     * connection was already active, or throws an IOException if the connection could not be started.
     *
     * @return Returns true if the connection was properly started, false if the connection was already active
     * @throws IOException if the connection could not be started
     */
    public boolean checkConnection() throws IOException {
        if(!isConnected()) {
            LOGGER.info("not connected, starting connection, this="+this);
            startConnection();
            return true;
        }

        return false;
    }


    /**
     * Tries to lock this ConnectionHandler and returns true if it could be locked, false if it is already locked.
     *
     * @return true if it could be locked, false if it is already locked.
     */
    public synchronized boolean acquireLock() {
        if(isLocked) {
            LOGGER.info("!!!!! acquireLock() returning false, should not happen !!!!!", new Throwable());
            return false;
        }

        isLocked = true;
        return true;
    }

    /**
     * Tries to release the lock on this ConnectionHandler and returns true if it could be locked, false if it
     * is not locked.
     *
     * @return true if it could be locked, false if it is not locked
     */
    public boolean releaseLock() {
        synchronized(this) {
            if(!isLocked) {
                LOGGER.info("!!!!! releaseLock() returning false, should not happen !!!!!", new Throwable());
                return false;
            }

            isLocked = false;
        }

        ConnectionPool.notifyConnectionHandlerLockReleased();

        return true;
    }

    /**
     * Returns <code>true</code> if this ConnectionHandler is currently locked.
     *
     * @return <code>true</code> if this ConnectionHandler is currently locked
     */
    public synchronized boolean isLocked() {
        return isLocked;
    }


    /**
     * Updates the time at which the connection managed by this ConnectionHandler was last used to now (current time).
     */
    public void updateLastActivityTimestamp() {
        lastActivityTimestamp = System.currentTimeMillis();
    }

    /**
     * Returns the time at which the connection managed by this ConnectionHandler was last used.
     *
     * @return the time at which the connection managed by this ConnectionHandler was last used
     */
    public long getLastActivityTimestamp() {
        return lastActivityTimestamp;
    }


    /**
     * Updates the time at which the connection managed by this ConnectionHandler was last kept alive to now (current time).
     */
    public void updateLastKeepAliveTimestamp() {
        lastKeepAliveTimestamp = System.currentTimeMillis();
    }

    /**
     * Returns the time at which the connection managed by this ConnectionHandler was last kept alive.
     *
     * @return the time at which the connection managed by this ConnectionHandler was last kept alive
     */
    public long getLastKeepAliveTimestamp() {
        return lastKeepAliveTimestamp;
    }


    /**
     * Returns the number of seconds of inactivity after which {@link ConnectionPool} will close the connection by
     * calling {@link #closeConnection()}, <code>-1</code> to indicate that the connection should not be automatically
     * closed.
     *
     * <p>By default, this value is 300 seconds (5 minutes).</p>
     *
     * @return the number of seconds of inactivity after which {@link ConnectionPool} will close the connection,
     * <code>-1</code> to indicate that the connection should not be automatically closed
     */
    public long getCloseOnInactivityPeriod() {
        return closeOnInactivityPeriod;
    }

    /**
     * Sets the number of seconds of inactivity after which {@link ConnectionPool} will close the connection by calling
     * {@link #closeConnection()}, <code>-1</code> to prevent the connection from being automatically closed.
     *
     * <p>By default, this value is 300 seconds (5 minutes).</p>
     *
     * @param nbSeconds the number of seconds of inactivity after which {@link ConnectionPool} will close the connection,
     * <code>-1</code> to indicate that the connection should not be automatically closed
     */
    public void setCloseOnInactivityPeriod(long nbSeconds) {
        closeOnInactivityPeriod = nbSeconds;
    }


    /**
     * Returns the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection alive
     * by calling {@link #keepAlive()}, <code>-1</code> to indicate that this connection should not be kept alive.
     *
     * <p>By default, this value is -1 (keep alive disabled).</p>
     *
     * @return the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection alive
     * by calling {@link #keepAlive()}, <code>-1</code> to indicate that this connection should not be kept alive
     */
    public long getKeepAlivePeriod() {
        return keepAlivePeriod;
    }

    /**
     * Returns the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection alive
     * by calling {@link #keepAlive()}, <code>-1</code> to indicate that this connection should not be kept alive.
     *
     * <p>By default, this value is -1 (keep alive disabled).</p>
     *
     * @param nbSeconds the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection
     * alive by calling {@link #keepAlive()}, <code>-1</code> to indicate that this connection should not be kept alive
     */
    public void setKeepAlivePeriod(long nbSeconds) {
        keepAlivePeriod = nbSeconds;
    }


    /**
     * Returns <code>true</code> if the given Object is a ConnectionHandler whose realm and credentials are equal to
     * those of this ConnectionHandler. The credentials comparison is password-sensitive.
     *
     * @param o the Object to compare for equality
     * @see Credentials#equals(Object, boolean)
     */
    public boolean equals(Object o) {
        if(o==null || !(o instanceof ConnectionHandler))
            return false;

        ConnectionHandler connHandler = (ConnectionHandler)o;

        return equals(connHandler.realm, connHandler.credentials);
    }


    /**
     * Returns <code>true</code> if both the given realm and credentials are equal to those of this ConnectionHandler.
     * The credentials comparison is password-sensitive.
     *
     * @param realm the FileURL to compare against this ConnectionHandler's
     * @param credentials the Credentials to compare against this ConnectionHandler's
     * @return true if both the given realm and credentials are equal to those of this ConnectionHandler
     * @see Credentials#equals(Object, boolean)
     */
    public boolean equals(FileURL realm, Credentials credentials) {

        if(!this.realm.equals(realm, false, true))
            return false;

        // Compare credentials. One or both Credentials instances may be null.

        // Note: Credentials.equals() considers null as equal to empty Credentials (see Credentials#isEmpty())
        return (this.credentials==null && credentials==null)
            || (this.credentials!=null && this.credentials.equals(credentials, true))
            || (credentials!=null && credentials.equals(this.credentials, true));
    }


    /**
     * Throws an {@link AuthException} using this connection handler's realm, credentials and the message passed as
     * an argument (can be <code>null</code>). The FileURL instance representing the realm that is used to create
     * the <code>AuthException</code> is a clone of this realm, making it safe for modification.
     *
     * @param message the message to pass to AuthException's constructor, can be <code>null</code>
     * @throws AuthException always throws the created AuthException
     */
    public void throwAuthException(String message) throws AuthException {
        FileURL clonedRealm = (FileURL)realm.clone();
        clonedRealm.setCredentials(credentials);

        throw new AuthException(clonedRealm, message);
    }


    //////////////////////
    // Abstract methods //
    //////////////////////

    /**
     * Starts the connection managed by this ConnectionHandler, and throws an IOException if the connection could not
     * be established. This method may be called several times during the life of this ConnectionHandler, if the
     * connection dropped and must be re-established.
     *
     * @throws IOException if an error occurred while trying to establish the connection
     * @throws AuthException if an authentication error occurred (incorrect login or password, insufficient privileges...)
     */
    public abstract void startConnection() throws IOException, AuthException;

    /**
     * Returns <code>true</code> if the connection managed by this ConnectionHandler is currently active/established,
     * in a state that makes it possible to serve client requests.
     *
     * <p>Implementation note: This method must not perform any I/O which could block the calling thread.</p>
     *
     * @return <code>true</code> if the connection managed by this ConnectionHandler is currently active/established
     */
    public abstract boolean isConnected();

    /**
     * Closes the connection managed by this ConnectionHandler.
     *
     * <p>Implementation note: the implementation must guarantee that any calls to {@link #isConnected()} after this
     * method has been called return false.</p>
     */
    public abstract void closeConnection();

    /**
     * Keeps this connection alive.
     *
     * <p>Implementation note: if keep alive is not available in the underlying protocol or
     * simply unnecessary, this method should be implemented as a no-op (do nothing).</p>
     */
    public abstract void keepAlive();
}
