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

namespace TwitchInGames {
    public partial class Chat : MonoBehaviour {
        public Dictionary<string, Channel> Channels => channels.ToDictionary(p => p.Value.Name, p => p.Value);
        public string Login { get; private set; }

        private Dictionary<IntPtr, Channel> channels = new Dictionary<IntPtr, Channel>();
        private IntPtr chat;
        private Exception exception;

        public Coroutine<object> SignIn(string clientId, string token) {
            return new Coroutine<object>(InternalSignIn(clientId, token));
        }

        private IEnumerator InternalSignIn(string clientId, string token) {
            // Validate the arguments.
            Utility.CheckArgument(nameof(clientId), clientId);
            Utility.CheckArgument(nameof(token), token);

            chat = NativeMethods.CreateChat();
            Login = null;
            Action signIn = () => {
                try {
                    Login = NativeMethods.SignInChat(chat, clientId, token);
                } catch(Exception ex) {
                    exception = ex;
                }
            };
            using(var thread = new SingleThread()) {
                exception = null;
                thread.EnqueueTask(signIn);
                yield return new WaitUntil(() => Login != null || exception != null);
            }
            if(exception != null) {
                throw exception;
            }
        }

        public void SignOut() {
            // Check for active channels.
            if(channels.Any()) {
                var message = $"{channels.Count} channels are still open";
                Debug.LogWarning(FormatMessage("SignOut", message));
            }

            channels.Clear();
            NativeMethods.SignOutChat(chat);
        }

        public Coroutine<Channel> JoinChannel(string channelName, Channel.ReceiveCallback receiveCallback) {
            return new Coroutine<Channel>(InternalJoinChannel(channelName, receiveCallback));
        }

        private IEnumerator InternalJoinChannel(string channelName, Channel.ReceiveCallback receiveCallback) {
            // Validate the arguments.
            Utility.CheckArgument(nameof(channelName), channelName);
            Utility.CheckArgument(nameof(receiveCallback), receiveCallback);

            // Check if the client already joined the channel.
            var channel = channels.FirstOrDefault(p => String.Compare(p.Value.Name, channelName, ignoreCase: true) == 0).Value;
            if(channel == null) {
                // No; create a channel and add it to the collection.
                var intPtr = IntPtr.Zero;
                Action createChannel = () => {
                    InternalReceiveCallback internalReceiveCallback = (IntPtr intPtr_, string userName, string message) => {
                        Channel channel_;
                        if(channels.TryGetValue(intPtr_, out channel_)) {
                            receiveCallback(channel_, userName, message);
                        } else {
                            Debug.LogWarning(FormatMessage("JoinChannel", $"cannot find channel for {intPtr_}"));
                        }
                    };
                    try {
                        intPtr = NativeMethods.JoinChannel(chat, channelName, internalReceiveCallback);
                        channel = new Channel(intPtr, this, channelName);
                    } catch(Exception ex) {
                        exception = ex;
                    }
                };
                using(var thread = new SingleThread()) {
                    exception = null;
                    thread.EnqueueTask(createChannel);
                    yield return new WaitUntil(() => channel != null || exception != null);
                }
                if(exception == null) {
                    channels.Add(intPtr, channel);
                } else {
                    throw exception;
                }
            } else {
                // Yes, return it.
                var message = $"Already joined channel \"{channelName}\".";
                Debug.LogWarning(FormatMessage("JoinChannel", message));
            }
            yield return channel;
        }

        private void OnDestroy() {
            SignOut();
        }

        private string FormatMessage(string methodName, string message) {
            var component = $"Chat.{methodName}";
            return Utility.FormatMessage(component, message);
        }

        private delegate void InternalReceiveCallback(IntPtr channel,
            [MarshalAs(UnmanagedType.LPWStr)] string userName,
            [MarshalAs(UnmanagedType.LPWStr)] string message);

#if UNITY_WINDOWS
        private const string ImportName = "UnityWin32";
#else
        private const string ImportName = "__Internal";
#endif

        private static class NativeMethods {
            [DllImport(ImportName, CallingConvention = CallingConvention.Cdecl)]
            internal static extern IntPtr CreateChat();

            [DllImport(ImportName, CallingConvention = CallingConvention.Cdecl)]
            internal static extern void DeleteChat(ref IntPtr chat);

            [DllImport(ImportName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, PreserveSig = false)]
            internal static extern string SignInChat(IntPtr chat, string clientId, string token);

            [DllImport(ImportName, CallingConvention = CallingConvention.Cdecl, PreserveSig = false)]
            internal static extern void SignOutChat(IntPtr chat);

            [DllImport(ImportName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, PreserveSig = false)]
            internal static extern IntPtr JoinChannel(IntPtr chat, string channelName, InternalReceiveCallback receiveCallback);
        }
    }
}
