/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is JIM.
 *
 * The Initial Developer of the Original Code is
 * Pawel Chmielowski <prefiks@o2.pl>
 *
 * Portions created by the Initial Developer are Copyright (C) 2003
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 * Pawel Chmielowski <prefiks@o2.pl>
 *
 * ***** END LICENSE BLOCK ***** */

/**
 * net.Socket
 *
 *
 */
ML.load("utils/exception.js");
ML.load("utils/observable.js");
ML.load('io/charsetcoder.js');

function Socket(asyncExceptionHandler) {
    var sockSrvClass = Components.classesByID["{c07e81e0-ef12-11d2-92b6-00105a1b0d64}"];

    this.base = Observable;
    this.base();

    if (!sockSrvClass)
        throw "Socket::SockService class not found.";
    var sockSrv = sockSrvClass.getService();
    
    if (!sockSrv)
        throw "Socket::SockService.getService fail.";
    
    this.__transportSrv = sockSrv.QueryInterface(Components.interfaces.nsISocketTransportService);
    this.__coder = new CharsetCoder();
    this.__asyncExceptionHandler = asyncExceptionHandler;
    this.thisObj = this;
}

Socket.prototype = {
    __proto__: Observable.prototype,
    __callbackProvider: null,
    __callbackListener: null,
    __readRequest: null,
    __writeRequest: null,
    __outputCharset: null,
    
    open: function(host, port, socketTypes)
    {
        if ("nsIStreamProvider" in Components.interfaces) {
            if (socketTypes && socketTypes.length > 0)
                this.__transport = this.__transportSrv.createTransportOfTypes(socketTypes, socketTypes.lenght,
                    host, port, null, -1, 0, 0);
            else
                this.__transport = this.__transportSrv.createTransport(host, port, null, -1, null, null);
            if (!this.__transport)
                throw "Socket:open:Unable to create transport.";

            this.__callbackProvider = new Socket__SocketProvider(0, this);
            this.__callbackListener = new Socket__SocketListener(this);
            this.__readRequest = this.__transport.asyncRead(this.__callbackListener, this, 0, -1, 0);
            this.__writeRequest = this.__transport.asyncWrite(this.__callbackProvider, this, 0, -1, 0);
        } else {
            if (socketTypes && socketTypes.length == 0)
                socketTypes = null;
            this.__transport = this.__transportSrv.createTransport(socketTypes, 
                socketTypes ? socketTypes.length : 0, host, port, null);
            if (!this.__transport)
                throw "Socket:open:Unable to create transport.";

            this.__outputStream = this.__transport.openOutputStream(0, 4096, -1);
            if (!this.__outputStream)
                throw "Socket:open:Error getting input stream.";

            this.__inputStream = this.__transport.openInputStream(0, 0, 0);
            if (!this.__inputStream)
                throw "Socket:open:Error getting input stream.";

            this.__callbackListener = new Socket__SocketListener(this);
            this.__inputStreamPump = Components.classes["@mozilla.org/network/input-stream-pump;1"].createInstance(
                Components.interfaces.nsIInputStreamPump);
            this.__inputStreamPump.init(this.__inputStream, -1, -1, 0, 0, false);
            this.__inputStreamPump.asyncRead(this.__callbackListener, this);
            this.fireEvent("onConnectionOpen");
        }
    },

    setOutputCharset: function(charset)
    {
        this.__coder.charset = charset;
    },

    close: function()
    {
        if (this.__readRequest) {
            if (this.__callbackProvider)
                this.__callbackProvider.close();
            this.__callbackProvider = null;
            this.__callbackListener = null;
        } else {
            if (this.__inputStream) {
                this.__inputStream.close();
                this.__inputStream = null;
                this.__callbackListener = null;
            }
            if (this.__outputStream) {
                this.__outputStream.close();
                this.__outputStream = null;
            }
        }
    },

    connected: function()
    {
        if ("nsIStreamProvider" in Components.interfaces)
            return this.__callbackProvider && this.__callbackListener && 
                !this.__callbackProvider.__closed && !this.__callbackListener.__closed;
        else
            return this.__inputStream && this.__outputStream;
    },

    write: function(data)
    {
        var i;

        data = this.__coder.encode(data);

        if ("nsIStreamProvider" in Components.interfaces) {
            if (this.__callbackProvider)
                this.__callbackProvider.write(data);
            else
                throw "Socket:write:Unopened stream.";
        } else {
            if (this.__outputStream) {
                this.__outputStream.write(data, data.length);
                this.fireEvent("onWriteData", data);
            }
        }
    },
}

function Socket__SocketProvider(maxbuf, parent) {
    this.__maxbuf = maxbuf;
    this.__parent = parent;
}

Socket__SocketProvider.prototype = {
    __outputData: "",
    __closed: true,

    close: function()
    {
        this.__closed = 1;
    },

    write: function(data)
    {
        if (this.__maxbuf && this.__maxbuf < this.__outputData.length + data.length)
            throw "Socket__SocketProvider:write:Buffer full.";
        this.__outputData += data;
    },

    onDataWritable: function(request, context, outputStream, offset, count)
    {
        if (this.__outputData != "") {
            try {
                var len = outputStream.write(this.__outputData, this.__outputData.length);
                var data = this.__outputData.substr(0, len);
                this.__outputData = this.__outputData.substr(len);

                this.__parent.fireEvent("onWriteData", data);
            } catch (ex) {
                this.__parent.__asyncExceptionHandler.handleError(
                    new SocketException("socket.onDataWritableException", ex));
            }
        } else if (this.__closed) {
            if (this.__parent.__callbackListener)
                this.__parent.__callbackListener.close();
            this.__parent.__callbackListener = null;
            this.__parent.__callbackProvider = null;
        }
    },

    onStartRequest: function(request, context)
    {
        this.__closed = 0;
    },

    onStopRequest: function(request, context, statusCode)
    {
        if (this.__closed)
            return;
        this.__closed = 1;
    }
}

function Socket__SocketListener(parent) {
    this.__parent = parent;
    this.__closed = 1;
    this.__inputStream = null;
}

Socket__SocketListener.prototype = {

    close: function()
    {
        if (this.__closed)
            return;

        try {
            if (this.__inputStream)
                this.__inputStream.close();
            this.onStopRequest("", this, 0);
            this.__closed = true;
        } catch (ex) {
            throw "Socket__SocketListener:close:Unable to close stream.";
        }
    },

    onDataAvailable: function(request, context, inputStream, offset, count)
    {
        var i;

        try {
            if (this.__closed)
                throw Components.results.NS_BASE_STREAM_CLOSED;
            if (!this.__inputStream)
                this.__inputStream = toScriptableInputStream(inputStream);
            var data = this.__inputStream.read(count);

            if (this.__closed)
                this.__inputStream.close();
            else
                this.__parent.fireEvent("onReadData", data);
        } catch (ex) {
            this.__parent.__asyncExceptionHandler.handleError(
                new SocketException("socket.onDataAvailableException", ex));
        }
    },

    onStartRequest: function(request, context)
    {
        this.__closed = 0;
        var i;

        if (!this.__parent.__inputStreamPump)
            this.__parent.fireEvent("onConnectionOpen");
    },

    onStopRequest: function(request, context, statusCode)
    {
        var i;

        if (this.__closed)
            return;
        this.__closed = 1;
        this.__parent.close();

        this.__parent.fireEvent("onConnectionClose", statusCode);
    },
}

function SocketException(type, reason)
{
    this.base = Exception;
    this.base(type, reason);
}

SocketException.prototype =
{
    __proto__: Exception.prototype
}

function toScriptableInputStream(i)
{
    var si = Components.classes["@mozilla.org/scriptableinputstream;1"];
    si = si.createInstance();
    si = si.QueryInterface(Components.interfaces.nsIScriptableInputStream);
    si.init(i);

    return si;
}

function statusCodeToString(code) {
    var i;

    for (i in Components.results) {
        if (Components.results[i] == code)
            return i;
    }
}