/* Copyright (c) 2008-2011 - Eric P. Mangold
 * Released under the terms of the MIT/X11 license - see LICENSE.txt */
using System;

namespace AMP
{
    internal interface IAMPTransport
    {
        void RegisterProtocol(IAmpProtocol proto);
        void StartReading();
        void Write(byte[] bytes);
    }

    internal class StreamTransport : IAMPTransport
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger("AMP");
        private const int readBufSize = 32768;
        private readonly byte[] readBuf = new byte[readBufSize];

        private System.IO.Stream stream;
        private IAmpProtocol proto;
        public StreamTransport(System.IO.Stream stream)
        {
            this.stream = stream;
        }

        public System.IO.Stream Stream
        {
            get { return stream; }
        }

        public void StartReading()
        {
            stream.BeginRead(readBuf, 0, readBuf.Length, new AsyncCallback(AsyncDataReceived), stream);
        }

        public void Write(byte[] bytes)
        {
            try
            {
                stream.BeginWrite(bytes, 0, bytes.Length, new AsyncCallback(AsyncDataSent), stream);
            }
            catch (Exception e)
            {
                HandleFatalNetworkError(e);
            }
        }

        private void AsyncDataSent(IAsyncResult ar)
        {
            var s = (System.IO.Stream)ar.AsyncState;
            try
            {
                s.EndWrite(ar);
            }
            catch (Exception e)
            {
                log.Error("Error sending to Server:", e);
                HandleFatalNetworkError(e);
                return;
            }
        }

        public void RegisterProtocol(IAmpProtocol proto)
        {
            this.proto = proto;
        }

        private void AsyncDataReceived(IAsyncResult ar)
        {
            // XXX TODO I'm not 100% sure but it might be possible that this callback gets called
            // even after this stream has been discarded. Which means we will
            // have failed any outstanding callRemote's that these bytes might be related too..
            // We should ignore these bytes in that case.
            //
            // UPDATE: Yes, it's definately possible.

            int bytesRead;
            var s = (System.IO.Stream)ar.AsyncState;

            try
            {
                bytesRead = s.EndRead(ar);
            }
            catch (System.IO.IOException e)
            {
                log.Error("Connection to peer closed uncleanly.");
                HandleFatalNetworkError(e);
                return;
            }
            catch (Exception e)
            {
                log.Error("Error reading from socket:", e);
                HandleFatalNetworkError(e);
                return;
            }
            if (bytesRead == 0)
            {
                log.Debug("Connection to peer closed cleanly.");
                proto.ConnectionLost(new Error.StreamClosedException());

            }
            else
            {
                proto.DataReceived(readBuf, bytesRead);

                try
                {
                    s.BeginRead(readBuf, 0, readBuf.Length, new AsyncCallback(AsyncDataReceived), s);
                }
                catch (Exception e)
                {
                    log.Error("Error calling BeginRead:", e);
                    HandleFatalNetworkError(e);
                }

            }
        }

        private void HandleFatalNetworkError(Exception err)
        {
            // XXX TODO we need to be double-plus sure that we 
            // don't call ConnectionLost more than once.
            proto.ConnectionLost(err);
        }

    }
}
