using System.ComponentModel;
using VoiceCat.App.Audio;
using VoiceCat.App.Models;
using VoiceCat.Interop;

namespace VoiceCat.App.Forms;

/// <summary>
/// Edits audio input settings: device selection, transmission mode (VAD/PTT/always-on),
/// VAD sensitivity, mic gain, and PTT key. Changes are applied live to the client for
/// immediate feedback; Cancel reverts them.
/// </summary>
public sealed class AudioSettingsForm : Form
{
    private readonly VoiceCatClient _client;
    private readonly VoiceSettings _settings;
    private readonly uint _micStreamId;

    // Aux-stream live-apply callbacks, supplied by MainForm (which owns the aux capture). Null
    // when not connected — the controls still edit settings, just without live effect.
    private readonly Action<bool>?    _applyAuxEnabled;
    private readonly Action<string?>? _applyAuxDevice;
    private readonly Action<float>?   _applyAuxGain;

    // Snapshot of original values so Cancel can restore them
    private readonly string? _origDeviceId;
    private readonly VcInputMode _origMode;
    private readonly int _origVadSlider;
    private readonly int _origMicGain;
    private readonly bool _origMicNoiseReduction;
    private readonly bool _origStereoMic;
    private readonly Keys _origPttKey;
    private readonly bool _origAuxEnabled;
    private readonly string? _origAuxDeviceId;
    private readonly int _origAuxGain;

    private readonly ComboBox _cboDevice;
    private readonly Button _btnRefresh;
    private readonly RadioButton _radioVad;
    private readonly RadioButton _radioPtt;
    private readonly RadioButton _radioAlwaysOn;
    private readonly Label _lblPttKey;
    private readonly Button _btnChangePtt;
    private readonly Label _lblSensitivity;
    private readonly TrackBar _trkVad;
    private readonly TrackBar _trkGain;
    private readonly CheckBox _chkNoiseReduction;
    private readonly CheckBox _chkStereoMic;
    private readonly CheckBox _chkAux;
    private readonly Label _lblAuxDevice;
    private readonly ComboBox _cboAuxDevice;
    private readonly Button _btnAuxRefresh;
    private readonly Label _lblAuxGain;
    private readonly TrackBar _trkAuxGain;

    private Keys _pttKey;

    public AudioSettingsForm(VoiceCatClient client, VoiceSettings settings, uint micStreamId,
        Action<bool>? applyAuxEnabled = null, Action<string?>? applyAuxDevice = null,
        Action<float>? applyAuxGain = null)
    {
        _client = client;
        _settings = settings;
        _micStreamId = micStreamId;
        _applyAuxEnabled = applyAuxEnabled;
        _applyAuxDevice = applyAuxDevice;
        _applyAuxGain = applyAuxGain;
        _pttKey = (Keys)settings.PttKey;

        _origDeviceId = settings.InputDeviceId;
        _origMode = (VcInputMode)settings.InputMode;
        _origVadSlider = settings.VadThresholdSlider;
        _origMicGain = settings.MicGain;
        _origMicNoiseReduction = settings.MicNoiseReduction;
        _origStereoMic = settings.StereoMic;
        _origPttKey = _pttKey;
        _origAuxEnabled = settings.AuxEnabled;
        _origAuxDeviceId = settings.AuxDeviceId;
        _origAuxGain = settings.AuxGain;

        Text = "Audio settings";
        FormBorderStyle = FormBorderStyle.FixedDialog;
        MaximizeBox = false;
        MinimizeBox = false;
        StartPosition = FormStartPosition.CenterParent;
        AutoScaleMode = AutoScaleMode.Font;
        ClientSize = new Size(420, 611);

        // ── Device row ────────────────────────────────────────────────────────
        var lblDevice = new Label
        {
            Text = "Input &device:",
            Location = new Point(12, 16),
            AutoSize = true,
        };

        _cboDevice = new ComboBox
        {
            Location = new Point(12, 36),
            Width = 300,
            DropDownStyle = ComboBoxStyle.DropDownList,
            DisplayMember = "Name",
            ValueMember = "Id",
            AccessibleName = "Input device",
            AccessibleDescription = "Select which microphone or audio input device to use.",
        };
        _cboDevice.SelectedIndexChanged += CboDevice_SelectedIndexChanged;

        _btnRefresh = new Button
        {
            Text = "&Refresh",
            Location = new Point(320, 34),
            Size = new Size(80, 26),
        };
        _btnRefresh.Click += (_, _) => LoadDevices();

        // ── Transmission mode ─────────────────────────────────────────────────
        var lblMode = new Label
        {
            Text = "Transmission mode:",
            Location = new Point(12, 76),
            AutoSize = true,
        };

        _radioVad = new RadioButton
        {
            Text = "&Voice activation",
            Location = new Point(20, 96),
            AutoSize = true,
        };
        _radioVad.CheckedChanged += RadioMode_CheckedChanged;

        _lblSensitivity = new Label
        {
            Text = "Sensitivity:",
            Location = new Point(36, 122),
            AutoSize = true,
        };
        _trkVad = new TrackBar
        {
            Location = new Point(36, 140),
            Size = new Size(200, 45),
            Minimum = 1,
            Maximum = 100,
            TickFrequency = 10,
            SmallChange = 1,
            LargeChange = 10,
            Value = Math.Clamp(settings.VadThresholdSlider, 1, 100),
        };
        _trkVad.AccessibleName = "VAD sensitivity";
        _trkVad.AccessibleDescription =
            "Voice detection sensitivity. Higher = more sensitive. Range 1–100.";
        _trkVad.Scroll += TrkVad_Scroll;

        _radioPtt = new RadioButton
        {
            Text = "&Push to talk",
            Location = new Point(20, 192),
            AutoSize = true,
        };
        _radioPtt.CheckedChanged += RadioMode_CheckedChanged;

        _lblPttKey = new Label
        {
            Location = new Point(140, 194),
            AutoSize = true,
        };

        _btnChangePtt = new Button
        {
            Text = "Change key...",
            Location = new Point(200, 189),
            Size = new Size(105, 26),
        };
        _btnChangePtt.Click += BtnChangePtt_Click;

        _radioAlwaysOn = new RadioButton
        {
            Text = "A&lways on",
            Location = new Point(20, 224),
            AutoSize = true,
        };
        _radioAlwaysOn.CheckedChanged += RadioMode_CheckedChanged;

        // ── Mic gain ──────────────────────────────────────────────────────────
        var lblGain = new Label
        {
            Text = "Microphone &volume:",
            Location = new Point(12, 268),
            AutoSize = true,
        };
        _trkGain = new TrackBar
        {
            Location = new Point(12, 288),
            Size = new Size(200, 45),
            Minimum = 0,
            Maximum = 400,
            TickFrequency = 25,
            SmallChange = 5,
            LargeChange = 25,
            Value = Math.Clamp(settings.MicGain, 0, 400),
        };
        _trkGain.AccessibleName = "Microphone volume";
        _trkGain.AccessibleDescription =
            "Boost a quiet microphone. 100 is unity gain; range 0–400 percent.";
        _trkGain.Scroll += TrkGain_Scroll;

        // ── Mic noise reduction ───────────────────────────────────────────────
        // Send-side RNNoise denoise of the mic stream. MIC-only (the core's NR runs before
        // the input gain and VAD/PTT gate; aux/screen are excluded). One pass for all listeners.
        _chkNoiseReduction = new CheckBox
        {
            Text = "Noise &reduction (RNNoise)",
            Location = new Point(12, 340),
            AutoSize = true,
            Checked = settings.MicNoiseReduction,
            AccessibleName = "Microphone noise reduction",
            AccessibleDescription =
                "RNNoise denoising of your microphone. Cleans your signal for everyone listening.",
        };
        _chkNoiseReduction.CheckedChanged += ChkNoiseReduction_CheckedChanged;

        // ── Stereo microphone ─────────────────────────────────────────────────
        // Opens the mic capture device in stereo (interleaved L/R) instead of mono. Real stereo
        // only reaches the wire on a stereo channel; the core folds a stereo mic to mono on a mono
        // channel. Toggling while connected restarts the capture device (vc_audio_restart) so the
        // new channel count takes effect immediately.
        _chkStereoMic = new CheckBox
        {
            Text = "&Stereo microphone",
            Location = new Point(12, 364),
            AutoSize = true,
            Checked = settings.StereoMic,
            AccessibleName = "Stereo microphone",
            AccessibleDescription =
                "Capture your microphone in stereo. Only transmitted in stereo on a stereo channel.",
        };
        _chkStereoMic.CheckedChanged += ChkStereoMic_CheckedChanged;

        // ── Aux input stream ──────────────────────────────────────────────────
        // A second outgoing stream from another hardware input device (e.g. line-in / aux),
        // captured client-side and fed to the core. Device + volume only — aux is always-on
        // (the core never gates AUX_DEVICE on VAD/PTT).
        _chkAux = new CheckBox
        {
            Text = "&Aux stream (second input device)",
            Location = new Point(12, 404),
            AutoSize = true,
            Checked = settings.AuxEnabled,
            AccessibleName = "Enable aux input stream",
            AccessibleDescription =
                "Transmit a second hardware input device alongside your microphone.",
        };
        _chkAux.CheckedChanged += ChkAux_CheckedChanged;

        _lblAuxDevice = new Label
        {
            Text = "Aux d&evice:",
            Location = new Point(12, 434),
            AutoSize = true,
        };
        _cboAuxDevice = new ComboBox
        {
            Location = new Point(12, 454),
            Width = 300,
            DropDownStyle = ComboBoxStyle.DropDownList,
            DisplayMember = "Name",
            ValueMember = "Id",
            AccessibleName = "Aux input device",
            AccessibleDescription = "Select the second audio input device to transmit.",
        };
        _cboAuxDevice.SelectedIndexChanged += CboAuxDevice_SelectedIndexChanged;

        _btnAuxRefresh = new Button
        {
            Text = "Re&fresh",
            Location = new Point(320, 452),
            Size = new Size(80, 26),
        };
        _btnAuxRefresh.Click += (_, _) => LoadAuxDevices();

        _lblAuxGain = new Label
        {
            Text = "Aux vo&lume:",
            Location = new Point(12, 490),
            AutoSize = true,
        };
        _trkAuxGain = new TrackBar
        {
            Location = new Point(12, 510),
            Size = new Size(200, 45),
            Minimum = 0,
            Maximum = 400,
            TickFrequency = 25,
            SmallChange = 5,
            LargeChange = 25,
            Value = Math.Clamp(settings.AuxGain, 0, 400),
        };
        _trkAuxGain.AccessibleName = "Aux volume";
        _trkAuxGain.AccessibleDescription =
            "Volume of the aux input stream. 100 is unity gain; range 0–400 percent.";
        _trkAuxGain.Scroll += TrkAuxGain_Scroll;

        // ── OK / Cancel ───────────────────────────────────────────────────────
        var btnOk = new Button
        {
            Text = "&OK",
            DialogResult = DialogResult.OK,
            Location = new Point(228, 572),
            Size = new Size(80, 27),
        };
        var btnCancel = new Button
        {
            Text = "&Cancel",
            DialogResult = DialogResult.Cancel,
            Location = new Point(316, 572),
            Size = new Size(80, 27),
        };

        AcceptButton = btnOk;
        CancelButton = btnCancel;

        btnOk.Click += (_, _) =>
        {
            _settings.Save();
        };
        FormClosing += AudioSettingsForm_FormClosing;

        Controls.AddRange([
            lblDevice, _cboDevice, _btnRefresh,
            lblMode, _radioVad, _lblSensitivity, _trkVad,
            _radioPtt, _lblPttKey, _btnChangePtt, _radioAlwaysOn,
            lblGain, _trkGain, _chkNoiseReduction, _chkStereoMic,
            _chkAux, _lblAuxDevice, _cboAuxDevice, _btnAuxRefresh, _lblAuxGain, _trkAuxGain,
            btnOk, btnCancel,
        ]);

        // Apply saved mode (fires RadioMode_CheckedChanged which sets visibility)
        switch ((VcInputMode)settings.InputMode)
        {
            case VcInputMode.PushToTalk: _radioPtt.Checked = true; break;
            case VcInputMode.AlwaysOn:   _radioAlwaysOn.Checked = true; break;
            default:                     _radioVad.Checked = true; break;
        }

        LoadDevices();
        LoadAuxDevices();
        UpdateAuxControlsEnabled();
    }

    private void LoadDevices()
    {
        string? currentId = (_cboDevice.SelectedItem as DeviceInfo)?.Id ?? _settings.InputDeviceId;

        var devices = _client.ListDevices(VcDeviceKind.Input);
        _cboDevice.SelectedIndexChanged -= CboDevice_SelectedIndexChanged;
        _cboDevice.DataSource = new BindingList<DeviceInfo>(devices);
        _cboDevice.SelectedIndexChanged += CboDevice_SelectedIndexChanged;

        // Try to restore previous selection
        int idx = -1;
        if (currentId is not null)
            idx = devices.FindIndex(d => d.Id == currentId);
        if (idx < 0)
            idx = devices.FindIndex(d => d.IsDefault);
        _cboDevice.SelectedIndex = idx >= 0 ? idx : (devices.Count > 0 ? 0 : -1);
    }

    private void CboDevice_SelectedIndexChanged(object? sender, EventArgs e)
    {
        if (_cboDevice.SelectedItem is not DeviceInfo dev) return;
        _settings.InputDeviceId = dev.IsDefault ? null : dev.Id;
        if (_micStreamId != 0)
            _client.SetInputDevice(_micStreamId, dev.IsDefault ? null : dev.Id);
    }

    // ── Aux input stream ──────────────────────────────────────────────────────

    private void LoadAuxDevices()
    {
        string? currentId = (_cboAuxDevice.SelectedItem as InputDeviceInfo)?.Id
                            ?? _settings.AuxDeviceId;

        var devices = InputDeviceEnumerator.List().ToList();

        // Set the data source AND restore the selection while unsubscribed so neither the initial
        // populate nor a Refresh fires a spurious device-change (which would restart the capture).
        _cboAuxDevice.SelectedIndexChanged -= CboAuxDevice_SelectedIndexChanged;
        _cboAuxDevice.DataSource = new BindingList<InputDeviceInfo>(devices);

        int idx = -1;
        if (currentId is not null)
            idx = devices.FindIndex(d => d.Id == currentId);
        if (idx < 0)
            idx = devices.FindIndex(d => d.IsDefault);
        _cboAuxDevice.SelectedIndex = idx >= 0 ? idx : (devices.Count > 0 ? 0 : -1);
        _cboAuxDevice.SelectedIndexChanged += CboAuxDevice_SelectedIndexChanged;

        // Persist the resolved selection (null = default) so Join Voice opens the same device.
        if (_cboAuxDevice.SelectedItem is InputDeviceInfo dev)
            _settings.AuxDeviceId = dev.IsDefault ? null : dev.Id;
    }

    private void ChkAux_CheckedChanged(object? sender, EventArgs e)
    {
        _settings.AuxEnabled = _chkAux.Checked;
        UpdateAuxControlsEnabled();
        _applyAuxEnabled?.Invoke(_chkAux.Checked);
    }

    private void CboAuxDevice_SelectedIndexChanged(object? sender, EventArgs e)
    {
        if (_cboAuxDevice.SelectedItem is not InputDeviceInfo dev) return;
        _settings.AuxDeviceId = dev.IsDefault ? null : dev.Id;
        if (_chkAux.Checked)
            _applyAuxDevice?.Invoke(dev.IsDefault ? null : dev.Id);
    }

    private void TrkAuxGain_Scroll(object? sender, EventArgs e)
    {
        _settings.AuxGain = _trkAuxGain.Value;
        if (_chkAux.Checked)
            _applyAuxGain?.Invoke(_trkAuxGain.Value / 100f);
    }

    private void UpdateAuxControlsEnabled()
    {
        bool on = _chkAux.Checked;
        _lblAuxDevice.Enabled  = on;
        _cboAuxDevice.Enabled  = on;
        _btnAuxRefresh.Enabled = on;
        _lblAuxGain.Enabled    = on;
        _trkAuxGain.Enabled    = on;
    }

    private void RadioMode_CheckedChanged(object? sender, EventArgs e)
    {
        var mode = CurrentMode();
        bool isVad = mode == VcInputMode.VoiceActivation;
        bool isPtt = mode == VcInputMode.PushToTalk;

        _lblSensitivity.Visible = isVad;
        _trkVad.Visible = isVad;
        _lblPttKey.Visible = isPtt;
        _btnChangePtt.Visible = isPtt;
        UpdatePttKeyLabel();

        _settings.InputMode = (int)mode;
        if (_micStreamId != 0)
        {
            _client.SetInputMode(mode);
            if (isVad) _client.SetVadThreshold(VadThresholdFromSlider());
            if (isPtt) _client.SetPushToTalk(false);
        }
    }

    private void TrkVad_Scroll(object? sender, EventArgs e)
    {
        _settings.VadThresholdSlider = _trkVad.Value;
        if (_micStreamId != 0 && CurrentMode() == VcInputMode.VoiceActivation)
            _client.SetVadThreshold(VadThresholdFromSlider());
    }

    private void TrkGain_Scroll(object? sender, EventArgs e)
    {
        _settings.MicGain = _trkGain.Value;
        if (_micStreamId != 0)
            _client.SetInputGain(_trkGain.Value / 100f);
    }

    private void ChkNoiseReduction_CheckedChanged(object? sender, EventArgs e)
    {
        _settings.MicNoiseReduction = _chkNoiseReduction.Checked;
        if (_micStreamId != 0)
            _client.SetInputNoiseReduction(_chkNoiseReduction.Checked);
    }

    private void ChkStereoMic_CheckedChanged(object? sender, EventArgs e)
    {
        _settings.StereoMic = _chkStereoMic.Checked;
        // Channel count only takes effect when the capture device (re)starts, so restart it live.
        if (_micStreamId != 0)
        {
            _client.SetCaptureChannels(_micStreamId, _chkStereoMic.Checked ? 2u : 1u);
            _client.AudioRestart();
        }
    }

    private void BtnChangePtt_Click(object? sender, EventArgs e)
    {
        using var dlg = new PttKeyCaptureDialog(_pttKey);
        if (dlg.ShowDialog(this) == DialogResult.OK)
        {
            _pttKey = dlg.CapturedKey;
            _settings.PttKey = (int)_pttKey;
            UpdatePttKeyLabel();
        }
    }

    private void AudioSettingsForm_FormClosing(object? sender, FormClosingEventArgs e)
    {
        if (DialogResult == DialogResult.OK) return;

        // Cancel: restore originals to settings and live client
        _settings.InputDeviceId = _origDeviceId;
        _settings.InputMode = (int)_origMode;
        _settings.VadThresholdSlider = _origVadSlider;
        _settings.MicGain = _origMicGain;
        _settings.MicNoiseReduction = _origMicNoiseReduction;
        _settings.StereoMic = _origStereoMic;
        _settings.PttKey = (int)_origPttKey;

        if (_micStreamId != 0)
        {
            _client.SetInputDevice(_micStreamId, _origDeviceId);
            _client.SetInputMode(_origMode);
            if (_origMode == VcInputMode.VoiceActivation)
                _client.SetVadThreshold(0.1f * (1f - (_origVadSlider - 1f) / 99f));
            _client.SetInputGain(_origMicGain / 100f);
            _client.SetInputNoiseReduction(_origMicNoiseReduction);
            // Restore capture channel count; restart the device only if it actually changed.
            if (_origStereoMic != _chkStereoMic.Checked)
            {
                _client.SetCaptureChannels(_micStreamId, _origStereoMic ? 2u : 1u);
                _client.AudioRestart();
            }
        }

        // Aux: restore originals to settings and re-apply live (order: device + gain first so a
        // re-enable starts with the right configuration).
        _settings.AuxEnabled  = _origAuxEnabled;
        _settings.AuxDeviceId = _origAuxDeviceId;
        _settings.AuxGain     = _origAuxGain;
        _applyAuxDevice?.Invoke(_origAuxDeviceId);
        _applyAuxGain?.Invoke(_origAuxGain / 100f);
        _applyAuxEnabled?.Invoke(_origAuxEnabled);
    }

    private void UpdatePttKeyLabel() =>
        _lblPttKey.Text = $"({_pttKey})";

    private VcInputMode CurrentMode() =>
        _radioPtt.Checked ? VcInputMode.PushToTalk :
        _radioAlwaysOn.Checked ? VcInputMode.AlwaysOn :
        VcInputMode.VoiceActivation;

    private float VadThresholdFromSlider() =>
        0.1f * (1f - (_trkVad.Value - 1f) / 99f);
}
