using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using UnityEngine;

namespace MeidoPhotoStudio.Plugin;

public class MeidoManager : IManager
{
    public const string header = "MEIDO";

    private static readonly CharacterMgr characterMgr = GameMain.Instance.CharacterMgr;

    private static bool active;

    private static int EditMaidIndex { get; set; }

    public event EventHandler<MeidoUpdateEventArgs> UpdateMeido;
    public event EventHandler EndCallMeidos;
    public event EventHandler BeginCallMeidos;

    private int selectedMeido;
    private bool globalGravity;
    private int undress;
    private int tempEditMaidIndex = -1;

    public Meido[] Meidos { get; private set; }
    public HashSet<int> SelectedMeidoSet { get; } = new();
    public List<int> SelectMeidoList { get; } = new();
    public List<Meido> ActiveMeidoList { get; } = new();

    public int SelectedMeido
    {
        get => selectedMeido;
        private set => selectedMeido = Utility.Bound(value, 0, ActiveMeidoList.Count - 1);
    }

    public bool Busy =>
        ActiveMeidoList.Any(meido => meido.Busy);

    public Meido ActiveMeido =>
        ActiveMeidoList.Count > 0 ? ActiveMeidoList[SelectedMeido] : null;

    public Meido EditMeido =>
        tempEditMaidIndex >= 0 ? Meidos[tempEditMaidIndex] : Meidos[EditMaidIndex];

    public bool HasActiveMeido =>
        ActiveMeido != null;

    public bool GlobalGravity
    {
        get => globalGravity;
        set
        {
            globalGravity = value;

            if (!HasActiveMeido)
                return;

            var activeMeido = ActiveMeido;
            var activeMeidoSlot = activeMeido.Slot;

            foreach (var meido in ActiveMeidoList)
            {
                if (meido.Slot == activeMeidoSlot)
                    continue;

                meido.HairGravityActive = value && activeMeido.HairGravityActive;
                meido.SkirtGravityActive = value && activeMeido.SkirtGravityActive;
            }
        }
    }

    static MeidoManager() =>
        InputManager.Register(MpsKey.MeidoUndressing, KeyCode.H, "All maid undressing");

    private static void SetEditorMaid(Maid maid)
    {
        if (maid == null)
        {
            Utility.LogWarning("Refusing to change editing maid because the new maid is null!");

            return;
        }

        if (SceneEdit.Instance.maid.status.guid == maid.status.guid)
        {
            Utility.LogDebug("Editing maid is the same as new maid");

            return;
        }

        var uiRoot = GameObject.Find("UI Root");

        if (!TryGetUIControl<PresetCtrl>(uiRoot, "PresetPanel", out var presetCtrl))
            return;

        if (!TryGetUIControl<PresetButtonCtrl>(uiRoot, "PresetButtonPanel", out var presetButtonCtrl))
            return;

        if (!TryGetUIControl<ProfileCtrl>(uiRoot, "ProfilePanel", out var profileCtrl))
            return;

        if (!TryGetUIControl<SceneEditWindow.CustomPartsWindow>(
            uiRoot, "Window/CustomPartsWindow", out var sceneEditWindow
        ))
            return;

        // Preset application
        presetCtrl.m_maid = maid;

        // Preset saving
        presetButtonCtrl.m_maid = maid;

        // Maid profile (name, description, experience etc)
        profileCtrl.m_maidStatus = maid.status;

        // Accessory/Parts placement
        sceneEditWindow.maid = maid;

        // Stopping maid animation and head movement when customizing parts placement
        sceneEditWindow.animation = maid.GetAnimation();

        // Clothing/body in general and maybe other things
        SceneEdit.Instance.m_maid = maid;

        // Body status, parts colours and maybe more
        GameMain.Instance.CharacterMgr.m_gcActiveMaid[0] = maid;

        static bool TryGetUIControl<T>(GameObject root, string hierarchy, out T uiControl) where T : MonoBehaviour
        {
            uiControl = null;

            var uiElement = UTY.GetChildObjectNoError(root, hierarchy);

            if (!uiElement)
                return false;

            uiControl = uiElement.GetComponent<T>();

            return uiControl;
        }
    }

    [HarmonyPostfix]
    [HarmonyPatch(typeof(SceneEdit), nameof(SceneEdit.Start))]
    private static void SceneEditStartPostfix()
    {
        EditMaidIndex = -1;

        if (SceneEdit.Instance.maid == null)
            return;

        var originalEditingMaid = SceneEdit.Instance.maid;

        EditMaidIndex = GameMain.Instance.CharacterMgr.GetStockMaidList()
            .FindIndex(maid => maid.status.guid == originalEditingMaid.status.guid);

        try
        {
            var editOkCancelButton = UTY.GetChildObject(GameObject.Find("UI Root"), "OkCancel")
                .GetComponent<EditOkCancel>();

            EditOkCancel.OnClick newEditOkCancelDelegate = RestoreOriginalEditingMaid;

            newEditOkCancelDelegate += editOkCancelButton.m_dgOnClickOk;

            editOkCancelButton.m_dgOnClickOk = newEditOkCancelDelegate;

            void RestoreOriginalEditingMaid()
            {
                // Only restore original editing maid when active.
                if (!active)
                    return;

                Utility.LogDebug($"Setting Editing maid back to '{originalEditingMaid.status.fullNameJpStyle}'");

                SetEditorMaid(originalEditingMaid);

                // Set SceneEdit's maid regardless of UI integration failing
                SceneEdit.Instance.m_maid = originalEditingMaid;
            }
        }
        catch (Exception e)
        {
            Utility.LogWarning($"Failed to hook onto Edit Mode OK button: {e}");
        }
    }

    public MeidoManager() =>
        Activate();

    public void ChangeMaid(int index) =>
        OnUpdateMeido(null, new MeidoUpdateEventArgs(index));

    public void Activate()
    {
        characterMgr.ResetCharaPosAll();

        if (!MeidoPhotoStudio.EditMode)
            characterMgr.DeactivateMaid(0);

        Meidos = characterMgr.GetStockMaidList()
            .Select((_, stockNo) => new Meido(stockNo)).ToArray();

        tempEditMaidIndex = -1;

        if (MeidoPhotoStudio.EditMode && EditMaidIndex >= 0)
            Meidos[EditMaidIndex].IsEditMaid = true;

        ClearSelectList();
        active = true;
    }

    public void Deactivate()
    {
        foreach (var meido in Meidos)
        {
            meido.UpdateMeido -= OnUpdateMeido;
            meido.GravityMove -= OnGravityMove;
            meido.Deactivate();
        }

        ActiveMeidoList.Clear();

        if (MeidoPhotoStudio.EditMode && !GameMain.Instance.MainCamera.IsFadeOut())
        {
            var meido = Meidos[EditMaidIndex];

            meido.Maid.Visible = true;
            meido.Stop = false;
            meido.EyeToCam = true;

            SetEditorMaid(meido.Maid);
        }

        active = false;
    }

    public void Update()
    {
        if (InputManager.GetKeyDown(MpsKey.MeidoUndressing))
            UndressAll();
    }

    public void CallMeidos()
    {
        BeginCallMeidos?.Invoke(this, EventArgs.Empty);

        var moreThanEditMaid = ActiveMeidoList.Count > 1;

        UnloadMeidos();

        if (SelectMeidoList.Count == 0)
        {
            OnEndCallMeidos(this, EventArgs.Empty);

            return;
        }

        void callMeidos() =>
            GameMain.Instance.StartCoroutine(LoadMeidos());

        if (MeidoPhotoStudio.EditMode && !moreThanEditMaid && SelectMeidoList.Count == 1)
            callMeidos();
        else
            GameMain.Instance.MainCamera.FadeOut(0.01f, f_bSkipable: false, f_dg: callMeidos);
    }

    public void SelectMeido(int index)
    {
        if (SelectedMeidoSet.Contains(index))
        {
            if (!MeidoPhotoStudio.EditMode || index != EditMaidIndex)
            {
                SelectedMeidoSet.Remove(index);
                SelectMeidoList.Remove(index);
            }
        }
        else
        {
            SelectedMeidoSet.Add(index);
            SelectMeidoList.Add(index);
        }
    }

    public void ClearSelectList()
    {
        SelectedMeidoSet.Clear();
        SelectMeidoList.Clear();

        if (MeidoPhotoStudio.EditMode)
        {
            SelectedMeidoSet.Add(EditMaidIndex);
            SelectMeidoList.Add(EditMaidIndex);
        }
    }

    public void SetEditMaid(Meido meido)
    {
        if (!MeidoPhotoStudio.EditMode)
            return;

        EditMeido.IsEditMaid = false;

        tempEditMaidIndex = meido.StockNo == EditMaidIndex ? -1 : meido.StockNo;

        EditMeido.IsEditMaid = true;

        SetEditorMaid(EditMeido.Maid);
    }

    public Meido GetMeido(string guid) =>
        string.IsNullOrEmpty(guid) ? null : ActiveMeidoList.Find(meido => meido.Maid.status.guid == guid);

    public Meido GetMeido(int activeIndex) =>
        activeIndex >= 0 && activeIndex < ActiveMeidoList.Count ? ActiveMeidoList[activeIndex] : null;

    public void PlaceMeidos(string placementType) =>
        MaidPlacementUtility.ApplyPlacement(placementType, ActiveMeidoList);

    private void UnloadMeidos()
    {
        SelectedMeido = 0;

        var commonMeidoIDs = new HashSet<int>(
            ActiveMeidoList.Where(meido => SelectedMeidoSet.Contains(meido.StockNo)).Select(meido => meido.StockNo)
        );

        foreach (var meido in ActiveMeidoList)
        {
            meido.UpdateMeido -= OnUpdateMeido;
            meido.GravityMove -= OnGravityMove;

            if (!commonMeidoIDs.Contains(meido.StockNo))
                meido.Unload();
        }

        ActiveMeidoList.Clear();
    }

    private System.Collections.IEnumerator LoadMeidos()
    {
        foreach (var slot in SelectMeidoList)
            ActiveMeidoList.Add(Meidos[slot]);

        for (var i = 0; i < ActiveMeidoList.Count; i++)
            ActiveMeidoList[i].Load(i);

        while (Busy)
            yield return null;

        yield return new WaitForEndOfFrame();

        OnEndCallMeidos(this, EventArgs.Empty);
    }

    private void UndressAll()
    {
        if (!HasActiveMeido)
            return;

        undress = ++undress % Enum.GetNames(typeof(Meido.Mask)).Length;

        foreach (var activeMeido in ActiveMeidoList)
            activeMeido.SetMaskMode((Meido.Mask)undress);

        UpdateMeido?.Invoke(ActiveMeido, new MeidoUpdateEventArgs(SelectedMeido));
    }

    private void OnUpdateMeido(object sender, MeidoUpdateEventArgs args)
    {
        if (!args.IsEmpty)
            SelectedMeido = args.SelectedMeido;

        UpdateMeido?.Invoke(ActiveMeido, args);
    }

    private void OnEndCallMeidos(object sender, EventArgs args)
    {
        GameMain.Instance.MainCamera.FadeIn(1f);
        EndCallMeidos?.Invoke(this, EventArgs.Empty);

        foreach (var meido in ActiveMeidoList)
        {
            meido.UpdateMeido += OnUpdateMeido;
            meido.GravityMove += OnGravityMove;
        }

        if (MeidoPhotoStudio.EditMode && tempEditMaidIndex >= 0 && !SelectedMeidoSet.Contains(tempEditMaidIndex))
            SetEditMaid(Meidos[EditMaidIndex]);
    }

    private void OnGravityMove(object sender, GravityEventArgs args)
    {
        if (!GlobalGravity)
            return;

        foreach (var meido in ActiveMeidoList)
            meido.ApplyGravity(args.LocalPosition, args.IsSkirt);
    }
}

public class MeidoUpdateEventArgs : EventArgs
{
    public static new MeidoUpdateEventArgs Empty { get; } = new(-1);

    public int SelectedMeido { get; }
    public bool IsBody { get; }
    public bool FromMeido { get; }

    public bool IsEmpty =>
        this == Empty || SelectedMeido == -1 && !FromMeido && IsBody;

    public MeidoUpdateEventArgs(int meidoIndex = -1, bool fromMaid = false, bool isBody = true)
    {
        SelectedMeido = meidoIndex;
        IsBody = isBody;
        FromMeido = fromMaid;
    }
}