using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Sirenix.OdinInspector;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using UnityEngine;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Unit.DataCalculator;

namespace VOID.Generic.Objects.Unit
{
    [Serializable]
    public class UnitObjectDataCalculator : ObjectComponent<UnitObject>
    {
        #region Editor

        [ShowInInspector]
        [NonSerialized]
        [ListDrawerSettings(
            OnTitleBarGUI = "CreateMarkersInfoBox",
            DraggableItems = false,
            HideRemoveButton = true,
            HideAddButton = true,
            DefaultExpandedState = false)]
        private List<string> _availableMarkers = new();

        private void CreateMarkersInfoBox()
        {
            if (!SirenixEditorGUI.ToolbarButton(EditorIcons.Refresh)) return;
            Debug.Log("Marker list refreshed!", parent);
            CreateCalcGroups();
            _availableMarkers = new List<string>();
            _calcGroups.ForEach(calcGroup =>
                calcGroup.GetMarkerItems()
                    .Select(item => $"{{{calcGroup.GetMarkerGroup()}.{item}}}")
                    .ForEach(marker => _availableMarkers.Add(marker)));
        }

        #endregion
        
        #region Initialize

        protected override void SelfInitialize()
        {
            MergeFormulas();
            CreateCalcGroups();
            CreateDependencyMap();
        }

        protected override void Initialize()
        {
            CalculateAllInstant();
        }

        private void MergeFormulas()
        {
            _mainAttributeFormulas.ForEach(formula => _formulas.Add(formula.Key, formula.Value));
            _subAttributeFormulas.ForEach(formula => _formulas.Add(formula.Key, formula.Value));
            _basicResourceFormulas.ForEach(formula => _formulas.Add(formula.Key, formula.Value));
            _unitResourceFormulas.ForEach(formula => _formulas.Add(formula.Key, formula.Value));
        }
        
        private void CreateCalcGroups()
        {
            _calcGroups = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(assembly => assembly.GetTypes())
                .Where(type => typeof(ICalcGroup).IsAssignableFrom(type) && type.IsClass)
                .Select(Activator.CreateInstance)
                .Cast<ICalcGroup>()
                .ToList();
        }

        private void CreateDependencyMap()
        {
            _formulas.ForEach(formulaPair =>
            {
                var forEnum = formulaPair.Key;
                var formula = formulaPair.Value;
                var markers = ParseFormulaMarkers(formula);
                markers.ForEach(marker => GetMarkerValue(marker));
                _dependencies.Add(forEnum, markers.Select(GetMarkerEnum).ToList());
            });
        }

        private void CalculateAllInstant()
        {
            // Force to calculate main attributes (all other should be queued to calculate by dependencies)
            Enum.GetValues(typeof(MainAttributeType)).Cast<Enum>().ForEach(Queue);
            
            while (!_calcQueue.IsNullOrEmpty()) ExecuteQueue();
        }

        #endregion

        [Serializable] private class MainAttributeFormulaDict : SerializedDictionary<MainAttributeType, string> {}
        [Serializable] private class SubAttributeFormulaDict : SerializedDictionary<SubAttributeType, string> {}
        [Serializable] private class BasicResourceFormulaDict : SerializedDictionary<BasicResourceType, string> {}
        [Serializable] private class UnitResourceFormulaDict : SerializedDictionary<UnitResourceType, string> {}
        
        [Title("Formulas used to calculate")]
        [SerializeField, EnableIn(PrefabKind.PrefabAsset)] private MainAttributeFormulaDict _mainAttributeFormulas = new();
        [SerializeField, EnableIn(PrefabKind.PrefabAsset)] private SubAttributeFormulaDict _subAttributeFormulas = new();
        [SerializeField, EnableIn(PrefabKind.PrefabAsset)] private BasicResourceFormulaDict _basicResourceFormulas = new();
        [SerializeField, EnableIn(PrefabKind.PrefabAsset)] private UnitResourceFormulaDict _unitResourceFormulas = new();
        
        private Dictionary<Enum, string> _formulas = new();
        private Dictionary<Enum, List<Enum>> _dependencies = new();
        private List<Enum> _calcQueue = new();

        private List<ICalcGroup> _calcGroups;

        public void Queue(Enum @enum)
        {
            if (_calcGroups.All(calcGroup => calcGroup.GetEnumType() != @enum.GetType()))
                throw new Exception($"Enum '{@enum}' is not acceptable in {nameof(UnitObjectDataCalculator)}.");
         
            if (_calcQueue.Contains(@enum)) return;
            _calcQueue.Add(@enum);
            parent.basicEvents.onFixedUpdate.AddListener(ExecuteQueue);
        }

        private void ExecuteQueue()
        {
            var tempCalcQueue = new List<Enum>(_calcQueue);
            _calcQueue.Clear();
            parent.basicEvents.onFixedUpdate.RemoveListener(ExecuteQueue);
            tempCalcQueue.ForEach(Calculate);
        }

        private void Calculate(Enum @enum)
        {
            var calcGroup = _calcGroups.FirstOrDefault(calcGroup => calcGroup.CheckEnum(@enum));
            if (calcGroup == null) throw new Exception($"Marker '{@enum}' does not exists!");
            calcGroup.UpdateWithValue(parent, @enum.ToString(), GetFormulaValue(@enum));
            _dependencies.Where(pair => pair.Value.Contains(@enum)).ForEach(pair => Queue(pair.Key));
        }
        
        private float GetFormulaValue(Enum @enum)
        {
            var formula = _formulas.GetValueOrDefault(@enum, "");
            ParseFormulaMarkers(formula).ForEach(marker => formula = formula.Replace(marker, GetMarkerValue(marker)));
            var isValid = ExpressionEvaluator.Evaluate(formula, out float result);
            if (!isValid) Debug.LogError($"Formula `{formula}` is invalid and can't be calculated! Using zero!");
            return result;
        }

        private List<string> ParseFormulaMarkers(string formula)
        {
            return Regex.Matches(formula, "{(.*?)}").Select(match => match.Value).Distinct().ToList();
        }
        
        private string GetMarkerValue(string marker)
        {
            var parsedMarker = ParseMarker(marker);
            var calcGroup = _calcGroups.FirstOrDefault(cg => cg.CheckMarker(parsedMarker.group, parsedMarker.item));
            if (calcGroup == null) throw new Exception($"Marker '{marker}' does not exists!");
            return calcGroup.GetValue(parent, parsedMarker.item).ToString("F2", CultureInfo.InvariantCulture);
        }

        private Enum GetMarkerEnum(string marker)
        {
            var parsedMarker = ParseMarker(marker);
            var calcGroup = _calcGroups.FirstOrDefault(cg => cg.CheckMarker(parsedMarker.group, parsedMarker.item));
            if (calcGroup == null) throw new Exception($"Marker '{marker}' does not exists!");
            return calcGroup.GetEnum(parsedMarker.item);
        }

        private (string group, string item) ParseMarker(string marker)
        {
            var arrayMarker = marker.Trim('{', '}').Split('.');
            return new ValueTuple<string, string>(arrayMarker[0], arrayMarker[1]);
        }
    }
}