//
// Connection.cs
//
// Copyright (c) 2007 Lukas Lipka.
//

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

using IcqSharp.Packets;
using IcqSharp.Util;

namespace IcqSharp.Connections
{
    public abstract class Connection
    {
        private Session session;

        private string host;
        private int port;

        private TcpClient client;
        private NetworkStream stream;

        private object client_lock = new object ();
        
        private Thread thread;
        private bool running;

        public event EventHandler Connected;
        public event EventHandler Disconnected;
        public event EventHandler ConnectionError;

        public Connection (Session session, string host, int port)
        {
            this.session = session;
            this.host = host;
            this.port = port;
        }

        public void Connect ()
        {
            Log.Debug ("Connecting to: {0}:{1}", host, port);

            // Connect to specified server, or if we fail signal
            // a connection error
            try {
                this.client = new TcpClient (host, port);
                this.stream = client.GetStream();
            } catch (SocketException e) {
                Log.Error("Network error on connect!");
                Log.Error(e);

                OnConnectionError(this, EventArgs.Empty);

                return;
            }

            Log.Debug ("Connected to: {0}:{1}", host, port);

            // Start our main-loop thread that will handle all
            // the incoming packets
            thread = new Thread (new ThreadStart (Worker));
            thread.Priority = ThreadPriority.BelowNormal;
            thread.IsBackground = true;
            thread.Start ();

            OnConnected(this, EventArgs.Empty);
        }

        public void Disconnect ()
        {
            if (client == null || !IsConnected)
                return;

            // Shutdown our main-loop thread and wait for it
            // to join at least for 1 second
            running = false;
            thread.Join (1000);

            stream.Close();

            client.Close();
            client = null;

            Log.Debug ("Disconnected from server ({0}:{1})", host, port);

            OnDisconnected(this, EventArgs.Empty);
        }

        protected void Write (byte[] data)
        {
            if (client == null || !IsConnected)
                return;

            try
            {
                Stream.Write(data, 0, data.Length);
                Stream.Flush();

                Log.Debug("Sent {0} bytes", data.Length);
            }
            catch (Exception e)
            {
                Log.Error("Network error on send!");
                Log.Error(e);

                Disconnect();

                OnConnectionError(this, EventArgs.Empty);
            }
        }

        private void Worker()
        {
            Log.Debug("{0} worker starting...", this.GetType().Name);

            running = true;

            // This try/catch block isn't ideal, but using Client.Poll
            // doesn't work on the CF for some reason. :-(

            try
            {
                DoWorker();
            }
            catch (Exception e)
            {
                Log.Error("Network error on worker!");
                Log.Error(e);

                Disconnect();

                OnConnectionError(this, EventArgs.Empty);
            }

            Log.Debug("{0} worker exiting...", this.GetType().Name);
        }

        private void DoWorker ()
        {
            while (running) {
                if (client.Client.Available > 0) {
                    byte[] data = null;

                    lock (client_lock)
                    {
                        data = ReadData();
                    }

                    if (data != null) {
                        Log.Debug ("Read {0} bytes", data.Length);
                        HandleData (data);
                    }
                }

                Thread.Sleep(100);
            }
        }

        private byte[] ReadData ()
        {
            byte[] data = new byte[4096];
            MemoryStream buffer = new MemoryStream ();
            int bytes_read = 0, total_bytes = 0, packet_data_length = 0;

            // If there is not enough data available bail
            // out and wait for enough data to be received
            if (client.Client.Available < 6)
                return null;

            // Read the FLAP header otherwise discard
            try {
                bytes_read = Stream.Read (data, 0, 6);

                if (data[0] != FlapPacket.Id)
                    throw new FormatException ("Invalid FLAP header");

                packet_data_length = LittleEndianBitConverter.Big.ToUInt16 (data, 4);
                buffer.Write (data, 0, bytes_read);
            } catch (Exception e) {
                Log.Error ("Error reading FLAP header!");
                Log.Error (e);
                return null;
            }

            // Read the packet data and if any other data
            // follows chunk it and leave it for another read
            do {
                bytes_read = 0;

                try {
                    bytes_read = Stream.Read(data, 0, packet_data_length - total_bytes > 4096 ? 4096 : packet_data_length - total_bytes);

                    if (bytes_read > 0)
                        buffer.Write (data, 0, bytes_read);
                } catch (Exception e) {
                    Log.Error (e);
                    return null;
                }

                total_bytes += bytes_read;
            } while (total_bytes < packet_data_length);

            buffer.Seek (0, SeekOrigin.Begin);

            return buffer.ToArray ();
        }

        protected virtual void OnConnected(object o, EventArgs args)
        {
            if (Connected != null)
                Connected(this, EventArgs.Empty);
        }

        protected virtual void OnDisconnected(object o, EventArgs args)
        {
            if (Disconnected != null)
                Disconnected(this, EventArgs.Empty);
        }

        protected virtual void OnConnectionError(object o, EventArgs args)
        {
            if (ConnectionError != null)
                ConnectionError(this, EventArgs.Empty);
        }

        protected virtual void HandleData (byte[] data)
        {
            throw new NotImplementedException (String.Format ("HandleData not implemented: '{0}'", this.GetType ()));
        }

        protected Session Session
        {
            get { return session; }
        }

        public string Host
        {
            get { return host; }
        }

        public int Port
        {
            get { return port; }
        }

        public bool IsConnected
        {
            get { return client != null && client.Client.Connected; }
        }

        protected TcpClient Client
        {
            get { return client; }
        }

        protected NetworkStream Stream
        {
            get { return stream; }
        }
    }
}
