using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LiteNetLib;
using Multiplayer.Client.Factions;
using Multiplayer.Client.Networking;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;
using RimWorld;
using Steamworks;
using UnityEngine;
using Verse;

namespace Multiplayer.Client
{

    [StaticConstructorOnStartup]
    public class ChatWindow : Window
    {
        public static ChatWindow Opened => Find.WindowStack?.WindowOfType<ChatWindow>();

        public override Vector2 InitialSize => new Vector2(640f, 460f);

        private static readonly Texture2D SelectedMsg = SolidColorMaterials.NewSolidColorTexture(new Color(0.17f, 0.17f, 0.17f, 0.85f));

        public bool saveSize = true;

        private Vector2 chatScroll;
        private Vector2 playerListScroll;
        private Vector2 steamScroll;
        private float messagesHeight;
        private string currentMsg = "";
        private bool hasBeenFocused;
        private Vector2 lastResolution;

        public override float Margin => 0f;

        public ChatWindow()
        {
            absorbInputAroundWindow = false;
            draggable = true;
            soundClose = null;
            preventCameraMotion = false;
            focusWhenOpened = true;
            closeOnClickedOutside = false;
            closeOnAccept = false;
            resizeable = true;
            onlyOneOfTypeAllowed = true;
            closeOnCancel = false;
            resizer = new WindowResizer { minWindowSize = new Vector2(350f, 200f) };

            lastResolution = MpUI.Resolution;

            if (!Multiplayer.session.desynced)
            {
                layer = WindowLayer.GameUI;
                doWindowBackground = !Multiplayer.settings.transparentChat;
                drawShadow = doWindowBackground;
            }
        }

        public override void DoWindowContents(Rect inRect)
        {
            if (Multiplayer.settings.transparentChat && !Mouse.IsOver(inRect))
            {
                GUI.contentColor = new Color(1, 1, 1, 0.8f);
                GUI.backgroundColor = new Color(1, 1, 1, 0.8f);
            }

            if (!drawShadow)
                Widgets.DrawShadowAround(inRect);

            if (Widgets.CloseButtonFor(inRect))
                Close();

            inRect = inRect.ContractedBy(18f);

            Multiplayer.session.hasUnread = false;

            Text.Font = GameFont.Small;

            if (Event.current.type == EventType.KeyDown)
            {
                if (Event.current.keyCode == KeyCode.Return)
                {
                    SendMsg();
                    Event.current.Use();
                }
            }

            const float infoWidth = 140f;
            Rect chat = new Rect(inRect.x, inRect.y, inRect.width - infoWidth - 10f, inRect.height);
            DrawChat(chat);

            GUI.BeginGroup(new Rect(chat.xMax + 10f, chat.y, infoWidth, inRect.height));

            if (Multiplayer.GameComp.multifaction)
            {
                DrawInfo(new Rect(0, 0, infoWidth, inRect.height - 30f));
                if (Widgets.ButtonText(new Rect(50f, inRect.height - 25f, infoWidth - 50f, 25f), "Factions"))
                    Find.WindowStack.Add(new FactionsWindow());
            }
            else
            {
                DrawInfo(new Rect(0, 0, infoWidth, inRect.height));
            }

            GUI.EndGroup();

            if (KeyBindingDefOf.Cancel.KeyDownEvent && Find.WindowStack.focusedWindow == this)
            {
                Close();
                Event.current.Use();
            }
        }

        private void DrawInfo(Rect inRect)
        {
            Widgets.Label(inRect, Multiplayer.session.gameName);
            inRect.yMin += Text.CalcHeight(Multiplayer.session.gameName, inRect.width) + 10f;

            if (Prefs.DevMode)
            {
                var rect = new Rect(inRect).Height(25f).Width(80f);
                if (Widgets.ButtonText(rect, "Net info"))
                    Find.WindowStack.Add(new DebugTextWindow(NetInfoText()));

                inRect.yMin += rect.height + 10f;
            }

            DrawList(
                "MpChatPlayers".Translate(Multiplayer.session.players.Count),
                Multiplayer.session.players,
                p => $"{p.username} ({p.latency}ms)",
                ref inRect,
                ref playerListScroll,
                ClickPlayer,
                extra: (p, rect) =>
                {
                    if (p.type == PlayerType.Steam)
                    {
                        var steamIcon = new Rect(rect.xMax - 24f, 0, 24f, 24f);
                        rect.width -= 24f;
                        GUI.DrawTexture(steamIcon, ContentSourceUtility.ContentSourceIcon_SteamWorkshop);
                        TooltipHandler.TipRegion(steamIcon, new TipSignal($"{p.steamPersonaName}\n{p.steamId}", p.id));
                    }

                    string toolTip = $"{p.username}\n\nPing: {p.latency}ms\n{p.ticksBehind} ticks behind";
                    if (p.simulating)
                        toolTip += "\n(Simulating)";
                    toolTip += $"\nAvg frame time: {p.frameTime:0.00}ms";

                    TooltipHandler.TipRegion(rect, new TipSignal(toolTip, p.id));
                },
                entryLabelColor: e => GetColor(e.status)
            );

            inRect.yMin += 10f;

            DrawList(
                "MpSteamAcceptTitle".Translate(),
                Multiplayer.session.pendingSteam.Keys.ToList(),
                SteamFriends.GetFriendPersonaName,
                ref inRect,
                ref steamScroll,
                SteamIntegration.AcceptPlayerJoinRequest,
                true,
                "MpSteamAcceptDesc".Translate()
            );
        }

        private void ClickPlayer(PlayerInfo p)
        {
            // todo
            return;

            if (p.id == 0 && Event.current.button == 1)
            {
                Find.WindowStack.Add(new FloatMenu(new List<FloatMenuOption>()
                {
                    //new FloatMenuOption("MpSeeModList".Translate(), () => DefMismatchWindow.ShowModList(Multiplayer.session.mods))
                }));
            }
        }

        private Color GetColor(PlayerStatus status)
        {
            switch (status)
            {
                case PlayerStatus.Simulating: return new Color(1, 1, 1, 0.6f);
                case PlayerStatus.Desynced: return new Color(0.8f, 0.4f, 0.4f, 0.8f);
                default: return new Color(1, 1, 1, 0.8f);
            }
        }

        private void DrawList<T>(string label, IList<T> entries, Func<T, string> entryString, ref Rect inRect, ref Vector2 scroll, Action<T> click = null, bool hideEmpty = false, string tooltip = null, Action<T, Rect> extra = null, Func<T, Color?> entryLabelColor = null)
        {
            if (hideEmpty && entries.Count == 0) return;

            Widgets.Label(inRect, label);
            inRect.yMin += 20f;

            float entryHeight = 24f;
            float height = entries.Count * entryHeight;

            Rect outRect = new Rect(0, inRect.yMin, inRect.width, Math.Min(height, Math.Min(230, inRect.height)));
            Rect viewRect = new Rect(0, 0, outRect.width - 16f, height);
            if (viewRect.height <= outRect.height)
                viewRect.width += 16f;

            Widgets.BeginScrollView(outRect, ref scroll, viewRect, true);
            GUI.color = new Color(1, 1, 1, 0.8f);

            float y = height;

            for (int i = entries.Count - 1; i >= 0; i--)
            {
                y -= entryHeight;

                T entry = entries[i];
                string entryLabel = entryString(entry);

                var entryRect = new Rect(0, y, viewRect.width, entryHeight);
                GUI.BeginGroup(entryRect);
                entryRect = entryRect.AtZero();

                if (i % 2 == 0)
                    Widgets.DrawAltRect(entryRect);

                if (Mouse.IsOver(entryRect))
                {
                    GUI.DrawTexture(entryRect, SelectedMsg);
                    if (click != null && Event.current.type == EventType.MouseUp)
                    {
                        click(entry);
                        Event.current.Use();
                    }
                }

                if (tooltip != null)
                    TooltipHandler.TipRegion(entryRect, tooltip);

                var prevColor = GUI.color;
                var labelColor = entryLabelColor?.Invoke(entry);
                if (labelColor != null)
                    GUI.color = labelColor.Value;

                using (MpStyle.Set(TextAnchor.MiddleLeft))
                    Widgets.Label(entryRect, entryLabel.Truncate(entryRect.width));

                if (labelColor != null)
                    GUI.color = prevColor;

                extra?.Invoke(entry, entryRect);

                GUI.EndGroup();
            }

            GUI.color = Color.white;
            Widgets.EndScrollView();

            inRect.yMin += outRect.height;
        }

        private void DrawChat(Rect inRect)
        {
            MpUI.TryUnfocusCurrentNamedControl(this);

            Rect outRect = new Rect(0f, 0f, inRect.width, inRect.height - 30f);
            Rect viewRect = new Rect(0f, 0f, inRect.width - 16f, messagesHeight + 10f);

            float width = viewRect.width;
            Rect textField = new Rect(20f, outRect.yMax + 5f, width - 70f, 25f);

            GUI.BeginGroup(inRect);

            GUI.SetNextControlName("chat_input");
            currentMsg = Widgets.TextField(textField, currentMsg);
            currentMsg = currentMsg.Substring(0, Math.Min(currentMsg.Length, ServerPlayingState.MaxChatMsgLength));

            Widgets.BeginScrollView(outRect, ref chatScroll, viewRect);

            float yPos = 0;
            GUI.color = Color.white;

            int i = 0;

            foreach (ChatMsg msg in Multiplayer.session.messages)
            {
                CalculateMessageSize(msg, width, out var height, out var textWidth);

                Rect msgRect = new Rect(20f, yPos, width - 20f, height);

                if (Mouse.IsOver(msgRect))
                {
                    GUI.DrawTexture(msgRect, SelectedMsg);

                    if (msg.TimeStamp != null)
                        TooltipHandler.TipRegion(msgRect, msg.TimeStamp.ToString("yyyy-MM-dd HH:mm"));
                }

                Color cursorColor = GUI.skin.settings.cursorColor;
                GUI.skin.settings.cursorColor = new Color(0, 0, 0, 0);

                msgRect.width = Math.Min(textWidth, msgRect.width);
                bool mouseOver = Mouse.IsOver(msgRect);

                if (mouseOver && msg.Clickable)
                    GUI.color = new Color(0.8f, 0.8f, 1);

                GUI.SetNextControlName("chat_msg_" + i++);
                DrawMessageTextArea(msgRect, msg);

                if (mouseOver && msg.Clickable)
                {
                    GUI.color = Color.white;

                    if (Event.current.type == EventType.MouseUp)
                        msg.Click();
                }

                GUI.skin.settings.cursorColor = cursorColor;

                yPos += height;
            }

            if (Event.current.type == EventType.Layout)
                messagesHeight = yPos;

            Widgets.EndScrollView();

            if (Widgets.ButtonText(new Rect(textField.xMax + 5f, textField.y, 55f, textField.height), "MpChatSend".Translate()))
                SendMsg();

            GUI.EndGroup();

            if (!hasBeenFocused && Event.current.type == EventType.Repaint)
            {
                chatScroll.y = messagesHeight;

                GUI.FocusControl("chat_input");
                TextEditor editor = (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl);
                editor.OnFocus();
                editor.MoveTextEnd();
                hasBeenFocused = true;
            }
        }

        public override void PreOpen()
        {
            base.PreOpen();
            hasBeenFocused = false;
        }

        public void SendMsg()
        {
            currentMsg = currentMsg.Trim();

            if (currentMsg.NullOrEmpty()) return;

            if (Multiplayer.Client == null)
                Multiplayer.session.AddMsg(Multiplayer.username + ": " + currentMsg);
            else
                Multiplayer.Client.Send(ClientChatPacket.Create(currentMsg, Multiplayer.settings.helpOnlyUsableCommands));

            currentMsg = "";
        }

        private string NetInfoText()
        {
            if (Multiplayer.session == null) return "";

            var text = new StringBuilder();

            if (Multiplayer.Client is ClientLiteNetConnection conn)
            {
                text.AppendLine("Client");
                text.AppendLine(conn.peer.Statistics.ToDebugString());
                text.AppendLine();
            }

            if (Multiplayer.LocalServer != null)
            {
                foreach (var man in Multiplayer.LocalServer.netManagers)
                {
                    text.AppendLine(man.GetDiagnosticsName());
                    text.AppendLine(man.GetDiagnosticsInfo() ?? "<no info>");
                }

                // todo thread problems?
                // foreach (var p in Multiplayer.LocalServer.players.ToList())
                //     if (p.conn is LiteNetConnection net)
                //         LogNetData($"Net Peer {p.Username}", net.peer.Statistics);
            }

            foreach (var remote in Multiplayer.session.knownUsers)
            {
                text.AppendLine($"Steam {SteamFriends.GetFriendPersonaName(remote)}");
                text.AppendLine(remote.ToString());

                var handle = HSteamNetConnection.Invalid;
                if (Multiplayer.LocalServer != null)
                {
                    var player = Multiplayer.LocalServer.playerManager.Players
                        .FirstOrDefault(p => p.conn is SteamSocketServerConn s && s.remoteId == remote);
                    if (player?.conn is SteamSocketServerConn serverConn) handle = serverConn.conn;
                }
                if (handle == HSteamNetConnection.Invalid &&
                    Multiplayer.Client is SteamSocketClientConn clientConn && clientConn.remoteId == remote)
                    handle = clientConn.conn;

                if (handle != HSteamNetConnection.Invalid &&
                    SteamP2PIntegration.TryGetRealTimeStatus(handle, out var status))
                {
                    text.AppendLine($"State: {status.m_eState}");
                    text.AppendLine($"Ping: {status.m_nPing}ms");
                    text.AppendLine($"Quality: {status.m_flConnectionQualityLocal:0.00} local / {status.m_flConnectionQualityRemote:0.00} remote");
                    text.AppendLine($"Pending: {status.m_cbPendingReliable}B reliable  {status.m_cbPendingUnreliable}B unreliable");
                    text.AppendLine($"Unacked: {status.m_cbSentUnackedReliable}B");
                }
                else
                {
                    text.AppendLine("No connection");
                }

                foreach (var pending in Multiplayer.session.pendingSteam.Keys)
                {
                    text.AppendLine($"Steam pending {pending}:{SteamFriends.GetFriendPersonaName(pending)}");
                }

                text.AppendLine();
            }

            return text.ToString();
        }

        public void OnChatReceived()
        {
            chatScroll.y = messagesHeight;
        }

        private static void CalculateMessageSize(ChatMsg msg, float width, out float height, out float textWidth)
        {
            var style = msg.RawMessage ? RawTextAreaStyle() : Text.CurTextAreaReadOnlyStyle;
            var content = new GUIContent(msg.Msg);
            height = style.CalcHeight(content, width - 20f);
            textWidth = style.CalcSize(content).x + 15;
        }

        private static void DrawMessageTextArea(Rect rect, ChatMsg msg)
        {
            if (!msg.RawMessage)
            {
                Widgets.TextArea(rect, msg.Msg, true);
                return;
            }

            GUI.Label(rect, msg.Msg, RawTextAreaStyle());
        }

        private static GUIStyle RawTextAreaStyle()
        {
            return new GUIStyle(Text.CurTextAreaReadOnlyStyle)
            {
                richText = false
            };
        }

        public override void PostClose()
        {
            if (Multiplayer.session != null && saveSize)
            {
                SaveChatSize();
                Multiplayer.settings.Write();
            }
        }

        private void SaveChatSize()
        {
            Multiplayer.settings.chatRect = windowRect;
            Multiplayer.settings.resolutionForChat = MpUI.Resolution;
        }

        public void SetSizeTo(Rect chatRect, Vector2 lastResolution)
        {
            windowRect = chatRect;

            if (chatRect.center.x > lastResolution.x / 2f)
                windowRect.x += UI.screenWidth - lastResolution.x;

            if (chatRect.center.y > lastResolution.y / 2f)
                windowRect.y += UI.screenHeight - lastResolution.y;
        }

        public override void Notify_ResolutionChanged()
        {
            SetSizeTo(windowRect, lastResolution);
            lastResolution = MpUI.Resolution;
        }

        public static void OpenChat()
        {
            var chatWindow = new ChatWindow();
            Find.WindowStack.Add(chatWindow);

            if (Multiplayer.settings.chatRect != default)
                chatWindow.SetSizeTo(Multiplayer.settings.chatRect, Multiplayer.settings.resolutionForChat);
        }
    }

    public abstract class ChatMsg
    {
        public virtual bool Clickable => false;
        public virtual bool RawMessage => false;
        public abstract string Msg { get; }
        public virtual DateTime TimeStamp { get; }

        public ChatMsg()
        {
            TimeStamp = DateTime.Now;
        }

        public virtual void Click() { }
    }

    public class ChatMsg_Text : ChatMsg
    {
        public override string Msg { get; }
        public override bool RawMessage { get; }

        public ChatMsg_Text(string msg, bool rawMessage = false)
        {
            this.Msg = msg;
            this.RawMessage = rawMessage;
        }
    }

    public class ChatMsg_Url : ChatMsg
    {
        public override string Msg => url;
        public override bool Clickable => true;

        private string url;

        public ChatMsg_Url(string url)
        {
            this.url = url;
        }

        public override void Click()
        {
            Application.OpenURL(url);
        }
    }

}
