using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Shadowsocks.Controller;
using Shadowsocks.Util.Sockets;

namespace Shadowsocks.Proxy
{
    public class Socks5Proxy : IProxy
    {
        private class FakeAsyncResult : IAsyncResult
        {
            public readonly Socks5State innerState;

            private readonly IAsyncResult r;

            public FakeAsyncResult(IAsyncResult orig, Socks5State state)
            {
                r = orig;
                innerState = state;
            }

            public bool IsCompleted => r.IsCompleted;
            public WaitHandle AsyncWaitHandle => r.AsyncWaitHandle;
            public object AsyncState => innerState.AsyncState;
            public bool CompletedSynchronously => r.CompletedSynchronously;
        }

        private class Socks5State
        {
            public AsyncCallback Callback { get; set; }

            public object AsyncState { get; set; }

            public int BytesToRead;

            public Exception ex { get; set; }
        }

        private readonly WrappedSocket _remote = new WrappedSocket();

        private const int Socks5PktMaxSize = 4 + 16 + 2;
        private readonly byte[] _receiveBuffer = new byte[Socks5PktMaxSize];

        public EndPoint LocalEndPoint => _remote.LocalEndPoint;
        public EndPoint ProxyEndPoint { get; private set; }
        public EndPoint DestEndPoint { get; private set; }

        public void BeginConnectProxy(EndPoint remoteEP, AsyncCallback callback, object state)
        {
            var st = new Socks5State();
            st.Callback = callback;
            st.AsyncState = state;

            ProxyEndPoint = remoteEP;

            _remote.BeginConnect(remoteEP, ConnectCallback, st);
        }

        public void EndConnectProxy(IAsyncResult asyncResult)
        {
            var state = ((FakeAsyncResult)asyncResult).innerState;

            if (state.ex != null)
            {
                throw state.ex;
            }
        }

        public void BeginConnectDest(EndPoint destEndPoint, AsyncCallback callback, object state, NetworkCredential auth = null)
        {
            // TODO: support SOCKS5 auth
            DestEndPoint = destEndPoint;

            byte[] request = null;
            byte atyp = 0;
            int port;

            var dep = destEndPoint as DnsEndPoint;
            if (dep != null)
            {
                // is a domain name, we will leave it to server

                atyp = 3; // DOMAINNAME
                var enc = Encoding.UTF8;
                var hostByteCount = enc.GetByteCount(dep.Host);

                request = new byte[4 + 1/*length byte*/ + hostByteCount + 2];
                request[4] = (byte)hostByteCount;
                enc.GetBytes(dep.Host, 0, dep.Host.Length, request, 5);

                port = dep.Port;
            }
            else
            {
                switch (DestEndPoint.AddressFamily)
                {
                    case AddressFamily.InterNetwork:
                        request = new byte[4 + 4 + 2];
                        atyp = 1; // IP V4 address
                        break;
                    case AddressFamily.InterNetworkV6:
                        request = new byte[4 + 16 + 2];
                        atyp = 4; // IP V6 address
                        break;
                    default:
                        throw new Exception(I18N.GetString("Proxy request failed"));
                }
                port = ((IPEndPoint) DestEndPoint).Port;
                var addr = ((IPEndPoint)DestEndPoint).Address.GetAddressBytes();
                Array.Copy(addr, 0, request, 4, request.Length - 4 - 2);
            }

            // 构造request包剩余部分
            request[0] = 5;
            request[1] = 1;
            request[2] = 0;
            request[3] = atyp;
            request[request.Length - 2] = (byte) ((port >> 8) & 0xff);
            request[request.Length - 1] = (byte) (port & 0xff);

            var st = new Socks5State();
            st.Callback = callback;
            st.AsyncState = state;

            _remote.BeginSend(request, 0, request.Length, 0, Socks5RequestSendCallback, st);
        }

        public void EndConnectDest(IAsyncResult asyncResult)
        {
            var state = ((FakeAsyncResult)asyncResult).innerState;

            if (state.ex != null)
            {
                throw state.ex;
            }
        }

        public void BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback,
            object state)
        {
            _remote.BeginSend(buffer, offset, size, socketFlags, callback, state);
        }

        public int EndSend(IAsyncResult asyncResult)
        {
            return _remote.EndSend(asyncResult);
        }

        public void BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback,
            object state)
        {
            _remote.BeginReceive(buffer, offset, size, socketFlags, callback, state);
        }

        public int EndReceive(IAsyncResult asyncResult)
        {
            return _remote.EndReceive(asyncResult);
        }

        public void Shutdown(SocketShutdown how)
        {
            _remote.Shutdown(how);
        }

        public void Close()
        {
            _remote.Dispose();
        }
        

        private void ConnectCallback(IAsyncResult ar)
        {
            var state = (Socks5State) ar.AsyncState;
            try
            {
                _remote.EndConnect(ar);

                _remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);

                byte[] handshake = {5, 1, 0};
                _remote.BeginSend(handshake, 0, handshake.Length, 0, Socks5HandshakeSendCallback, state);
            }
            catch (Exception ex)
            {
                state.ex = ex;
                state.Callback?.Invoke(new FakeAsyncResult(ar, state));
            }
        }

        private void Socks5HandshakeSendCallback(IAsyncResult ar)
        {
            var state = (Socks5State)ar.AsyncState;
            try
            {
                _remote.EndSend(ar);

                _remote.BeginReceive(_receiveBuffer, 0, 2, 0, Socks5HandshakeReceiveCallback, state);
            }
            catch (Exception ex)
            {
                state.ex = ex;
                state.Callback?.Invoke(new FakeAsyncResult(ar, state));
            }
        }

        private void Socks5HandshakeReceiveCallback(IAsyncResult ar)
        {
            Exception ex = null;
            var state = (Socks5State)ar.AsyncState;
            try
            {
                var bytesRead = _remote.EndReceive(ar);
                if (bytesRead >= 2)
                {
                    if (_receiveBuffer[0] != 5 || _receiveBuffer[1] != 0)
                    {
                        ex = new Exception(I18N.GetString("Proxy handshake failed"));
                    }
                }
                else
                {
                    ex = new Exception(I18N.GetString("Proxy handshake failed"));
                }
            }
            catch (Exception ex2)
            {
                ex = ex2;
            }
            state.ex = ex;
            state.Callback?.Invoke(new FakeAsyncResult(ar, state));
        }


        private void Socks5RequestSendCallback(IAsyncResult ar)
        {
            var state = (Socks5State)ar.AsyncState;
            try
            {
                _remote.EndSend(ar);

                _remote.BeginReceive(_receiveBuffer, 0, 4, 0, Socks5ReplyReceiveCallback, state);
            }
            catch (Exception ex)
            {
                state.ex = ex;
                state.Callback?.Invoke(new FakeAsyncResult(ar, state));
            }
        }

        private void Socks5ReplyReceiveCallback(IAsyncResult ar)
        {
            var state = (Socks5State)ar.AsyncState;
            try
            {
                var bytesRead = _remote.EndReceive(ar);
                if (bytesRead >= 4)
                {
                    if (_receiveBuffer[0] == 5 && _receiveBuffer[1] == 0)
                    {
                        // 跳过剩下的reply
                        switch (_receiveBuffer[3]) // atyp
                        {
                            case 1:
                                state.BytesToRead = 4 + 2;
                                _remote.BeginReceive(_receiveBuffer, 0, 4 + 2, 0, Socks5ReplyReceiveCallback2, state);
                                break;
                            case 4:
                                state.BytesToRead = 16 + 2;
                                _remote.BeginReceive(_receiveBuffer, 0, 16 + 2, 0, Socks5ReplyReceiveCallback2, state);
                                break;
                            default:
                                state.ex = new Exception(I18N.GetString("Proxy request failed"));
                                state.Callback?.Invoke(new FakeAsyncResult(ar, state));
                                break;
                        }
                    }
                    else
                    {
                        state.ex = new Exception(I18N.GetString("Proxy request failed"));
                        state.Callback?.Invoke(new FakeAsyncResult(ar, state));
                    }
                }
                else
                {
                    state.ex = new Exception(I18N.GetString("Proxy request failed"));
                    state.Callback?.Invoke(new FakeAsyncResult(ar, state));
                }
            }
            catch (Exception ex)
            {
                state.ex = ex;
                state.Callback?.Invoke(new FakeAsyncResult(ar, state));
            }
        }


        private void Socks5ReplyReceiveCallback2(IAsyncResult ar)
        {
            Exception ex = null;
            var state = (Socks5State)ar.AsyncState;
            try
            {
                var bytesRead = _remote.EndReceive(ar);
                var bytesNeedSkip = state.BytesToRead;

                if (bytesRead < bytesNeedSkip)
                {
                    ex = new Exception(I18N.GetString("Proxy request failed"));
                }
            }
            catch (Exception ex2)
            {
                ex = ex2;
            }

            state.ex = ex;
            state.Callback?.Invoke(new FakeAsyncResult(ar, state));
        }
    }
}
