using VoiceCat.App.Audio;

namespace VoiceCat.App.Forms;

/// <summary>
/// Modal dialog for selecting which apps' audio to share.
/// Returns the chosen <see cref="AppAudioScope"/> via <see cref="ChosenScope"/>,
/// or <see cref="DialogResult.Cancel"/> if the user dismissed without sharing.
/// </summary>
public sealed class AppAudioPickerDialog : Form
{
    private readonly RadioButton  _rdoAll;
    private readonly RadioButton  _rdoOnly;
    private readonly RadioButton  _rdoExcept;
    private readonly CheckBox     _chkExcludeSelf;
    private readonly TextBox      _txtFilter;
    private readonly ListView     _appList;
    private readonly Label        _lblApps;

    // Snapshot taken when the dialog opens (refresh on open, not on every check change).
    private IReadOnlyList<AudioAppInfo> _apps = [];

    // Checked apps survive filtering (a filtered-out row keeps its check here).
    // pid → clean display name (used for the shared scope's Names).
    private readonly Dictionary<int, string> _checked = new();

    public AppAudioScope? ChosenScope { get; private set; }

    public AppAudioPickerDialog()
    {
        // ── Radio buttons ───────────────────────────────────────────────────
        _rdoAll = new RadioButton
        {
            Text      = "&Entire desktop",
            Checked   = true,
            Location  = new Point(12, 12),
            Size      = new Size(360, 20),
            TabIndex  = 0,
        };
        _rdoOnly = new RadioButton
        {
            Text     = "&Only selected apps",
            Location = new Point(12, 36),
            Size     = new Size(360, 20),
            TabIndex = 1,
        };
        _rdoExcept = new RadioButton
        {
            Text     = "All apps e&xcept selected",
            Location = new Point(12, 60),
            Size     = new Size(360, 20),
            TabIndex = 2,
        };

        _rdoAll.CheckedChanged    += OnModeChanged;
        _rdoOnly.CheckedChanged   += OnModeChanged;
        _rdoExcept.CheckedChanged += OnModeChanged;

        // ── Self-exclude (echo prevention) ───────────────────────────────────
        // Captures the whole desktop minus VoiceCat's own playback, so the channel
        // doesn't echo back the voices you're already hearing. Only applies to the
        // entire-desktop mode (the per-app modes already exclude this app's tree).
        _chkExcludeSelf = new CheckBox
        {
            Text     = "E&xclude VoiceCat's own audio (prevents echo)",
            Checked  = true,
            Location = new Point(28, 84),
            Size     = new Size(344, 20),
            TabIndex = 3,
        };

        // ── App list ────────────────────────────────────────────────────────
        _lblApps = new Label
        {
            Text     = "Apps (▶ = currently playing):",
            AutoSize = true,
            Location = new Point(12, 112),
            Visible  = false,
            TabIndex = 4,
        };

        _txtFilter = new TextBox
        {
            Location        = new Point(12, 132),
            Size            = new Size(360, 23),
            PlaceholderText = "Filter apps…",
            Visible         = false,
            TabIndex        = 5,
        };
        _txtFilter.TextChanged += (_, _) => PopulateList();

        _appList = new ListView
        {
            Location      = new Point(12, 160),
            Size          = new Size(360, 150),
            CheckBoxes    = true,
            View          = View.List,
            Visible       = false,
            TabIndex      = 6,
            FullRowSelect = true,
        };
        // Exclude mode supports only one target process tree (WASAPI EXCLUDE takes a single
        // PID), so enforce a single check while "All apps except selected" is active.
        _appList.ItemCheck += OnAppItemCheck;

        // ── Buttons ─────────────────────────────────────────────────────────
        var btnShare = new Button
        {
            Text         = "&Share",
            DialogResult = DialogResult.OK,
            Location     = new Point(216, 320),
            Size         = new Size(75, 27),
            TabIndex     = 7,
        };
        var btnCancel = new Button
        {
            Text         = "&Cancel",
            DialogResult = DialogResult.Cancel,
            Location     = new Point(297, 320),
            Size         = new Size(75, 27),
            TabIndex     = 8,
        };
        btnShare.Click += OnShareClick;

        AcceptButton  = btnShare;
        CancelButton  = btnCancel;
        AutoScaleMode = AutoScaleMode.Font;
        ClientSize    = new Size(384, 360);
        Controls.AddRange([_rdoAll, _rdoOnly, _rdoExcept, _chkExcludeSelf, _lblApps, _txtFilter,
                           _appList, btnShare, btnCancel]);
        FormBorderStyle = FormBorderStyle.FixedDialog;
        MaximizeBox     = false;
        MinimizeBox     = false;
        StartPosition   = FormStartPosition.CenterParent;
        Text            = "Share App Audio";
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        RefreshAppList();
    }

    private void RefreshAppList()
    {
        _apps = AudioSessionEnumerator.GetAudioApps();
        PopulateList();
    }

    // Rebuild the visible rows from _apps + the current filter text, restoring check
    // state from _checked so a selection persists while the user filters.
    private void PopulateList()
    {
        string filter = _txtFilter.Text.Trim();

        _suppressItemCheck = true;
        _appList.BeginUpdate();
        _appList.Items.Clear();
        foreach (var app in _apps)
        {
            if (filter.Length > 0 &&
                app.DisplayName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
                continue;

            string marker = app.IsPlaying ? "▶ " : "";
            _appList.Items.Add(new ListViewItem($"{marker}{app.DisplayName}  (PID {app.Pid})")
            {
                Tag     = app,
                Checked = _checked.ContainsKey(app.Pid),
            });
        }
        _appList.EndUpdate();
        _suppressItemCheck = false;
    }

    private void OnModeChanged(object? sender, EventArgs e)
    {
        bool showList = _rdoOnly.Checked || _rdoExcept.Checked;
        _lblApps.Visible   = showList;
        _txtFilter.Visible = showList;
        _appList.Visible   = showList;
        _lblApps.Text = _rdoExcept.Checked
            ? "App to exclude (everything else is shared):"
            : "Apps (▶ = currently playing):";

        // Self-exclude only applies to entire-desktop; the per-app modes already exclude
        // this app's own tree (Include) or spend the single EXCLUDE slot on the chosen app.
        _chkExcludeSelf.Enabled = _rdoAll.Checked;

        // Switching into exclude mode: collapse any multi-selection down to a single item.
        if (_rdoExcept.Checked)
            TrimCheckedToOne();

        if (showList && _appList.Items.Count == 0)
            RefreshAppList();
        else
            PopulateList(); // re-render markers/labels and restore check state
    }

    // In exclude mode the list behaves like radio buttons: checking one item clears the rest.
    private bool _suppressItemCheck;

    private void OnAppItemCheck(object? sender, ItemCheckEventArgs e)
    {
        if (_suppressItemCheck) return;
        if (_appList.Items[e.Index].Tag is not AudioAppInfo app) return;

        if (e.NewValue == CheckState.Checked)
        {
            if (_rdoExcept.Checked)
            {
                // Radio behavior: clear every other visible check and the persisted set.
                _suppressItemCheck = true;
                foreach (ListViewItem item in _appList.Items)
                    if (item.Index != e.Index && item.Checked) item.Checked = false;
                _suppressItemCheck = false;
                _checked.Clear();
            }
            _checked[app.Pid] = app.DisplayName;
        }
        else
        {
            _checked.Remove(app.Pid);
        }
    }

    // Reduce the persisted selection to a single (first) entry — used when entering
    // exclude mode, whose single EXCLUDE slot can only target one process tree.
    private void TrimCheckedToOne()
    {
        if (_checked.Count <= 1) return;
        var first = _checked.First();
        _checked.Clear();
        _checked[first.Key] = first.Value;
    }

    private void OnShareClick(object? sender, EventArgs e)
    {
        if (_rdoAll.Checked)
        {
            ChosenScope = new EntireDesktop(_chkExcludeSelf.Checked);
            return;
        }

        if (_checked.Count == 0)
        {
            MessageBox.Show(
                "Select at least one app, or choose 'Entire desktop'.",
                "No apps selected",
                MessageBoxButtons.OK,
                MessageBoxIcon.Warning);
            DialogResult = DialogResult.None; // prevent close
            return;
        }

        var pids  = _checked.Keys.ToList();
        var names = _checked.Values.ToList();

        ChosenScope = _rdoOnly.Checked
            ? new OnlyApps(pids, names)
            : new AllExceptApps(pids, names);
    }
}
