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;

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_Server/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 (partially applied) scroll, and request it to rerender the description. 
    /// </para>
    /// </summary>
    /// <param name="scroll">The (partially applied) scroll to be rendered</param>
    private void SetScrollContent(Scroll scroll)
    {
        // If the description is plaintext, create a generic MathML version
        if (!Regex.IsMatch(scroll.description, ".*<scroll-description.*"))
        {
            scroll.description = CreateGenericScrollDescription(scroll);
        }

        ForwardDataToScrollView("scrollDynamic", scroll);
        _ = webViewComponent.tab.Evaluate("RenderScroll()");
        Debug.Log("Called for scrollView to rerender");

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

    /// <summary>
    /// Create a MathML description for a scroll without one.
    /// Obviously cannot be very fancy. 
    /// </summary>
    /// <param name="scroll">The rendered scroll to create a description for</param>
    /// <returns>A fitting scroll-description element as a string</returns>
    private string CreateGenericScrollDescription(Scroll scroll)
    {
        // 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>");

        return $"<scroll-description><p>{scroll.description}</p><div id='legacySlots'>{String.Join("", factSlots)}</div></scroll-description>";
    }

    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>
    /// Send data to the ScrollView.
    /// <para>JSON serialize the <paramref name="dataValue"/> and add it to the dataset of the Unity-Data-Interface Node. JS can then retrieve it by calling
    /// <code>const unityData = JSON.parse(document.querySelector("#Unity-Data-Interface").dataset.<paramref name="dataID"/>);</code> 
    /// </para>
    /// </summary>
    /// <param name="dataID">The name of the dataset-member <paramref name="dataValue"/> will be assigned to</param>
    /// <param name="dataValue">The data to transmit</param>
    public async void ForwardDataToScrollView(string dataID, object dataValue)
    {
        string payload = JsonConvert.SerializeObject(dataValue);
        //payload = WebUtility.HtmlEncode(payload);
        // Using the DomNodeWrapper methods requires 3 requests, and since those go to an altogether
        // different prozess, I would like to avoid that.
        await webViewComponent.tab.Evaluate(
            $"document.querySelector('#Unity-Data-Interface').dataset.{dataID} = '{payload}'"
            );
    }

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

    #region ObsoleteFunctions : Functions that are no longer used. Since similar functionality may be of use sometime later, they are here for reference.


    /// <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 needs to be updated iff the assignment is changed in Unity, JS doesn't need a way to to request it independently.")]
    private void UpdateAssignmentsHandler(string youHappyNowJS = "")
    {
        Scroll scroll = SwitchScrollUI.activeScrollData.RenderedScroll;
        Dictionary<string, (string, string)> assignments = new();

        foreach (MMTFact slot in scroll.requiredFacts)
        {
            string slotId = slot.@ref.uri;
            ActiveScroll.SlotAssignment assignment = SwitchScrollUI.activeScrollData.Assignments[slotId];
            if (assignment.IsSet)
            {
                Fact fact = assignment.fact;
                assignments.Add(slotId, (fact.GetLabel(StageStatic.stage.factState), fact.Id));
            }
            else
            {
                assignments.Add(slotId, (slot.label, ""));
            }
        }
        foreach (MMTFact fact in scroll.acquiredFacts)
        {
            assignments.Add(fact.@ref.uri, (fact.label, ""));
        }
        ForwardDataToScrollView("assignments", JsonConvert.SerializeObject(assignments));
    }

    [Obsolete("Is done by JS. How to render the scroll is the scrolls buisness.")]
    private string createCustomScrollDescription(Scroll scroll)
    {
        string description = scroll.description;
        // replace slot templates
        //the scroll-slot syntax has changed by now, so the match would have to be adapted.
        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>";
        });

        return description;
    }

    [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}'");
    }
    #endregion ObsoleteFunctions
}



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;
    }
}