using bessw.Unity.WebView;
using bessw.Unity.WebView.ChromeDevTools;
using bessw.Unity.WebView.ChromeDevTools.Protocol.DOM;
using MoreLinq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using REST_JSON_API;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.UIElements;

public class WebViewController : ScrollView
{
    private WebViewComponent webViewComponent;
    private DomNodeWrapper[] dropzones;

    private void Awake()
    {
        webViewComponent = GetComponent<WebViewComponent>();
        WebViewComponent.serializerSettings.Converters.Add(new FactObjectUIConverter());
        webViewComponent.targetUrl = Path.Join(CommunicationEvents.Get_DataPath(), "scrollView.html");
        webViewComponent.OnWebViewComponentReady += OnWebViewComponentReady;

        // TODO: handle webViewComponent.onDomDocumentUpdated
    }
    private void OnWebViewComponentReady()
    {
        webViewComponent.onDomDocumentUpdated += DocumentUpdatedHandler;
        SwitchScrollUI.activeScrollData.OnScrollChanged.AddListener(SetScrollContent);
        SwitchScrollUI.activeScrollData.OnScrollDynamicInfoUpdated.AddListener(SetScrollContent);
        SwitchScrollUI.activeScrollData.HintAvailableEvent.AddListener(OnHintAvailable);
        webViewComponent.tab.AddJSBinding("applyScroll", ApplyScrollHandler);
        webViewComponent.tab.AddJSBinding("getHint", GetHintHandler);
        // webViewComponent.tab.AddJSBinding("updateAssignments", UpdateAssignmentsHandler); // The browser can just ask the MMTServer directly
        if (SwitchScrollUI.activeScrollData.Scroll is not null)
        {
            SetScrollContent(SwitchScrollUI.activeScrollData.RenderedScroll);
        }
        RegisterBrowserEventhandlers();
    }
    private void OnDisable()
    {
        webViewComponent.onDomDocumentUpdated -= DocumentUpdatedHandler;
        SwitchScrollUI.activeScrollData.OnScrollChanged.RemoveListener(SetScrollContent);
        SwitchScrollUI.activeScrollData.OnScrollDynamicInfoUpdated.RemoveListener(SetScrollContent);
        webViewComponent.tab.RemoveJSBinding("applyScroll");
        webViewComponent.tab.RemoveJSBinding("getHint");
        DeRegisterBrowserEventhandlers();
    }

    private void DocumentUpdatedHandler(documentUpdatedEvent _)
    {
        // all old DomNodeWrapper objects are invalid, because the whole document has been updated
        dropzones = null;
        RegisterBrowserEventhandlers();
    }

    private async void RegisterBrowserEventhandlers()
    {
        // register js event handlers in the browser
        _ = webViewComponent.tab.Evaluate("addDropZoneEventListeners()");
        
        // register c# event handlers
        // get the dropzones from the scroll and add the event handler to track when something is dropped
        DomNodeWrapper document = await webViewComponent.tab.Document;
        dropzones = await document.querySelectorAllAsync("[dropzone='copy']");
        dropzones.ForEach(dropzone => dropzone.OnAttributeModified += DropzoneAttributeModifiedHandler);
    }

    private void DeRegisterBrowserEventhandlers()
    {
        dropzones?.ForEach(dropzone => dropzone.OnAttributeModified -= DropzoneAttributeModifiedHandler);
    }

    /// <summary>
    /// <para>
    /// Hand the ScrollView the labels (and potentially assignments) of all facts, so it can render the scroll content. (And then tell it to do so)
    /// </para><para>
    /// More precisely: Put a stringified and Json serialized Dictionary 
    /// <see cref="OMS.uri"/> slotID  -> (<see cref="string"/> label, <see cref="Fact.Id"/> factId) <br/>
    /// into the "data-assignments" attribute of the "assignments" node
    /// </para>
    /// </summary>
    /// <param name="scroll"></param>
    private async void SetScrollContent(Scroll scroll)
    {
        // update scroll container content
        DomNodeWrapper document = await webViewComponent.tab.Document;
        DomNodeWrapper scrollContainer = await document.querySelectorAsync("#scrollContainer"); 

        var description = scroll.description;
        if (!Regex.IsMatch(description, ".*<scroll-description.*"))
        {
            // scroll is a legacy plain text scroll, generate html with slots for the required facts
            var factSlots = SwitchScrollUI.activeScrollData.Assignments
                .Where(pair => pair.Value.IsVisible)
                .Select(pair =>
                    @$"<span class='legacySlot' dropzone='copy' data-slot-id='{pair.Key}' {(pair.Value.IsSet ? $"data-fact-id='{pair.Value.fact.Id}'" : "")}>
                        {(pair.Value.IsSet ?
                            pair.Value.fact.GetLabel(StageStatic.stage.factState) :
                            scroll.requiredFacts.Find(fact => fact.@ref.uri == pair.Key).label)}
                    </span>");

            description = $"<scroll-description><p>{scroll.description}</p><div id='legacySlots'>{String.Join("", factSlots)}</div></scroll-description>";
            
        }
        //else // Is done by JS. How to render the scroll is the scrolls buisness
        //{
        //    // replace slot templates
        //    description = Regex.Replace(description, "<scroll-slot([^>]*)>([^<]*)</scroll-slot>", match =>
        //    {
        //        var extraAttributes = match.Groups[1].Value;
        //        var slotId = match.Groups[2].Value;
        //        var assignment = SwitchScrollUI.activeScrollData.Assignments[slotId];
        //        /** label of the assigned fact, or the slot label if nothing assigned */
        //        var label = assignment.IsSet    ? assignment.fact.GetLabel(StageStatic.stage.factState) 
        //                                        : scroll.requiredFacts.Find(fact => fact.@ref.uri == slotId).label;
        //        /** id of the assigned fact. If nothing is assigned don't add the attribute */
        //        var fact_id = assignment.IsSet  ? $"data-fact-id='{assignment.fact.Id}'" 
        //                                        : "";
        //        return $"<mi dropzone='copy' data-slot-id='{slotId}' {fact_id} {extraAttributes}>{label}</mi>";
        //    });
        //    // replace solution templates
        //    description = Regex.Replace(description, "<scroll-solution([^>]*)>([^<]*)</scroll-solution>", match =>
        //    {
        //        var extraAttributes = match.Groups[1].Value;
        //        var solutionId = match.Groups[2].Value;
        //        var label = scroll.acquiredFacts.Find(fact => fact.@ref.uri == solutionId).label;
        //        return $"<mi data-solution-id='{solutionId}' {extraAttributes}>{label}</mi>";
        //    });
        //}

        // Display the scroll description.
        await scrollContainer.setOuterHtmlAsync($"<div id='scrollContainer'>{description}</div>");


        DomNodeWrapper assignmentsNode = await document.querySelectorAsync("#assignments");
        Dictionary<string, (string, string)> assignments = new();

        foreach (MMTFact fact in scroll.requiredFacts)
        {
            string slotId = fact.@ref.uri;
            ActiveScroll.SlotAssignment assignment = SwitchScrollUI.activeScrollData.Assignments[slotId];
            if (assignment.IsSet)
            {
                assignments.Add(slotId, (assignment.fact.GetLabel(StageStatic.stage.factState), assignment.fact.Id));
            }
            else
            {
                assignments.Add(slotId, (fact.label, ""));
            }
        }
        foreach (MMTFact fact in scroll.acquiredFacts)
        {
            assignments.Add(fact.@ref.uri, (fact.label, ""));
        }
        await assignmentsNode.setAttributeValue("data-assignments", JsonConvert.SerializeObject(assignments));
        _ = webViewComponent.tab.Evaluate("RenderScroll()");

        // Update the interactive components
        dropzones = null;
        RegisterBrowserEventhandlers();
    }

    private async void DropzoneAttributeModifiedHandler(attributeModifiedEvent attributeModifiedEvent)
    {
        //Debug.Log($"dropzoneAttributeModifiedHandler: '{attributeModifiedEvent.name}'");

        // call the onFactAssignment event if the data-fact-id attribute was modified
        if (attributeModifiedEvent.name == "data-fact-id")
        {
            // get the slot id
            var node = await webViewComponent.tab.GetNode(attributeModifiedEvent.nodeId);
            if (! (node.Node.attributes.TryGetValue("data-slot-id", out string slot)
                || (await node.getAttributesAsync()).TryGetValue("data-slot-id", out slot)))
            {
                Debug.LogError($"dropzoneAttributeModifiedHandler: data-slot-id attribute not found on dropzone");
                throw new Exception("data-slot-id attribute not found on dropzone");
            }

            // if fact has been unassigned from the scroll
            if (attributeModifiedEvent.value == "null")
            {
                // remove fact from slot
                SwitchScrollUI.activeScrollData.AssignFact(slot, null);
                return;
            }

            // get the fact from the fact id
            if (!FactRecorder.AllFacts.TryGetValue(attributeModifiedEvent.value, out Fact fact))
            {
                Debug.LogError($"dropzoneAttributeModifiedHandler: fact with id '{attributeModifiedEvent.value}' not found");
                throw new Exception($"fact with id '{attributeModifiedEvent.value}' not found");
            }

            // assign fact to slot
            SwitchScrollUI.activeScrollData.AssignFact(slot, fact);
        }
    }

    private async void OnHintAvailable(IReadOnlyList<string> url)
    {
        dropzones = await (await webViewComponent.tab.Document).querySelectorAllAsync("[dropzone='copy']");
        foreach (var dropzone in dropzones)
        {
            if (url.Contains(dropzone.Node.attributes["data-slot-id"]))
            {
                _ = dropzone.setAttributeValue("data-hint-available", "true");
            }
            else if (dropzone.Node.attributes.ContainsKey("data-hint-available"))
            {
                _ = dropzone.removeAttributeAsync("data-hint-available");
            }
        }
    }

    private void ApplyScrollHandler(string button)
    {
        SwitchScrollUI.activeScrollData.ButtonClicked(new ApplyScrollButton());
    }

    private void GetHintHandler(string url)
    {
        SwitchScrollUI.activeScrollData.ButtonClicked(new HintScrollButton(url));
    }

    /// <summary>
    /// <para>
    /// Hand the ScrollView the labels (and potentially assignments) of all facts, so it can render the scroll.
    /// </para><para>
    /// More precisely: Put a stringified and Json serialized Dictionary 
    /// <see cref="OMS.uri"/> slotID  -> (<see cref="string"/> label, <see cref="Fact.Id"/> factId) <br/>
    /// into the "data-assignments" attribute of the "assignments" node
    /// </para>
    /// </summary>
    /// <param name="youHappyNowJS">Is ignored. JS bindings need to take a string argument, so here it is.</param>
    [Obsolete("This data can be aquired directly from the MMTServer")]
    private async void UpdateAssignmentsHandler(string youHappyNowJS = "")
    {
        DomNodeWrapper document = await webViewComponent.tab.Document;
        DomNodeWrapper assignmentsNode = await document.querySelectorAsync("#assignments");
        Scroll scroll = SwitchScrollUI.activeScrollData.RenderedScroll;
        Dictionary<string, (string,string)> assignments = new();
        foreach (var (slotId,assignment) in SwitchScrollUI.activeScrollData.Assignments)
        {
            string label = assignment.IsSet ? assignment.fact.GetLabel(StageStatic.stage.factState)
                                                : scroll.requiredFacts.Find(fact => fact.@ref.uri == slotId).label;
            /** id of the assigned fact. If nothing is assigned don't add the attribute */
            string fact_id = assignment.IsSet ? assignment.fact.Id : "";
            assignments.Add(slotId,(label,fact_id));
        }
        foreach(MMTFact fact in scroll.acquiredFacts)
        {
            string label = scroll.acquiredFacts.Find(f => fact.@ref.uri == f.@ref.uri).label;
            Debug.Log(label);
            Debug.Log(fact.label);
            assignments.Add(fact.@ref.uri, (fact.label, ""));
        }
    

        await assignmentsNode.setAttributeValue("data-assignments", JsonConvert.SerializeObject(assignments));
    }

    public string[] GetFactAssignments()
    {
        return dropzones.Select(dropzone => dropzone.Node.attributes.GetValueOrDefault("data-fact-id", null)).ToArray();
    }

    [Obsolete()]
    private async void GetDropzoneStateAsync()
    {
        // alternative way to get the dropzone state
        DomNodeWrapper document = await DomNodeWrapper.getDocumentAsync(webViewComponent.tab);
        Debug.LogWarning($"dropzone 1: '{document}'");
        var dropzones = await document.querySelectorAllAsync("[dropzone='copy']");

        Debug.LogWarning($"dropzone 2: '{dropzones.Index().Aggregate("", (currentString, dropzone) => $"{currentString}, {dropzone.Key}: {dropzone.Value.Node.attributes.GetValueOrDefault("data-fact-id")}")}'");

        string[] factIDs = new string[dropzones.Length];

        // get attributes for each dropzone
        //for (int i = 0; i < dropzones.Length; i++)
        //{
        //    factIDs[i] = ( await dropzones[i].getAttributesAsync() ).GetValueOrDefault("data-fact-id", null);
        //}
        //Debug.LogWarning($"dropzone 3: '{string.Join(", ", factIDs)}'");
    }


    /// <summary>
    /// First try to get the dropzone state using coroutines.
    /// Doesn't work because coroutines can't access method local temporary variables.
    /// </summary>
    /// <returns></returns>
    [Obsolete("Use getDropzoneStateAsync instead")]
    private IEnumerator GetDropzoneState()
    {
        Debug.LogWarning($"dropzone pre");

        DomNodeWrapper doc = null;
        yield return DomNodeWrapper.getDocument(webViewComponent.tab, (document) => {
            Debug.LogWarning($"dropzone 1: '{document}'");
            doc = document;

            StartCoroutine(document.querySelectorAll("[dropzone='copy']", (dropzones) => {
                foreach (var dropzone in dropzones)
                {
                    Debug.LogWarning($"dropzone 2: Node is Null?: '{dropzone.Node == null}'");
                    StartCoroutine(dropzone.getAttributes((attributes) =>
                    {
                        Debug.LogWarning($"dropzone 3 getAttributes: '{string.Join(", ", attributes.Values)}'");
                    }));
                }
            }));
        });
        Debug.LogWarning($"dropzone post: '{doc}'");
    }
}

public class FactObjectUIConverter : JsonConverter<FactObjectUI>
{
    public override void WriteJson(JsonWriter writer, FactObjectUI value, JsonSerializer serializer)
    {
        //serializer.Serialize(writer, value.Fact);
        JObject o = JObject.FromObject(value.Fact, WebViewComponent.serializer);
        o.AddFirst(new JProperty("id", value.Fact.Id));

        o.WriteTo(writer);
    }
    public override FactObjectUI ReadJson(JsonReader reader, Type objectType, FactObjectUI existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        var factObject = new FactObjectUI();
        factObject.Fact = serializer.Deserialize<Fact>(reader);
        return factObject;
    }
}