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;
#if !NET40
using RioJoy.Overlay; // SkiaSharp wallpaper generation — modern flavor only
#endif

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.
///
/// The coordinator starts <b>dormant</b> and only takes the serial port when a
/// profiled game is in the foreground (via the watcher) or while a profile is being
/// edited (<see cref="BeginEditorSession"/>). This keeps the COM port free the rest
/// of the time so the native games can open it without clashing.
/// </summary>
public sealed class RioCoordinator : IDisposable
{
    private readonly Func<AppConfig> _config;
    private readonly Func<string, IRioTransport> _transportFactory;

    private bool _auto = true;
    private bool _editorSession;

    // While editing, real keyboard/mouse + joystick run through these gates (off until
    // the editor's "send output" toggle opens them).
    private GatedInputSink? _editorInput;
    private GatedJoystickSink? _editorJoystick;

    // 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, _config().RioBaudRate));
    }

    /// <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; } = "Dormant";

    /// <summary>Whether a profile is currently being edited (port held for the editor).</summary>
    public bool IsEditing => _editorSession;

    /// <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)");
    }

    /// <summary>
    /// Acquire the RIO for <paramref name="profile"/> while its editor is open. The
    /// editor takes precedence over the watcher (so the port stays put while editing);
    /// call <see cref="EndEditorSession"/> when the editor closes.
    /// </summary>
    public void BeginEditorSession(RioProfile profile)
    {
        _editorSession = true;
        _auto = false; // editor override beats the auto-switch watcher
        Activate(profile, routeInput: false); // hold the port but don't inject keystrokes/joystick
    }

    /// <summary>
    /// Open or close the editor's output gates: when enabled, RIO button presses
    /// drive real keyboard/mouse and the virtual joystick; when disabled they don't.
    /// No-op outside an editor session.
    /// </summary>
    public void SetEditorOutputs(bool enabled)
    {
        if (_editorInput is null || _editorJoystick is null)
            return;

        _editorInput.Enabled = enabled;
        _editorJoystick.Enabled = enabled;

        if (_activeProfileName is not null)
            SetStatus($"Editing: {_activeProfileName} — outputs {(enabled ? "ON (sending to PC)" : "off")}");
    }

    /// <summary>Release the editor's port hold and hand control back to the watcher.</summary>
    public void EndEditorSession()
    {
        _editorSession = false;
        _auto = true;
        GoDormant("Dormant"); // the next watcher poll re-activates if a profiled game is foreground
    }

    // Pick the best available virtual-joystick sink and record it for disposal: the
    // signed ViGEmBus Xbox 360 pad first, then the (test-signed) RioGamepad HID
    // feeder, then a no-op when neither driver is present.
    private IJoystickSink CreateJoystickSink(out string note)
    {
#if !NET40 // ViGEmBus is Win10+; on XP the HID feeder drives RioGamepadXP.sys
        if (ViGEmJoystickSink.TryCreate(out ViGEmJoystickSink? vigem))
        {
            _joystick = vigem;
            note = string.Empty;
            return vigem!;
        }
#endif

        if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
        {
            _joystick = feeder;
            note = string.Empty;
            return feeder!;
        }

        _joystick = null;
        note = " [no joystick driver]";
        return new NullJoystickSink();
    }

    // routeInput=false holds the RIO link (and lamp feedback) but sends keyboard/
    // mouse/joystick output to no-op sinks — used for the editor's live button check.
    private void Activate(RioProfile profile, bool routeInput = true)
    {
        if (_activeProfileName == profile.Name && _runtime is not null && !_editorSession)
            return; // already running this profile (editor sessions always re-arm)

        Teardown();

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

        try
        {
            // Keyboard/mouse + joystick are gated off while editing so the RIO can be
            // exercised without injecting input (the editor toggle opens the gates);
            // lamp feedback over serial still applies.
            IInputSink input;
            IJoystickSink joystick;
            string note;
            IJoystickSink realJoystick = CreateJoystickSink(out string joystickNote);
            if (!routeInput)
            {
                // Keyboard/mouse + joystick are gated off while editing so the RIO can
                // be exercised without injecting input (the editor toggle opens them).
                _editorInput = new GatedInputSink(new SendInputSink());
                _editorJoystick = new GatedJoystickSink(realJoystick);
                input = _editorInput;
                joystick = _editorJoystick;
                note = " [editing — outputs off]";
            }
            else
            {
                input = new SendInputSink();
                joystick = realJoystick;
                note = joystickNote;
            }

            _transport = _transportFactory(port);
            _link = new RioSerialLink(_transport, new RioSerialLinkOptions
            {
                AnalogPollInterval = TimeSpan.FromMilliseconds(
                    Math.Max(10, config.AnalogPollMs)), // floor guards a typo'd config
            });
            _runtime = new RioRuntime(
                _link,
                profile.ToInputMap(),
                input,
                joystick,
                new AxisCalibrator(profile.Calibration));

            _runtime.EchoAllLamps = !routeInput; // editor: light any pressed button on the RIO

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

        if (routeInput)
            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)
    {
#if NET40
        // XP flavor: no SkiaSharp, so no generation — apply the profile's
        // pre-rendered wallpaper (authored on a modern machine) if it exists.
        if (string.IsNullOrWhiteSpace(profile.WallpaperPath) || !File.Exists(profile.WallpaperPath))
            return;
        try
        {
            WallpaperApplier.Apply(profile.WallpaperPath!);
        }
        catch (Exception ex)
        {
            SetStatus($"{Status}  [wallpaper: {ex.Message}]");
        }
#else
        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}]");
        }
#endif
    }

    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;
        _editorInput = null;
        _editorJoystick = 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();
}
