using RioJoy.Core;
using RioJoy.Core.Mapping;
using RioJoy.Core.Overlay;
using RioJoy.Core.Profiles;
using RioJoy.Tray.Editor;
using RioJoy.Tray.Os;

namespace RioJoy.Tray;

/// <summary>
/// Owns the tray icon, menu, and the RIOJoy runtime. The menu mirrors the legacy
/// console menu (axis resets, version/status, diagnostic toggles, quit) and adds
/// profile selection (auto vs. manual). The app's start/stop lifecycle is owned by
/// the TeslaConsole launcher, so there is no "start with Windows" toggle. The
/// auto-switch watcher is polled on a UI timer so menu/status updates stay on the
/// UI thread.
/// </summary>
internal sealed class TrayApplicationContext : ApplicationContext
{
    private static readonly string ConfigPath =
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RIOJoy", "config.json");

    private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(1);

    private readonly AppConfig _config;
    private readonly RioCoordinator _coordinator;
    private readonly AutoSwitchWatcher _watcher;
    private readonly System.Windows.Forms.Timer _pollTimer;
    private readonly NotifyIcon _trayIcon;
    private readonly ToolStripMenuItem _statusItem;

    public TrayApplicationContext()
    {
        _config = ConfigStore.Load(ConfigPath);

        _coordinator = new RioCoordinator(() => _config);
        _coordinator.StatusChanged += _ => RefreshOnUiThread();

        _watcher = new AutoSwitchWatcher(new ForegroundProcessProvider(), () => _config);
        _watcher.DecisionChanged += d => _coordinator.ApplyDecision(d);

        _statusItem = new ToolStripMenuItem("Status: starting…") { Enabled = false };

        _trayIcon = new NotifyIcon
        {
            Icon = SystemIcons.Application,
            Text = "RIOJoy",
            Visible = true,
            ContextMenuStrip = BuildMenu(),
        };

        // Poll the foreground app on the UI thread.
        _pollTimer = new System.Windows.Forms.Timer { Interval = (int)PollInterval.TotalMilliseconds };
        _pollTimer.Tick += (_, _) => _watcher.Poll();
        _pollTimer.Start();
        _watcher.Poll();
    }

    private ContextMenuStrip BuildMenu()
    {
        var menu = new ContextMenuStrip();

        menu.Items.Add(_statusItem);
        menu.Items.Add(new ToolStripSeparator());

        // Profile selection: Auto + each profile + manual dormant.
        var profileMenu = new ToolStripMenuItem("Profile");
        profileMenu.DropDownOpening += (_, _) => RebuildProfileMenu(profileMenu);
        RebuildProfileMenu(profileMenu);
        menu.Items.Add(profileMenu);

        var editMenu = new ToolStripMenuItem("Edit profile");
        editMenu.DropDownOpening += (_, _) => RebuildEditMenu(editMenu);
        RebuildEditMenu(editMenu);
        menu.Items.Add(editMenu);

#if !NET40 // wallpaper maker needs SkiaSharp — modern flavor only (XP applies pre-rendered)
        var wallpaperMenu = new ToolStripMenuItem("Wallpaper maker");
        wallpaperMenu.DropDownOpening += (_, _) => RebuildWallpaperMenu(wallpaperMenu);
        RebuildWallpaperMenu(wallpaperMenu);
        menu.Items.Add(wallpaperMenu);
#endif

        menu.Items.Add("Import .ini…", null, (_, _) => ImportIniProfiles());

        // RIO commands mirroring the legacy console menu.
        var commands = new ToolStripMenuItem("RIO commands");
        AddCommand(commands, "Reset all analog axes", RioCommandCode.ResetAllAxes);
        AddCommand(commands, "Reset throttle", RioCommandCode.ResetThrottle);
        AddCommand(commands, "Reset left pedal", RioCommandCode.ResetLeftPedal);
        AddCommand(commands, "Reset right pedal", RioCommandCode.ResetRightPedal);
        AddCommand(commands, "Reset vertical joystick", RioCommandCode.ResetVerticalJoystick);
        AddCommand(commands, "Reset horizontal joystick", RioCommandCode.ResetHorizontalJoystick);
        commands.DropDownItems.Add(new ToolStripSeparator());
        AddCommand(commands, "Reset digital (general)", RioCommandCode.GeneralReset);
        AddCommand(commands, "Request version && status", RioCommandCode.RequestVersionAndCheck);
        AddCommand(commands, "Toggle raw-axes readout", RioCommandCode.ToggleRawAxesReadout);
        AddCommand(commands, "Toggle poll-rate readout", RioCommandCode.TogglePollRateReadout);
        menu.Items.Add(commands);

        menu.Items.Add(new ToolStripSeparator());

        // No "start with Windows" toggle: the TeslaConsole launcher owns the app's
        // start/stop lifecycle, so RIOJoy deliberately registers no auto-start entry.
        menu.Items.Add("Exit", null, (_, _) => Quit());

        return menu;
    }

    private void RebuildProfileMenu(ToolStripMenuItem profileMenu)
    {
        profileMenu.DropDownItems.Clear();

        var auto = new ToolStripMenuItem("Auto (foreground app)") { Checked = _coordinator.IsAuto };
        auto.Click += (_, _) => _coordinator.SetAuto();
        profileMenu.DropDownItems.Add(auto);
        profileMenu.DropDownItems.Add(new ToolStripSeparator());

        foreach (RioProfile profile in _config.Profiles)
        {
            RioProfile captured = profile;
            var item = new ToolStripMenuItem(profile.Name)
            {
                Checked = !_coordinator.IsAuto && _coordinator.ActiveProfileName == profile.Name,
            };
            item.Click += (_, _) => _coordinator.SetManualProfile(captured);
            profileMenu.DropDownItems.Add(item);
        }

        if (_config.Profiles.Count == 0)
            profileMenu.DropDownItems.Add(new ToolStripMenuItem("(no profiles configured)") { Enabled = false });

        profileMenu.DropDownItems.Add(new ToolStripSeparator());
        var dormant = new ToolStripMenuItem("Dormant (release port)")
        {
            Checked = !_coordinator.IsAuto && _coordinator.ActiveProfileName is null,
        };
        dormant.Click += (_, _) => _coordinator.SetManualDormant();
        profileMenu.DropDownItems.Add(dormant);
    }

    // Rebuild the "Edit profile" submenu: one entry per profile (opens it in the
    // editor) plus an entry to create a new one.
    private void RebuildEditMenu(ToolStripMenuItem editMenu)
    {
        editMenu.DropDownItems.Clear();

        foreach (RioProfile profile in _config.Profiles)
        {
            RioProfile captured = profile;
            editMenu.DropDownItems.Add(new ToolStripMenuItem(profile.Name, null, (_, _) => OpenEditor(captured)));
        }

        if (_config.Profiles.Count > 0)
            editMenu.DropDownItems.Add(new ToolStripSeparator());

        editMenu.DropDownItems.Add(new ToolStripMenuItem("New profile…", null, (_, _) => OpenEditor(NewProfile())));
    }

#if !NET40
    // Rebuild the "Wallpaper maker" submenu: one entry per profile.
    private void RebuildWallpaperMenu(ToolStripMenuItem wallpaperMenu)
    {
        wallpaperMenu.DropDownItems.Clear();

        foreach (RioProfile profile in _config.Profiles)
        {
            RioProfile captured = profile;
            wallpaperMenu.DropDownItems.Add(new ToolStripMenuItem(profile.Name, null, (_, _) => OpenWallpaperMaker(captured)));
        }

        if (_config.Profiles.Count == 0)
            wallpaperMenu.DropDownItems.Add(new ToolStripMenuItem("(no profiles configured)") { Enabled = false });
    }

    // Open the wallpaper maker for a profile. Purely offline (no serial session):
    // it renders the profile's labels onto the overlay template, prompting for the
    // template (regions.json) the first time and remembering it in config.
    private void OpenWallpaperMaker(RioProfile profile)
    {
        string? path = _config.OverlayTemplatePath;
        if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
        {
            using var dialog = new OpenFileDialog
            {
                Title = "Pick the cockpit overlay template (regions.json)",
                Filter = "Overlay template (regions.json)|*.json|All files (*.*)|*.*",
            };
            if (dialog.ShowDialog() != DialogResult.OK)
                return;
            path = dialog.FileName;
            _config.OverlayTemplatePath = path;
            ConfigStore.Save(_config, ConfigPath);
        }

        try
        {
            OverlayTemplate template = OverlayTemplateStore.Load(path!);
            string templateDir = Path.GetDirectoryName(Path.GetFullPath(path!)) ?? ".";

            var maker = new WallpaperMakerForm(profile, template, templateDir, path);
            maker.Saved += _ => ConfigStore.Save(_config, ConfigPath);
            maker.Show();
        }
        catch (Exception ex)
        {
            MessageBox.Show($"Could not open the wallpaper maker:\n{ex.Message}",
                "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }
#endif

    // Create, register and persist a fresh empty profile with a unique name.
    private RioProfile NewProfile()
    {
        string name = "New profile";
        for (int i = 2; _config.FindProfile(name) is not null; i++)
            name = $"New profile {i}";

        var profile = new RioProfile { Name = name };
        _config.Profiles.Add(profile);
        ConfigStore.Save(_config, ConfigPath);
        return profile;
    }

    private void OpenEditor(RioProfile profile)
    {
        // Acquiring the RIO is one of the two reasons we take the serial port (the
        // other being a profiled game) — so the editor can check button function. Input
        // is suppressed (no keystrokes); the editor only shows which button is pressed.
        _coordinator.BeginEditorSession(profile);

        var editor = new ProfileEditorForm(profile);
        editor.IsNameAvailable = name => !_config.Profiles.Any(
            p => !ReferenceEquals(p, profile) && string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
        editor.Saved += _ => ConfigStore.Save(_config, ConfigPath);
        editor.CommandRequested += cmd => _coordinator.Runtime?.Trigger(cmd);
        editor.OutputsEnabledChanged += enabled => _coordinator.SetEditorOutputs(enabled);

        RioRuntime? runtime = _coordinator.Runtime;
        Action<int, bool>? activity = null;
        if (runtime is not null)
        {
            activity = editor.ShowLiveActivity;
            runtime.ButtonActivity += activity;
            runtime.AxesUpdated += editor.ShowAxes;
            runtime.VersionReceived += editor.ShowVersion;
            runtime.CheckReceived += editor.AddCheckStatus;
        }

        editor.FormClosed += (_, _) =>
        {
            if (runtime is not null && activity is not null)
            {
                runtime.ButtonActivity -= activity;
                runtime.AxesUpdated -= editor.ShowAxes;
                runtime.VersionReceived -= editor.ShowVersion;
                runtime.CheckReceived -= editor.AddCheckStatus;
            }
            _coordinator.EndEditorSession();
            _watcher.Reset();   // re-sync with the foreground app (may re-activate a game)
            _watcher.Poll();
        };
        editor.Show();
    }

    // Import one or more legacy RIO.ini files (the gsheet "INI_V2" format) as
    // profiles, naming each after its filename and de-duping collisions.
    private void ImportIniProfiles()
    {
        using var dialog = new OpenFileDialog
        {
            Title = "Import RIO .ini profile(s)",
            Filter = "RIO INI files (*.ini)|*.ini|All files (*.*)|*.*",
            Multiselect = true,
        };
        if (dialog.ShowDialog() != DialogResult.OK)
            return;

        var imported = new List<string>();
        var failures = new List<string>();
        foreach (string path in dialog.FileNames)
        {
            try
            {
                string name = UniqueProfileName(Path.GetFileNameWithoutExtension(path));
                RioProfile profile = RioIniImporter.Import(File.ReadAllText(path), name);
                _config.Profiles.Add(profile);
                imported.Add($"{name} ({profile.Buttons.Count} buttons mapped)");
            }
            catch (Exception ex)
            {
                failures.Add($"{Path.GetFileName(path)}: {ex.Message}");
            }
        }

        if (imported.Count > 0)
            ConfigStore.Save(_config, ConfigPath);

        string summary = imported.Count > 0
            ? "Imported:\n  " + string.Join("\n  ", imported)
            : "No profiles imported.";
        if (failures.Count > 0)
            summary += "\n\nFailed:\n  " + string.Join("\n  ", failures);

        MessageBox.Show(summary, "RIOJoy — Import .ini",
            MessageBoxButtons.OK,
            failures.Count > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
    }

    // Ensure the imported profile name is unique within the library.
    private string UniqueProfileName(string desired)
    {
        string baseName = string.IsNullOrWhiteSpace(desired) ? "Imported" : desired.Trim();
        string candidate = baseName;
        for (int i = 2; _config.FindProfile(candidate) is not null; i++)
            candidate = $"{baseName} ({i})";
        return candidate;
    }

    private void AddCommand(ToolStripMenuItem parent, string text, RioCommandCode code)
    {
        var item = new ToolStripMenuItem(text);
        item.Click += (_, _) => _coordinator.Runtime?.Trigger(code);
        parent.DropDownItems.Add(item);
    }

    private void RefreshOnUiThread()
    {
        if (_trayIcon.ContextMenuStrip?.InvokeRequired == true)
        {
            _trayIcon.ContextMenuStrip.BeginInvoke(RefreshStatus);
            return;
        }

        RefreshStatus();
    }

    private void RefreshStatus()
    {
        _statusItem.Text = $"Status: {_coordinator.Status}";
        _trayIcon.Text = $"RIOJoy — {_coordinator.Status}".Length <= 63
            ? $"RIOJoy — {_coordinator.Status}"
            : "RIOJoy";
    }

    private void Quit()
    {
        _pollTimer.Stop();
        _coordinator.Dispose();
        _trayIcon.Visible = false;
        ExitThread();
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _pollTimer.Dispose();
            _coordinator.Dispose();
            _trayIcon.Dispose();
        }

        base.Dispose(disposing);
    }
}
