using RioJoy.Core.Overlay;
using RioJoy.Core.Profiles;
using SkiaSharp;

namespace RioJoy.Tray.Editor;

/// <summary>
/// Wallpaper maker: the interactive replacement for the legacy Google-Sheet →
/// GIMP/Script-Fu pipeline. Shows the profile's cockpit wallpaper rendered live
/// (via <see cref="RioJoy.Overlay.SkiaOverlayRenderer"/> on the overlay template),
/// lets the user click any of the template's cells — including the heading/banner
/// cells the button editor can't reach — and edit its text with immediate
/// re-render. Clicking a stacked cell repeatedly cycles through the regions at
/// that spot (the wallpaper feeds six chroma-split displays, so cells overlap).
/// Labels write back to <see cref="RioProfile.OverlayLabels"/>; <c>Save</c> raises
/// <see cref="Saved"/>. Also imports the legacy <c>.data</c> label sheet, exports
/// the PNG, and applies the wallpaper on demand.
/// </summary>
public sealed class WallpaperMakerForm : Form
{
    private readonly RioProfile _profile;
    private readonly OverlayTemplate _template;
    private readonly string? _templatePath;
    private readonly RioJoy.Overlay.SkiaOverlayRenderer _renderer = new();
    private readonly SKBitmap _baseImage;

    private readonly WallpaperCanvas _canvas = new() { Dock = DockStyle.Fill };
    private readonly Label _cellInfo = new() { AutoSize = true, Location = new Point(12, 40), MaximumSize = new Size(310, 0), Text = "Click a cell to edit it." };
    private readonly TextBox _labelBox = new() { Location = new Point(70, 74), Width = 230, Enabled = false };
    private readonly CheckBox _outlines = new() { Text = "Show cell outlines", Location = new Point(12, 104), AutoSize = true, Checked = true };
    private readonly CheckBox _editBoxes = new() { Text = "Edit cell boxes", Location = new Point(150, 104), AutoSize = true };
    private readonly NumericUpDown _numX = new() { Location = new Point(34, 130), Width = 66, Enabled = false };
    private readonly NumericUpDown _numY = new() { Location = new Point(132, 130), Width = 66, Enabled = false };
    private readonly NumericUpDown _numW = new() { Location = new Point(34, 156), Width = 66, Minimum = 1, Enabled = false };
    private readonly NumericUpDown _numH = new() { Location = new Point(132, 156), Width = 66, Minimum = 1, Enabled = false };
    private readonly Button _saveTemplate = new() { Text = "Save template", Location = new Point(204, 130), Width = 114, Enabled = false };
    private readonly Button _reloadTemplate = new() { Text = "Reload template", Location = new Point(204, 156), Width = 114, Enabled = false };
    private readonly ListBox _list = new() { Location = new Point(12, 188), Size = new Size(306, 260), IntegralHeight = false, DrawMode = DrawMode.OwnerDrawFixed };
    private readonly Label _hoverInfo = new() { AutoSize = true, Location = new Point(12, 456), ForeColor = SystemColors.GrayText };
    private readonly Button _import = new() { Text = "Import labels (.data)…", Location = new Point(12, 484), Width = 306 };
    private readonly Button _export = new() { Text = "Export PNG…", Location = new Point(12, 514), Width = 306 };
    private readonly Button _applyWallpaper = new() { Text = "Apply as desktop wallpaper", Location = new Point(12, 544), Width = 306 };
    private readonly Button _save = new() { Text = "Save profile", Location = new Point(12, 582), Width = 148 };
    private readonly Button _close = new() { Text = "Close", Location = new Point(170, 582), Width = 148 };

    // Re-render shortly after typing stops rather than per keystroke.
    private readonly System.Windows.Forms.Timer _renderTimer = new() { Interval = 300 };

    // Briefly flips a button to a confirmation, then reverts (one at a time).
    private readonly System.Windows.Forms.Timer _saveFlash = new() { Interval = 1600 };
    private Action? _saveFlashReset;

    private byte[] _lastPng = Array.Empty<byte>();
    private bool _loading;

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

    /// <param name="templateDirectory">
    /// Directory the template's relative <see cref="OverlayTemplate.BaseImagePath"/>
    /// is resolved against (normally the folder holding the regions.json).
    /// </param>
    /// <param name="templatePath">
    /// The template file itself; enables the box editor's "Save template" /
    /// "Reload template" (cell geometry is shared by every profile). Null = the
    /// geometry can still be edited for this session but not persisted.
    /// </param>
    public WallpaperMakerForm(RioProfile profile, OverlayTemplate template, string templateDirectory, string? templatePath = null)
    {
        _profile = profile ?? throw new ArgumentNullException(nameof(profile));
        _template = template ?? throw new ArgumentNullException(nameof(template));
        _templatePath = templatePath;
        if (string.IsNullOrWhiteSpace(template.BaseImagePath))
            throw new InvalidOperationException("OverlayTemplate.BaseImagePath is not set.");

        string imagePath = template.BaseImagePath!;
        string basePath = Path.IsPathRooted(imagePath)
            ? imagePath
            : Path.Combine(templateDirectory, imagePath);
        _baseImage = SKBitmap.Decode(basePath)
            ?? throw new InvalidOperationException($"Could not decode base image: {basePath}");

        Text = $"RIOJoy — Wallpaper: {profile.Name}";
        ClientSize = new Size(1360, 640);
        MinimumSize = new Size(960, 560);
        StartPosition = FormStartPosition.CenterScreen;

        _canvas.Template = template;
        _canvas.TemplateClicked += OnCanvasClicked;
        _canvas.HoverChanged += r => _hoverInfo.Text = r is null ? "" : Describe(r);
        _canvas.SelectedGeometryChanged += () => LoadGeometryFields(SelectedRegion());
        _canvas.GeometryCommitted += () =>
        {
            LoadGeometryFields(SelectedRegion());
            RenderPreview();
        };

        _numX.Maximum = _numW.Maximum = Math.Max(1, template.Width);
        _numY.Maximum = _numH.Maximum = Math.Max(1, template.Height);
        foreach (NumericUpDown n in new[] { _numX, _numY, _numW, _numH })
            n.ValueChanged += OnGeometryFieldChanged;

        _editBoxes.CheckedChanged += (_, _) =>
        {
            _canvas.EditMode = _editBoxes.Checked;
            UpdateGeometryEnabled();
        };
        _saveTemplate.Click += (_, _) => SaveTemplate();
        _reloadTemplate.Click += (_, _) => ReloadTemplate();

        _list.Items.AddRange(template.Regions.Select(r => (object)r.Name).ToArray());
        _list.DrawItem += DrawListItem;
        _list.SelectedIndexChanged += (_, _) => { if (_list.SelectedItem is string name) SelectRegion(name); };

        _labelBox.TextChanged += OnLabelChanged;
        _outlines.CheckedChanged += (_, _) => _canvas.ShowOutlines = _outlines.Checked;
        _import.Click += (_, _) => ImportData();
        _export.Click += (_, _) => ExportPng();
        _applyWallpaper.Click += (_, _) => ApplyAsWallpaper();
        _save.Click += (_, _) => SaveProfile();
        _close.Click += (_, _) => Close();

        _renderTimer.Tick += (_, _) => { _renderTimer.Stop(); RenderPreview(); };
        _saveFlash.Tick += (_, _) =>
        {
            _saveFlash.Stop();
            _saveFlashReset?.Invoke();
            _saveFlashReset = null;
        };

        var side = new Panel { Dock = DockStyle.Right, Width = 330, Padding = new Padding(8), BorderStyle = BorderStyle.FixedSingle, AutoScroll = true };
        side.Controls.Add(new Label { Text = "Cells:", Location = new Point(12, 12), AutoSize = true });
        side.Controls.Add(_cellInfo);
        side.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 77), AutoSize = true });
        side.Controls.Add(new Label { Text = "X:", Location = new Point(12, 133), AutoSize = true });
        side.Controls.Add(new Label { Text = "Y:", Location = new Point(110, 133), AutoSize = true });
        side.Controls.Add(new Label { Text = "W:", Location = new Point(12, 159), AutoSize = true });
        side.Controls.Add(new Label { Text = "H:", Location = new Point(110, 159), AutoSize = true });
        side.Controls.AddRange(new Control[]
        {
            _labelBox, _outlines, _editBoxes,
            _numX, _numY, _numW, _numH, _saveTemplate, _reloadTemplate,
            _list, _hoverInfo, _import, _export, _applyWallpaper, _save, _close,
        });

        Controls.Add(_canvas);
        Controls.Add(side);

        FormClosed += (_, _) =>
        {
            _renderTimer.Dispose();
            _saveFlash.Dispose();
            _baseImage.Dispose();
        };

        RenderPreview();
    }

    /// <summary>The selected region name (for tests / host inspection).</summary>
    public string? SelectedRegionName => _list.SelectedItem as string;

    /// <summary>Select a region programmatically (e.g. jump from the button editor).</summary>
    public void SelectRegion(string name)
    {
        _loading = true;
        int index = _list.Items.IndexOf(name);
        if (index >= 0 && _list.SelectedIndex != index)
            _list.SelectedIndex = index;
        _labelBox.Text = _profile.OverlayLabels.TryGetValue(name, out string? text) ? text : string.Empty;
        _labelBox.Enabled = true;
        _canvas.SelectedRegionName = name;
        OverlayRegion? region = _template.FindRegion(name);
        _cellInfo.Text = region is { } r ? Describe(r) : name;
        _loading = false;

        LoadGeometryFields(region);
        UpdateGeometryEnabled();
    }

    private OverlayRegion? SelectedRegion() =>
        _list.SelectedItem is string name ? _template.FindRegion(name) : null;

    // Reflect a region's box into the X/Y/W/H fields without triggering edits.
    private void LoadGeometryFields(OverlayRegion? region)
    {
        if (region is not null)
            _cellInfo.Text = Describe(region);

        _loading = true;
        _numX.Value = Clamp(region?.X ?? 0, _numX);
        _numY.Value = Clamp(region?.Y ?? 0, _numY);
        _numW.Value = Clamp(region?.Width ?? 1, _numW);
        _numH.Value = Clamp(region?.Height ?? 1, _numH);
        _loading = false;

        static decimal Clamp(double value, NumericUpDown n) =>
            Math.Min(n.Maximum, Math.Max(n.Minimum, (decimal)Math.Round(value)));
    }

    private void UpdateGeometryEnabled()
    {
        bool editing = _editBoxes.Checked;
        bool hasRegion = SelectedRegion() is not null;
        foreach (NumericUpDown n in new[] { _numX, _numY, _numW, _numH })
            n.Enabled = editing && hasRegion;
        _saveTemplate.Enabled = editing && _templatePath is not null;
        _reloadTemplate.Enabled = editing && _templatePath is not null;
    }

    // Typed box geometry: write through to the region and re-render (debounced).
    private void OnGeometryFieldChanged(object? sender, EventArgs e)
    {
        if (_loading || SelectedRegion() is not { } region)
            return;

        region.X = (double)_numX.Value;
        region.Y = (double)_numY.Value;
        region.Width = (double)_numW.Value;
        region.Height = (double)_numH.Value;

        _canvas.Invalidate();
        _renderTimer.Stop();
        _renderTimer.Start();
    }

    // Persist the (shared, all-profile) cell geometry back to the regions.json.
    private void SaveTemplate()
    {
        if (_templatePath is null)
            return;

        try
        {
            OverlayTemplateStore.Save(_template, _templatePath);
            Flash(_saveTemplate, "Saved ✓");
        }
        catch (Exception ex)
        {
            MessageBox.Show(this, $"Could not save the template:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }

    // Discard unsaved box edits by re-reading the cell geometry from disk (labels
    // are untouched — they live in the profile, not the template).
    private void ReloadTemplate()
    {
        if (_templatePath is null)
            return;

        try
        {
            OverlayTemplate fresh = OverlayTemplateStore.Load(_templatePath);
            foreach (OverlayRegion r in _template.Regions)
            {
                if (fresh.FindRegion(r.Name) is { } f)
                    (r.X, r.Y, r.Width, r.Height) = (f.X, f.Y, f.Width, f.Height);
            }

            LoadGeometryFields(SelectedRegion());
            RenderPreview();
        }
        catch (Exception ex)
        {
            MessageBox.Show(this, $"Could not reload the template:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }

    // Briefly show a confirmation on a button, then restore its caption.
    private void Flash(Button button, string confirmation)
    {
        _saveFlash.Stop();
        _saveFlashReset?.Invoke();

        string original = button.Text;
        Color originalColor = button.ForeColor;
        button.Text = confirmation;
        button.ForeColor = Color.ForestGreen;
        _saveFlashReset = () => { button.Text = original; button.ForeColor = originalColor; };
        _saveFlash.Start();
    }

    // A click on the preview: select the cell there, cycling through stacked cells.
    private void OnCanvasClicked(double x, double y)
    {
        if (OverlayHitTester.NextAt(_template, x, y, _canvas.SelectedRegionName) is { } hit)
            SelectRegion(hit.Name);
    }

    private void OnLabelChanged(object? sender, EventArgs e)
    {
        if (_loading || _list.SelectedItem is not string name)
            return;

        string text = _labelBox.Text.Trim();
        if (text.Length == 0)
            _profile.OverlayLabels.Remove(name);
        else
            _profile.OverlayLabels[name] = text;

        _renderTimer.Stop();
        _renderTimer.Start();
        _list.Invalidate();
    }

    // Render the profile's labels onto the base image and show it (kept as PNG
    // bytes so Export/Apply write exactly what is previewed).
    private void RenderPreview()
    {
        _lastPng = _renderer.RenderToPng(_template, _profile.OverlayLabels, _baseImage);

        // Copy so the Bitmap does not depend on the stream staying open (GDI+ rule).
        using var ms = new MemoryStream(_lastPng);
        using var streamed = new Bitmap(ms);
        _canvas.SetImage(new Bitmap(streamed));
    }

    // "name · 197×54 @ (810,332)" with the label text when set.
    private string Describe(OverlayRegion r)
    {
        string text = _profile.OverlayLabels.TryGetValue(r.Name, out string? t) ? $"  “{t}”" : "";
        return $"{r.Name} — {r.Width:0}×{r.Height:0} @ ({r.X:0},{r.Y:0}){text}";
    }

    // Owner-drawn so label edits show up in the list immediately on Invalidate.
    private void DrawListItem(object? sender, DrawItemEventArgs e)
    {
        e.DrawBackground();
        if (e.Index >= 0 && _list.Items[e.Index] is string name)
        {
            string text = _profile.OverlayLabels.TryGetValue(name, out string? t) ? t : "";
            bool selected = (e.State & DrawItemState.Selected) != 0;
            using var nameBrush = new SolidBrush(selected ? SystemColors.HighlightText : SystemColors.WindowText);
            using var textBrush = new SolidBrush(selected ? SystemColors.HighlightText : SystemColors.GrayText);
            e.Graphics.DrawString(name, e.Font!, nameBrush, e.Bounds.X + 2, e.Bounds.Y + 1);
            e.Graphics.DrawString(text, e.Font!, textBrush, e.Bounds.X + 90, e.Bounds.Y + 1);
        }
        e.DrawFocusRectangle();
    }

    // Import a legacy Goobie .data label sheet (one row per game) into the profile.
    private void ImportData()
    {
        using var dialog = new OpenFileDialog
        {
            Title = "Import Goobie label data",
            Filter = "Goobie label data (*.data)|*.data|All files (*.*)|*.*",
        };
        if (dialog.ShowDialog(this) != DialogResult.OK)
            return;

        try
        {
            GoobieDataImporter.Sheet sheet = GoobieDataImporter.Load(dialog.FileName);
            if (sheet.Rows.Count == 0)
            {
                MessageBox.Show(this, "The file contains no label rows.", "RIOJoy",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            int row = sheet.Rows.Count == 1 ? 0 : PickRow(sheet);
            if (row < 0)
                return;

            // Field 0 is the row's output-file tag, not a label.
            string tagField = sheet.Fields.Count > 0 ? sheet.Fields[0] : "";
            var labels = GoobieDataImporter.LabelsForRow(sheet, row)
                .Where(kv => !string.Equals(kv.Key, tagField, StringComparison.OrdinalIgnoreCase))
                .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);

            if (_profile.OverlayLabels.Count > 0 &&
                MessageBox.Show(this,
                    $"Replace the profile's {_profile.OverlayLabels.Count} existing label(s) with the imported row?",
                    "RIOJoy — Import labels", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
                return;

            _profile.OverlayLabels.Clear();
            foreach (KeyValuePair<string, string> kv in labels)
                _profile.OverlayLabels[kv.Key] = kv.Value;

            if (_list.SelectedItem is string current)
                SelectRegion(current); // refresh the label box from the new set
            _list.Invalidate();
            RenderPreview();
        }
        catch (Exception ex)
        {
            MessageBox.Show(this, $"Could not import the label data:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }

    // Let the user pick which sheet row (game) to import; -1 = cancelled.
    private int PickRow(GoobieDataImporter.Sheet sheet)
    {
        using var dlg = new Form
        {
            Text = "RIOJoy — Pick a label row",
            ClientSize = new Size(320, 360),
            FormBorderStyle = FormBorderStyle.FixedDialog,
            MinimizeBox = false,
            MaximizeBox = false,
            StartPosition = FormStartPosition.CenterParent,
        };
        var list = new ListBox { Location = new Point(12, 12), Size = new Size(296, 296), IntegralHeight = false };
        string tagField = sheet.Fields.Count > 0 ? sheet.Fields[0] : "";
        for (int i = 0; i < sheet.Rows.Count; i++)
        {
            list.Items.Add(sheet.Rows[i].TryGetValue(tagField, out string? tag) && !string.IsNullOrEmpty(tag)
                ? tag
                : $"Row {i + 1}");
        }
        list.SelectedIndex = 0;
        list.DoubleClick += (_, _) => dlg.DialogResult = DialogResult.OK;

        var ok = new Button { Text = "Import", Location = new Point(152, 320), Width = 75, DialogResult = DialogResult.OK };
        var cancel = new Button { Text = "Cancel", Location = new Point(233, 320), Width = 75, DialogResult = DialogResult.Cancel };
        dlg.Controls.AddRange(new Control[] { list, ok, cancel });
        dlg.AcceptButton = ok;
        dlg.CancelButton = cancel;

        return dlg.ShowDialog(this) == DialogResult.OK ? list.SelectedIndex : -1;
    }

    private void ExportPng()
    {
        using var dialog = new SaveFileDialog
        {
            Title = "Export wallpaper PNG",
            Filter = "PNG image (*.png)|*.png",
            FileName = _profile.Name + ".png",
        };
        if (dialog.ShowDialog(this) != DialogResult.OK)
            return;

        try
        {
            File.WriteAllBytes(dialog.FileName, _lastPng);
        }
        catch (Exception ex)
        {
            MessageBox.Show(this, $"Could not write the PNG:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }

    // Write the previewed PNG to the same per-profile path the runtime uses on
    // profile activation, and set it as the desktop wallpaper now.
    private void ApplyAsWallpaper()
    {
        try
        {
            string outDir = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "RIOJoy", "wallpapers");
            Directory.CreateDirectory(outDir);
            string outPath = Path.Combine(outDir, SafeFileName(_profile.Name) + ".png");

            File.WriteAllBytes(outPath, _lastPng);
            _profile.WallpaperPath = outPath;
            WallpaperApplier.Apply(outPath);
        }
        catch (Exception ex)
        {
            MessageBox.Show(this, $"Could not apply the wallpaper:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }

    private void SaveProfile()
    {
        try
        {
            Saved?.Invoke(_profile);
            Flash(_save, "Saved ✓");
        }
        catch (Exception ex)
        {
            MessageBox.Show(this, $"Could not save the profile:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }

    private static string SafeFileName(string name)
    {
        foreach (char c in Path.GetInvalidFileNameChars())
            name = name.Replace(c, '_');
        return name;
    }
}
