using System.Text;
using RioJoy.Core.Calibration;
using RioJoy.Core.Editing;
using RioJoy.Core.Output;
using RioJoy.Core.Mapping;
using RioJoy.Core.Profiles;
using RioJoy.Core.Protocol;

namespace RioJoy.Tray.Editor;

/// <summary>
/// Profile editor: shows the cockpit control panel (the functional grouping from
/// the original Win32 RIO design — MFD clusters, board columns, keypads, encoder
/// gauges, via <see cref="CockpitPanel"/>) as a clickable map, and edits the
/// selected button's label and action/modifiers/lamp. Writes back to the
/// <see cref="RioProfile"/>'s <see cref="RioProfile.OverlayLabels"/> (keyed by the
/// wallpaper region <c>b-XX</c>) and <see cref="RioProfile.Buttons"/> (the iRIO
/// word); <c>Save</c> raises <see cref="Saved"/>.
/// </summary>
public sealed class ProfileEditorForm : Form
{
    private static readonly Dictionary<int, string> GroupOf =
        CockpitPanel.Buttons().ToDictionary(b => b.Address, b => b.Group.Title);

    private static readonly Dictionary<int, bool> LampCapableOf =
        CockpitPanel.Buttons().ToDictionary(b => b.Address, b => b.LampCapable);

    private readonly RioProfile _profile;
    private readonly PanelCanvas _canvas = new();

    private readonly TextBox _nameBox = new() { Location = new Point(66, 12), Width = 234 };
    private readonly TextBox _matchBox = new() { Location = new Point(66, 40), Width = 234 };
    private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 68), MaximumSize = new Size(310, 0) };
    private readonly TextBox _labelBox = new() { Location = new Point(70, 100), Width = 230 };
    private readonly ComboBox _kindBox = new() { Location = new Point(70, 134), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
    private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 171), AutoSize = true };
    private readonly ComboBox _valueCombo = new() { Location = new Point(70, 168), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
    private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 200), AutoSize = true };
    private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 200), AutoSize = true };
    private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 200), AutoSize = true };
    private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 200), AutoSize = true };
    private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 228), AutoSize = true };
    private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 262), Width = 110 };
    private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 262), Width = 110 };
    private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 296), Width = 110 };
    private readonly Button _close = new() { Text = "Close", Location = new Point(190, 296), Width = 80 };
    private readonly CheckBox _outputToggle = new() { Text = "Send button output to the PC", Location = new Point(12, 328), AutoSize = true };

    private readonly TextBox _statusBox = new()
    {
        Location = new Point(12, 646),
        Size = new Size(306, 145),
        Multiline = true,
        ReadOnly = true,
        ScrollBars = ScrollBars.Vertical,
        BackColor = Color.FromArgb(24, 24, 24),
        ForeColor = Color.Gainsboro,
        Font = new Font("Consolas", 8f),
        Text = "Press \"Request version & status\" to query the RIO.",
    };

    // Briefly flips the Save button to a confirmation, then reverts.
    private readonly System.Windows.Forms.Timer _saveFlash = new() { Interval = 1600 };

    private int? _address;
    private bool _loading;

    // The most recent version/status reply, accumulated as CheckReply items stream in.
    private VersionInfo? _version;
    private readonly List<CheckStatus> _checks = new();

    // The RIO device commands, mirroring the tray menu, surfaced as live buttons so
    // they can be fired against the RIO while editing (the editor holds the port).
    private static readonly (string Label, RioCommandCode Code)[] RioCommands =
    {
        ("Reset all analog axes", RioCommandCode.ResetAllAxes),
        ("Reset throttle", RioCommandCode.ResetThrottle),
        ("Reset left pedal", RioCommandCode.ResetLeftPedal),
        ("Reset right pedal", RioCommandCode.ResetRightPedal),
        ("Reset vertical joystick", RioCommandCode.ResetVerticalJoystick),
        ("Reset horizontal joystick", RioCommandCode.ResetHorizontalJoystick),
        ("Reset digital (general)", RioCommandCode.GeneralReset),
        ("Request version & status", RioCommandCode.RequestVersionAndCheck),
    };

    /// <summary>A pick-list entry: what the user sees and the byte it maps to.</summary>
    private sealed record ValueItem(string Display, byte Value)
    {
        public override string ToString() => Display;
    }

    /// <summary>Raised when the user saves; the argument is the edited profile.</summary>
    public event Action<RioProfile>? Saved;

    /// <summary>
    /// Returns whether a candidate profile name is free to use (i.e. not taken by a
    /// <em>different</em> profile). Set by the host; null = no uniqueness check.
    /// </summary>
    public Func<string, bool>? IsNameAvailable { get; set; }

    /// <summary>Raised when the user clicks one of the live RIO-command buttons.</summary>
    public event Action<RioCommandCode>? CommandRequested;

    /// <summary>Raised when the "send output to the PC" toggle changes (true = send).</summary>
    public event Action<bool>? OutputsEnabledChanged;

    public ProfileEditorForm(RioProfile profile)
    {
        _profile = profile ?? throw new ArgumentNullException(nameof(profile));

        Text = $"RIOJoy — Edit profile: {profile.Name}";
        _nameBox.Text = profile.Name;
        _matchBox.Text = string.Join(", ", profile.MatchExecutables);
        ClientSize = new Size(1320, 820);
        StartPosition = FormStartPosition.CenterScreen;
        MinimumSize = new Size(900, 500);

        _kindBox.Items.AddRange(Enum.GetNames(typeof(RioRouteKind)));
        _kindBox.SelectedIndexChanged += OnKindChanged;

        _canvas.LabelProvider = a => _profile.OverlayLabels.TryGetValue(RegionFor(a), out string? t) ? t : null;
        _canvas.FunctionProvider = a => _profile.Buttons.TryGetValue(a, out int w) ? ButtonBinding.FromWord((ushort)w).Describe() : null;
        _canvas.LitProvider = a => _profile.Buttons.TryGetValue(a, out int w) && ButtonBinding.FromWord((ushort)w).IsLit;
        _canvas.CalibrationProvider = ReadCalibration;
        _canvas.CalibrationToggled += ToggleCalibration;
        _canvas.AddressSelected += OnAddressSelected;

        var scroller = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) };
        scroller.Controls.Add(_canvas);

        Panel editPanel = BuildEditPanel();

        Controls.Add(scroller);
        Controls.Add(editPanel);

        _apply.Click += (_, _) => ApplyToCell();
        _unassign.Click += (_, _) => Unassign();
        _save.Click += (_, _) => SaveProfile();
        _close.Click += (_, _) => Close();
        _outputToggle.CheckedChanged += (_, _) => OutputsEnabledChanged?.Invoke(_outputToggle.Checked);

        _saveFlash.Tick += (_, _) =>
        {
            _saveFlash.Stop();
            _save.Text = "Save profile";
            _save.ForeColor = SystemColors.ControlText;
        };
        FormClosed += (_, _) => _saveFlash.Dispose();

        SetEditingEnabled(false);
        _info.Text = "Click a button to edit it.";
    }

    /// <summary>The selected RIO address (for tests / offline rendering).</summary>
    public int? SelectedAddress => _address;

    private Panel BuildEditPanel()
    {
        var panel = new Panel { Dock = DockStyle.Right, Width = 330, Padding = new Padding(8), BorderStyle = BorderStyle.FixedSingle, AutoScroll = true };

        panel.Controls.Add(new Label { Text = "Profile:", Location = new Point(12, 15), AutoSize = true });
        panel.Controls.Add(_nameBox);
        panel.Controls.Add(new Label { Text = "Triggers:", Location = new Point(12, 43), AutoSize = true });
        panel.Controls.Add(_matchBox);
        panel.Controls.Add(_info);
        panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 103), AutoSize = true });
        panel.Controls.Add(_labelBox);
        panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 137), AutoSize = true });
        panel.Controls.Add(_kindBox);
        panel.Controls.Add(_valueLabel);
        panel.Controls.Add(_valueCombo);
        panel.Controls.AddRange(new Control[] { _shift, _ctrl, _alt, _ext, _lit, _apply, _unassign, _save, _close, _outputToggle });
        panel.Controls.Add(BuildCommandGroup());
        panel.Controls.Add(new Label { Text = "RIO reply:", Location = new Point(12, 628), AutoSize = true });
        panel.Controls.Add(_statusBox);

        return panel;
    }

    // A button per RIO device command, fired against the live RIO via CommandRequested.
    private GroupBox BuildCommandGroup()
    {
        var group = new GroupBox { Text = "RIO commands (live)", Location = new Point(12, 358), Size = new Size(306, 262) };

        int y = 24;
        foreach ((string label, RioCommandCode code) in RioCommands)
        {
            RioCommandCode captured = code;
            var button = new Button { Text = label, Location = new Point(10, y), Width = 286, Height = 26, UseMnemonic = false };
            button.Click += (_, _) =>
            {
                // The version/status reply streams back into the box below; clear it first.
                if (captured == RioCommandCode.RequestVersionAndCheck)
                {
                    _version = null;
                    _checks.Clear();
                    _statusBox.Text = "Requesting version & status…";
                }
                CommandRequested?.Invoke(captured);
            };
            group.Controls.Add(button);
            y += 29;
        }

        return group;
    }

    private void OnAddressSelected(int addr)
    {
        _address = addr;
        string region = RegionFor(addr);
        string group = GroupOf.TryGetValue(addr, out string? gn) ? gn : "—";
        _info.Text = $"Address 0x{addr:X2}    {group}    (wallpaper {region})";

        ButtonBinding b = ButtonBinding.FromWord(
            _profile.Buttons.TryGetValue(addr, out int w) ? (ushort)w : (ushort)0);

        _loading = true;
        _labelBox.Text = _profile.OverlayLabels.TryGetValue(region, out string? label) ? label : string.Empty;
        _kindBox.SelectedItem = b.Kind.ToString();
        PopulateValues(b.Kind);
        SelectValue(b.Value);
        _shift.Checked = b.Shift;
        _ctrl.Checked = b.Ctrl;
        _alt.Checked = b.Alt;
        _ext.Checked = b.Extended;
        _lit.Checked = b.IsLit;
        _lit.Visible = LampCapableOf.GetValueOrDefault(addr); // keypads have no lamp
        _loading = false;

        SetEditingEnabled(true);
    }

    private void OnKindChanged(object? sender, EventArgs e)
    {
        if (_loading) return;
        PopulateValues(SelectedKind());
        if (_valueCombo.Items.Count > 0) _valueCombo.SelectedIndex = 0;
        UpdateModifierAvailability();
    }

    // Fill the value picker with friendly choices for the chosen routing kind.
    private void PopulateValues(RioRouteKind kind)
    {
        _valueLabel.Text = kind switch
        {
            RioRouteKind.Keyboard => "Key:",
            RioRouteKind.Joystick => "Button:",
            RioRouteKind.Hat => "Direction:",
            RioRouteKind.Mouse => "Action:",
            RioRouteKind.RioCommand => "Command:",
            _ => "Value:",
        };

        _valueCombo.BeginUpdate();
        _valueCombo.Items.Clear();
        switch (kind)
        {
            case RioRouteKind.Keyboard:
                foreach (KeyName k in KeyCatalog.Entries)
                    _valueCombo.Items.Add(new ValueItem(k.Name, k.Value));
                break;
            case RioRouteKind.Joystick:
#if NET40
                // XP: RioGamepadXP exposes the full 96-button HID layout.
                for (int btn = 1; btn <= RioJoy.Core.Hid.RioHidReport.ButtonCount; btn++)
                    _valueCombo.Items.Add(new ValueItem($"Button {btn}", (byte)btn));
#else
                // The signed ViGEmBus Xbox 360 pad exposes 11 buttons; show their names.
                for (int btn = 1; btn <= ViGEmJoystickSink.MappableButtonCount; btn++)
                    _valueCombo.Items.Add(new ValueItem($"Button {btn} ({ViGEmJoystickSink.ButtonNames[btn - 1]})", (byte)btn));
#endif
                break;
            case RioRouteKind.Hat:
                foreach (RioHat h in new[] { RioHat.Up, RioHat.Right, RioHat.Down, RioHat.Left })
                    _valueCombo.Items.Add(new ValueItem(h.ToString(), (byte)(int)h));
                break;
            case RioRouteKind.Mouse:
                foreach (MouseAction m in (MouseAction[])Enum.GetValues(typeof(MouseAction)))
                    _valueCombo.Items.Add(new ValueItem(m.ToString(), (byte)m));
                break;
            case RioRouteKind.RioCommand:
                foreach (RioCommandCode c in (RioCommandCode[])Enum.GetValues(typeof(RioCommandCode)))
                    _valueCombo.Items.Add(new ValueItem(c.ToString(), (byte)c));
                break;
        }
        _valueCombo.EndUpdate();
        UpdateModifierAvailability();
    }

    private void SelectValue(byte value)
    {
        for (int i = 0; i < _valueCombo.Items.Count; i++)
        {
            if (_valueCombo.Items[i] is ValueItem v && v.Value == value)
            {
                _valueCombo.SelectedIndex = i;
                return;
            }
        }
        int idx = _valueCombo.Items.Add(new ValueItem($"0x{value:X2}", value));
        _valueCombo.SelectedIndex = idx;
    }

    private void UpdateModifierAvailability()
    {
        bool keyboard = SelectedKind() == RioRouteKind.Keyboard;
        foreach (Control c in new Control[] { _shift, _ctrl, _alt, _ext })
            c.Enabled = keyboard;
    }

    private RioRouteKind SelectedKind() =>
        Enum.TryParse(_kindBox.SelectedItem?.ToString(), out RioRouteKind k) ? k : RioRouteKind.Keyboard;

    private void ApplyToCell()
    {
        if (_address is not { } addr) return;

        string region = RegionFor(addr);
        string label = _labelBox.Text.Trim();
        if (string.IsNullOrEmpty(label))
            _profile.OverlayLabels.Remove(region);
        else
            _profile.OverlayLabels[region] = label;

        RioRouteKind kind = SelectedKind();
        bool keyboard = kind == RioRouteKind.Keyboard;
        var binding = new ButtonBinding
        {
            Kind = kind,
            Value = _valueCombo.SelectedItem is ValueItem vi ? vi.Value : (byte)0,
            Shift = keyboard && _shift.Checked,
            Ctrl = keyboard && _ctrl.Checked,
            Alt = keyboard && _alt.Checked,
            Extended = keyboard && _ext.Checked,
            IsLit = LampCapableOf.GetValueOrDefault(addr) && _lit.Checked, // keypads never lit
        };

        ushort word = binding.ToWord();
        if (word == 0)
            _profile.Buttons.Remove(addr);
        else
            _profile.Buttons[addr] = word;

        _canvas.Refresh(addr); // update lamp shade + label on the panel
    }

    // Apply the pending cell edit, persist via the Saved handler, and confirm on the
    // button (the actual write happens in the host's Saved handler — config.json).
    private void SaveProfile()
    {
        if (!ApplyName())
            return;

        // The foreground executables that auto-activate this profile (comma-separated).
        _profile.MatchExecutables = _matchBox.Text
            .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
            .Select(s => s.Trim())
            .Where(s => s.Length > 0)
            .ToList();

        ApplyToCell();
        try
        {
            Saved?.Invoke(_profile);
            _save.Text = "Saved ✓";
            _save.ForeColor = Color.ForestGreen;
        }
        catch (Exception ex)
        {
            _save.Text = "Save failed";
            _save.ForeColor = Color.Firebrick;
            MessageBox.Show(this, $"Could not save the profile:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
        _saveFlash.Stop();
        _saveFlash.Start();
    }

    // Validate and apply the profile name from the name box. Returns false (and warns)
    // if the name is blank or already taken by another profile, so the save is aborted.
    private bool ApplyName()
    {
        string name = _nameBox.Text.Trim();
        if (string.IsNullOrEmpty(name))
        {
            MessageBox.Show(this, "Profile name cannot be empty.", "RIOJoy",
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
            return false;
        }

        bool changed = !string.Equals(name, _profile.Name, StringComparison.OrdinalIgnoreCase);
        if (changed && IsNameAvailable?.Invoke(name) == false)
        {
            MessageBox.Show(this, $"A profile named \"{name}\" already exists.", "RIOJoy",
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
            return false;
        }

        _profile.Name = name;
        Text = $"RIOJoy — Edit profile: {name}";
        return true;
    }

    // Clear the selected button's action mapping (the label is left untouched).
    private void Unassign()
    {
        if (_address is not { } addr) return;
        _profile.Buttons.Remove(addr);

        _loading = true;
        _kindBox.SelectedItem = RioRouteKind.Keyboard.ToString();
        PopulateValues(RioRouteKind.Keyboard);
        if (_valueCombo.Items.Count > 0) _valueCombo.SelectedIndex = 0;
        _shift.Checked = _ctrl.Checked = _alt.Checked = _ext.Checked = _lit.Checked = false;
        _loading = false;

        _canvas.Refresh(addr);
    }

    private void SetEditingEnabled(bool enabled)
    {
        foreach (Control c in new Control[] { _labelBox, _kindBox, _valueCombo, _shift, _ctrl, _alt, _ext, _lit, _apply, _unassign })
            c.Enabled = enabled;
        if (enabled) UpdateModifierAvailability();
    }

    // Read a [JoyStick] setting from the profile's calibration (drives the cell shade).
    private bool ReadCalibration(JoyStickSetting s) => s switch
    {
        JoyStickSetting.InvertX => _profile.Calibration.InvertX,
        JoyStickSetting.InvertY => _profile.Calibration.InvertY,
        JoyStickSetting.InvertZ => _profile.Calibration.InvertZ,
        JoyStickSetting.InvertRx => _profile.Calibration.InvertXR,
        JoyStickSetting.InvertRy => _profile.Calibration.InvertYR,
        JoyStickSetting.InvertRz => _profile.Calibration.InvertZR,
        JoyStickSetting.EnableZR => _profile.Calibration.EnableZR,
        _ => false,
    };

    // Flip a [JoyStick] setting in place; persisted with the profile on Save.
    private void ToggleCalibration(JoyStickSetting s)
    {
        var c = _profile.Calibration;
        _profile.Calibration = s switch
        {
            JoyStickSetting.InvertX => c with { InvertX = !c.InvertX },
            JoyStickSetting.InvertY => c with { InvertY = !c.InvertY },
            JoyStickSetting.InvertZ => c with { InvertZ = !c.InvertZ },
            JoyStickSetting.InvertRx => c with { InvertXR = !c.InvertXR },
            JoyStickSetting.InvertRy => c with { InvertYR = !c.InvertYR },
            JoyStickSetting.InvertRz => c with { InvertZR = !c.InvertZR },
            JoyStickSetting.EnableZR => c with { EnableZR = !c.EnableZR },
            _ => c,
        };
    }

    private static string RegionFor(int address) => $"b-{address:X2}";

    /// <summary>Select an address programmatically (for tests / offline preview).</summary>
    public void SelectAddress(int address)
    {
        _canvas.Select(address);
        OnAddressSelected(address);
    }

    /// <summary>
    /// Show live RIO button activity on the panel (called from the serial read thread).
    /// Lets the user press a physical button and see which cell it is without any
    /// keystrokes being injected.
    /// </summary>
    public void ShowLiveActivity(int address, bool pressed) =>
        RunOnUi(() => _canvas.SetLivePressed(address, pressed));

    /// <summary>Show the live calibrated readout on the encoder gauges (serial thread).</summary>
    public void ShowAxes(AxisReadout axes) =>
        RunOnUi(() => _canvas.SetAxes(axes));

    /// <summary>Show the firmware version from a RIO version reply (serial thread).</summary>
    public void ShowVersion(VersionInfo version) =>
        RunOnUi(() => { _version = version; RenderStatus(); });

    /// <summary>Append a board/lamp status item from a RIO check reply (serial thread).</summary>
    public void AddCheckStatus(CheckStatus status) =>
        RunOnUi(() => { _checks.Add(status); RenderStatus(); });

    // Marshal an action onto the UI thread, tolerating a closing/disposed form.
    private void RunOnUi(Action action)
    {
        if (IsDisposed)
            return;
        if (InvokeRequired)
        {
            try { BeginInvoke(action); }
            catch (ObjectDisposedException) { }
            catch (InvalidOperationException) { } // handle not created / form closing
            return;
        }
        action();
    }

    // Render the accumulated version + check reply into the status box.
    private void RenderStatus()
    {
        var sb = new StringBuilder();
        sb.AppendLine(_version is { } v ? $"Firmware: {v}" : "Firmware: (awaiting reply…)");

        if (_checks.Count == 0)
        {
            sb.Append("Status: (awaiting reply…)");
        }
        else
        {
            foreach (IGrouping<RioStatusType, CheckStatus> group in _checks.GroupBy(c => c.Type))
                sb.AppendLine($"{Friendly(group.Key)}: {string.Join(", ", group.Select(c => c.Number))}");
        }

        _statusBox.Text = sb.ToString().TrimEnd();
    }

    private static string Friendly(RioStatusType type) => type switch
    {
        RioStatusType.BoardOk => "Boards OK",
        RioStatusType.BoardMissing => "Boards missing",
        RioStatusType.BoardBad => "Boards bad",
        RioStatusType.LampBad => "Lamps bad",
        RioStatusType.RestartCount => "Restart count",
        RioStatusType.AbandonCount => "Abandon count",
        RioStatusType.FullBufferCount => "Full-buffer count",
        _ => type.ToString(),
    };
}
