using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Media;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
using PKHeX.Drawing;
using static PKHeX.Core.MessageStrings;

namespace PKHeX.WinForms.Controls;

public partial class SAVEditor : UserControl, ISlotViewer<PictureBox>, ISaveFileProvider
{
    public SaveDataEditor<PictureBox> EditEnv = null!;

    public void SetEditEnvironment(SaveDataEditor<PictureBox> value)
    {
        EditEnv = value;
        M.Env = value;
        menu.Editor = value;
        SAV = value.SAV;
        value.Slots.Publisher.Subscribe(this);
        value.Slots.Publisher.Subscribe(SL_Party);
        value.Slots.Publisher.Subscribe(Box);
        value.Slots.Publisher.Subscribe(SL_Extra);
    }

    public SaveFile SAV { get; private set; } = FakeSaveFile.Default;
    public int CurrentBox => Box.CurrentBox;
    public SlotChangeManager M { get; }
    public readonly ContextMenuSAV menu;
    public readonly BoxMenuStrip SortMenu;

    public bool HaX;
    public bool ModifyPKM { private get; set; }
    private bool _hideSecret;
    public bool HideSecretDetails { private get => _hideSecret; set => ToggleSecrets(SAV, _hideSecret = value); }
    public ToolStripMenuItem Menu_Redo { get; set; } = null!;
    public ToolStripMenuItem Menu_Undo { get; set; } = null!;
    private bool FieldsLoaded;

    public IList<PictureBox> SlotPictureBoxes { get; }

    public int ViewIndex { get; set; } = -1;

    public bool FlagIllegal
    {
        get => Box.FlagIllegal;
        set
        {
            SL_Extra.FlagIllegal = SL_Party.FlagIllegal = Box.FlagIllegal = value && !HaX;
            if (SAV is FakeSaveFile)
                return;
            ReloadSlots();
        }
    }

    public void ReloadSlots()
    {
        UpdateBoxViewers(all: true);
        ResetNonBoxSlots();
    }

    public SAVEditor()
    {
        InitializeComponent();

        L_SlotOccupied = [L_DC1, L_DC2];
        TB_SlotEXP = [TB_Daycare1XP, TB_Daycare2XP];
        L_SlotEXP = [L_XP1, L_XP2];
        SlotPictureBoxes = [dcpkx1, dcpkx2];

        Tab_Box.ContextMenuStrip = SortMenu = new BoxMenuStrip(this);
        M = new SlotChangeManager(this) { Env = EditEnv };
        Box.Setup(M);
        SL_Party.Setup(M);

        SL_Extra.ViewIndex = -2;
        menu = new ContextMenuSAV { Manager = M };
        InitializeEvents();
    }

    private void InitializeEvents()
    {
        foreach (PictureBox pb in SlotPictureBoxes)
        {
            InitializeDragDrop(pb);
            pb.ContextMenuStrip = menu.mnuVSD;
        }

        GiveFeedback += (_, e) => e.UseDefaultCursors = false;
        Tab_Box.MouseWheel += (_, e) =>
        {
            if (menu.mnuVSD.Visible)
                return;
            Box.CurrentBox = e.Delta > 1 ? Box.Editor.MoveLeft() : Box.Editor.MoveRight();
        };

        GB_Daycare.Click += (_, _) => SwitchDaycare();
        FLP_SAVtools.Scroll += WinFormsUtil.PanelScroll;
        SortMenu.Opening += (_, x) => x.Cancel = !tabBoxMulti.GetTabRect(tabBoxMulti.SelectedIndex).Contains(PointToClient(MousePosition));
    }

    private void InitializeDragDrop(Control pb)
    {
        pb.MouseEnter += M.MouseEnter;
        pb.MouseLeave += M.MouseLeave;
        pb.MouseClick += M.MouseClick;
        pb.MouseMove += M.MouseMove;
        pb.MouseDown += M.MouseDown;
        pb.MouseUp += M.MouseUp;

        pb.DragEnter += M.DragEnter;
        pb.DragDrop += M.DragDrop;
        pb.QueryContinueDrag += M.QueryContinueDrag;
        pb.GiveFeedback += (_, e) => e.UseDefaultCursors = false;
        pb.AllowDrop = true;
        pb.ContextMenuStrip = menu.mnuVSD;
    }

    /// <summary>Occurs when the Control Collection requests a cloning operation to the current box.</summary>
    public event EventHandler? RequestCloneData;

    /// <summary>Occurs when the Control Collection requests a save to be reloaded.</summary>
    public event EventHandler? RequestReloadSave;

    public void EnableDragDrop(DragEventHandler enter, DragEventHandler drop)
    {
        AllowDrop = true;
        DragDrop += drop;
        foreach (var tab in tabBoxMulti.TabPages.OfType<TabPage>())
        {
            tab.AllowDrop = true;
            tab.DragEnter += enter;
            tab.DragDrop += drop;
        }
        M.Drag.RequestExternalDragDrop += drop;
    }

    // Generic Subfunctions //

    public int SwapBoxesViewer(int viewBox)
    {
        int mainBox = Box.CurrentBox;
        Box.CurrentBox = viewBox;
        return mainBox;
    }

    public void UpdateBoxViewers(bool all = false)
    {
        foreach (var v in M.Boxes.Where(v => v.CurrentBox == Box.CurrentBox || all))
        {
            v.FlagIllegal = Box.FlagIllegal;
            v.ResetSlots();
            v.ResetBoxNames();
        }
    }

    public void NotifySlotOld(ISlotInfo previous)
    {
        var index = GetViewIndex(previous);
        if (index < 0)
            return;

        var pb = SlotPictureBoxes[index];
        pb.BackgroundImage = null;
    }

    public int GetViewIndex(ISlotInfo slot)
    {
        if (GetCurrentDaycare() is not { } dc)
            return -1;

        for (int i = 0; i < SlotPictureBoxes.Count; i++)
        {
            if (dc.DaycareSlotCount == i)
                break;
            var data = GetSlotData(i);
            if (data.Equals(slot))
                return i;
        }
        return -1;
    }

    public void NotifySlotChanged(ISlotInfo slot, SlotTouchType type, PKM pk)
    {
        var index = GetViewIndex(slot);
        if (index < 0)
            return;

        if (type.IsContentChange() && slot is SlotInfoParty)
            ResetParty(); // lots of slots change, just update

        var pb = SlotPictureBoxes[index];
        SlotUtil.UpdateSlot(pb, slot, pk, SAV, Box.FlagIllegal, type);
    }

    public ISlotInfo GetSlotData(PictureBox view)
    {
        var index = SlotPictureBoxes.IndexOf(view);
        return GetSlotData(index);
    }

    private SlotInfoMisc GetSlotData(int index)
    {
        if (GetCurrentDaycare() is not { } s)
            throw new Exception();
        return new SlotInfoMisc(s.GetDaycareSlot(index), index) { Type = StorageSlotType.Daycare };
    }

    public void SetPKMBoxes()
    {
        if (SAV.HasBox)
            Box.ResetSlots();

        ResetNonBoxSlots();
    }

    private void ResetNonBoxSlots()
    {
        ResetParty();
        ResetDaycare();
        ResetMiscSlots();
    }

    private void ResetMiscSlots()
    {
        var slots = SL_Extra.SlotPictureBoxes;
        for (int i = 0; i < SL_Extra.SlotCount; i++)
        {
            var info = SL_Extra.GetSlotData(i);
            var pb = slots[i];
            var showLegality = info is not SlotInfoMisc { HideLegality: true };
            SlotUtil.UpdateSlot(pb, info, info.Read(SAV), SAV, Box.FlagIllegal && showLegality);
        }
    }

    private void ResetParty()
    {
        if (SAV.HasParty)
            SL_Party.ResetSlots();
    }

    private readonly Label[] L_SlotOccupied;
    private readonly TextBox[] TB_SlotEXP;
    private readonly Label[] L_SlotEXP;

    private void ResetDaycare()
    {
        IDaycareStorage? s = GetCurrentDaycare();
        if (s is null)
            return;

        int slotCount = s.DaycareSlotCount;
        for (int i = 0; i < 2; i++)
        {
            if (i >= slotCount)
            {
                L_SlotOccupied[i].Visible = false;
                TB_SlotEXP[i].Visible = false;
                continue;
            }

            if (s is IDaycareExperience dExp)
            {
                var exp = dExp.GetDaycareEXP(i);
                L_SlotEXP[i].Visible = TB_SlotEXP[i].Visible = true;
                TB_SlotEXP[i].Text = exp.ToString();
            }
            else
            {
                L_SlotEXP[i].Visible = TB_SlotEXP[i].Visible = false;
            }

            bool occ = s.IsDaycareOccupied(i);
            L_SlotOccupied[i].Visible = true;
            if (occ) // If Occupied
            {
                L_SlotOccupied[i].Text = $"{i + 1}: ✓";
                UpdateSlot(i);
            }
            else
            {
                L_SlotOccupied[i].Text = $"{i + 1}: ✘";
                var pb = UpdateSlot(i);
                var current = pb.Image;
                if (current is not null)
                    pb.Image = ImageUtil.ChangeOpacity(current, 0.6);
            }
        }

        LoadDaycareEggState(s);
        LoadDaycareSeed(s);
    }

    private IDaycareStorage? GetCurrentDaycare()
    {
        if (SAV is IDaycareMulti m)
        {
            if (DaycareIndex < m.DaycareCount)
                return m[DaycareIndex];
            return m[DaycareIndex = 0];
        }
        if (SAV is IDaycareStorage s)
            return s;
        return null;
    }

    private void LoadDaycareEggState(IDaycareStorage s)
    {
        if (s is IDaycareEggState dEgg)
        {
            DayCare_HasEgg.Visible = true;
            DayCare_HasEgg.Checked = dEgg.IsEggAvailable;
        }
        else
        {
            DayCare_HasEgg.Visible = false;
        }
    }

    private void LoadDaycareSeed(IDaycareStorage s)
    {
        if (s is IDaycareRandomState<ushort> u16)
        {
            TB_RNGSeed.Visible = true;
            TB_RNGSeed.MaxLength = 4;
            TB_RNGSeed.Text = $"{u16.Seed:X4}";
        }
        else if (s is IDaycareRandomState<uint> u32)
        {
            TB_RNGSeed.Visible = true;
            TB_RNGSeed.MaxLength = 8;
            TB_RNGSeed.Text = $"{u32.Seed:X8}";
        }
        else if (s is IDaycareRandomState<ulong> u64)
        {
            TB_RNGSeed.Visible = true;
            TB_RNGSeed.MaxLength = 16;
            TB_RNGSeed.Text = $"{u64.Seed:X16}";
        }
        else if (s is IDaycareRandomState<UInt128> u128)
        {
            TB_RNGSeed.Visible = true;
            TB_RNGSeed.MaxLength = 32;
            TB_RNGSeed.Text = $"{u128.Seed:X32}";
        }
        else
        {
            L_DaycareSeed.Visible = TB_RNGSeed.Visible = false;
            return;
        }

        L_DaycareSeed.Visible = TB_RNGSeed.Visible = true;
    }

    private PictureBox UpdateSlot(int relIndex)
    {
        var info = GetSlotData(relIndex);
        var pb = SlotPictureBoxes[relIndex];
        SlotUtil.UpdateSlot(pb, info, info.Read(SAV), SAV, Box.FlagIllegal);
        return pb;
    }

    public void SetParty() => ResetParty();

    public void ClickUndo()
    {
        EditEnv.Slots.Undo();
        UpdateUndoRedo();
    }

    public void ClickRedo()
    {
        EditEnv.Slots.Redo();
        UpdateUndoRedo();
    }

    public void UpdateUndoRedo()
    {
        Menu_Undo.Enabled = EditEnv.Slots.Changelog.CanUndo;
        Menu_Redo.Enabled = EditEnv.Slots.Changelog.CanRedo;
    }

    public void SetClonesToBox(PKM pk)
    {
        if (WinFormsUtil.Prompt(MessageBoxButtons.YesNo, string.Format(MsgSaveBoxCloneFromTabs, Box.CurrentBoxName)) != DialogResult.Yes)
            return;

        int slotSkipped = SetClonesToCurrentBox(pk, Box.CurrentBox);
        if (slotSkipped > 0)
            WinFormsUtil.Alert(string.Format(MsgSaveBoxImportSkippedLocked, slotSkipped));

        UpdateBoxViewers();
    }

    private int SetClonesToCurrentBox(PKM pk, int box)
    {
        var arr = new PKM[SAV.BoxSlotCount];
        for (int i = 0; i < SAV.BoxSlotCount; i++) // set to every slot in box
            arr[i] = pk;

        int slotSkipped = SAV.SetBoxData(arr, box);
        Box.ResetSlots();
        return slotSkipped;
    }

    public void ClickSlot(object sender, EventArgs e)
    {
        switch (ModifierKeys)
        {
            case Keys.Control | Keys.Alt:
                ClickClone(sender, e);
                break;
            default: // forward to contextmenu for default behavior
                menu.OmniClick(sender, e, ModifierKeys);
                break;
        }
    }

    private void ClickBoxSort(object sender, MouseEventArgs e)
    {
        if (tabBoxMulti.SelectedTab != Tab_Box)
            return;
        if (!tabBoxMulti.GetTabRect(tabBoxMulti.SelectedIndex).Contains(PointToClient(MousePosition)))
            return;
        if ((e.Button & MouseButtons.Right) == 0)
        {
            if ((ModifierKeys & Keys.Alt) != 0)
                SortMenu.Clear();
            else if ((ModifierKeys & Keys.Control) != 0)
                SortMenu.Sort();
            return;
        }
        var pt = Tab_Box.PointToScreen(new Point(0, 0));
        SortMenu.Show(pt);
    }

    public void FinishBoxManipulation(string message, bool all, int count)
    {
        SetPKMBoxes();
        UpdateBoxViewers(all);
        if (!string.IsNullOrWhiteSpace(message))
            WinFormsUtil.Alert(message + $" ({count})");
        else
            SystemSounds.Asterisk.Play();
    }

    private void ClickBoxDouble(object sender, MouseEventArgs e)
    {
        if (tabBoxMulti.SelectedTab == Tab_SAV)
        {
            RequestReloadSave?.Invoke(sender, e);
            return;
        }
        if (tabBoxMulti.SelectedTab != Tab_Box)
            return;
        if (!SAV.HasBox)
            return;
        if (ModifierKeys == Keys.Shift)
        {
            if (M.Boxes.Count > 1) // subview open
            {
                // close all subviews
                for (int i = 1; i < M.Boxes.Count; i++)
                    M.Boxes[i].ParentForm?.Close();
            }
            new SAV_BoxList(this, M).Show();
            return;
        }
        if (M.Boxes.Count > 1) // subview open
        {
            var z = M.Boxes[1].ParentForm;
            if (z is null)
                return;
            z.CenterToForm(ParentForm);
            z.BringToFront();
            return;
        }
        new SAV_BoxViewer(this, M, Box.CurrentBox).Show();
    }

    private void ClickClone(object sender, EventArgs e)
    {
        var detail = Box.GetSlotData((PictureBox)sender);
        if (detail is SlotInfoBox)
            RequestCloneData?.Invoke(sender, e);
    }

    private void UpdateSaveSlot(object sender, EventArgs e)
    {
        var index = WinFormsUtil.GetIndex(CB_SaveSlot);
        if (SAV is not SAV4BR br || br.CurrentSlot == index)
            return;

        var form = WinFormsUtil.FirstFormOfType<SAV_BattlePass>();
        form?.Close();

        br.CurrentSlot = index;
        Box.ResetBoxNames(); // fix box names
        SetPKMBoxes();
        UpdateBoxViewers(true);
    }

    private void UpdateStringSeed(object sender, EventArgs e)
    {
        if (!FieldsLoaded)
            return;

        if (sender is not TextBox tb)
            return;

        if (string.IsNullOrWhiteSpace(tb.Text))
        {
            tb.Undo();
            return;
        }

        string filterText = Util.GetOnlyHex(tb.Text);
        if (string.IsNullOrWhiteSpace(filterText) || filterText.Length != tb.Text.Length)
        {
            WinFormsUtil.Alert(MsgProgramErrorExpectedHex, tb.Text);
            tb.Undo();
            return;
        }

        // Write final value back to the save
        if (tb == TB_RNGSeed)
        {
            if (GetCurrentDaycare() is { } s)
                SetDaycareSeed(s, filterText);
        }
        else if (tb == TB_GameSync && SAV is IGameSync sync)
        {
            var value = filterText.PadLeft(sync.GameSyncIDSize, '0');
            sync.GameSyncID = value;
            SAV.State.Edited = true;
        }
        else if (SAV is ISecureValueStorage s)
        {
            var value = Convert.ToUInt64(filterText, 16);
            if (tb == TB_Secure1)
                s.TimeStampCurrent = value;
            else if (tb == TB_Secure2)
                s.TimeStampPrevious = value;
            SAV.State.Edited = true;
        }
    }

    private void SetDaycareSeed(IDaycareStorage daycare, string filterText)
    {
        if (daycare is IDaycareRandomState<ushort> u16)
        {
            if (ushort.TryParse(filterText, System.Globalization.NumberStyles.HexNumber, null, out var v16))
                u16.Seed = v16;
        }
        else if (daycare is IDaycareRandomState<uint> u32)
        {
            if (uint.TryParse(filterText, System.Globalization.NumberStyles.HexNumber, null, out var v32))
                u32.Seed = v32;
        }
        else if (daycare is IDaycareRandomState<ulong> u64)
        {
            if (ulong.TryParse(filterText, System.Globalization.NumberStyles.HexNumber, null, out var v64))
                u64.Seed = v64;
        }
        else if (daycare is IDaycareRandomState<UInt128> u128)
        {
            if (UInt128.TryParse(filterText, System.Globalization.NumberStyles.HexNumber, null, out var v128))
                u128.Seed = v128;
        }
        SAV.State.Edited = true;
    }

    private int DaycareIndex;

    private void SwitchDaycare()
    {
        if (SAV is not IDaycareMulti m)
            return;
        var current = string.Format(MsgSaveSwitchDaycareCurrent, DaycareIndex + 1);
        var next = (DaycareIndex + 1) % m.DaycareCount;
        if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgSaveSwitchDaycareView, current))
            return;
        DaycareIndex = next;
        ResetDaycare();
    }

    private void B_SaveBoxBin_Click(object sender, EventArgs e)
    {
        if (!SAV.HasBox)
        { WinFormsUtil.Alert(MsgSaveBoxFailNone); return; }
        Box.SaveBoxBinary();
    }

    // Subfunction Save Buttons //
    private static void OpenDialog(Form f)
    {
        f.ShowDialog();
        f.Dispose();
    }

    private void B_OpenWondercards_Click(object sender, EventArgs e) => OpenDialog(new SAV_Wondercard(SAV, sender as DataMysteryGift));
    private void B_OpenPokepuffs_Click(object sender, EventArgs e) => OpenDialog(new SAV_Pokepuff((ISaveBlock6Main)SAV));
    private void B_OpenPokeBeans_Click(object sender, EventArgs e) => OpenDialog(new SAV_Pokebean((SAV7)SAV));
    private void B_OpenItemPouch_Click(object sender, EventArgs e) => OpenDialog(new SAV_Inventory(SAV));
    private void B_OpenBerryField_Click(object sender, EventArgs e) => OpenDialog(new SAV_BerryFieldXY((SAV6XY)SAV));
    private void B_OpenPokeblocks_Click(object sender, EventArgs e) => OpenDialog(new SAV_PokeBlockORAS((SAV6AO)SAV));
    private void B_OpenSuperTraining_Click(object sender, EventArgs e) => OpenDialog(new SAV_SuperTrain((SAV6)SAV));
    private void B_CellsStickers_Click(object sender, EventArgs e) => OpenDialog(new SAV_ZygardeCell((SAV7)SAV));
    private void B_LinkInfo_Click(object sender, EventArgs e) => OpenDialog(new SAV_Link6(SAV));
    private void B_OpenApricorn_Click(object sender, EventArgs e) => OpenDialog(new SAV_Apricorn((SAV4HGSS)SAV));
    private void B_DLC_Click(object sender, EventArgs e) => OpenDialog(new SAV_DLC5((SAV5)SAV));
    private void B_OpenTrainerInfo_Click(object sender, EventArgs e) => OpenDialog(GetTrainerEditor(SAV));
    private void B_OpenOPowers_Click(object sender, EventArgs e) => OpenDialog(new SAV_OPower((ISaveBlock6Main)SAV));
    private void B_OpenHoneyTreeEditor_Click(object sender, EventArgs e) => OpenDialog(new SAV_HoneyTree((SAV4Sinnoh)SAV));
    private void B_OpenGeonetEditor_Click(object sender, EventArgs e) => OpenDialog(new SAV_Geonet4((SAV4)SAV));
    private void B_OpenUnityTowerEditor_Click(object sender, EventArgs e) => OpenDialog(new SAV_UnityTower((SAV5)SAV));
    private void B_OpenChatterEditor_Click(object sender, EventArgs e) => OpenDialog(new SAV_Chatter(SAV));
    private void B_OpenGear_Click(object sender, EventArgs e) => OpenDialog(new SAV_Gear((SAV4BR)SAV));

    private void B_OpenSecretBase_Click(object sender, EventArgs e)
    {
        if (SAV is SAV3 s3)
            OpenDialog(new SAV_SecretBase3(s3));
        else if (SAV is SAV6AO ao)
            OpenDialog(new SAV_SecretBase(ao));
    }

    private void B_Roamer_Click(object sender, EventArgs e)
    {
        if (SAV is SAV3 s3)
            OpenDialog(new SAV_Roamer3(s3));
        else if (SAV is SAV6XY xy)
            OpenDialog(new SAV_Roamer6(xy));
    }

    private void B_OpenEventFlags_Click(object sender, EventArgs e)
    {
        using var form = SAV switch
        {
            SAV1 s => (Form)new SAV_EventReset1(s),
            SAV7b s => new SAV_EventWork(s),
            SAV8BS s => new SAV_FlagWork8b(s),
            IEventFlag37 g37 => new SAV_EventFlags(g37, SAV.Version),
            IEventFlagProvider37 p => new SAV_EventFlags(p.EventWork, SAV.Version),
            SAV2 s => new SAV_EventFlags2(s),
            SAV9ZA za => new SAV_FlagWork9a(za),
            _ => throw new Exception(),
        };
        form.ShowDialog();
    }

    private void B_OpenBoxLayout_Click(object sender, EventArgs e)
    {
        OpenDialog(new SAV_BoxLayout(SAV, Box.CurrentBox));
        Box.ResetBoxNames(); // fix box names
        Box.ResetSlots(); // refresh box background
        UpdateBoxViewers(all: true); // update subviewers
    }

    private static Form GetTrainerEditor(SaveFile sav) => sav switch
    {
        SAV6 s6 => new SAV_Trainer(s6),
        SAV7 s7 => new SAV_Trainer7(s7),
        SAV7b b7 => new SAV_Trainer7GG(b7),
        SAV8SWSH swsh => new SAV_Trainer8(swsh),
        SAV8BS bs => new SAV_Trainer8b(bs),
        SAV8LA la => new SAV_Trainer8a(la),
        SAV9SV sv => new SAV_Trainer9(sv),
        SAV9ZA za => new SAV_Trainer9a(za),
        SAV4BR br => new SAV_Trainer4BR(br),
        _ => new SAV_SimpleTrainer(sav),
    };

    private void B_OpenRaids_Click(object sender, EventArgs e)
    {
        if (SAV is SAV9SV sv)
        {
            if (sender == B_Raids)
                OpenDialog(new SAV_Raid9(sv, TeraRaidOrigin.Paldea));
            else if (sender == B_RaidsDLC1)
                OpenDialog(new SAV_Raid9(sv, TeraRaidOrigin.Kitakami));
            else if (sender == B_RaidsDLC2)
                OpenDialog(new SAV_Raid9(sv, TeraRaidOrigin.BlueberryAcademy));
            else if (sender == B_RaidsSevenStar)
                OpenDialog(new SAV_RaidSevenStar9(sv));
        }
        else if (SAV is SAV8SWSH swsh)
        {
            if (sender == B_Raids)
                OpenDialog(new SAV_Raid8(swsh, MaxRaidOrigin.Galar));
            else if (sender == B_RaidsDLC1)
                OpenDialog(new SAV_Raid8(swsh, MaxRaidOrigin.IsleOfArmor));
            else if (sender == B_RaidsDLC2)
                OpenDialog(new SAV_Raid8(swsh, MaxRaidOrigin.CrownTundra));
        }
    }

    private void B_OtherSlots_Click(object sender, EventArgs e)
    {
        void TryOpen(SaveFile sav, IReadOnlyList<SlotGroup> g)
        {
            var form = WinFormsUtil.FirstFormOfType<SAV_GroupViewer>();
            if (form is not null)
            {
                form.CenterToForm(ParentForm);
            }
            else
            {
                form = new SAV_GroupViewer(sav, M.Env.PKMEditor, g)
                {
                    Owner = ParentForm,
                };
            }
            form.BringToFront();
            form.Show();
        }

        if (SAV is SAV_STADIUM s0)
            TryOpen(s0, s0.GetRegisteredTeams());
    }

    private void B_OpenBattlePass_Click(object sender, EventArgs e)
    {
        void TryOpen(SAV4BR sav)
        {
            var form = WinFormsUtil.FirstFormOfType<SAV_BattlePass>();
            if (form is not null)
                form.CenterToForm(ParentForm);
            else
                form = new SAV_BattlePass(sav, M.Env.PKMEditor) { Owner = ParentForm };
            form.BringToFront();
            form.Show();
        }

        if (SAV is SAV4BR br)
            TryOpen(br);
    }

    private void B_Blocks_Click(object sender, EventArgs e)
    {
        var form = GetAccessorForm(SAV);
        form.ShowDialog();
        form.Dispose();
    }

    private static Form GetAccessorForm(SaveFile sav) => sav switch
    {
        SAV5BW s => new SAV_Accessor<SaveBlockAccessor5BW>(s, s.Blocks),
        SAV5B2W2 s => new SAV_Accessor<SaveBlockAccessor5B2W2>(s, s.Blocks),
        SAV6XY s => new SAV_Accessor<SaveBlockAccessor6XY>(s, s.Blocks),
        SAV6AO s => new SAV_Accessor<SaveBlockAccessor6AO>(s, s.Blocks),
        SAV6AODemo s => new SAV_Accessor<SaveBlockAccessor6AODemo>(s, s.Blocks),
        SAV7SM s => new SAV_Accessor<SaveBlockAccessor7SM>(s, s.Blocks),
        SAV7USUM s => new SAV_Accessor<SaveBlockAccessor7USUM>(s, s.Blocks),
        SAV7b s => new SAV_Accessor<SaveBlockAccessor7b>(s, s.Blocks),
        ISCBlockArray s => new SAV_BlockDump8(s),
        _ => GetPropertyForm(sav),
    };

    private static Form GetPropertyForm(object sav)
    {
        var form = new Form
        {
            Text = "Simple Editor",
            StartPosition = FormStartPosition.CenterParent,
            MinimumSize = new Size(350, 380),
            MinimizeBox = false,
            MaximizeBox = false,
            Icon = Properties.Resources.Icon,
        };
        var pg = new PropertyGrid { SelectedObject = sav, Dock = DockStyle.Fill };
        form.Controls.Add(pg);
        return form;
    }

    private void B_OpenFriendSafari_Click(object sender, EventArgs e)
    {
        if (SAV is not SAV6XY xy)
            return;

        var dr = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgSaveGen6FriendSafari, MsgSaveGen6FriendSafariCheatDesc);
        if (dr == DialogResult.Yes)
            xy.UnlockAllFriendSafariSlots();
    }

    private void B_OpenPokedex_Click(object sender, EventArgs e)
    {
        using var form = SAV switch
        {
            SAV1 s1 => new SAV_SimplePokedex(s1),
            SAV2 s2 => new SAV_SimplePokedex(s2),
            SAV3 s3 => new SAV_SimplePokedex(s3),
            SAV4 s4 => new SAV_Pokedex4(s4),
            SAV5 s5 => new SAV_Pokedex5(s5),
            SAV6XY xy => new SAV_PokedexXY(xy),
            SAV6AO ao => new SAV_PokedexORAS(ao),
            SAV7 s7 => new SAV_PokedexSM(s7),
            SAV7b b7 => new SAV_PokedexGG(b7),
            SAV8SWSH swsh => new SAV_PokedexSWSH(swsh),
            SAV8BS bs => new SAV_PokedexBDSP(bs),
            SAV8LA la => new SAV_PokedexLA(la),
            SAV9SV sv => sv.SaveRevision == 0 ? new SAV_PokedexSV(sv) : new SAV_PokedexSVKitakami(sv),
            SAV9ZA za => new SAV_Pokedex9a(za),
            _ => (Form?)null,
        };
        form?.ShowDialog();
    }

    private void B_OpenMiscEditor_Click(object sender, EventArgs e)
    {
        using var form = SAV switch
        {
            SAV2 sav2 => new SAV_Misc2(sav2),
            SAV3 sav3 => new SAV_Misc3(sav3),
            SAV4 sav4 => new SAV_Misc4(sav4),
            SAV5 sav5 => new SAV_Misc5(sav5),
            SAV8BS bs => new SAV_Misc8b(bs),
            _ => (Form?)null,
        };
        form?.ShowDialog();
    }

    private void B_OpenRTCEditor_Click(object sender, EventArgs e)
    {
        switch (SAV.Generation)
        {
            case 2:
                var sav2 = ((SAV2)SAV);
                var msg = MsgSaveGen2RTCResetBitflag;
                if (!sav2.Japanese) // show Reset Key for non-Japanese saves
                    msg = string.Format(MsgSaveGen2RTCResetPassword, sav2.ResetKey) + Environment.NewLine + Environment.NewLine + msg;
                var dr = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, msg);
                if (dr == DialogResult.Yes)
                    sav2.ResetRTC();
                break;
            case 3:
                OpenDialog(new SAV_RTC3(SAV));
                break;
        }
    }

    private void B_OUTPasserby_Click(object sender, EventArgs e)
    {
        if (SAV.Generation != 6)
            return;
        if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgSaveGen6Passerby))
            return;
        var result = PSS6.GetPSSParse((SAV6)SAV);
        WinFormsUtil.SetClipboardText(string.Join(Environment.NewLine, result));
    }

    private void B_HallofFame_Click(object sender, EventArgs e)
    {
        using var form = SAV switch
        {
            SAV1 s1 => new SAV_HallOfFame1(s1),
            SAV3 s3 => new SAV_HallOfFame3(s3),
            SAV6 s6 => new SAV_HallOfFame(s6),
            SAV7 s7 => new SAV_HallOfFame7(s7),
            _ => (Form?)null,
        };
        form?.ShowDialog();
    }

    private void B_JPEG_Click(object sender, EventArgs e)
    {
        var s6 = (SAV6)SAV;
        var jpeg = s6.GetJPEGData();
        if (jpeg.Length == 0)
        {
            WinFormsUtil.Alert(MsgSaveJPEGExportFail);
            return;
        }
        string filename = $"{s6.JPEGTitle}'s picture";
        using var sfd = new SaveFileDialog();
        sfd.FileName = filename;
        sfd.Filter = "JPEG|*.jpeg";
        if (sfd.ShowDialog() != DialogResult.OK)
            return;
        File.WriteAllBytes(sfd.FileName, jpeg);
    }

    private void B_OpenFashion_Click(object sender, EventArgs e)
    {
        var sav = (SAV9ZA)SAV;
        using var form = new SAV_Fashion9a(sav);
        form.ShowDialog();
    }

    private void B_ConvertKorean_Click(object sender, EventArgs e)
    {
        if (SAV.Generation != 4)
            return;
        var s4 = (SAV4)SAV;
        var isKorean = s4.Magic == SAV4.MAGIC_KOREAN;
        var msg = isKorean ? MsgSaveGen4ConvertInternational : MsgSaveGen4ConvertKorean;
        if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, msg))
            return;
        s4.Magic = isKorean ? SAV4.MAGIC_JAPAN_INTL : SAV4.MAGIC_KOREAN;
        SAV.State.Edited = true;
    }

    private void ClickVerifyCHK(object sender, EventArgs e)
    {
        if (SAV.State.Edited)
        {
            WinFormsUtil.Alert(MsgSaveChecksumFailEdited);
            return;
        }
        if (SAV.ChecksumsValid)
        {
            WinFormsUtil.Alert(MsgSaveChecksumValid);
            return;
        }

        if (DialogResult.Yes == WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgSaveChecksumFailExport))
            WinFormsUtil.SetClipboardText(SAV.ChecksumInfo);
    }

    private void ClickVerifyStoredEntities(object sender, EventArgs e)
    {
        var bulk = new Core.Bulk.BulkAnalysis(SAV, Main.Settings.Legality.Bulk);
        if (bulk.Valid)
        {
            WinFormsUtil.Alert("Clean!");
            return;
        }

        if (WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgClipboardLegalityExport) != DialogResult.Yes)
            return;

        var localization = LegalityLocalizationSet.GetLocalization(Main.CurrentLanguage);
        var msg = bulk.Report(localization);
        WinFormsUtil.SetClipboardText(msg);
        SystemSounds.Asterisk.Play();
    }

    // File I/O
    public bool GetBulkImportSettings(out bool clearAll, out bool overwrite, out EntityImportSettings settings)
    {
        clearAll = false; settings = default; overwrite = false;
        var dr = WinFormsUtil.Prompt(MessageBoxButtons.YesNoCancel, MsgSaveBoxImportClear, MsgSaveBoxImportClearNo);
        if (dr == DialogResult.Cancel)
            return false;

        clearAll = dr == DialogResult.Yes;
        settings = GetImportSettingsOverride();
        return true;
    }

    private static bool IsFolderPath(out string path)
    {
        if (Clipboard.ContainsText())
        {
            var directory = Clipboard.GetText();
            // Ask user if they want to use clipboard directory before showing folder browser
            if (Directory.Exists(directory) && WinFormsUtil.Prompt(MessageBoxButtons.YesNo, string.Format(MsgSaveBoxUseClipboard, directory)) == DialogResult.Yes)
            {
                path = directory;
                return true;
            }
        }

        using var fbd = new FolderBrowserDialog();
        var result = fbd.ShowDialog() == DialogResult.OK;
        path = fbd.SelectedPath;
        return result;
    }

    public bool ExportSaveFile()
    {
        ValidateChildren();
        bool reload = SAV is IStorageCleanup b && b.FixStoragePreWrite();
        if (reload)
            ReloadSlots();
        return WinFormsUtil.ExportSAVDialog(SAV, SAV.CurrentBox);
    }

    public bool ExportBackup()
    {
        if (!SAV.State.Exportable || SAV.Metadata.FilePath is not { } file)
            return false;

        if (!File.Exists(file))
        {
            WinFormsUtil.Error(MsgSaveBackupNotFound, file);
            return false;
        }

        var suggestion = PathUtil.CleanFileName(SAV.Metadata.BAKName);
        using var sfd = new SaveFileDialog();
        sfd.FileName = suggestion;
        if (sfd.ShowDialog() != DialogResult.OK)
            return false;

        string path = sfd.FileName;
        if (!File.Exists(file)) // did they move it again?
        {
            WinFormsUtil.Error(MsgSaveBackupNotFound, file);
            return false;
        }
        File.Copy(file, path, true);
        WinFormsUtil.Alert(MsgSaveBackup, path);

        return true;
    }

    public bool OpenPCBoxBin(ReadOnlySpan<byte> input, out string c)
    {
        if (SAV.GetPCBinary().Length == input.Length)
        {
            if (SAV.IsAnySlotLockedInBox(0, SAV.BoxCount - 1))
            { c = MsgSaveBoxImportPCFailBattle; return false; }
            if (!SAV.SetPCBinary(input))
            { c = string.Format(MsgSaveCurrentGeneration, SAV.Generation); return false; }

            c = MsgSaveBoxImportPCBinary;
        }
        else if (SAV.GetBoxBinary(Box.CurrentBox).Length == input.Length)
        {
            if (SAV.IsAnySlotLockedInBox(Box.CurrentBox, Box.CurrentBox))
            { c = MsgSaveBoxImportBoxFailBattle; return false; }
            if (!SAV.SetBoxBinary(input, Box.CurrentBox))
            { c = string.Format(MsgSaveCurrentGeneration, SAV.Generation); return false; }

            c = MsgSaveBoxImportBoxBinary;
        }
        else
        {
            c = string.Format(MsgSaveCurrentGeneration, SAV.Generation);
            return false;
        }
        SetPKMBoxes();
        UpdateBoxViewers();
        return true;
    }

    public bool OpenGroup(IPokeGroup b, out string c)
    {
        var msg = string.Format(MsgSaveBoxImportGroup, Box.CurrentBoxName);
        var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, msg, MsgSaveBoxImportOverwrite);
        if (prompt != DialogResult.Yes)
        {
            c = string.Empty;
            return false;
        }

        var settings = GetImportSettingsOverride();
        var slotSkipped = ImportGroup(b.Contents, SAV, Box.CurrentBox, settings);

        SetPKMBoxes();
        UpdateBoxViewers();

        c = slotSkipped > 0 ? string.Format(MsgSaveBoxImportSkippedLocked, slotSkipped) : MsgSaveBoxImportGroupSuccess;

        return true;
    }

    private static int ImportGroup(IEnumerable<PKM> data, SaveFile sav, int box, EntityImportSettings settings)
    {
        var type = sav.PKMType;
        int slotSkipped = 0;
        int index = 0;
        foreach (var x in data)
        {
            var i = index++;
            if (sav.IsBoxSlotOverwriteProtected(box, i))
            {
                slotSkipped++;
                continue;
            }

            var convert = EntityConverter.ConvertToType(x, type, out _);
            if (convert?.GetType() != type)
            {
                slotSkipped++;
                continue;
            }
            sav.SetBoxSlotAtIndex(x, box, i, settings);
        }

        return slotSkipped;
    }

    public bool LoadBoxes(out string result, string? path = null)
    {
        result = string.Empty;
        if (!SAV.HasBox)
            return false;

        if (path is null && !IsFolderPath(out path))
        {
            result = path;
            return false;
        }

        if (!Directory.Exists(path))
            return false;

        if (!GetBulkImportSettings(out bool clearAll, out var overwrite, out var settings))
            return false;

        SAV.LoadBoxes(path, out result, Box.CurrentBox, clearAll, overwrite, settings);
        SetPKMBoxes();
        UpdateBoxViewers();
        return true;
    }

    public bool ToggleInterface()
    {
        FieldsLoaded = false;

        ToggleViewReset();
        ToggleViewSubEditors(SAV);

        bool WindowTranslationRequired = false;
        WindowTranslationRequired |= ToggleViewBox(SAV);
        int BoxTab = tabBoxMulti.TabPages.IndexOf(Tab_Box);
        WindowTranslationRequired |= ToggleViewParty(SAV, BoxTab);
        int PartyTab = tabBoxMulti.TabPages.IndexOf(Tab_PartyBattle);
        WindowTranslationRequired |= ToggleViewDaycare(SAV, BoxTab, PartyTab);
        SetPKMBoxes(); // Reload all Entity picture boxes

        ToggleViewMisc(SAV);

        FieldsLoaded = true;
        return WindowTranslationRequired;
    }

    private void ToggleViewReset()
    {
        // Close sub-forms that are SaveFile dependent
        foreach (var z in M.Boxes.Skip(1).ToArray())
            z.FindForm()?.Close();

        Box.M = M;
        SL_Party.M = M;
        if (SAV.HasBox)
        {
            bool newSlots = Box.InitializeFromSAV(SAV);
            if (newSlots)
            {
                Box.HorizontallyCenter(Tab_Box);
                foreach (var pb in Box.SlotPictureBoxes)
                    pb.ContextMenuStrip = menu.mnuVSD;

                var grid = Box.BoxPokeGrid;
                var height = grid.Height + grid.Location.Y + Box.Location.Y; // needed height
                var allowed = Tab_Box.Height;
                if (height > allowed)
                {
                    var form = FindForm();
                    if (form is not null)
                        form.Height += height - allowed;
                }
            }
        }
        if (SAV.HasParty)
        {
            bool newSlots = SL_Party.InitializeFromSAV(SAV);
            if (newSlots)
            {
                SL_Party.HorizontallyCenter(Tab_PartyBattle);
                foreach (var pb in SL_Party.SlotPictureBoxes)
                    pb.ContextMenuStrip = menu.mnuVSD;
            }
        }

        SortMenu.ToggleVisibility();
    }

    private bool ToggleViewBox(SaveFile sav)
    {
        if (!sav.HasBox)
        {
            if (tabBoxMulti.TabPages.Contains(Tab_Box))
                tabBoxMulti.TabPages.Remove(Tab_Box);
            B_SaveBoxBin.Enabled = false;
            return false;
        }

        B_SaveBoxBin.Enabled = true;
        tabBoxMulti.SelectedIndex = 0;

        var box = sav.CurrentBox;
        Box.CurrentBox = (uint)box >= sav.BoxCount ? 0 : box;

        if (tabBoxMulti.TabPages.Contains(Tab_Box))
            return false;
        tabBoxMulti.TabPages.Insert(0, Tab_Box);
        return true;
    }

    private bool ToggleViewParty(SaveFile sav, int BoxTab)
    {
        if (!sav.HasParty || !sav.State.Exportable)
        {
            if (tabBoxMulti.TabPages.Contains(Tab_PartyBattle))
                tabBoxMulti.TabPages.Remove(Tab_PartyBattle);
            return false;
        }

        if (tabBoxMulti.TabPages.Contains(Tab_PartyBattle))
            return false;

        int index = BoxTab;
        if (index < 0)
            index = -1;
        tabBoxMulti.TabPages.Insert(index + 1, Tab_PartyBattle);
        return true;
    }

    private bool ToggleViewDaycare(SaveFile sav, int BoxTab, int PartyTab)
    {
        if ((GetCurrentDaycare() is null && SL_Extra.SlotCount == 0) || !sav.State.Exportable)
        {
            if (tabBoxMulti.TabPages.Contains(Tab_Other))
                tabBoxMulti.TabPages.Remove(Tab_Other);
            return false;
        }

        SlotPictureBoxes[1].Visible = sav.Generation >= 2; // Second daycare slot
        if (tabBoxMulti.TabPages.Contains(Tab_Other))
            return false;

        int index = PartyTab;
        if (index < 0)
            index = BoxTab;
        if (index < 0)
            index = -1;
        tabBoxMulti.TabPages.Insert(index + 1, Tab_Other);
        return true;
    }

    private void ToggleViewSubEditors(SaveFile sav)
    {
        if (!sav.State.Exportable || sav is BulkStorage)
        {
            FLP_SAVtools.Visible = false;
            B_JPEG.Visible = false;
            B_ConvertKorean.Visible = false;
            SL_Extra.HideAllSlots();
            return;
        }

        DaycareIndex = 0;
        GB_Daycare.Visible = sav is IDaycareStorage or IDaycareMulti;
        B_ConvertKorean.Visible = sav is SAV4;
        B_OpenPokeblocks.Visible = sav is SAV6AO;
        B_OpenSecretBase.Visible = sav is SAV6AO or IGen3Hoenn;
        B_OpenPokepuffs.Visible = sav is ISaveBlock6Main;
        B_JPEG.Visible = B_OpenLinkInfo.Visible = B_OpenSuperTraining.Visible = B_OUTPasserby.Visible = sav is ISaveBlock6Main;
        B_OpenBoxLayout.Visible = sav is IBoxDetailName;
        B_OpenWondercards.Visible = sav is IMysteryGiftStorageProvider;
        B_OpenHallofFame.Visible = sav is ISaveBlock6Main or SAV7 or SAV3 { IsMisconfiguredSize: false } or SAV1;
        B_OpenOPowers.Visible = sav is ISaveBlock6Main;
        B_OpenPokedex.Visible = sav.HasPokeDex;
        B_OpenBerryField.Visible = sav is SAV6XY; // OR/AS undocumented
        B_OpenFriendSafari.Visible = sav is SAV6XY;
        B_OpenEventFlags.Visible = sav is IEventFlag37 or IEventFlagProvider37 or SAV1 or SAV2 or SAV8BS or SAV7b or SAV9ZA;
        B_DLC.Visible = sav.Generation == 5;
        B_OpenPokeBeans.Visible = B_CellsStickers.Visible = B_FestivalPlaza.Visible = sav is SAV7;

        B_OtherSlots.Visible = sav is SAV1StadiumJ or SAV1Stadium or SAV2Stadium;
        B_OpenTrainerInfo.Visible = sav.HasParty || SAV is SAV7b; // Box RS
        B_OpenItemPouch.Visible = (sav.HasParty && SAV is not SAV4BR) || SAV is SAV7b; // Box RS & Battle Revolution
        B_OpenMiscEditor.Visible = sav is SAV2 { Version: GameVersion.C } or SAV3 or SAV4 or SAV5 or SAV8BS;
        B_Roamer.Visible = sav is SAV3 or SAV6XY;

        B_OpenHoneyTreeEditor.Visible = sav is SAV4Sinnoh;
        B_OpenUGSEditor.Visible = sav is SAV4Sinnoh or SAV8BS;
        B_OpenGeonetEditor.Visible = sav is SAV4;
        B_OpenUnityTowerEditor.Visible = sav is SAV5;
        B_OpenChatterEditor.Visible = sav is SAV4 or SAV5;
        B_OpenBattlePass.Visible = B_OpenGear.Visible = sav is SAV4BR;
        B_OpenSealStickers.Visible = B_Poffins.Visible = sav is SAV8BS;
        B_OpenApricorn.Visible = sav is SAV4HGSS;
        B_OpenRTCEditor.Visible = (sav.Generation == 2 && sav is not SAV2Stadium) || sav is IGen3Hoenn;
        B_MailBox.Visible = sav is SAV2 or SAV2Stadium or SAV3 or SAV4 or SAV5;

        B_Raids.Visible = sav is SAV8SWSH or SAV9SV;
        B_RaidsSevenStar.Visible = sav is SAV9SV;
        B_RaidsDLC1.Visible = sav is SAV8SWSH { SaveRevision: >= 1 } or SAV9SV { SaveRevision: >= 1 };
        B_RaidsDLC2.Visible = sav is SAV8SWSH { SaveRevision: >= 2 } or SAV9SV { SaveRevision: >= 2 };
        FLP_SAVtools.Visible = B_Blocks.Visible = true;

        B_OpenFashion.Visible = sav is SAV9ZA;

        var list = FLP_SAVtools.Controls.OfType<Control>().OrderBy(z => z.Text).ToArray();
        FLP_SAVtools.Controls.Clear();
        FLP_SAVtools.Controls.AddRange(list);

        SL_Extra.SAV = sav;
        SL_Extra.Initialize(sav.GetExtraSlots(HaX), InitializeDragDrop);
    }

    private void ToggleViewMisc(SaveFile sav)
    {
        // Generational Interface
        ToggleSecrets(sav, HideSecretDetails);
        B_VerifyCHK.Visible = SAV.State.Exportable;
        Menu_ExportBAK.Visible = SAV.State.Exportable && SAV.Metadata.FilePath is not null;

        if (sav is SAV4BR br)
        {
            L_SaveSlot.Visible = CB_SaveSlot.Visible = true;
            var current = br.CurrentSlot;
            var list = br.SaveNames.Select((z, i) => new ComboItem(z, i)).ToList();
            CB_SaveSlot.InitializeBinding();
            CB_SaveSlot.DataSource = new BindingSource(list, string.Empty);
            CB_SaveSlot.SelectedValue = current;
        }
        else
        {
            L_SaveSlot.Visible = CB_SaveSlot.Visible = false;
        }

        if (sav is ISecureValueStorage s)
        {
            TB_Secure1.Text = s.TimeStampCurrent.ToString("X16");
            TB_Secure2.Text = s.TimeStampPrevious.ToString("X16");
        }

        if (sav is IGameSync sync)
        {
            var gsid = sync.GameSyncID;
            TB_GameSync.Enabled = !string.IsNullOrEmpty(gsid);
            TB_GameSync.MaxLength = sync.GameSyncIDSize;
            TB_GameSync.Text = (string.IsNullOrEmpty(gsid) ? 0.ToString() : gsid).PadLeft(sync.GameSyncIDSize, '0');
        }
    }

    private void ToggleSecrets(SaveFile sav, bool hide)
    {
        var shouldShow = sav.State.Exportable && !hide;
        TB_Secure1.Visible = TB_Secure2.Visible = L_Secure1.Visible = L_Secure2.Visible = shouldShow && sav is ISecureValueStorage;
        TB_GameSync.Visible = L_GameSync.Visible = shouldShow && sav is IGameSync;
    }

    // DragDrop
    private void MultiDragOver(object sender, DragEventArgs e)
    {
        // iterate over all tabs to see if a tab switch should occur when drag/dropping
        Point pt = tabBoxMulti.PointToClient(new Point(e.X, e.Y));
        for (int i = 0; i < tabBoxMulti.TabCount; i++)
        {
            if (tabBoxMulti.SelectedIndex == i || !tabBoxMulti.GetTabRect(i).Contains(pt))
                continue;
            tabBoxMulti.SelectedIndex = i;
            return;
        }
    }

    public void ClickShowdownExportParty(object sender, EventArgs e) => ExportShowdownText(SAV, MsgSimulatorExportParty, sav => sav.PartyData);

    public void ClickShowdownExportCurrentBox(object sender, EventArgs e)
    {
        if (!SAV.HasBox)
            return;
        ExportShowdownText(SAV, MsgSimulatorExportList,
            sav => (ModifierKeys & Keys.Control) != 0 ? sav.BoxData : sav.GetBoxData(CurrentBox));
    }

    private static void ExportShowdownText(SaveFile sav, string success, Func<SaveFile, IEnumerable<PKM>> fetch)
    {
        var list = fetch(sav);
        var programLanguage = Language.GetLanguageValue(Main.Settings.Startup.Language);
        var settings = Main.Settings.BattleTemplate.Export.GetSettings(programLanguage, sav.Context);
        var result = ShowdownParsing.GetShowdownSets(list, Environment.NewLine + Environment.NewLine, settings);
        if (string.IsNullOrWhiteSpace(result))
            return;
        if (WinFormsUtil.SetClipboardText(result))
            WinFormsUtil.Alert(success);
    }

    private void B_OpenUGSEditor_Click(object sender, EventArgs e)
    {
        Form form;
        if (SAV is SAV4Sinnoh s)
            form = new SAV_Underground(s);
        else if (SAV is SAV8BS bs)
            form = new SAV_Underground8b(bs);
        else
            return;
        form.ShowDialog();
        form.Dispose();
    }

    private void B_OpenSealStickers_Click(object sender, EventArgs e)
    {
        if (SAV is not SAV8BS bs)
            return;
        using var form = new SAV_SealStickers8b(bs);
        form.ShowDialog();
    }

    private void B_Poffins_Click(object sender, EventArgs e)
    {
        if (SAV is not SAV8BS bs)
            return;
        using var form = new SAV_Poffin8b(bs);
        form.ShowDialog();
    }

    private void B_FestivalPlaza_Click(object sender, EventArgs e)
    {
        if (SAV is not SAV7 s)
            return;
        using var form = new SAV_FestivalPlaza(s);
        form.ShowDialog();
    }

    private void B_MailBox_Click(object sender, EventArgs e)
    {
        using var form = new SAV_MailBox(SAV);
        form.ShowDialog();
        ResetParty();
    }

    private static EntityImportSettings GetImportSettingsOverride()
    {
        var choice = WinFormsUtil.Prompt(MessageBoxButtons.YesNoCancel,
            MsgSaveBoxImportModifyIntro,
            MsgSaveBoxImportModifyYes + Environment.NewLine +
            MsgSaveBoxImportModifyNo + Environment.NewLine +
            string.Format(MsgSaveBoxImportModifyCurrent, SaveFile.SetUpdateSettings));
        return choice switch
        {
            DialogResult.Yes => EntityImportSettings.All,
            DialogResult.No => EntityImportSettings.None,
            _ => default,
        };
    }

    private void Menu_ExportBAK_Click(object sender, EventArgs e) => ExportBackup();

    public bool IsBoxDragActive;
    private Point DragStartPoint;

    private void TabMouseDown(object sender, MouseEventArgs e)
    {
        if (!Main.Settings.SlotExport.AllowBoxDataDrop)
            return;
        if (e.Button != MouseButtons.Left)
            return;
        if (ModifierKeys is Keys.Alt or Keys.Shift)
            return;

        // If the event was fired from the Box tab rectangle, initiate a box drag drop.
        var boxIndex = tabBoxMulti.TabPages.IndexOf(Tab_Box);
        if (!tabBoxMulti.GetTabRect(boxIndex).Contains(e.Location))
            return;

        IsBoxDragActive = true;
        DragStartPoint = e.Location;
    }

    public void TabMouseUp(object sender, MouseEventArgs e)
    {
        if (IsBoxDragActive)
            IsBoxDragActive = false;
    }

    private async void TabMouseMove(object sender, MouseEventArgs e)
    {
        try
        {
            if (!IsBoxDragActive)
                return;

            if (e.Location == DragStartPoint)
                return;

            // Gather data
            var src = SAV.CurrentBox;
            var bin = SAV.GetBoxBinary(src);

            // Create Temp File to Drag
            var newFile = Path.Combine(Path.GetTempPath(), $"box_{src}.bin");
            try
            {
                using var img = new Bitmap(Box.Width, Box.Height);
                Box.DrawToBitmap(img, new Rectangle(0, 0, Box.Width, Box.Height));
                using var cursor = Cursor = new Cursor(img.GetHicon());
                await File.WriteAllBytesAsync(newFile, bin).ConfigureAwait(true);
                DoDragDrop(new DataObject(DataFormats.FileDrop, new[] { newFile }), DragDropEffects.Copy);
            }
            // Tons of things can happen with drag & drop; don't try to handle things, just indicate failure.
            catch (Exception x)
            { WinFormsUtil.Error("Drag && Drop Error", x); }
            finally
            {
                Cursor = Cursors.Default;
                await Task.Delay(100).ConfigureAwait(false);
                IsBoxDragActive = false;
                await DeleteAsync(newFile, 20_000).ConfigureAwait(false);
            }
        }
        catch
        {
            // Ignore.
        }
    }

    private static async Task DeleteAsync(string path, int delay)
    {
        await Task.Delay(delay).ConfigureAwait(true);
        if (!File.Exists(path))
            return;

        try { File.Delete(path); }
        catch (Exception ex) { Debug.WriteLine(ex.Message); }
    }
}
