using Rowa.Lib.Security.Net;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using CareFusion.Mosaic.Core.Logging;
using CareFusion.Mosaic.Interfaces.Components;
using CareFusion.Mosaic.Interfaces.Connectors;
using CareFusion.Mosaic.Interfaces.Types.Components;

namespace CareFusion.Mosaic.Connectors.Tcp
{
    /// <summary>
    /// Class which implements a connector that accepts incomming TCP/IP connections.
    /// </summary>
    public class TcpInConnector : IInboundConnector
    {
        #region Members

        /// <summary>
        /// Holds the connector configuration.
        /// </summary>
        private TcpInConnectorConfiguration _configuration = null;

        /// <summary>
        /// TCP listener used for incoming connections.
        /// </summary>
        private TcpListener _tcpListener = null;

        /// <summary>
        /// Outstanding listening operations.
        /// </summary>
        private Task<TcpClient> _listenResult;

        /// <summary>
        /// Event which is used to cancel pending listen operations.
        /// </summary>
        private readonly CancellationTokenSource _cancelEvent = new CancellationTokenSource();

        /// <summary>
        /// Event which is used to signal the end of a listening operation.
        /// </summary>
        private readonly ManualResetEvent _listenFinishedEvent = new ManualResetEvent(true);

        /// <summary>
        /// The list of currently active TCP connections.
        /// </summary>
        private readonly List<TcpConnection> _activeConnections = new List<TcpConnection>();

        #endregion

        #region Properties

        /// <summary>
        /// Gets the identifier of the connector which has been passed to the Initialize method. 
        /// </summary>
        public int ID
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets an informational English description of the connector (e.g. "TCP/IP Socket Connector").
        /// </summary>
        public string Description
        {
            get { return "TCP/IP Socket Listener"; }
        }

        public TcpInConnectorConfiguration Configuration => this._configuration;

        #endregion

        /// <summary>
        /// Gets an object with public properties which defines a configuration for this connector.
        /// </summary>
        /// <returns>Appropriate configuration object for this connector.</returns>
        public IComponentConfiguration GetConfigurationObject()
        {
            return new TcpInConnectorConfiguration();
        }

        /// <summary>
        /// Initializes the connector with the specified configuration.
        /// </summary>
        /// <param name="connectorID">The identifier of the connector.</param>
        /// <param name="configurationValueList">The list of configuration values, assigned to this connector.</param>
        /// <returns><c>true</c> if initialization was successful; <c>false</c> otherwise.</returns>
        public bool Initialize(int connectorID, List<ConfigurationValue> configurationValueList)
        {
            return this.Initialize( connectorID, configurationValueList, true );
        }

        public bool Initialize(int connectorID, List<ConfigurationValue> configurationValueList, bool startListening )
        {
            this.ID = connectorID;
            _configuration = new TcpInConnectorConfiguration(configurationValueList);

            if( startListening )
            {
                return this.Listen();
            }else
            {
                return true;
            }
        }

        public bool Listen()
        {
            if (_tcpListener is not null)
            {
                this.Error("The connector is already initialized.");
                return false;
            }

            try
            {
                var usedAddressList = new List<string>();
                var ipAddressList = Dns.GetHostAddresses(Dns.GetHostName());

                if (ipAddressList.Length == 0)
                {
                    this.Error("No network interfaces are available.");
                    return false;
                }
                this.Debug("Starting TCP listener for port '{0}'.", _configuration.Port);

                _tcpListener = new TcpListener(IPAddress.Any, _configuration.Port);
                _tcpListener.Start();

                _listenFinishedEvent.Set();
                return true;
            }
            catch (Exception ex)
            {
                this.Error(ex, "Starting TCP listeners for port '{0}' failed.", _configuration.Port);
            }

            return false;
        }

        /// <summary>
        /// Starts to wait for incoming connections. This method will block until a new incoming connection 
        /// has been accepted, the connector has been cancelled or an error occurred.
        /// </summary>
        /// <returns>Newly created incoming connection object if successful; <c>null</c> otherwise.</returns>
        public IConnection WaitForConnections()
        {
            if (_cancelEvent.IsCancellationRequested)
                return null;

            try
            {
                _listenFinishedEvent.Reset();

                if (!WaitForFreeConnectionSlot())
                    return null;

                TcpClient tcpClient = null;

                _listenResult = _tcpListener.AcceptTcpClientAsync(_cancelEvent.Token).AsTask();
                Task.WaitAny(_listenResult);

                if (_listenResult.IsCanceled)
                {
                    this.Info("Waiting for incoming connections was cancelled.");
                    return null;
                }


                tcpClient = _listenResult.Result;
                _listenResult = null;
                tcpClient.ReceiveTimeout = (int)(_configuration.ReadTimeout * 1000);
                tcpClient.SendTimeout = (int)(_configuration.WriteTimeout * 1000);

                IPEndPoint remoteEndpoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint;

                this.Info("Successfully accepted connection from '{0}:{1}'.",
                          remoteEndpoint.Address.ToString(), remoteEndpoint.Port);

                var endPoint = string.Format("{0}:{1}", remoteEndpoint.Address.ToString(), _configuration.Port);

                System.IO.Stream streamToUse =
                    Configuration.TlsEnable ?
                        Tls.AuthenticateAsServer(tcpClient.GetStream(), Configuration.ClientCertificateRequired) :
                        tcpClient.GetStream();

                var connection = new TcpConnection(
                    this.ID,
                    _configuration.Category,
                    tcpClient,
                    streamToUse,
                    endPoint);

                connection.Closed += OnConnectionClosed;

                lock (_activeConnections)
                {
                    _activeConnections.Add(connection);
                }

                return connection;
            }
            catch (Exception ex)
            {
                this.Error(ex, "Waiting for incomming connections failed.");
            }
            finally
            {
                _listenFinishedEvent.Set();
            }

            return null;
        }

        /// <summary>
        /// Event which is called when a connection has been closed.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        private void OnConnectionClosed(object sender, EventArgs e)
        {
            var connection = (TcpConnection)sender;
            lock (_activeConnections)
            {
                _activeConnections.Remove(connection);
            }

            connection.Dispose();
        }

        /// <summary>
        /// Waits until a free slot for a new incoming connection is available.
        /// Connection slots are defined by the maximum number of allowed concurrent connections.
        /// </summary>
        /// <returns><c>true</c> if connection slot has been acquired;<c>false</c> otherwise.</returns>
        private bool WaitForFreeConnectionSlot()
        {
            var writeLog = true;

            do
            {
                var activeConnectionCount = 0;

                lock (_activeConnections)
                {
                    activeConnectionCount = _activeConnections.Count;
                }

                if (activeConnectionCount < _configuration.MaxConcurrentConnections)
                    return true;

                if (writeLog)
                {
                    this.Error("Maximum number of {0} allowed concurrent connections reached -> waiting for connections to be closed!", activeConnectionCount);
                    writeLog = false;
                }
            }
            while (!_cancelEvent.Token.WaitHandle.WaitOne(50));

            return false;
        }

        /// <summary>
        /// Cancels any pending operations like creating or accepting connections.
        /// This method will block and not return until all outstanding operations have finished.
        /// </summary>
        public void Cancel()
        {
            this.Debug("Cancelling all active TCP listeners...");

            _cancelEvent.Cancel();
            _listenFinishedEvent.WaitOne();
            _tcpListener.Stop();

            this.Debug("All active TCP listeners cancelled.");

        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Debug("Cleanup active TCP listener.");

            _listenFinishedEvent.Dispose();
            _cancelEvent.Dispose();
            _listenResult = null;
        }
    }
}