﻿using System;
using System.Collections.Generic;
using ABI.CCK.Components;
using ABI.CCK.Scripts.Editor;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using AnimatorController = UnityEditor.Animations.AnimatorController;
using AnimatorControllerParameter = UnityEngine.AnimatorControllerParameter;
using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType;

[CustomEditor(typeof(AnimatorDriver))]
public class CCK_AnimatorDriverEditor : Editor
{
    private AnimatorDriver _animatorDriver;
    
    private ReorderableList _onEnterList;
    private ReorderableList _onExitList;

    private readonly List<string> _animatorParamNames = new();
    private readonly List<AnimatorDriverTask.ParameterType> _animatorParamTypes = new();

    private bool _showLocalOnlyHelp;
    private bool _isInPlayMode;
    
    #region Unity Events

    private void OnEnable()
    {
        if (target == null) return;
        _animatorDriver = (AnimatorDriver)target;

        _isInPlayMode = Application.isPlaying;
        
        // TODO: add parameter type display in parameter dropdown
        _animatorParamNames.Clear();
        _animatorParamTypes.Clear();
        
        // TODO: This does not work in play mode. There is a hack we can do instead to find the Animator window
        // with reflection, but that is not ideal...
        var behaviorContext = AnimatorController.FindStateMachineBehaviourContext(_animatorDriver);
        if (behaviorContext.Length > 0)
        {
            AnimatorController controller = behaviorContext[0].animatorController;
            foreach (AnimatorControllerParameter parameter in controller.parameters)
            {
                switch (parameter.type)
                {
                    case AnimatorControllerParameterType.Bool:
                        _animatorParamNames.Add($"{parameter.name}");
                        _animatorParamTypes.Add(AnimatorDriverTask.ParameterType.Bool);
                        break;
                    case AnimatorControllerParameterType.Float:
                        _animatorParamNames.Add($"{parameter.name}");
                        _animatorParamTypes.Add(AnimatorDriverTask.ParameterType.Float);
                        break;
                    case AnimatorControllerParameterType.Int:
                        _animatorParamNames.Add($"{parameter.name}");
                        _animatorParamTypes.Add(AnimatorDriverTask.ParameterType.Int);
                        break;
                    case AnimatorControllerParameterType.Trigger:
                        _animatorParamNames.Add($"{parameter.name}");
                        _animatorParamTypes.Add(AnimatorDriverTask.ParameterType.Trigger);
                        break;
                }
            }
        }
        
        if (_onEnterList == null)
        {
            _onEnterList = new ReorderableList(_animatorDriver.EnterTasks, typeof(AnimatorDriverTask),
                true, true, !_isInPlayMode, !_isInPlayMode)
            {
                drawHeaderCallback = OnDrawHeaderTaskEnter,
                drawElementCallback = OnDrawElementTaskEnter,
                elementHeightCallback = OnHeightElementTaskEnter
            };
        }

        if (_onExitList == null)
        {
            _onExitList = new ReorderableList(_animatorDriver.ExitTasks, typeof(AnimatorDriverTask),
                true, true, !_isInPlayMode, !_isInPlayMode)
            {
                drawHeaderCallback = OnDrawHeaderTaskExit,
                drawElementCallback = OnDrawElementTaskExit,
                elementHeightCallback = OnHeightElementTaskExit
            };
        }
    }

    public override void OnInspectorGUI()
    {
        if (_animatorDriver == null)
            return;

        if (_isInPlayMode)
        {
            // This is a bandaid for the animator name / types not being populated in play mode. This would not be
            // an issue if the GUI drawing loop also didn't configure the animator driver.
            EditorGUILayout.HelpBox("Animator Driver is not editable in play mode.", MessageType.Warning);
            return;
        }

        EditorGUI.BeginChangeCheck();
        using (new EditorGUILayout.VerticalScope(new GUIStyle() { padding = new RectOffset(10, 10, 10, 10) }))
        {
            DrawLocalOnlyToggle();
            DrawTaskLists();
        }

        if (EditorGUI.EndChangeCheck())
            EditorUtility.SetDirty(_animatorDriver);
    }
    
    #endregion

    #region GUI Drawing
    
    private void DrawLocalOnlyToggle()
    {
        using (new EditorGUILayout.HorizontalScope())
        {
            _animatorDriver.localOnly = EditorGUILayout.Toggle("Local Only", _animatorDriver.localOnly);
            if (GUILayout.Button("?", GUILayout.Width(25)))
                _showLocalOnlyHelp = !_showLocalOnlyHelp;
            GUILayout.Space(5);
        }
        if (_showLocalOnlyHelp)
        {
            // TODO: Add to LocalizationProvider
            EditorGUILayout.HelpBox("When 'Local Only' is enabled, the animator driver is executed locally and not for remote users.", MessageType.Info);
            EditorGUILayout.Space(5);
            EditorGUILayout.HelpBox("Avatars: Only the wearer.\nSpawnables: Only the prop's owner.\nWorlds: This option is ignored.", MessageType.Info);
        }
        EditorGUILayout.Space();
    }

    private void DrawTaskLists()
    {
        EditorGUILayout.Space();
        _onEnterList.DoLayoutList();
        
        EditorGUILayout.Space();
        _onExitList.DoLayoutList();
    }

    #endregion

    #region ReorderableList Drawing

    private void OnDrawHeaderTaskEnter(Rect rect)
    {
        Rect labelRect = new(rect.x, rect.y, rect.width - 35, EditorGUIUtility.singleLineHeight);
        GUI.Label(labelRect, "On Enter Tasks");
        EditorGUIExtensions.UtilityMenu(rect, _onEnterList,                 
            appendAdditionalMenuItems: (menuBuilder, list) => { 
            AppendComponentMenu(menuBuilder, list, true);
        });
    }

    private void OnDrawElementTaskEnter(Rect rect, int index, bool isactive, bool isfocused)
    {
        if (index >= _animatorDriver.EnterTasks.Count) return;
        AnimatorDriverTask element = _animatorDriver.EnterTasks[index];
        using (new EditorGUI.DisabledScope(_isInPlayMode))
            RenderTask(rect, element);
    }

    private float OnHeightElementTaskEnter(int index)
    {
        const int length = 3;
        if (index >= _animatorDriver.EnterTasks.Count) 
            return 1.25f * length * EditorGUIUtility.singleLineHeight;
        
        AnimatorDriverTask task = _animatorDriver.EnterTasks[index];
        return (length + TaskHeight(task)) * 1.25f * EditorGUIUtility.singleLineHeight;
    }
    
    private void OnDrawHeaderTaskExit(Rect rect)
    {
        Rect labelRect = new(rect.x, rect.y, rect.width - 35, EditorGUIUtility.singleLineHeight);
        GUI.Label(labelRect, "On Exit Tasks");
        EditorGUIExtensions.UtilityMenu(rect, _onExitList,                 
            appendAdditionalMenuItems: (menuBuilder, list) => { 
            AppendComponentMenu(menuBuilder, list, false);
        });
    }

    private void OnDrawElementTaskExit(Rect rect, int index, bool isactive, bool isfocused)
    {
        if (index >= _animatorDriver.ExitTasks.Count) return;
        AnimatorDriverTask element = _animatorDriver.ExitTasks[index];
        using (new EditorGUI.DisabledScope(_isInPlayMode))
            RenderTask(rect, element);
    }

    private float OnHeightElementTaskExit(int index)
    {
        const int length = 3;
        if (index >= _animatorDriver.ExitTasks.Count) 
            return 1.25f * length * EditorGUIUtility.singleLineHeight;
        
        AnimatorDriverTask task = _animatorDriver.ExitTasks[index];
        return (length + TaskHeight(task)) * 1.25f * EditorGUIUtility.singleLineHeight;
    }
    
    #endregion

    #region AnimatorDriverTask Drawing

    private static int TaskHeight(AnimatorDriverTask task)
    {
        int length = 2;
        if (task.aType == AnimatorDriverTask.SourceType.Random) 
            length += 1;
        
        if (task.op is AnimatorDriverTask.Operator.Set 
            or AnimatorDriverTask.Operator.IPart 
            or AnimatorDriverTask.Operator.FPart) 
            return length;
        
        length += task.bType == AnimatorDriverTask.SourceType.Random ? 3 : 2;
        
        // Add height for parameter C if operation is Conditional
        if (task.op == AnimatorDriverTask.Operator.Conditional)
            length += task.cType == AnimatorDriverTask.SourceType.Random ? 3 : 2;
            
        return length;
    }

    private void RenderTask(Rect rect, AnimatorDriverTask task)
    {
        Rect _rect = new(rect.x, rect.y + 2, rect.width, EditorGUIUtility.singleLineHeight);
        
        float spacing = EditorGUIUtility.singleLineHeight * 1.25f;
        float originalLabelWidth = EditorGUIUtility.labelWidth;
        EditorGUIUtility.labelWidth = 100;

        task.targetName = EditorGUIExtensions.AdvancedDropdownInput(_rect, task.targetName, _animatorParamNames,
            "Parameter", "No Parameters");
        _rect.y += spacing;
        
        var formulaDisplay = $"{task.targetName} = ";
        int parameterIndex = Math.Max(_animatorParamNames.FindIndex(m => m == task.targetName), 0);
        if (_animatorParamNames.Count != 0 && _animatorParamTypes.Count >= parameterIndex)
            task.targetType = _animatorParamTypes[parameterIndex];
    
        task.op = (AnimatorDriverTask.Operator)EditorGUI.EnumPopup(_rect, "Operation", task.op);
        _rect.y += spacing;

        task.aType = (AnimatorDriverTask.SourceType)EditorGUI.EnumPopup(_rect, "A Type", task.aType);
        _rect.y += spacing;
        
        switch (task.aType)
        {
            case AnimatorDriverTask.SourceType.Static:
                task.aValue = EditorGUI.FloatField(_rect, "A Value", task.aValue);
                _rect.y += spacing;
                formulaDisplay += $"{task.aValue}";
                break;
            case AnimatorDriverTask.SourceType.Parameter:
                task.aName = EditorGUIExtensions.AdvancedDropdownInput(_rect, task.aName, _animatorParamNames,
                    "Parameter A", "No Parameters");
                _rect.y += spacing;
                parameterIndex = Math.Max(_animatorParamNames.FindIndex(m => m == task.aName), 0);
                task.aParamType = _animatorParamTypes[parameterIndex];
                formulaDisplay += $"{task.aName}";
                break;
            case AnimatorDriverTask.SourceType.Random:
                task.aValue = EditorGUI.FloatField(_rect, "A Min", task.aValue);
                _rect.y += spacing;
                task.aMax = EditorGUI.FloatField(_rect, "A Max", task.aMax);
                _rect.y += spacing;
                formulaDisplay += $"Rand({task.aValue}, {task.aMax})";
                break;
        }

        // Handle special cases for single-operand operations
        if (task.op == AnimatorDriverTask.Operator.Set)
        {
            // No additional operands needed
        }
        else if (task.op == AnimatorDriverTask.Operator.IPart)
        {
            formulaDisplay = $" {task.targetName} = floor({formulaDisplay[(task.targetName.Length + 3)..]})";
        }
        else if (task.op == AnimatorDriverTask.Operator.FPart)
        {
            formulaDisplay = $" {task.targetName} = frac({formulaDisplay[(task.targetName.Length + 3)..]})";
        }
        else
        {
            // Add operator to formula display
            switch (task.op)
            {
                case AnimatorDriverTask.Operator.Addition:
                    formulaDisplay += " + ";
                    break;
                case AnimatorDriverTask.Operator.Subtraction:
                    formulaDisplay += " - ";
                    break;
                case AnimatorDriverTask.Operator.Multiplication:
                    formulaDisplay += " * ";
                    break;
                case AnimatorDriverTask.Operator.Division:
                    formulaDisplay += " / ";
                    break;
                case AnimatorDriverTask.Operator.Modulo:
                    formulaDisplay += " % ";
                    break;
                case AnimatorDriverTask.Operator.Power:
                    formulaDisplay += " pow ";
                    break;
                case AnimatorDriverTask.Operator.Log:
                    formulaDisplay += " log ";
                    break;
                case AnimatorDriverTask.Operator.Equal:
                    formulaDisplay += " == ";
                    break;
                case AnimatorDriverTask.Operator.LessThen:
                    formulaDisplay += " < ";
                    break;
                case AnimatorDriverTask.Operator.LessEqual:
                    formulaDisplay += " <= ";
                    break;
                case AnimatorDriverTask.Operator.MoreThen:
                    formulaDisplay += " > ";
                    break;
                case AnimatorDriverTask.Operator.MoreEqual:
                    formulaDisplay += " >= ";
                    break;
                case AnimatorDriverTask.Operator.NotEqual:
                    formulaDisplay += " != ";
                    break;
                case AnimatorDriverTask.Operator.LogicalAnd:
                    formulaDisplay += " && ";
                    break;
                case AnimatorDriverTask.Operator.LogicalOr:
                    formulaDisplay += " || ";
                    break;
                case AnimatorDriverTask.Operator.BitwiseAnd:
                    formulaDisplay += " & ";
                    break;
                case AnimatorDriverTask.Operator.BitwiseOr:
                    formulaDisplay += " | ";
                    break;
                case AnimatorDriverTask.Operator.BitwiseXor:
                    formulaDisplay += " ^ ";
                    break;
                case AnimatorDriverTask.Operator.LeftShift:
                    formulaDisplay += " << ";
                    break;
                case AnimatorDriverTask.Operator.RightShift:
                    formulaDisplay += " >> ";
                    break;
                case AnimatorDriverTask.Operator.RotateLeft:
                    formulaDisplay += " ROL ";
                    break;
                case AnimatorDriverTask.Operator.RotateRight:
                    formulaDisplay += " ROR ";
                    break;
                case AnimatorDriverTask.Operator.Conditional:
                    formulaDisplay = $" {task.targetName} = ({formulaDisplay[(task.targetName.Length + 3)..]}) ? ";
                    break;
            }
            
            task.bType = (AnimatorDriverTask.SourceType) EditorGUI.EnumPopup(_rect, "B Type", task.bType);
            _rect.y += spacing;
            
            switch (task.bType)
            {
                case AnimatorDriverTask.SourceType.Static:
                    task.bValue = EditorGUI.FloatField(_rect, "B Value", task.bValue);
                    _rect.y += spacing;
                    formulaDisplay += $" {task.bValue} ";
                    break;
                case AnimatorDriverTask.SourceType.Parameter:
                    task.bName = EditorGUIExtensions.AdvancedDropdownInput(_rect, task.bName, _animatorParamNames,
                        "Parameter B", "No Parameters");
                    _rect.y += spacing;               
                    parameterIndex = Math.Max(_animatorParamNames.FindIndex(m => m == task.bName), 0);
                    task.bParamType = _animatorParamTypes[parameterIndex];
                    formulaDisplay += $" {task.bName} ";
                    break;
                case AnimatorDriverTask.SourceType.Random:
                    task.bValue = EditorGUI.FloatField(_rect, "B Min", task.bValue);
                    _rect.y += spacing;
                    task.bMax = EditorGUI.FloatField(_rect, "B Max", task.bMax);
                    _rect.y += spacing;
                    formulaDisplay += $" Rand({task.bValue}, {task.bMax}) ";
                    break;
            }
            
            // Conditional operation
            if (task.op == AnimatorDriverTask.Operator.Conditional)
            {
                formulaDisplay += " : ";
                
                task.cType = (AnimatorDriverTask.SourceType) EditorGUI.EnumPopup(_rect, "C Type", task.cType);
                _rect.y += spacing;
                
                switch (task.cType)
                {
                    case AnimatorDriverTask.SourceType.Static:
                        task.cValue = EditorGUI.FloatField(_rect, "C Value", task.cValue);
                        _rect.y += spacing;
                        formulaDisplay += $" {task.cValue} ";
                        break;
                    case AnimatorDriverTask.SourceType.Parameter:
                        task.cName = EditorGUIExtensions.AdvancedDropdownInput(_rect, task.cName, _animatorParamNames,
                            "Parameter C", "No Parameters");
                        _rect.y += spacing;               
                        parameterIndex = Math.Max(_animatorParamNames.FindIndex(m => m == task.cName), 0);
                        task.cParamType = _animatorParamTypes[parameterIndex];
                        formulaDisplay += $" {task.cName} ";
                        break;
                    case AnimatorDriverTask.SourceType.Random:
                        task.cValue = EditorGUI.FloatField(_rect, "C Min", task.cValue);
                        _rect.y += spacing;
                        task.cMax = EditorGUI.FloatField(_rect, "C Max", task.cMax);
                        _rect.y += spacing;
                        formulaDisplay += $" Rand({task.cValue}, {task.cMax}) ";
                        break;
                }
            }
        }

        EditorGUI.LabelField(_rect, task.targetName == "" ? "No Parameter" : formulaDisplay,
            new GUIStyle(GUI.skin.label) { fontStyle = FontStyle.Bold });
        
        EditorGUIUtility.labelWidth = originalLabelWidth;
    }

    #endregion AnimatorDriverTask Drawing

    #region Utility
    
    private void AppendComponentMenu(GenericMenuBuilder genericMenuBuilder, ReorderableList list, bool isEnterList)
    {
        bool hasSelectedTask = list.index != -1;
        bool hasTasks = list.count > 0;
    
        if (isEnterList)
        {
            genericMenuBuilder.AddMenuItem("To Exit Task", hasTasks && hasSelectedTask, () => ConvertSelectedTask(list, _animatorDriver.EnterTasks, _animatorDriver.ExitTasks));
            genericMenuBuilder.AddMenuItem("All to Exit Task", hasTasks, ConvertAllTasksToExit);
        }
        else
        {
            genericMenuBuilder.AddMenuItem("To Enter Task", hasTasks && hasSelectedTask, () => ConvertSelectedTask(list, _animatorDriver.ExitTasks, _animatorDriver.EnterTasks));
            genericMenuBuilder.AddMenuItem("All to Enter Task", hasTasks, ConvertAllTasksToEnter);
        }
    }
    
    private void ConvertAllTasksToEnter()
    {
        _animatorDriver.EnterTasks.AddRange(_animatorDriver.ExitTasks);
        _animatorDriver.ExitTasks.Clear();
    }

    private void ConvertAllTasksToExit()
    {
        _animatorDriver.ExitTasks.AddRange(_animatorDriver.EnterTasks);
        _animatorDriver.EnterTasks.Clear();
    }
    
    private static void ConvertSelectedTask(ReorderableList list, List<AnimatorDriverTask> fromTasks, List<AnimatorDriverTask> toTasks)
    {
        if (list.index == -1 || list.index >= fromTasks.Count) return;

        AnimatorDriverTask selectedTask = fromTasks[list.index];
        fromTasks.RemoveAt(list.index);
        toTasks.Add(selectedTask);
    }
    
    #endregion
}