using System;
using System.Collections;
using System.Runtime.InteropServices;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Networking;

namespace TwitchInGames {
    public class ClientAuth : IDisposable {
        public string AccessToken { get; private set; } = "";

        private IntPtr clientAuth;

        private const string urlFormat = "https://id.twitch.tv/oauth2/authorize?response_type=token&client_id={0}&redirect_uri={1}&scope={2}";
        private string state;

        public Coroutine<string> StartAuth(string clientId, string redirectUri, IEnumerable<string> scopes, RectOffset margins) {
            return new Coroutine<string>(InternalStartAuth(clientId, redirectUri, scopes, margins));
        }

        public IEnumerator InternalStartAuth(string clientId, string redirectUri, IEnumerable<string> scopes, RectOffset margins) {
            // Validate the arguments.
            Utility.CheckArgument(nameof(clientId), clientId);
            Utility.CheckArgument(nameof(redirectUri), redirectUri);
            if(clientAuth != IntPtr.Zero) {
                throw new InvalidOperationException("client auth already started");
            }

            // Extract the scheme from the redirect URI.
            var i = redirectUri.IndexOf(':');
            var scheme = redirectUri.Substring(0, i + 1);

            // Compose the authentication URL.
            var scope = scopes != null ? String.Join(" ", scopes.ToArray()) : "";
            var escapedClientId = UnityWebRequest.EscapeURL(clientId);
            var escapedRedirectUri = UnityWebRequest.EscapeURL(redirectUri);
            var escapedScope = UnityWebRequest.EscapeURL(scope);
            var url = String.Format(urlFormat, escapedClientId, escapedRedirectUri, escapedScope);

            // Include thirty-two characters of random state.
            var r = new System.Random();
            var q = Enumerable.Range(0, 32).Select(_ => (char)(((r.Next() & 0x6f) | 0x40) + 1));
            state = new String(q.ToArray());
            url += "&state=";
            url += state;

            yield return DoClientAuth(url, scheme, margins);
        }

        public void Cancel() {
            if(clientAuth != IntPtr.Zero) {
                NativeMethods.CancelClientAuth(clientAuth);
            }
        }

        private bool HasReceivedToken() {
            var responseUri = NativeMethods.GetClientAuthResponseUri(clientAuth);
            if(responseUri != null) {
                AccessToken = "";
                var i = responseUri.IndexOfAny(new[] { '#', '?' });
                if(i >= 0) {
                    var parts = responseUri.Substring(i + 1).Split('&');
                    var q = from s in parts
                        let p = s.Split('=')
                        let n = UnityWebRequest.UnEscapeURL(p.First())
                        let v = UnityWebRequest.UnEscapeURL(p.Last())
                        select new { Name = n, Value = v };
                    var d = q.ToDictionary(a => a.Name, a => a.Value);
                    string accessToken;
                    if(d.TryGetValue("access_token", out accessToken)) {
                        string actualState;
                        if(d.TryGetValue("state", out actualState)) {
                            actualState = UnityWebRequest.UnEscapeURL(actualState);
                            if(actualState == state) {
                                AccessToken = UnityWebRequest.UnEscapeURL(accessToken);
                            }
                        }
                    }
                }
                return true;
            }
            return false;
        }

        private IEnumerator DoClientAuth(string url, string scheme, RectOffset margins) {
            clientAuth = NativeMethods.CreateClientAuth(margins.left, margins.top, margins.right, margins.bottom);
            NativeMethods.StartClientAuth(clientAuth, url, scheme);
            AccessToken = null;
            yield return new WaitUntil(HasReceivedToken);
            NativeMethods.DeleteClientAuth(ref clientAuth);
            yield return AccessToken;
        }

        public static void ClearCookies() {
            NativeMethods.ClearClientAuthCookies();
        }

        #region IDisposable Support

        private bool isDisposed = false;

        protected virtual void Dispose(bool disposing) {
            if(!isDisposed) {
                if(disposing) {
                    // Dispose of managed state.
                }

                // Free unmanaged resources.
                NativeMethods.DeleteClientAuth(ref clientAuth);

                isDisposed = true;
            }
        }

        ~ClientAuth() {
            Dispose(false);
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        private static class NativeMethods {
            static NativeMethods() {
                SetApplicationName(Application.productName);
            }

            [DllImport("__Internal")]
            internal static extern IntPtr CreateClientAuth(int left, int top, int right, int bottom);

            [DllImport("__Internal")]
            internal static extern void StartClientAuth(IntPtr clientAuth, string url, string scheme);

            [DllImport("__Internal")]
            internal static extern string GetClientAuthResponseUri(IntPtr clientAuth);

            [DllImport("__Internal")]
            internal static extern void CancelClientAuth(IntPtr clientAuth);

            [DllImport("__Internal")]
            internal static extern void DeleteClientAuth(ref IntPtr clientAuth);

            [DllImport("__Internal")]
            internal static extern void ClearClientAuthCookies();

            [DllImport("__Internal")]
            internal static extern void SetApplicationName(string applicationName);
        }
    }
}
