using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using SuperSocket.Common;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Command;
using SuperSocket.SocketBase.Protocol;
using SuperWebSocket.Protocol;
using SuperWebSocket.SubProtocol;

namespace SuperWebSocket
{
    /// <summary>
    /// WebSocketSession basic interface
    /// </summary>
    public interface IWebSocketSession : IAppSession
    {
        /// <summary>
        /// Gets or sets the method.
        /// </summary>
        /// <value>
        /// The method.
        /// </value>
        string Method { get; set; }

        /// <summary>
        /// Gets the host.
        /// </summary>
        string Host { get; }

        /// <summary>
        /// Gets or sets the path.
        /// </summary>
        /// <value>
        /// The path.
        /// </value>
        string Path { get; set; }

        /// <summary>
        /// Gets or sets the HTTP version.
        /// </summary>
        /// <value>
        /// The HTTP version.
        /// </value>
        string HttpVersion { get; set; }

        /// <summary>
        /// Gets the sec web socket version.
        /// </summary>
        string SecWebSocketVersion { get; }

        /// <summary>
        /// Gets the origin.
        /// </summary>
        string Origin { get; }

        /// <summary>
        /// Gets the URI scheme.
        /// </summary>
        string UriScheme { get; }

        /// <summary>
        /// Gets a value indicating whether this <see cref="IWebSocketSession" /> is handshaked.
        /// </summary>
        /// <value>
        ///   <c>true</c> if handshaked; otherwise, <c>false</c>.
        /// </value>
        bool Handshaked { get; }

        /// <summary>
        /// Sends the raw binary data to client.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="length">The length.</param>
        void SendRawData(byte[] data, int offset, int length);

        /// <summary>
        /// Try to send the raw binary data to client.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="length">The length.</param>
        /// <returns>if the data to be sent is queued, return true, else the queue is full, then return false</returns>
        bool TrySendRawData(byte[] data, int offset, int length);

        /// <summary>
        /// Gets the app server.
        /// </summary>
        new IWebSocketServer AppServer { get; }

        /// <summary>
        /// Gets or sets the protocol processor.
        /// </summary>
        /// <value>
        /// The protocol processor.
        /// </value>
        IProtocolProcessor ProtocolProcessor { get; set; }

        /// <summary>
        /// Gets the available sub protocol.
        /// </summary>
        /// <param name="protocol">The protocol.</param>
        /// <returns></returns>
        string GetAvailableSubProtocol(string protocol);
    }

    /// <summary>
    /// WebSocket AppSession
    /// </summary>
    public class WebSocketSession : WebSocketSession<WebSocketSession>
    {
        /// <summary>
        /// Gets the app server.
        /// </summary>
        public new WebSocketServer AppServer
        {
            get { return (WebSocketServer)base.AppServer; }
        }
    }

    /// <summary>
    /// WebSocket AppSession class
    /// </summary>
    /// <typeparam name="TWebSocketSession">The type of the web socket session.</typeparam>
    public class WebSocketSession<TWebSocketSession> : AppSession<TWebSocketSession, IWebSocketFragment>, IWebSocketSession, IAppSession
        where TWebSocketSession : WebSocketSession<TWebSocketSession>, new()
    {
        /// <summary>
        /// Gets or sets the method.
        /// </summary>
        /// <value>
        /// The method.
        /// </value>
        public string Method { get; set; }

        /// <summary>
        /// Gets or sets the path.
        /// </summary>
        /// <value>
        /// The path.
        /// </value>
        public string Path { get; set; }

        /// <summary>
        /// Gets or sets the HTTP version.
        /// </summary>
        /// <value>
        /// The HTTP version.
        /// </value>
        public string HttpVersion { get; set; }

        /// <summary>
        /// Gets the host.
        /// </summary>
        public string Host { get { return this.Items.GetValue<string>(WebSocketConstant.Host, string.Empty); } }

        /// <summary>
        /// Gets the origin.
        /// </summary>
        public string Origin { get; internal set; }

        /// <summary>
        /// Gets the upgrade.
        /// </summary>
        public string Upgrade { get { return this.Items.GetValue<string>(WebSocketConstant.Upgrade, string.Empty); } }

        /// <summary>
        /// Gets the connection.
        /// </summary>
        public string Connection { get { return this.Items.GetValue<string>(WebSocketConstant.Connection, string.Empty); } }

        /// <summary>
        /// Gets the sec web socket version.
        /// </summary>
        public string SecWebSocketVersion { get { return this.Items.GetValue<string>(WebSocketConstant.SecWebSocketVersion, string.Empty); } }

        /// <summary>
        /// Gets the sec web socket protocol.
        /// </summary>
        public string SecWebSocketProtocol { get { return this.Items.GetValue<string>(WebSocketConstant.SecWebSocketProtocol, string.Empty); } }

        internal List<WebSocketDataFrame> Frames { get; private set; }

        internal DateTime StartClosingHandshakeTime { get; private set; }

        private const string m_CurrentTokenSlotName = "CurrentRequestToken";

        internal LocalDataStoreSlot SetCurrentToken(string token)
        {
            var slot = Thread.GetNamedDataSlot(m_CurrentTokenSlotName);
            Thread.SetData(slot, token);
            return slot;
        }

        /// <summary>
        /// Gets the current token.
        /// </summary>
        public string CurrentToken
        {
            get
            {
                return Thread.GetData(Thread.GetNamedDataSlot(m_CurrentTokenSlotName)) as string;
            }
        }


        /// <summary>
        /// Gets the app server.
        /// </summary>
        public new WebSocketServer<TWebSocketSession> AppServer
        {
            get { return (WebSocketServer<TWebSocketSession>)base.AppServer; }
        }

        IWebSocketServer IWebSocketSession.AppServer
        {
            get { return (IWebSocketServer)base.AppServer; }
        }

        string IWebSocketSession.GetAvailableSubProtocol(string protocol)
        {
            
            if (string.IsNullOrEmpty(protocol))
            {
                SubProtocol = AppServer.DefaultSubProtocol;
                return string.Empty;
            }

            var arrNames = protocol.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

            if (protocol.ToLower().Contains("ocpp2.0"))
            {
                arrNames = new string[] { "ocpp2.0" };
            }

            foreach (var name in arrNames)
            {

                var subProtocol = AppServer.GetSubProtocol(name);

                if (subProtocol != null)
                {
                    SubProtocol = subProtocol;
                    return name;
                }
            }

            return string.Empty;
        }

        /// <summary>
        /// Gets the URI scheme, ws or wss
        /// </summary>
        public string UriScheme
        {
            get
            {
                if (SocketSession.SecureProtocol == SslProtocols.None)
                    return WebSocketConstant.WsSchema;
                else
                    return WebSocketConstant.WssSchema;
            }
        }

        /// <summary>
        /// Gets the sub protocol.
        /// </summary>
        public ISubProtocol<TWebSocketSession> SubProtocol { get; private set; }

        private bool m_Handshaked = false;

        /// <summary>
        /// Gets a value indicating whether this <see cref="IWebSocketSession" /> is handshaked.
        /// </summary>
        /// <value>
        ///   <c>true</c> if handshaked; otherwise, <c>false</c>.
        /// </value>
        public bool Handshaked
        {
            get { return m_Handshaked; }
        }

        internal void OnHandshakeSuccess()
        {
            m_Handshaked = true;
            SetCookie();
            OnSessionStarted();
            AppServer.FireOnNewSessionConnected(this);
        }

        /// <summary>
        /// Gets a value indicating whether the session [in closing].
        /// </summary>
        /// <value>
        ///   <c>true</c> if [in closing]; otherwise, <c>false</c>.
        /// </value>
        public bool InClosing { get; private set; }

        /// <summary>
        /// Called when [init].
        /// </summary>
        protected override void OnInit()
        {
            Frames = new List<WebSocketDataFrame>();
            base.OnInit();
        }

        void IAppSession.StartSession()
        {
            //Do nothing. Avoid firing thhe OnSessionStarted() method of base class
        }

        /// <summary>
        /// Sets the cookie.
        /// </summary>
        private void SetCookie()
        {
            string cookieValue = this.Items.GetValue<string>(WebSocketConstant.Cookie, string.Empty);

            if (string.IsNullOrEmpty(cookieValue))
                return;

            var cookies = new StringDictionary();
            this.Cookies = cookies;

            string[] pairs = cookieValue.Split(';');

            int pos;
            string key, value;

            foreach (var p in pairs)
            {
                pos = p.IndexOf('=');

                if (pos <= 0)
                    continue;

                key = p.Substring(0, pos).Trim();

                pos += 1;

                if (pos < p.Length)
                    value = p.Substring(pos).Trim();
                else
                    value = string.Empty;

                if (string.IsNullOrEmpty(value))
                {
                    cookies[key] = string.Empty;
                    continue;
                }

                try
                {
                    cookies[key] = Uri.UnescapeDataString(value);
                }
                catch (Exception e)
                {
                    Logger.Error(this, string.Format("Failed to read cookie, key: {0}, value: {1}.", key, value), e);
                }
            }
        }


        /// <summary>
        /// Gets the cookies.
        /// </summary>
        public StringDictionary Cookies { get; private set; }


        /// <summary>
        /// Sends the message to client.
        /// </summary>
        /// <param name="message">The message.</param>
        public override void Send(string message)
        {
            ProtocolProcessor.SendMessage(this, message);
        }

        /// <summary>
        /// Tries to send.
        /// </summary>
        /// <param name="message">The message to be sent.</param>
        /// <returns></returns>
        public override bool TrySend(string message)
        {
            return ProtocolProcessor.TrySendMessage(this, message);
        }

        /// <summary>
        /// Sends the data to client.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="length">The length.</param>
        public override void Send(byte[] data, int offset, int length)
        {
            if (!ProtocolProcessor.CanSendBinaryData)
            {
                if (Logger.IsErrorEnabled)
                    Logger.Error("The websocket of this version cannot used for sending binary data!");
                return;
            }

            ProtocolProcessor.SendData(this, data, offset, length);
        }

        /// <summary>
        /// Tries to send the data over the websocket connection.
        /// </summary>
        /// <param name="segment">The segment to be sent.</param>
        /// <returns></returns>
        public override bool TrySend(ArraySegment<byte> segment)
        {
            if (!ProtocolProcessor.CanSendBinaryData)
            {
                if (Logger.IsErrorEnabled)
                    Logger.Error("The websocket of this version cannot used for sending binary data!");
                return false;
            }

            return ProtocolProcessor.TrySendData(this, segment.Array, segment.Offset, segment.Count);
        }

        /// <summary>
        /// Tries to send the data over the websocket connection.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="length">The length.</param>
        /// <returns></returns>
        public override bool TrySend(byte[] data, int offset, int length)
        {
            if (!ProtocolProcessor.CanSendBinaryData)
            {
                if (Logger.IsErrorEnabled)
                    Logger.Error("The websocket of this version cannot used for sending binary data!");
                return false;
            }

            return ProtocolProcessor.TrySendData(this, data, offset, length);
        }

        /// <summary>
        /// Sends the segment to client.
        /// </summary>
        /// <param name="segment">The segment.</param>
        public override void Send(ArraySegment<byte> segment)
        {
            this.Send(segment.Array, segment.Offset, segment.Count);
        }

        /// <summary>
        /// Sends the raw binary data.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="length">The length.</param>
        void IWebSocketSession.SendRawData(byte[] data, int offset, int length)
        {
            base.Send(data, offset, length);
        }


        /// <summary>
        /// Try to send the raw binary data to client.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="length">The length.</param>
        /// <returns>
        /// if the data to be sent is queued, return true, else the queue is full, then return false
        /// </returns>
        bool IWebSocketSession.TrySendRawData(byte[] data, int offset, int length)
        {
            return base.TrySend(new ArraySegment<byte>(data, offset, length));
        }

        /// <summary>
        /// Tries the send raw data segments.
        /// </summary>
        /// <param name="segments">The segments.</param>
        /// <returns></returns>
        internal bool TrySendRawData(IList<ArraySegment<byte>> segments)
        {
            return base.TrySend(segments);
        }

        /// <summary>
        /// Closes the with handshake.
        /// </summary>
        /// <param name="reasonText">The reason text.</param>
        public void CloseWithHandshake(string reasonText)
        {
            this.CloseWithHandshake(ProtocolProcessor.CloseStatusClode.NormalClosure, reasonText);
        }

        /// <summary>
        /// Closes the with handshake.
        /// </summary>
        /// <param name="statusCode">The status code.</param>
        /// <param name="reasonText">The reason text.</param>
        public void CloseWithHandshake(int statusCode, string reasonText)
        {
            if (!InClosing)
                InClosing = true;

            ProtocolProcessor.SendCloseHandshake(this, statusCode, reasonText);

            StartClosingHandshakeTime = DateTime.Now;
            AppServer.PushToCloseHandshakeQueue(this);
        }

        /// <summary>
        /// Sends the close handshake response.
        /// </summary>
        /// <param name="statusCode">The status code.</param>
        public void SendCloseHandshakeResponse(int statusCode)
        {
            if (!InClosing)
                InClosing = true;

            ProtocolProcessor.SendCloseHandshake(this, statusCode, string.Empty);
        }

        /// <summary>
        /// Closes the specified reason.
        /// </summary>
        /// <param name="reason">The reason.</param>
        public override void Close(CloseReason reason)
        {
            if (reason == CloseReason.TimeOut && ProtocolProcessor != null)
            {
                CloseWithHandshake(ProtocolProcessor.CloseStatusClode.NormalClosure, "Session timeOut");
                return;
            }

            base.Close(reason);
        }

        /// <summary>
        /// Gets or sets the protocol processor.
        /// </summary>
        /// <value>
        /// The protocol processor.
        /// </value>
        public IProtocolProcessor ProtocolProcessor { get; set; }

        /// <summary>
        /// Handles the unknown command.
        /// </summary>
        /// <param name="requestInfo">The request info.</param>
        internal protected virtual void HandleUnknownCommand(SubRequestInfo requestInfo)
        {

        }

        /// <summary>
        /// Handles the unknown request.
        /// </summary>
        /// <param name="requestInfo">The request info.</param>
        protected override void HandleUnknownRequest(IWebSocketFragment requestInfo)
        {
            base.Close();
        }
    }
}
