//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library 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 2.1 of the License, or
// (at your option) any later version.
//
// This library 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 library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.presents.server.net;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.io.EOFException;
import java.io.IOException;

import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

import com.threerings.io.FramedInputStream;
import com.threerings.io.FramingOutputStream;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;

import com.threerings.presents.net.Message;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.util.DatagramSequencer;

import static com.threerings.presents.Log.log;

/**
 * The base connection class implements the net event handler interface and processes raw incoming
 * network data into a stream of parsed {@link Message} objects. It also provides the means to send
 * messages to the client and facilities for checking delinquency.
 */
public class Connection implements NetEventHandler
{
    /** Used with {@link Connection#setMessageHandler}. */
    public static interface MessageHandler {
        /** Called when a complete message has been parsed from incoming network data. */
        void handleMessage (Message message);
    }

    /** The key used by the NIO code to track this connection. */
    public SelectionKey selkey;

    /**
     * Initializes a connection object with a socket and related info.
     *
     * @param cmgr The connection manager with which this connection is associated.
     * @param channel The socket channel from which we'll be reading messages.
     * @param createStamp The time at which this connection was created.
     */
    public void init (ConnectionManager cmgr, SocketChannel channel, long createStamp)
        throws IOException
    {
        _cmgr = cmgr;
        _channel = channel;
        _lastEvent = createStamp;
        _connectionId = ++_lastConnectionId;
    }

    /**
     * Instructs the connection to pass parsed messages on to this handler for processing. This
     * should be done before the connection is turned loose to process messages.
     */
    public void setMessageHandler (MessageHandler handler)
    {
        _handler = handler;
    }

    /**
     * Configures this connection with a custom class loader.
     */
    public void setClassLoader (ClassLoader loader)
    {
        _loader = loader;
        if (_oin != null) {
            _oin.setClassLoader(loader);
        }
    }

    /**
     * Returns the connection's unique identifier.
     */
    public int getConnectionId ()
    {
        return _connectionId;
    }

    /**
     * Returns the non-blocking socket object used to construct this connection.
     */
    public SocketChannel getChannel ()
    {
        return _channel;
    }

    /**
     * Returns the address associated with this connection or null if it has no underlying socket
     * channel.
     */
    public InetAddress getInetAddress ()
    {
        return (_channel == null) ? null : _channel.socket().getInetAddress();
    }

    /**
     * Sets whether we should transmit datagrams.
     */
    public void setTransmitDatagrams (boolean transmit)
    {
        _transmitDatagrams = transmit;
    }

    /**
     * Checks whether we should transmit datagrams.
     */
    public boolean getTransmitDatagrams ()
    {
        return _transmitDatagrams;
    }

    /**
     * Returns the address to which datagrams should be sent or null if no datagram address has
     * been established.
     */
    public InetSocketAddress getDatagramAddress ()
    {
        return _datagramAddress;
    }

    /**
     * Sets the secret string used to authenticate datagrams from the client.
     */
    public void setDatagramSecret (String secret)
    {
        try {
            _datagramSecret = secret.getBytes("UTF-8");
        } catch (Exception e) {
            _datagramSecret = new byte[0]; // shouldn't happen
        }
    }

    /**
     * Returns true if this connection is closed.
     */
    public boolean isClosed ()
    {
        return (_channel == null);
    }

    /**
     * Closes this connection and unregisters it from the connection manager. This should only be
     * called from the conmgr thread.
     */
    public void close ()
    {
        // we shouldn't be closed twice
        if (isClosed()) {
            log.warning("Attempted to re-close connection " + this + ".", new Exception());
            return;
        }

        // unregister from the select set
        _cmgr.connectionClosed(this);

        // close our socket
        closeSocket();
    }

    /**
     * Queues up a request to have this connection closed by the connection manager once all
     * messages in its queue have been written to its target.
     */
    public void asyncClose ()
    {
        _cmgr.postAsyncClose(this);
    }

    /**
     * Posts a message for delivery to this connection. The message will be delivered by the conmgr
     * thread as soon as it gets to it.
     */
    public void postMessage (Message msg)
    {
        // pass this along to the connection manager
        _cmgr.postMessage(this, msg);
    }

    /**
     * Called when an outgoing socket experiences a connect failure. The connection manager will
     * have cleaned up the partial registration needed during the connect process, so we are only
     * responsible for closing our socket.
     */
    public void connectFailure (IOException ioe)
    {
        closeSocket();
    }

    /**
     * Called when there is a failure reading or writing to this connection. We notify the
     * connection manager and close ourselves down.
     */
    public void networkFailure (IOException ioe)
    {
        // if we're already closed, then something is seriously funny
        if (isClosed()) {
            log.warning("Failure reported on closed connection " + this + ".", new Exception());
            return;
        }

        // let the connection manager know we're hosed
        _cmgr.connectionFailed(this, ioe);

        // and close our socket
        closeSocket();
    }

    /**
     * Processes a datagram sent to this connection.
     */
    public void handleDatagram (InetSocketAddress source, ByteBuffer buf, long when)
    {
        // lazily create our various bits and bobs
        if (_digest == null) {
            try {
                _digest = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException nsae) {
                log.warning("Missing MD5 algorithm.");
                return;
            }
            _sequencer = _cmgr.createDatagramSequencer();
        }

        // verify the hash
        buf.position(12);
        _digest.update(buf);
        byte[] hash = _digest.digest(_datagramSecret);
        buf.position(4);
        for (int ii = 0; ii < 8; ii++) {
            if (hash[ii] != buf.get()) {
                log.warning("Datagram failed hash check", "id", _connectionId, "source", source);
                return;
            }
        }

        // update our target address
        _datagramAddress = source;

        // read the contents through the sequencer
        try {
            Message msg = _sequencer.readDatagram();
            if (msg == null) {
                return; // received out of order
            }
            msg.received = when;
            _handler.handleMessage(msg);

        } catch (ClassNotFoundException cnfe) {
            log.warning("Error reading datagram", "error", cnfe);

        } catch (IOException ioe) {
            log.warning("Error reading datagram", "error", ioe);
        }
    }

    // from interface NetEventHandler
    public int handleEvent (long when)
    {
        // make a note that we received an event as of this time
        _lastEvent = when;

        int bytesIn = 0;
        try {
            // we're lazy about creating our input streams because we may be inheriting them from
            // our authing connection and we don't want to unnecessarily create them in that case
            if (_fin == null) {
                _fin = new FramedInputStream();
                _oin = new ObjectInputStream(_fin);
                if (_loader != null) {
                    _oin.setClassLoader(_loader);
                }
            }

            // there may be more than one frame in the buffer, so we keep reading them until we run
            // out of data
            while (_fin.readFrame(_channel)) {
                // make a note of how many bytes are in this frame (including the frame length
                // bytes which aren't reported in available())
                bytesIn = _fin.available() + 4;
                // parse the message and pass it on
                Message msg = (Message)_oin.readObject();
                msg.received = when;
//                 Log.info("Read message " + msg + ".");
                _handler.handleMessage(msg);
            }

        } catch (EOFException eofe) {
            // close down the socket gracefully
            close();

        } catch (ClassNotFoundException cnfe) {
            log.warning("Error reading message from socket", "channel", _channel, "error", cnfe);
            // deal with the failure
            String errmsg = "Unable to decode incoming message.";
            networkFailure((IOException) new IOException(errmsg).initCause(cnfe));

        } catch (IOException ioe) {
            // don't log a warning for the ever-popular "the client dropped the connection" failure
            String msg = ioe.getMessage();
            if (msg == null || msg.indexOf("reset by peer") == -1) {
                log.warning("Error reading message from socket", "channel", _channel, "error", ioe);
            }
            // deal with the failure
            networkFailure(ioe);
        }

        return bytesIn;
    }

    // from interface NetEventHandler
    public boolean checkIdle (long now)
    {
        long idleMillis = now - _lastEvent;
        if (idleMillis < PingRequest.PING_INTERVAL + LATENCY_GRACE) {
            return false;
        }
        if (isClosed()) {
            return true;
        }
        log.info("Disconnecting non-communicative client", "conn", this, "idle", idleMillis + "ms");
        return true;
    }

    // from interface NetEventHandler
    public void becameIdle ()
    {
        _cmgr.closeConnection(this);
    }

    @Override // from Object
    public String toString ()
    {
        return "[id=" + (hashCode() % 1000) + ", addr=" + getInetAddress() + "]";
    }

    /**
     * Returns the object input stream associated with this connection.  This should only be used
     * by the connection manager.
     */
    protected ObjectInputStream getObjectInputStream ()
    {
        return _oin;
    }

    /**
     * Instructs this connection to inherit its streams from the supplied connection object. This
     * is called by the connection manager when the time comes to pass streams from the authing
     * connection to the running connection.
     */
    protected void inheritStreams (Connection other)
    {
        _fin = other._fin;
        _oin = other._oin;
        _oout = other._oout;
        if (_loader != null) {
            _oin.setClassLoader(_loader);
        }
    }

    /**
     * Returns the object output stream associated with this connection (creating it if
     * necessary). This should only be used by the connection manager.
     */
    protected ObjectOutputStream getObjectOutputStream (FramingOutputStream fout)
    {
        // we're lazy about creating our output stream because we may be inheriting it from our
        // authing connection and we don't want to unnecessarily create it in that case
        if (_oout == null) {
            _oout = new ObjectOutputStream(fout);
        }
        return _oout;
    }

    /**
     * Sets the object output stream used by this connection. This should only be called by the
     * connection manager.
     */
    protected void setObjectOutputStream (ObjectOutputStream oout)
    {
        _oout = oout;
    }

    /**
     * Returns a reference to the connection's datagram sequencer.  This should only be called by
     * the connection manager.
     */
    protected DatagramSequencer getDatagramSequencer ()
    {
        return _sequencer;
    }

    /**
     * Closes the socket associated with this connection. This happens when we receive EOF, are
     * requested to close down or when our connection fails.
     */
    protected void closeSocket ()
    {
        if (_channel == null) {
            return;
        }

        log.debug("Closing channel " + this + ".");
        try {
            _channel.close();
        } catch (IOException ioe) {
            log.warning("Error closing connection", "conn", this, "error", ioe);
        }

        // clear out our references to prevent repeat closings
        _channel = null;
    }

    protected ConnectionManager _cmgr;
    protected SocketChannel _channel;

    protected long _lastEvent;

    protected int _connectionId;

    protected FramedInputStream _fin;
    protected ObjectInputStream _oin;
    protected ObjectOutputStream _oout;

    protected InetSocketAddress _datagramAddress;
    protected byte[] _datagramSecret;
    protected boolean _transmitDatagrams;

    protected MessageDigest _digest;
    protected DatagramSequencer _sequencer;

    protected MessageHandler _handler;
    protected ClassLoader _loader;

    /** The last connection id assigned. */
    protected static int _lastConnectionId;

    /** The number of milliseconds beyond the ping interval that we allow a client's network
     * connection to be idle before we forcibly disconnect them. */
    protected static final long LATENCY_GRACE = 30 * 1000L;
}
