//
// $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.client {

import flash.errors.IOError;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.events.TimerEvent;

import flash.net.Socket;
import flash.utils.ByteArray;
import flash.utils.Endian;
import flash.utils.Timer;

import com.threerings.util.Log;

import com.threerings.io.FrameAvailableEvent;
import com.threerings.io.FrameReader;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;

import com.threerings.presents.data.AuthCodes;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.DownstreamMessage;
import com.threerings.presents.net.LogoffRequest;
import com.threerings.presents.net.UpstreamMessage;
import com.threerings.presents.net.ThrottleUpdatedMessage;

public class Communicator
{
    public function Communicator (client :Client)
    {
        _client = client;
    }

    public function logon () :void
    {
        // create our input/output business
        _outBuffer = new ByteArray();
        _outBuffer.endian = Endian.BIG_ENDIAN;
        _outStream = new ObjectOutputStream(_outBuffer);
        _inStream = new ObjectInputStream(null, { invDir: _client.getInvocationDirector() });

        attemptLogon(0);
    }

    public function logoff () :void
    {
        if (_socket == null) {
            return;
        }
        postMessage(new LogoffRequest());
    }

    public function postMessage (msg :UpstreamMessage) :void
    {
        _outq.push(msg);
        if (_writer != null) {
            sendPendingMessages(null);

        } else {
            log.warning("Posting message prior to opening socket", "msg", msg);
        }
    }

    /**
     * Detects if the communicator currently has an open channel to the server. This is guaranteed
     * to return false while the logoff event is being dispatched. It also returns true during
     * authentication, but prior to the client appearing to be logged on.
     */
    public function isConnected () :Boolean
    {
        return _writer != null;
    }

    /**
     * Detects is the communicator has a logoff request pending, i.e. will logoff in the near
     * future.
     */
    public function hasPendingLogoff () :Boolean
    {
        // check for a logoff message
        for each (var message :UpstreamMessage in _outq) {
            if (message is LogoffRequest) {
                return true;
            }
        }
        return false;
    }

    /**
     * Attempts to logon on using the port at the specified index.
     */
    protected function attemptLogon (portIdx :int) :Boolean
    {
        var ports :Array = _client.getPorts();
        _portIdx = portIdx; // note the port we're about to try
        if (_portIdx >= ports.length) {
            return false;
        }
        if (_portIdx != 0) {
            _client.reportLogonTribulations(new LogonError(AuthCodes.TRYING_NEXT_PORT, true));
            removeListeners();
        }

        // create the socket and set up listeners
        _socket = new Socket();
        _socket.endian = Endian.BIG_ENDIAN;
        _socket.addEventListener(Event.CONNECT, socketOpened);
        _socket.addEventListener(IOErrorEvent.IO_ERROR, socketError);
        _socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socketError);
        _socket.addEventListener(Event.CLOSE, socketClosed);

        _frameReader = new FrameReader(_socket);
        _frameReader.addEventListener(FrameAvailableEvent.FRAME_AVAILABLE, inputFrameReceived);

        var host :String = _client.getHostname();
        var pport :int = ports[0];
        var ppidx :int = Math.max(0, ports.indexOf(pport));
        var port :int = (ports[(_portIdx + ppidx) % ports.length] as int);

        log.info("Connecting", "host", host, "port", port, "svcs", _client.getBootGroups());
        _socket.connect(host, port);

        return true;
    }

    protected function removeListeners () :void
    {
        _socket.removeEventListener(Event.CONNECT, socketOpened);
        _socket.removeEventListener(IOErrorEvent.IO_ERROR, socketError);
        _socket.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, socketError);
        _socket.removeEventListener(Event.CLOSE, socketClosed);

        _frameReader.removeEventListener(FrameAvailableEvent.FRAME_AVAILABLE, inputFrameReceived);
        _frameReader.shutdown();
    }

    protected function shutdown (logonError :Error) :void
    {
        if (_socket != null)  {
            if (_socket.connected) {
                try {
                    _socket.close();
                } catch (err :Error) {
                    log.warning("Error closing failed socket", err);
                }
            }
            removeListeners();
            _socket = null;
            _outStream = null;
            _inStream = null;
            _frameReader = null;
            _outBuffer = null;
        }

        if (_writer != null) {
            _writer.stop();
            _writer = null;
        }

        // if we never got our client object, we never dispatched a DID_LOGON and therefore we
        // don't want to dispatch a DID_LOGOFF, the observers basically never know anything ever
        // happened
        if (_client.getClientObject() != null) {
            _client.notifyObservers(ClientEvent.CLIENT_DID_LOGOFF, null);
        }
        _client.cleanup(logonError);
    }

    /**
     * Sends all pending messages from our outgoing message queue. If we hit our throttle while
     * sending, we stop and wait for the next time around when we'll try sending them again.
     */
    protected function sendPendingMessages (event :TimerEvent) :void
    {
        while (_outq.length > 0) {
            // if we've exceeded our throttle, stop for now
            if (_client.getOutgoingMessageThrottle().throttleOp()) {
                if (!_notedThrottle) {
                    log.info("Throttling outgoing messages", "queue", _outq.length,
                             "throttle", _client.getOutgoingMessageThrottle());
                    _notedThrottle = true;
                }
                return;
            }
            _notedThrottle = false;

            // grab the next message from the queue and send it
            var msg :UpstreamMessage = (_outq.shift() as UpstreamMessage);
            sendMessage(msg);

            // if this was a logoff request, shutdown
            if (msg is LogoffRequest) {
                shutdown(null);
            } else if (msg is ThrottleUpdatedMessage) {
                _client.finalizeOutgoingMessageThrottle(ThrottleUpdatedMessage(msg).messagesPerSec);
            }
        }
    }

    /**
     * Writes a single message to our outgoing socket.
     */
    protected function sendMessage (msg :UpstreamMessage) :void
    {
        if (_outStream == null) {
            log.warning("No socket, dropping msg", "msg", msg);
            return;
        }

        // write the message (ends up in _outBuffer)
        _outStream.writeObject(msg);

//         Log.debug("outBuffer: " + StringUtil.unhexlate(_outBuffer));

        // Frame it by writing the length, then the bytes.
        // We add 4 to the length, because the length is of the entire frame
        // including the 4 bytes used to encode the length!
        _socket.writeInt(_outBuffer.length + 4);
        _socket.writeBytes(_outBuffer);
        _socket.flush();

        // clean up the output buffer
        _outBuffer.length = 0;
        _outBuffer.position = 0;

        // make a note of our most recent write time
        updateWriteStamp();
    }

    /**
     * Called when a frame of data from the server is ready to be decoded into a DownstreamMessage.
     */
    protected function inputFrameReceived (event :FrameAvailableEvent) :void
    {
        // convert the frame data into a message from the server
        var frameData :ByteArray = event.getFrameData();
        _inStream.setSource(frameData);
        var msg :DownstreamMessage;
        try {
            msg = (_inStream.readObject(DownstreamMessage) as DownstreamMessage);
        } catch (e :Error) {
            log.warning("Error processing downstream message", e);
            return;
        }

        if (frameData.bytesAvailable > 0) {
            log.warning("Beans! We didn't fully read a frame, is there a bug in streaming code?",
                "bytesLeftOver", frameData.bytesAvailable, "msg", msg);
        }

        if (_omgr != null) {
            // if we're logged on, then just do the normal thing
            _omgr.processMessage(msg);
            return;
        }

        // otherwise, this would be the AuthResponse to our logon attempt
        var rsp :AuthResponse = (msg as AuthResponse);
        var data :AuthResponseData = rsp.getData();
        if (data.code !== AuthResponseData.SUCCESS) {
            shutdown(new Error(data.code));
            return;
        }

        // logon success
        _omgr = new ClientDObjectMgr(this, _client);
        _client.setAuthResponseData(data);
    }

    /**
     * Called when the connection to the server was successfully opened.
     */
    protected function socketOpened (event :Event) :void
    {
        // reset our port index now that we're successfully logged on; this way if the socket
        // fails, we won't think that we're in the middle of trying to logon
        _portIdx = -1;

        // check for a logoff message
        if (hasPendingLogoff()) {
            // don't bother authing, just bail
            log.info("Logged off prior to socket opening, shutting down");
            shutdown(null);
            return;
        }

        // send our authentication request (do so directly rather than putting it on the outgoing
        // write queue)
        sendMessage(new AuthRequest(_client.getCredentials(), _client.getVersion(),
                                    _client.getBootGroups()));

        // kick off our writer thread now that we know we're ready to write
        _writer = new Timer(1);
        _writer.addEventListener(TimerEvent.TIMER, sendPendingMessages);
        _writer.start();

        // clear the queue, the server doesn't like anything sent prior to auth
        _outq.length = 0;
    }

    /**
     * Called when there is an io error with the socket.
     */
    protected function socketError (event :Event) :void
    {
        // if we're still trying ports, try the next one.
        if (_portIdx != -1) {
            if (attemptLogon(_portIdx+1)) {
                return;
            }
        }

        // total failure
        log.warning("Socket error", "event", event, "target", event.target, new Error());
        shutdown(new Error(AuthCodes.NETWORK_ERROR));
    }

    /**
     * Called when the connection to the server was closed.
     */
    protected function socketClosed (event :Event) :void
    {
        log.info("Socket was closed", "event", event);
        _client.notifyObservers(ClientEvent.CLIENT_CONNECTION_FAILED);
        // if we hadn't loaded our client object yet, behave as if this was a logon failure because
        // we failed before we dispatched a DID_LOGON
        var wasLoggedOn :Boolean = (_client.getClientObject() != null);
        shutdown(wasLoggedOn ? null : new LogonError(AuthCodes.NETWORK_ERROR));
    }

    /**
     * Returns the time at which we last sent a packet to the server.
     */
    internal function getLastWrite () :uint
    {
        return _lastWrite;
    }

    /**
     * Makes a note of the time at which we last communicated with the server.
     */
    internal function updateWriteStamp () :void
    {
        _lastWrite = flash.utils.getTimer();
    }

    protected var _client :Client;
    protected var _omgr :ClientDObjectMgr;

    protected var _outBuffer :ByteArray;
    protected var _outStream :ObjectOutputStream;

    protected var _inStream :ObjectInputStream;
    protected var _frameReader :FrameReader;

    protected var _socket :Socket;
    protected var _lastWrite :uint;

    protected var _outq :Array = new Array();
    protected var _writer :Timer;
    protected var _notedThrottle :Boolean = false;

    protected const log :Log = Log.getLog(this);

    /** The current port we'll try to connect to. */
    protected var _portIdx :int = -1;
}
}
