using System.Runtime.Versioning;
using RioJoy.Core;
using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;
using RioJoy.Core.Output;
using RioJoy.Core.Overlay;
using RioJoy.Core.Profiles;
using RioJoy.Core.Serial;
using RioJoy.Overlay;

namespace RioJoy.Tray;

/// <summary>
/// Owns the active RIO connection and drives the three-state auto-switch: it
/// opens/closes the serial link and assembles a <see cref="RioRuntime"/> for the
/// active profile, releasing the COM port when a native game runs or nothing
/// matches (see docs/PLAN.md §Profiles). Manual override from the tray takes
/// precedence over the watcher.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class RioCoordinator : IDisposable
{
    private readonly Func<AppConfig> _config;
    private readonly Func<string, IRioTransport> _transportFactory;

    private bool _auto = true;

    // The active connection (null when dormant).
    private IRioTransport? _transport;
    private RioSerialLink? _link;
    private RioRuntime? _runtime;
    private CancellationTokenSource? _cts;
    private IDisposable? _joystick;
    private string? _activeProfileName;

    public RioCoordinator(Func<AppConfig> config, Func<string, IRioTransport>? transportFactory = null)
    {
        _config = config ?? throw new ArgumentNullException(nameof(config));
        _transportFactory = transportFactory ?? (port => new SerialPortTransport(port));
    }

    /// <summary>Raised (with a short status string) whenever the active state changes.</summary>
    public event Action<string>? StatusChanged;

    /// <summary>Current status line for the tray.</summary>
    public string Status { get; private set; } = "Starting…";

    /// <summary>The name of the active profile, or null when dormant.</summary>
    public string? ActiveProfileName => _activeProfileName;

    /// <summary>Whether the auto-switch watcher controls the active profile.</summary>
    public bool IsAuto => _auto;

    /// <summary>The currently running runtime, for manual RIO-command triggers.</summary>
    public RioRuntime? Runtime => _runtime;

    /// <summary>Apply a watcher decision (ignored while a manual override is in effect).</summary>
    public void ApplyDecision(SwitchDecision decision)
    {
        if (!_auto)
            return;

        switch (decision.Mode)
        {
            case SwitchMode.Activate when decision.Profile is not null:
                Activate(decision.Profile);
                break;
            case SwitchMode.Yield:
                GoDormant("Dormant — native game has the port");
                break;
            default:
                GoDormant("Idle");
                break;
        }
    }

    /// <summary>Hand control back to the auto-switch watcher.</summary>
    public void SetAuto()
    {
        _auto = true;
        SetStatus("Auto");
    }

    /// <summary>Manually activate a profile; disables auto until <see cref="SetAuto"/>.</summary>
    public void SetManualProfile(RioProfile profile)
    {
        _auto = false;
        Activate(profile);
    }

    /// <summary>Manually go dormant (release the port); disables auto.</summary>
    public void SetManualDormant()
    {
        _auto = false;
        GoDormant("Dormant (manual)");
    }

    private void Activate(RioProfile profile)
    {
        if (_activeProfileName == profile.Name && _runtime is not null)
            return; // already running this profile

        Teardown();

        AppConfig config = _config();
        string port = profile.RioComPort ?? config.DefaultRioComPort;

        try
        {
            // Use the real virtual-joystick driver if installed; otherwise fall back
            // to a no-op sink (keyboard/mouse still work) and note it in the status.
            IJoystickSink joystick;
            string joystickNote;
            if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
            {
                joystick = feeder!;
                _joystick = feeder;
                joystickNote = string.Empty;
            }
            else
            {
                joystick = new NullJoystickSink();
                _joystick = null;
                joystickNote = " [no joystick driver]";
            }

            _transport = _transportFactory(port);
            _link = new RioSerialLink(_transport);
            _runtime = new RioRuntime(
                _link,
                profile.ToInputMap(),
                new SendInputSink(),
                joystick,
                new AxisCalibrator(profile.Calibration));

            _cts = new CancellationTokenSource();
            _ = _link.RunAsync(_cts.Token);
            _runtime.Start();
            _activeProfileName = profile.Name;
            SetStatus($"Active: {profile.Name} ({_link.Description}){joystickNote}");
        }
        catch (Exception ex)
        {
            Teardown();
            SetStatus($"Error opening {port}: {ex.Message}");
            return;
        }

        ApplyWallpaper(profile);
    }

    /// <summary>
    /// Generate the profile's cockpit wallpaper from the configured overlay template
    /// and apply it. Best-effort and opt-in (only when <see cref="AppConfig.OverlayTemplatePath"/>
    /// is set), so a missing template or render failure never breaks activation.
    /// </summary>
    private void ApplyWallpaper(RioProfile profile)
    {
        string? templatePath = _config().OverlayTemplatePath;
        if (string.IsNullOrWhiteSpace(templatePath) || !File.Exists(templatePath))
            return;

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

            string outDir = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "RIOJoy", "wallpapers");
            string outPath = Path.Combine(outDir, SafeFileName(profile.Name) + ".png");

            new ProfileWallpaperGenerator().Generate(template, templateDir, profile.OverlayLabels, outPath);
            profile.WallpaperPath = outPath;
            WallpaperApplier.Apply(outPath);
        }
        catch (Exception ex)
        {
            SetStatus($"{Status}  [wallpaper: {ex.Message}]");
        }
    }

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

    private void GoDormant(string status)
    {
        Teardown();
        SetStatus(status);
    }

    private void Teardown()
    {
        _runtime?.Dispose();
        _runtime = null;

        _cts?.Cancel();
        _cts?.Dispose();
        _cts = null;

        _link = null;

        _joystick?.Dispose(); // closes the HID feeder handle
        _joystick = null;

        _transport?.Dispose(); // releases the COM port
        _transport = null;

        _activeProfileName = null;
    }

    private void SetStatus(string status)
    {
        Status = status;
        StatusChanged?.Invoke(status);
    }

    public void Dispose() => Teardown();
}
