using MoreLinq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using static CommunicationEvents;

public class DisplayFacts : MonoBehaviour, ISerializationCallbackReceiver
{
    private static DisplayFacts _instance;
    public static DisplayFacts Instance
    {
        get => _instance;
        private set { _instance ??= value; }
    }

    public static Transform FactscreenContent => Instance.factscreenContent;
    public static GameObject FactSpotPrefab => Instance.factSpotPrefab;

    //cannot populate static dict while serialization => "singelton"
    public static Dictionary<Type, GameObject> PrefabDictionary { get => Instance._PrefabDictionary; }
    private Dictionary<Type, GameObject> _PrefabDictionary = new();

    public static Dictionary<string, GameObject> displayedFacts = new();

    [SerializeField] private Transform factscreenContent;
    [SerializeField] private GameObject factSpotPrefab;

    private bool sortDescending = false;
    private bool showGrouped = false;
    private bool showOnlyFavorites = false;

    #region ISerializationCallbackReceiver
    [ReadOnly(true), SerializeField]
    private List<string> PrefabtTypeReadonly;
    [SerializeField]
    private List<GameObject> PrefabDataConfig;

    public void OnBeforeSerialize()
    {
        PrefabtTypeReadonly = TypeExtensions<Fact>.UAssemblyInheritenceTypes.Select(type => type.ToString()).ToList();
        PrefabtTypeReadonly.Sort(); //for convinience

        // if order has changed => do not forget
        var keys = _PrefabDictionary.Keys.Select(type => type.ToString()).ToList();
        var keys_permutation = PrefabtTypeReadonly.Select(type => keys.IndexOf(type)).ToArray();

        _PrefabDictionary.TryGetValue(typeof(TestFact), out GameObject POdefault);

        var vals = _PrefabDictionary.Values.ToArray();
        PrefabDataConfig = new(keys_permutation.Length);
        for (int i = 0; i < keys_permutation.Length; i++)
        {
            GameObject data = keys_permutation[i] < 0
                ? POdefault
                : vals[keys_permutation[i]];

            if (PrefabDataConfig.Count <= i)
                PrefabDataConfig.Add(data);
            else
                PrefabDataConfig[i] = data;
        }
    }

    public void OnAfterDeserialize()
    {
        _PrefabDictionary = new();
        for (int i = 0; i != Math.Min(PrefabtTypeReadonly.Count, PrefabDataConfig.Count); i++)
        {
            //// No Idea how to reproduce
            //if (TypeExtensions<Fact>.UAssemblyInheritenceTypes.FirstOrDefault(type => type.ToString() == PrefabtTypeReadonly[i]) == null)
            //    continue;

            _PrefabDictionary.TryAdd(
                TypeExtensions<Fact>.UAssemblyInheritenceTypes.FirstOrDefault(type => type.ToString() == PrefabtTypeReadonly[i]),
                PrefabDataConfig[i]
            );
        }
    }
    #endregion ISerializationCallbackReceiver

    #region UnityMethods
    void Awake()
    {
        Instance = this; //first come, first serve
        displayedFacts = new(); // reset static properties
    }

    private void OnEnable()
    {
        AddFactEvent.AddListener(AddFact);
        RemoveFactEvent.AddListener(RemoveFact);
        FactFavorisation.ChangeFavoriteEvent.AddListener(OnFavoriteChange);
    }

    private void OnDisable()
    {
        AddFactEvent.RemoveListener(AddFact);
        RemoveFactEvent.RemoveListener(RemoveFact);
        FactFavorisation.ChangeFavoriteEvent.RemoveListener(OnFavoriteChange);
    }

    private void RemoveFact(Fact fact)
        => displayedFacts.Remove(fact.Id);
    #endregion UnityMethods

    #region Implementation
    public void AddFact(Fact fact)
    {
        // index where the new display will be inserted
        int siblingIdx = sortDescending ? 0 : factscreenContent.childCount;
        if (showGrouped)
        {
            var facts = factscreenContent.GetComponentsInChildren<FactObject>().Select(f => f.Fact).ToList();
            if (!sortDescending)
                siblingIdx = GetIndexInSortedList(fact, facts);
            else
            {
                facts.Reverse();
                var _siblingIdx = GetIndexInSortedList(fact, facts);
                siblingIdx = factscreenContent.childCount - _siblingIdx;
            }
        }

        // create display
        var display = CreateDisplay(fact, factscreenContent);
        display.transform.localPosition = Vector3.zero;
        displayedFacts.TryAdd(fact.Id, display);

        display.transform.parent.gameObject.SetActive(
            !showOnlyFavorites
            || display.GetComponent<FactFavorisation>().IsFavorite
        );

        display.transform.parent.transform.SetSiblingIndex(siblingIdx);
    }

    public static GameObject CreateDisplay(Fact fact, Transform transform)
    {
        var spot = Instantiate(FactSpotPrefab, transform);
        spot.GetComponent<FactWrapper>().Fact = fact;

        return InstantiateDisplay(fact, spot.transform);
    }

    public static GameObject InstantiateDisplay(Fact fact, Transform transform)
    {
        Type fact_type = fact.GetType();
        if (fact_type.IsConstructedGenericType)
            fact_type = fact_type.GetGenericTypeDefinition();

        var ret = Instantiate(PrefabDictionary[fact_type], transform);
        ret.GetComponent<FactObject>().Fact = fact;

        return ret;
    }

    #region Sorting
    #region AscDesc
    public void AscDescChanged(Toggle t)
    {
        sortDescending = !sortDescending; // t.isOn
        factscreenContent.gameObject.ForAllChildren(child => child.transform.SetAsFirstSibling());
    }
    #endregion AscDesc

    #region Grouping
    public void GroupingChanged(Toggle t)
    {
        showGrouped = t.isOn;

        var vals = factscreenContent.gameObject.GetDirectChildren();

        var ordered = showGrouped
            ? vals.OrderBy(tr => tr.GetComponent<FactWrapper>().Fact, new FactTypeComparer()).ToList()
            : vals.OrderBy(tr =>
                displayedFacts.Keys.ToList()
                    .IndexOf(
                          tr.GetComponent<FactWrapper>().URI
                )).ToList();

        if (sortDescending)
            ordered.Reverse();

        for (int i = 0; i < ordered.Count; i++)
            ordered[i].transform.SetSiblingIndex(i);
    }

    private int GetIndexInSortedList(Fact f, List<Fact> toCheck)
    {
        var index = toCheck.BinarySearch(f, new FactTypeComparer());
        if (index < 0) index = ~index;
        return index;
    }

    private class FactTypeComparer : IComparer<Fact>
    {
        /// <summary>
        /// Compare two facts by type and label
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public int Compare(Fact x, Fact y)
        {
            int ret, index_x;

#pragma warning disable CS0642 // Möglicherweise falsche leere Anweisung

            if (0 != (ret = (index_x = TypeRank.IndexOf(x.GetType())) - TypeRank.IndexOf(y.GetType()))) ;
            else
            if (index_x < 0) // Types not in List and same
                if (0 != (ret = x.DependentFactIds.Length - y.DependentFactIds.Length)) ;
                else
                if (0 != (ret = string.Compare(x.GetType().ToString(), y.GetType().ToString()))) ;
                else;
            else if (true) // keep auto format // Types in List and same
                if (0 != (ret = x.DependentFactIds.Zip(y.DependentFactIds, (x, y) => string.Compare(x, y)).Aggregate(0, (r1, r2) => r1 + r2))) ;
                else
                if (0 != (ret = string.Compare(x._LastLabel, y._LastLabel))) ;

#pragma warning restore CS0642 // Möglicherweise falsche leere Anweisung

            return ret;
        }

        private List<Type> TypeRank = new()
        {
            typeof(PointFact),

            typeof(OnLineFact),
            typeof(LineFact),
            typeof(RayFact),
            typeof(ParallelLineFact),

            typeof(RightAngleFact),
            typeof(AngleFact),

            typeof(CircleFact),
            typeof(RadiusFact),
            typeof(AreaCircleFact),
            typeof(EqualCirclesFact),
            typeof(UnEqualCirclesFact),
            typeof(OnCircleFact),
            typeof(AngleCircleLineFact),
            typeof(OrthogonalCircleLineFact),

            typeof(CylinderVolumeFact),
            typeof(ConeVolumeFact),
            typeof(TruncatedConeVolumeFact),

            typeof(FunctionFact),
            typeof(FunctionCallFact),

            typeof(TestFact),
        };
    }
    #endregion Grouping
    #endregion Sorting

    #region Favorites
    public void FavoritesFilterChanged(Toggle t)
    {
        showOnlyFavorites = t.isOn;

        if (!showOnlyFavorites) // show all
            displayedFacts.Values.ForEach(nFav => nFav.transform.parent.gameObject.SetActive(true));
        else
        {   // hide not favorites
            displayedFacts.Values
                 .Where(gO => gO != null && !gO.GetComponent<FactFavorisation>().IsFavorite)
                 .ForEach(nFav => nFav.transform.parent.gameObject.SetActive(false));
        }
    }

    private void OnFavoriteChange(Fact changedFact, bool isFavourite)
    {
        if (!showOnlyFavorites)
            return;

        if (displayedFacts.TryGetValue(changedFact.Id, out GameObject display))
            display.transform.parent.gameObject.SetActive(isFavourite);
    }
    #endregion Favorites

    #endregion Implementation
}