#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Text;
using System.Threading;

using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
using Best.HTTP.Shared.Streams;

namespace Best.HTTP.Proxies.Implementations
{
    internal enum SOCKSVersions : byte
    {
        Unknown = 0x00,
        V5 = 0x05
    }

    /// <summary>
    /// https://tools.ietf.org/html/rfc1928
    ///   The values currently defined for METHOD are:
    ///     o  X'00' NO AUTHENTICATION REQUIRED
    ///     o  X'01' GSSAPI
    ///     o  X'02' USERNAME/PASSWORD
    ///     o  X'03' to X'7F' IANA ASSIGNED
    ///     o  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
    ///     o  X'FF' NO ACCEPTABLE METHODS
    /// </summary>
    internal enum SOCKSMethods : byte
    {
        NoAuthenticationRequired = 0x00,
        GSSAPI = 0x01,
        UsernameAndPassword = 0x02,
        NoAcceptableMethods = 0xFF
    }

    internal enum SOCKSReplies : byte
    {
        Succeeded = 0x00,
        GeneralSOCKSServerFailure = 0x01,
        ConnectionNotAllowedByRuleset = 0x02,
        NetworkUnreachable = 0x03,
        HostUnreachable = 0x04,
        ConnectionRefused = 0x05,
        TTLExpired = 0x06,
        CommandNotSupported = 0x07,
        AddressTypeNotSupported = 0x08
    }

    internal enum SOCKSAddressTypes
    {
        IPV4 = 0x00,
        DomainName = 0x03,
        IPv6 = 0x04
    }

    internal sealed class SOCKSV5Negotiator : IContentConsumer
    {
        public PeekableContentProviderStream ContentProvider { get; private set; }

        enum NegotiationStates
        {
            MethodSelection,
            ExpectAuthenticationResponse,
            ConnectResponse
        }

        NegotiationStates _state;

        SOCKSProxy _proxy;
        ProxyConnectParameters _parameters;

        public SOCKSV5Negotiator(SOCKSProxy proxy, ProxyConnectParameters parameters)
        {
            this._proxy = proxy;
            this._parameters = parameters;

            //(this._parameters.stream as IPeekableContentProvider).Consumer = this;
            (this._parameters.stream as PeekableContentProviderStream).SetTwoWayBinding(this);

            SendHandshake();
        }

        public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
        public void UnsetBinding() => this.ContentProvider = null;

        public void OnConnectionClosed()
        {
            CallOnError(new Exception($"{nameof(SOCKSV5Negotiator)}: connection closed unexpectedly!"));
        }

        public void OnError(Exception ex)
        {
            CallOnError(ex);
        }

        void SendHandshake()
        {
            var buffer = BufferPool.Get(1024, true);
            try
            {
                int count = 0;

                // https://tools.ietf.org/html/rfc1928
                //   The client connects to the server, and sends a version
                //   identifier/method selection message:
                //
                //                   +----+----------+----------+
                //                   |VER | NMETHODS | METHODS  |
                //                   +----+----------+----------+
                //                   | 1  |    1     | 1 to 255 |
                //                   +----+----------+----------+
                //
                //   The VER field is set to X'05' for this version of the protocol.  The
                //   NMETHODS field contains the number of method identifier octets that
                //   appear in the METHODS field.
                //

                buffer[count++] = (byte)SOCKSVersions.V5;
                if (this._proxy.Credentials != null)
                {
                    buffer[count++] = 0x02; // method count
                    buffer[count++] = (byte)SOCKSMethods.UsernameAndPassword;
                    buffer[count++] = (byte)SOCKSMethods.NoAuthenticationRequired;
                }
                else
                {
                    buffer[count++] = 0x01; // method count
                    buffer[count++] = (byte)SOCKSMethods.NoAuthenticationRequired;
                }

                if (HTTPManager.Logger.IsDiagnostic)
                    HTTPManager.Logger.Information("SOCKSProxy", $"Sending method negotiation - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);

                // enqueue buffer and move its ownership to the tcp streamer
                this._parameters.stream.Write(buffer.AsBuffer(count));

                // null out the buffer so it won't be released 
                buffer = null;

            }
            catch (Exception ex)
            {
                CallOnError(ex);
            }
            finally
            {
                BufferPool.Release(buffer);
            }
        }

        void SendConnect()
        {
            //   The SOCKS request is formed as follows:
            //
            //        +----+-----+-------+------+----------+----------+
            //        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
            //        +----+-----+-------+------+----------+----------+
            //        | 1  |  1  | X'00' |  1   | Variable |    2     |
            //        +----+-----+-------+------+----------+----------+
            //
            //     Where:
            //
            //          o  VER    protocol version: X'05'
            //          o  CMD
            //             o  CONNECT X'01'
            //             o  BIND X'02'
            //             o  UDP ASSOCIATE X'03'
            //          o  RSV    RESERVED
            //          o  ATYP   address type of following address
            //             o  IP V4 address: X'01'
            //             o  DOMAINNAME: X'03'
            //             o  IP V6 address: X'04'
            //          o  DST.ADDR       desired destination address
            //          o  DST.PORT desired destination port in network octet
            //             order

            var buffer = BufferPool.Get(512, true);
            int count = 0;
            buffer[count++] = (byte)SOCKSVersions.V5; // version: 5
            buffer[count++] = 0x01; // command: connect
            buffer[count++] = 0x00; // reserved, bust be 0x00

            if (this._parameters.uri.IsHostIsAnIPAddress())
            {
                bool isIPV4 = Extensions.IsIpV4AddressValid(this._parameters.uri.Host);
                buffer[count++] = isIPV4 ? (byte)SOCKSAddressTypes.IPV4 : (byte)SOCKSAddressTypes.IPv6;

                var ipAddress = System.Net.IPAddress.Parse(this._parameters.uri.Host);
                var ipBytes = ipAddress.GetAddressBytes();
                WriteBytes(buffer, ref count, ipBytes); // destination address
            }
            else
            {
                buffer[count++] = (byte)SOCKSAddressTypes.DomainName;

                // The first octet of the address field contains the number of octets of name that
                // follow, there is no terminating NUL octet.
                WriteString(buffer, ref count, this._parameters.uri.Host);
            }

            // destination port in network octet order
            buffer[count++] = (byte)((this._parameters.uri.Port >> 8) & 0xFF);
            buffer[count++] = (byte)(this._parameters.uri.Port & 0xFF);

            if (HTTPManager.Logger.IsDiagnostic)
                HTTPManager.Logger.Information("SOCKSProxy", $"Sending connect request - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);

            this._parameters.stream.Write(buffer.AsBuffer(count));

            this._state = NegotiationStates.ConnectResponse;
        }

        public void OnContent()
        {
            try
            {
                switch (this._state)
                {
                    case NegotiationStates.MethodSelection:
                        {
                            if (this.ContentProvider.Length < 2)
                                return;

                            // Read method selection result

                            //count = stream.Read(buffer, 0, buffer.Length);
                            var buffer = BufferPool.Get(BufferPool.MIN_BUFFER_SIZE, true);
                            int count = this.ContentProvider.Read(buffer, 0, buffer.Length);

                            if (HTTPManager.Logger.IsDiagnostic)
                                HTTPManager.Logger.Information("SOCKSProxy", $"Negotiation response - count: {count} buffer: {buffer.AsBuffer(count)}", this._parameters.context);

                            //   The server selects from one of the methods given in METHODS, and
                            //   sends a METHOD selection message:
                            //
                            //                         +----+--------+
                            //                         |VER | METHOD |
                            //                         +----+--------+
                            //                         | 1  |   1    |
                            //                         +----+--------+
                            //
                            //   If the selected METHOD is X'FF', none of the methods listed by the
                            //   client are acceptable, and the client MUST close the connection.
                            //
                            //   The values currently defined for METHOD are:
                            //
                            //          o  X'00' NO AUTHENTICATION REQUIRED
                            //          o  X'01' GSSAPI
                            //          o  X'02' USERNAME/PASSWORD
                            //          o  X'03' to X'7F' IANA ASSIGNED
                            //          o  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
                            //          o  X'FF' NO ACCEPTABLE METHODS
                            //
                            //   The client and server then enter a method-specific sub-negotiation.

                            SOCKSVersions version = (SOCKSVersions)buffer[0];
                            SOCKSMethods method = (SOCKSMethods)buffer[1];

                            // Expected result:
                            //  1.) Received bytes' count is 2: version + preferred method
                            //  2.) Version must be 5
                            //  3.) Preferred method must NOT be 0xFF
                            if (count != 2)
                                throw new Exception($"SOCKS Proxy - Expected read count: 2! buffer: {buffer.AsBuffer(count)}");
                            else if (version != SOCKSVersions.V5)
                                throw new Exception("SOCKS Proxy - Expected version: 5, received version: " + buffer[0].ToString("X2"));
                            else if (method == SOCKSMethods.NoAcceptableMethods)
                                throw new Exception("SOCKS Proxy - Received 'NO ACCEPTABLE METHODS' (0xFF)");
                            else
                            {
                                HTTPManager.Logger.Information("SOCKSProxy", "Method negotiation over. Method: " + method.ToString(), this._parameters.context);
                                switch (method)
                                {
                                    case SOCKSMethods.NoAuthenticationRequired:
                                        SendConnect();
                                        break;

                                    case SOCKSMethods.UsernameAndPassword:
                                        if (this._proxy.Credentials.UserName.Length > 255)
                                            throw new Exception($"SOCKS Proxy - Credentials.UserName too long! {this._proxy.Credentials.UserName.Length} > 255");
                                        if (this._proxy.Credentials.Password.Length > 255)
                                            throw new Exception($"SOCKS Proxy - Credentials.Password too long! {this._proxy.Credentials.Password.Length} > 255");

                                        // https://tools.ietf.org/html/rfc1929 : Username/Password Authentication for SOCKS V5
                                        //   Once the SOCKS V5 server has started, and the client has selected the
                                        //   Username/Password Authentication protocol, the Username/Password
                                        //   subnegotiation begins.  This begins with the client producing a
                                        //   Username/Password request:
                                        //
                                        //           +----+------+----------+------+----------+
                                        //           |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
                                        //           +----+------+----------+------+----------+
                                        //           | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
                                        //           +----+------+----------+------+----------+

                                        HTTPManager.Logger.Information("SOCKSProxy", "starting sub-negotiation", this._parameters.context);
                                        count = 0;
                                        buffer[count++] = 0x01; // version of sub negotiation

                                        WriteString(buffer, ref count, this._proxy.Credentials.UserName);
                                        WriteString(buffer, ref count, this._proxy.Credentials.Password);

                                        if (HTTPManager.Logger.IsDiagnostic)
                                            HTTPManager.Logger.Information("SOCKSProxy", $"Sending username and password sub-negotiation - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);

                                        // Write negotiation and transfer ownership of buffer
                                        this._parameters.stream.Write(buffer.AsBuffer(count));

                                        this._state = NegotiationStates.ExpectAuthenticationResponse;
                                        break;

                                    case SOCKSMethods.GSSAPI:
                                        throw new Exception("SOCKS proxy: GSSAPI not supported!");

                                    case SOCKSMethods.NoAcceptableMethods:
                                        throw new Exception("SOCKS proxy: No acceptable method");
                                }
                            }
                            break;
                        }

                    case NegotiationStates.ExpectAuthenticationResponse:
                        {
                            if (this.ContentProvider.Length < 2)
                                return;

                            // Read result
                            var buffer = BufferPool.Get(512, true);
                            var count = this._parameters.stream.Read(buffer, 0, buffer.Length);

                            if (HTTPManager.Logger.IsDiagnostic)
                                HTTPManager.Logger.Information("SOCKSProxy", $"Username and password sub-negotiation response - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);

                            //   The server verifies the supplied UNAME and PASSWD, and sends the
                            //   following response:
                            //
                            //                        +----+--------+
                            //                        |VER | STATUS |
                            //                        +----+--------+
                            //                        | 1  |   1    |
                            //                        +----+--------+

                            // A STATUS field of X'00' indicates success. If the server returns a
                            // `failure' (STATUS value other than X'00') status, it MUST close the
                            // connection.
                            bool success = buffer[1] == 0;

                            if (count != 2)
                                throw new Exception($"SOCKS Proxy - Expected read count: 2! buffer: {buffer.AsBuffer(count)}");
                            else if (!success)
                                throw new Exception("SOCKS proxy: username+password authentication failed!");

                            HTTPManager.Logger.Information("SOCKSProxy", "Authenticated!", this._parameters.context);

                            // Send connect
                            SendConnect();
                            break;
                        }

                    case NegotiationStates.ConnectResponse:
                        {
                            if (this.ContentProvider.Length < 10)
                                return;

                            var buffer = BufferPool.Get(512, true);
                            var count = this._parameters.stream.Read(buffer, 0, buffer.Length);

                            if (HTTPManager.Logger.IsDiagnostic)
                                HTTPManager.Logger.Information("SOCKSProxy", $"Connect response - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);

                            //   The SOCKS request information is sent by the client as soon as it has
                            //   established a connection to the SOCKS server, and completed the
                            //   authentication negotiations.  The server evaluates the request, and
                            //   returns a reply formed as follows:
                            //
                            //        +----+-----+-------+------+----------+----------+
                            //        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
                            //        +----+-----+-------+------+----------+----------+
                            //        | 1  |  1  | X'00' |  1   | Variable |    2     |
                            //        +----+-----+-------+------+----------+----------+
                            //
                            //     Where:
                            //          o  VER    protocol version: X'05'
                            //          o  REP    Reply field:
                            //             o  X'00' succeeded
                            //             o  X'01' general SOCKS server failure
                            //             o  X'02' connection not allowed by ruleset
                            //             o  X'03' Network unreachable
                            //             o  X'04' Host unreachable
                            //             o  X'05' Connection refused
                            //             o  X'06' TTL expired
                            //             o  X'07' Command not supported
                            //             o  X'08' Address type not supported
                            //             o  X'09' to X'FF' unassigned
                            //          o  RSV    RESERVED
                            //          o  ATYP   address type of following address
                            //             o  IP V4 address: X'01'
                            //             o  DOMAINNAME: X'03'
                            //             o  IP V6 address: X'04'
                            //          o  BND.ADDR       server bound address
                            //          o  BND.PORT       server bound port in network octet order
                            //
                            //   Fields marked RESERVED (RSV) must be set to X'00'.

                            SOCKSVersions version = (SOCKSVersions)buffer[0];
                            SOCKSReplies reply = (SOCKSReplies)buffer[1];

                            // at least 10 bytes expected as a result
                            if (count < 10)
                                throw new Exception($"SOCKS proxy: not enough data returned by the server. Expected count is at least 10 bytes, server returned {count} bytes! content: {buffer.AsBuffer(count)}");
                            else if (reply != SOCKSReplies.Succeeded)
                                throw new Exception("SOCKS proxy error: " + reply.ToString());

                            HTTPManager.Logger.Information("SOCKSProxy", "Connected!", this._parameters.context);

                            CallOnSuccess();
                            break;
                        }
                }
            }
            catch(Exception ex)
            {
                CallOnError(ex);
            }
        }

        void CallOnError(Exception ex)
        {
            var callback = Interlocked.Exchange(ref this._parameters.OnError, null);
            Interlocked.Exchange(ref this._parameters.OnSuccess, null);

            this.ContentProvider.Unbind();
            callback?.Invoke(this._parameters, ex, false);
        }

        void CallOnSuccess()
        {
            var callback = Interlocked.Exchange(ref this._parameters.OnSuccess, null);
            Interlocked.Exchange(ref this._parameters.OnError, null);

            this.ContentProvider.Unbind();

            callback?.Invoke(this._parameters);
        }

        private void WriteString(byte[] buffer, ref int count, string str)
        {
            // Get the bytes
            int byteCount = Encoding.UTF8.GetByteCount(str);
            if (byteCount > 255)
                throw new Exception(string.Format("SOCKS Proxy - String is too large ({0}) to fit in 255 bytes!", byteCount.ToString()));

            // number of bytes
            buffer[count++] = (byte)byteCount;

            // and the bytes itself
            Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, count);

            count += byteCount;
        }

        private void WriteBytes(byte[] buffer, ref int count, byte[] bytes)
        {
            Array.Copy(bytes, 0, buffer, count, bytes.Length);
            count += bytes.Length;
        }
    }
}
#endif
