using System.Globalization;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;

namespace MBN_STOCK_WEBVIEW.Playout.Interop;

internal interface IK3dSession : IDisposable
{
    bool IsConnected { get; }

    PlayoutKtapConnectState LastKtapConnectState { get; }

    bool SupportsLifecycleCallbacks { get; }

    bool? KtapHelloObserved { get; }

    bool HasPendingLifecycleCallbacks { get; }

    bool HasPendingPlayCallbacks { get; }

    bool HasPendingTakeOutCallbacks { get; }

    bool TryPreventKtapConnect();

    void Connect(ValidatedPlayoutOptions options);

    void Disconnect();

    void Prepare(PlayoutCue cue, int layoutIndex);

    void UpdateOnAir(PlayoutCue cue, int layoutIndex);

    void Play(int layoutIndex);

    void TakeOut(int layoutIndex, PlayoutTakeOutScope scope);

    K3dCallbackDrainResult ProcessPendingCallbacks(int layoutIndex);

    void Abandon();
}

internal readonly record struct K3dCallbackDrainResult(
    int CallbackCount,
    int UnloadedSceneCount,
    bool HelloObserved);

internal interface IK3dGuardedConnectSession : IK3dSession
{
    void ConnectGuarded(
        ValidatedPlayoutOptions options,
        Func<bool> finalSafetyCheck,
        IK3dSdkDispatchLatch dispatchLatch);
}

internal interface IK3dSdkDispatchLatch
{
    IDisposable? TryClaim();
}

internal interface IK3dSessionFactory
{
    IK3dSession Create();
}

internal sealed class DynamicK3dSessionFactory : IK3dSessionFactory
{
    private readonly ILateBoundComActivator _activator;
    private readonly ILateBoundComMethodInvoker _invoker;
    private readonly IK3dEventHandlerFactory _eventHandlerFactory;
    private readonly bool _rollbackOnPrepareFailure;

    public DynamicK3dSessionFactory()
        : this(
            new InstalledK3dInteropComActivator(),
            new InstalledK3dInteropMethodInvoker(),
            new InstalledK3dEventHandlerFactory(),
            rollbackOnPrepareFailure: true)
    {
    }

    internal DynamicK3dSessionFactory(bool rollbackOnPrepareFailure)
        : this(
            new InstalledK3dInteropComActivator(),
            new InstalledK3dInteropMethodInvoker(),
            new InstalledK3dEventHandlerFactory(),
            rollbackOnPrepareFailure)
    {
    }

    internal DynamicK3dSessionFactory(ILateBoundComActivator activator)
        : this(
            activator,
            new InstalledK3dInteropMethodInvoker(),
            new ActivatorK3dEventHandlerFactory(activator),
            rollbackOnPrepareFailure: true)
    {
    }

    internal DynamicK3dSessionFactory(
        ILateBoundComActivator activator,
        ILateBoundComMethodInvoker invoker)
        : this(
            activator,
            invoker,
            new ActivatorK3dEventHandlerFactory(activator),
            rollbackOnPrepareFailure: true)
    {
    }

    internal DynamicK3dSessionFactory(
        ILateBoundComActivator activator,
        ILateBoundComMethodInvoker invoker,
        IK3dEventHandlerFactory eventHandlerFactory,
        bool rollbackOnPrepareFailure = true)
    {
        _activator = activator;
        _invoker = invoker;
        _eventHandlerFactory = eventHandlerFactory;
        _rollbackOnPrepareFailure = rollbackOnPrepareFailure;
    }

    public IK3dSession Create() => new DynamicK3dSession(
        _activator,
        new RuntimeComObjectReleaser(),
        _invoker,
        _eventHandlerFactory,
        _rollbackOnPrepareFailure);
}

internal interface ILateBoundComActivator
{
    object Create(Guid classId);
}

internal interface ILateBoundComObjectReleaser
{
    void Release(object value);
}

internal interface ILateBoundComMethodInvoker
{
    object? Invoke(object target, string method, params object?[] arguments);
}

internal sealed class RegisteredComActivator : ILateBoundComActivator
{
    public object Create(Guid classId)
    {
        var type = Type.GetTypeFromCLSID(classId, throwOnError: true)
            ?? throw new InvalidOperationException("The registered COM type is unavailable.");
        return Activator.CreateInstance(type)
            ?? throw new InvalidOperationException("The registered COM object could not be created.");
    }
}

internal sealed class RuntimeComObjectReleaser : ILateBoundComObjectReleaser
{
    public void Release(object value)
    {
        if (Marshal.IsComObject(value))
        {
            Marshal.FinalReleaseComObject(value);
        }
    }
}

internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
{
    private readonly ILateBoundComActivator _activator;
    private readonly ILateBoundComObjectReleaser _releaser;
    private readonly ILateBoundComMethodInvoker _invoker;
    private readonly IK3dEventHandlerFactory _eventHandlerFactory;
    private readonly bool _rollbackOnPrepareFailure;
    private readonly K3dEventQueue _eventQueue = new();
    private object? _engine;
    private object? _eventHandler;
    private object? _player;
    private SceneReference? _preparedScene;
    private SceneReference? _currentScene;
    private readonly List<SceneReference> _retiredScenes = [];
    private readonly List<SceneReference> _pendingTakeOutScenes = [];
    private int? _outputChannel;
    private int? _ownerThreadId;
    private int _lastKtapConnectState;
    private int _ktapDispatchGate;
    private bool _disposed;
    private int _pendingPlayCallbacks;
    private int _pendingCutOutCallbacks;
    private int _pendingStopAllCallbacks;

    public DynamicK3dSession(ILateBoundComActivator activator)
        : this(
            activator,
            new RuntimeComObjectReleaser(),
            new InstalledK3dInteropMethodInvoker(),
            new ActivatorK3dEventHandlerFactory(activator),
            rollbackOnPrepareFailure: true)
    {
    }

    internal DynamicK3dSession(
        ILateBoundComActivator activator,
        ILateBoundComObjectReleaser releaser)
        : this(activator, releaser, new InstalledK3dInteropMethodInvoker())
    {
    }

    internal DynamicK3dSession(
        ILateBoundComActivator activator,
        ILateBoundComObjectReleaser releaser,
        ILateBoundComMethodInvoker invoker)
        : this(
            activator,
            releaser,
            invoker,
            new ActivatorK3dEventHandlerFactory(activator))
    {
    }

    internal DynamicK3dSession(
        ILateBoundComActivator activator,
        ILateBoundComObjectReleaser releaser,
        ILateBoundComMethodInvoker invoker,
        IK3dEventHandlerFactory eventHandlerFactory,
        bool rollbackOnPrepareFailure = true)
    {
        _activator = activator;
        _releaser = releaser;
        _invoker = invoker;
        _eventHandlerFactory = eventHandlerFactory;
        _rollbackOnPrepareFailure = rollbackOnPrepareFailure;
    }

    public bool IsConnected => _engine is not null && _player is not null;

    public PlayoutKtapConnectState LastKtapConnectState =>
        (PlayoutKtapConnectState)Volatile.Read(ref _lastKtapConnectState);

    public bool SupportsLifecycleCallbacks => _eventHandlerFactory.SupportsCallbacks;

    public bool? KtapHelloObserved => SupportsLifecycleCallbacks
        ? _eventQueue.HelloObserved
        : null;

    public bool HasPendingLifecycleCallbacks => SupportsLifecycleCallbacks &&
        (_pendingPlayCallbacks != 0 ||
         _pendingCutOutCallbacks != 0 ||
         _pendingStopAllCallbacks != 0 ||
         _eventQueue.PendingCount != 0);

    public bool HasPendingPlayCallbacks =>
        SupportsLifecycleCallbacks && _pendingPlayCallbacks != 0;

    public bool HasPendingTakeOutCallbacks => SupportsLifecycleCallbacks &&
        (_pendingCutOutCallbacks != 0 || _pendingStopAllCallbacks != 0);

    public bool TryPreventKtapConnect()
    {
        var previous = Interlocked.CompareExchange(ref _ktapDispatchGate, 2, 0);
        if (previous == 1)
        {
            Interlocked.CompareExchange(
                ref _lastKtapConnectState,
                (int)PlayoutKtapConnectState.Attempted,
                (int)PlayoutKtapConnectState.NotAttempted);
        }

        return previous == 0;
    }

    public void Connect(ValidatedPlayoutOptions options)
        => ConnectCore(options, finalSafetyCheck: null, disconnectOnFailure: true);

    public void ConnectGuarded(
        ValidatedPlayoutOptions options,
        Func<bool> finalSafetyCheck,
        IK3dSdkDispatchLatch dispatchLatch)
    {
        ArgumentNullException.ThrowIfNull(finalSafetyCheck);
        ArgumentNullException.ThrowIfNull(dispatchLatch);
        ConnectCore(
            options,
            finalSafetyCheck,
            disconnectOnFailure: false,
            dispatchLatch: dispatchLatch);
    }

    private void ConnectCore(
        ValidatedPlayoutOptions options,
        Func<bool>? finalSafetyCheck,
        bool disconnectOnFailure,
        IK3dSdkDispatchLatch? dispatchLatch = null)
    {
        ArgumentNullException.ThrowIfNull(options);
        EnsureThread();
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (IsConnected)
        {
            return;
        }

        var ktapConnectAttempted = false;
        try
        {
            _eventHandler = _eventHandlerFactory.Create(_eventQueue);
            _engine = _activator.Create(K3dComConstants.KaEngineClassGuid);
            if (finalSafetyCheck is not null && !finalSafetyCheck())
            {
                throw new K3dConnectSafetyException();
            }

            object? result;
            using (var dispatchClaim = dispatchLatch?.TryClaim())
            {
                if (dispatchLatch is not null && dispatchClaim is null)
                {
                    throw new K3dConnectSafetyException();
                }

                if (Interlocked.CompareExchange(ref _ktapDispatchGate, 1, 0) != 0)
                {
                    throw new InvalidOperationException(
                        "The K3D connection was abandoned before KTAP dispatch.");
                }

                ktapConnectAttempted = true;
                SetKtapConnectState(PlayoutKtapConnectState.Attempted);
                result = Invoke(
                    _engine,
                    "KTAPConnect",
                    options.TcpMode,
                    options.Host,
                    options.Port,
                    options.ClientPort,
                    _eventHandler);
            }

            var hresult = result is null
                ? 0
                : Convert.ToInt32(result, CultureInfo.InvariantCulture);
            if (hresult != 1)
            {
                SetKtapConnectState(PlayoutKtapConnectState.Failed);
                throw new InvalidOperationException("The K3D connection was rejected.");
            }

            SetKtapConnectState(PlayoutKtapConnectState.AcceptedUnconfirmed);

            var outputChannel = options.OutputChannel;
            if (finalSafetyCheck is not null && !finalSafetyCheck())
            {
                throw new K3dConnectSafetyException();
            }

            using (var dispatchClaim = dispatchLatch?.TryClaim())
            {
                if (dispatchLatch is not null && dispatchClaim is null)
                {
                    throw new K3dConnectSafetyException();
                }

                _player = outputChannel is { } channel
                    ? InvokeRequired(_engine, "GetScenePlayerOnChannel", channel)
                    : InvokeRequired(_engine, "GetScenePlayer");
            }

            _outputChannel = outputChannel;
        }
        catch (Exception exception)
        {
            if (LastKtapConnectState == PlayoutKtapConnectState.Attempted)
            {
                SetKtapConnectState(PlayoutKtapConnectState.Failed);
            }

            var originalFailure = ExceptionDispatchInfo.Capture(exception);
            if (disconnectOnFailure && ktapConnectAttempted && _engine is not null)
            {
                try
                {
                    Invoke(_engine, "Disconnect");
                }
                catch
                {
                    // Preserve the initialization failure while still releasing every RCW.
                }
            }

            ReleaseAll();
            originalFailure.Throw();
            throw;
        }
    }

    private void SetKtapConnectState(PlayoutKtapConnectState state) =>
        Volatile.Write(ref _lastKtapConnectState, (int)state);

    public void Disconnect()
    {
        EnsureThread();
        if (_engine is null)
        {
            ReleaseAll();
            return;
        }

        Exception? failure = null;
        try
        {
            Invoke(_engine, "Disconnect");
        }
        catch (Exception exception)
        {
            failure = exception;
        }
        finally
        {
            ReleaseAll();
        }

        if (failure is not null)
        {
            ExceptionDispatchInfo.Capture(failure).Throw();
        }
    }

    public void Prepare(PlayoutCue cue, int layoutIndex)
    {
        ArgumentNullException.ThrowIfNull(cue);
        EnsureThread();
        EnsureConnected();
        var engine = _engine!;
        var player = _player!;
        var outputChannel = _outputChannel;
        SceneReference? nextScene = null;
        var transactionStarted = false;

        try
        {
            // MainForm refreshes the selected DTO immediately before TAKE IN and
            // Page NEXT can target the alias already on air. Tornado2 rejects a
            // second LoadScene for an alias it still owns, so mutate and re-Prepare
            // the retained loaded scene instead of creating a duplicate alias.
            nextScene =
                ReusableScene(_preparedScene, cue.SceneName) ??
                ReusableScene(_currentScene, cue.SceneName) ??
                new SceneReference(
                    InvokeRequired(engine, "LoadScene", cue.SceneFile, cue.SceneName),
                    cue.SceneName);
            if (outputChannel.HasValue)
            {
                Invoke(nextScene.Value, "SetOutputChannelIndex", outputChannel.GetValueOrDefault());
            }

            Invoke(
                nextScene.Value,
                "SetSceneEffectType",
                K3dComConstants.SceneEffectInEnabled,
                K3dComConstants.SceneChangeEffectFade,
                cue.FadeDuration);

            foreach (var mutation in cue.Mutations ?? [])
            {
                if (IsBeforeTransactionSceneSetupMutation(mutation))
                {
                    ApplySceneSetupMutation(nextScene.Value, mutation);
                }
            }

            Invoke(engine, "BeginTransaction");
            transactionStarted = true;
            foreach (var field in cue.Fields ?? [])
            {
                ApplyField(nextScene.Value, field);
            }

            foreach (var mutation in cue.Mutations ?? [])
            {
                if (!IsBeforeTransactionSceneSetupMutation(mutation))
                {
                    ApplyTransactionMutation(nextScene.Value, mutation);
                }
            }

            Invoke(nextScene.Value, "QueryVariables");

            if (outputChannel.HasValue)
            {
                Invoke(engine, "EndTransactionOnChannel", outputChannel.GetValueOrDefault());
            }
            else
            {
                Invoke(engine, "EndTransaction");
            }

            transactionStarted = false;
            Invoke(player, "Prepare", layoutIndex, nextScene.Value);

            var replacedPreparedScene = _preparedScene;
            _preparedScene = nextScene;
            nextScene = null;
            RetainRetiredScene(replacedPreparedScene);
        }
        catch
        {
            if (transactionStarted && _rollbackOnPrepareFailure)
            {
                try
                {
                    Invoke(engine, "RollbackTransaction");
                }
                catch
                {
                    // Preserve the original SDK failure. The engine will recycle this session.
                }
            }

            throw;
        }
        finally
        {
            RetainRetiredScene(nextScene);
        }
    }

    public void Play(int layoutIndex)
    {
        EnsureThread();
        EnsureConnected();
        InvokeTrackedPlay(layoutIndex);

        var preparedScene = _preparedScene;
        if (preparedScene is not null)
        {
            _preparedScene = null;
            var previousCurrentScene = _currentScene;
            _currentScene = preparedScene;
            if (previousCurrentScene is not null &&
                !ReferenceEquals(previousCurrentScene.Value, preparedScene.Value))
            {
                RetainRetiredScene(previousCurrentScene);
            }
        }

    }

    private void InvokeTrackedPlay(int layoutIndex)
    {
        if (SupportsLifecycleCallbacks)
        {
            _pendingPlayCallbacks++;
        }

        try
        {
            Invoke(_player!, "Play", layoutIndex);
        }
        catch
        {
            if (SupportsLifecycleCallbacks)
            {
                _pendingPlayCallbacks--;
            }

            throw;
        }
    }

    public void UpdateOnAir(PlayoutCue cue, int layoutIndex)
    {
        ArgumentNullException.ThrowIfNull(cue);
        EnsureThread();
        EnsureConnected();
        var engine = _engine!;
        var player = _player!;
        var outputChannel = _outputChannel;
        // Tornado2 can return an incomplete GetPlayingScene proxy. The scene loaded,
        // prepared, and promoted by Play is the only trusted reference for an in-place
        // refresh, and it remains retained until the normal take-out/unload lifecycle.
        if (_preparedScene is not null)
        {
            throw new InvalidOperationException(
                "An on-air update cannot run while another scene is prepared.");
        }

        var currentScene = ReusableScene(_currentScene, cue.SceneName)
            ?? throw new InvalidOperationException(
                "No matching retained on-air scene is available.");
        if (SupportsLifecycleCallbacks && _pendingPlayCallbacks != 0)
        {
            throw new InvalidOperationException(
                "An on-air update cannot run while scene playback is pending.");
        }

        var transactionStarted = false;

        try
        {
            foreach (var mutation in cue.Mutations ?? [])
            {
                if (IsBeforeTransactionSceneSetupMutation(mutation))
                {
                    ApplySceneSetupMutation(currentScene.Value, mutation);
                }
            }

            Invoke(engine, "BeginTransaction");
            transactionStarted = true;

            foreach (var field in cue.Fields ?? [])
            {
                ApplyField(currentScene.Value, field);
            }

            foreach (var mutation in cue.Mutations ?? [])
            {
                // Match Show_PlayList(idx: 1): common background setup runs before the
                // transaction, while builder-owned setup and object changes keep their
                // explicit in-transaction ordering.
                if (!IsBeforeTransactionSceneSetupMutation(mutation))
                {
                    ApplyTransactionMutation(currentScene.Value, mutation);
                }
            }

            Invoke(currentScene.Value, "QueryVariables");
            if (outputChannel.HasValue)
            {
                Invoke(engine, "EndTransactionOnChannel", outputChannel.GetValueOrDefault());
            }
            else
            {
                Invoke(engine, "EndTransaction");
            }

            transactionStarted = false;
            Invoke(player, "Prepare", layoutIndex, currentScene.Value);
            InvokeTrackedPlay(layoutIndex);
        }
        catch
        {
            if (transactionStarted)
            {
                try
                {
                    Invoke(engine, "RollbackTransaction");
                }
                catch
                {
                    // Preserve the original SDK failure. The engine quarantines this session.
                }
            }

            throw;
        }
    }

    public void TakeOut(int layoutIndex, PlayoutTakeOutScope scope)
    {
        EnsureThread();
        EnsureConnected();
        if (SupportsLifecycleCallbacks)
        {
            if (scope == PlayoutTakeOutScope.All)
            {
                _pendingStopAllCallbacks++;
            }
            else
            {
                _pendingCutOutCallbacks++;
            }
        }

        try
        {
            if (scope == PlayoutTakeOutScope.All)
            {
                Invoke(_player!, "StopAll");
            }
            else
            {
                Invoke(_player!, "CutOut", layoutIndex);
            }
        }
        catch
        {
            if (SupportsLifecycleCallbacks)
            {
                if (scope == PlayoutTakeOutScope.All)
                {
                    _pendingStopAllCallbacks--;
                }
                else
                {
                    _pendingCutOutCallbacks--;
                }
            }

            throw;
        }

        if (SupportsLifecycleCallbacks)
        {
            MoveAllScenesToPendingTakeOut();
        }
        else
        {
            var currentScene = _currentScene;
            _currentScene = null;
            if (currentScene is not null)
            {
                RetainRetiredScene(currentScene);
            }
        }

    }

    public K3dCallbackDrainResult ProcessPendingCallbacks(int layoutIndex)
    {
        EnsureThread();
        EnsureConnected();
        if (!SupportsLifecycleCallbacks)
        {
            return new K3dCallbackDrainResult(0, 0, false);
        }

        if (_eventQueue.HasOverflowed)
        {
            throw new InvalidOperationException("The K3D callback queue overflowed.");
        }

        var callbackCount = 0;
        var unloadedSceneCount = 0;
        while (_eventQueue.TryDequeue(out var callback))
        {
            callbackCount++;
            switch (callback.Kind)
            {
                case K3dLifecycleCallbackKind.ScenePlayed:
                    if (_pendingPlayCallbacks == 0 ||
                        !MatchesOutput(callback, layoutIndex))
                    {
                        continue;
                    }

                    _pendingPlayCallbacks--;
                    if (!callback.IsSuccess)
                    {
                        throw new InvalidOperationException("The K3D play callback reported failure.");
                    }

                    unloadedSceneCount += UnloadAndRelease(_retiredScenes);
                    break;
                case K3dLifecycleCallbackKind.CutOut:
                    if (_pendingCutOutCallbacks == 0 ||
                        !MatchesOutput(callback, layoutIndex))
                    {
                        continue;
                    }

                    _pendingCutOutCallbacks--;
                    if (!callback.IsSuccess)
                    {
                        throw new InvalidOperationException("The K3D cut-out callback reported failure.");
                    }

                    // CutOut terminates the selected layer. Any Play completion for that
                    // layer which has not arrived yet can no longer be required for safe
                    // lifetime accounting.
                    _pendingPlayCallbacks = 0;
                    unloadedSceneCount += UnloadAndRelease(_pendingTakeOutScenes);
                    break;
                case K3dLifecycleCallbackKind.StopAll:
                    if (_pendingStopAllCallbacks == 0)
                    {
                        continue;
                    }

                    _pendingStopAllCallbacks--;
                    if (!callback.IsSuccess)
                    {
                        throw new InvalidOperationException("The K3D stop-all callback reported failure.");
                    }

                    // StopAll terminates every layer owned by this player. A stopped Play
                    // is not guaranteed to produce a later OnScenePlayed callback.
                    _pendingPlayCallbacks = 0;
                    unloadedSceneCount += UnloadAndRelease(_pendingTakeOutScenes);
                    break;
                default:
                    throw new InvalidOperationException("The K3D callback is not allowlisted.");
            }
        }

        return new K3dCallbackDrainResult(
            callbackCount,
            unloadedSceneCount,
            _eventQueue.HelloObserved);
    }

    public void Abandon()
    {
        if (_disposed)
        {
            return;
        }

        EnsureThread();
        TryPreventKtapConnect();
        _disposed = true;
        ReleaseAll();
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        EnsureThread();
        _disposed = true;
        try
        {
            if (_engine is not null)
            {
                Invoke(_engine, "Disconnect");
            }
        }
        catch
        {
            // Dispose must still release every RCW on the owning STA.
        }
        finally
        {
            ReleaseAll();
        }
    }

    private void ApplyField(object scene, PlayoutField field)
    {
        if (string.IsNullOrWhiteSpace(field.ObjectName))
        {
            throw new InvalidOperationException("A scene object name is required.");
        }

        var sceneObject = InvokeRequired(scene, "GetObject", field.ObjectName);
        try
        {
            if (field.Value is not null)
            {
                Invoke(sceneObject, "SetValue", field.Value);
            }

            if (field.IsVisible is { } visible)
            {
                Invoke(sceneObject, "SetVisible", visible ? 1 : 0);
            }
        }
        finally
        {
            ReleaseComObject(sceneObject);
        }
    }

    private void ApplySceneSetupMutation(object scene, PlayoutMutation mutation)
    {
        switch (mutation)
        {
            case PlayoutUseBackground background:
                Invoke(scene, "UseBackground", background.IsEnabled ? 1 : 0);
                break;
            case PlayoutSetBackgroundTexture texture:
                Invoke(scene, "SetBackgroundTexture", texture.AssetPath);
                break;
            case PlayoutSetBackgroundVideo video:
                Invoke(
                    scene,
                    "SetBackgroundVideo",
                    video.AssetPath,
                    video.LoopCount,
                    video.LoopInfinite);
                break;
            default:
                throw new InvalidOperationException("The scene setup mutation is not allowlisted.");
        }
    }

    private void ApplyTransactionMutation(object scene, PlayoutMutation mutation)
    {
        if (IsSceneLevelMutation(mutation))
        {
            ApplySceneSetupMutation(scene, mutation);
            return;
        }

        ApplyMutation(scene, mutation);
    }

    private static bool IsSceneLevelMutation(PlayoutMutation mutation) => mutation is
        PlayoutUseBackground or PlayoutSetBackgroundTexture or PlayoutSetBackgroundVideo;

    private static bool IsBeforeTransactionSceneSetupMutation(PlayoutMutation mutation) =>
        mutation switch
        {
            PlayoutUseBackground background =>
                background.Timing == PlayoutMutationTiming.BeforeTransaction,
            PlayoutSetBackgroundTexture texture =>
                texture.Timing == PlayoutMutationTiming.BeforeTransaction,
            PlayoutSetBackgroundVideo video =>
                video.Timing == PlayoutMutationTiming.BeforeTransaction,
            _ => false
        };

    private void ApplyMutation(object scene, PlayoutMutation mutation)
    {
        var objectName = mutation switch
        {
            PlayoutSetValue value => value.ObjectName,
            PlayoutSetAssetValue value => value.ObjectName,
            PlayoutSetVisible visible => visible.ObjectName,
            PlayoutSetFaceColor color => color.ObjectName,
            PlayoutSetPosition position => position.ObjectName,
            PlayoutSetPositionKey positionKey => positionKey.ObjectName,
            PlayoutSetScale scale => scale.ObjectName,
            PlayoutSetCropKey crop => crop.ObjectName,
            PlayoutSetCircleAngleKey angle => angle.ObjectName,
            PlayoutSetPathPoints path => path.ObjectName,
            PlayoutSetPathShapePoints shape => shape.ObjectName,
            _ => throw new InvalidOperationException("The scene mutation is not allowlisted.")
        };

        var sceneObject = InvokeRequired(scene, "GetObject", objectName);
        try
        {
            switch (mutation)
            {
                case PlayoutSetValue value:
                    Invoke(sceneObject, "SetValue", value.Value);
                    break;
                case PlayoutSetAssetValue asset:
                    Invoke(sceneObject, "SetValue", asset.AssetPath);
                    break;
                case PlayoutSetVisible visible:
                    Invoke(sceneObject, "SetVisible", visible.IsVisible ? 1 : 0);
                    break;
                case PlayoutSetFaceColor color:
                    Invoke(
                        sceneObject,
                        "SetFaceColor",
                        color.Red,
                        color.Green,
                        color.Blue,
                        color.Alpha);
                    break;
                case PlayoutSetPosition position:
                    Invoke(
                        sceneObject,
                        "SetPosition",
                        position.X,
                        position.Y,
                        position.Z,
                        (int)position.Components);
                    break;
                case PlayoutSetPositionKey positionKey:
                    Invoke(
                        sceneObject,
                        "SetPositionKey",
                        positionKey.KeyIndex,
                        positionKey.X,
                        positionKey.Y,
                        positionKey.Z,
                        (int)positionKey.Components);
                    break;
                case PlayoutSetScale scale:
                    Invoke(
                        sceneObject,
                        "SetScale",
                        scale.X,
                        scale.Y,
                        scale.Z,
                        (int)scale.Components);
                    break;
                case PlayoutSetCropKey crop:
                    Invoke(
                        sceneObject,
                        "SetCropKey",
                        crop.KeyIndex,
                        crop.Left,
                        crop.Top,
                        crop.Right,
                        crop.Bottom,
                        (int)crop.Edges);
                    break;
                case PlayoutSetCircleAngleKey angle:
                    Invoke(
                        sceneObject,
                        "SetCircleAngleKey",
                        angle.KeyIndex,
                        angle.Start,
                        angle.End,
                        (int)angle.Components);
                    break;
                case PlayoutSetPathPoints path:
                    ReplacePathPoints(sceneObject, path.Points, shape: false);
                    break;
                case PlayoutSetPathShapePoints shape:
                    ReplacePathPoints(sceneObject, shape.Points, shape: true);
                    break;
                default:
                    throw new InvalidOperationException("The scene mutation is not allowlisted.");
            }
        }
        finally
        {
            ReleaseComObject(sceneObject);
        }
    }

    private void ReplacePathPoints(
        object sceneObject,
        IReadOnlyList<PlayoutPoint> points,
        bool shape)
    {
        var beginMethod = shape ? "BeginPathShapePoint" : "BeginPathPoint";
        var clearMethod = shape ? "ClearPathShapePoints" : "ClearPathPoints";
        var addMethod = shape ? "AddPathShapePoint" : "AddPathPoint";
        var endMethod = shape ? "EndPathShapePoint" : "EndPathPoint";

        Invoke(sceneObject, beginMethod);
        Invoke(sceneObject, clearMethod);
        foreach (var point in points)
        {
            Invoke(sceneObject, addMethod, point.X, point.Y, point.Z);
        }

        Invoke(sceneObject, endMethod);
    }

    private void EnsureThread()
    {
        var currentThreadId = Environment.CurrentManagedThreadId;
        if (_ownerThreadId is null)
        {
            if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
            {
                throw new InvalidOperationException("K3D access requires an STA thread.");
            }

            _ownerThreadId = currentThreadId;
            return;
        }

        if (_ownerThreadId.Value != currentThreadId)
        {
            throw new InvalidOperationException("K3D access changed STA threads.");
        }
    }

    private void EnsureConnected()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (!IsConnected)
        {
            throw new InvalidOperationException("The K3D session is not connected.");
        }
    }

    private void ReleaseAll()
    {
        var scenes = new List<SceneReference>();
        AddDistinctScene(scenes, _preparedScene);
        _preparedScene = null;
        AddDistinctScene(scenes, _currentScene);
        _currentScene = null;
        foreach (var retiredScene in _retiredScenes)
        {
            AddDistinctScene(scenes, retiredScene);
        }

        _retiredScenes.Clear();
        foreach (var pendingTakeOutScene in _pendingTakeOutScenes)
        {
            AddDistinctScene(scenes, pendingTakeOutScene);
        }

        _pendingTakeOutScenes.Clear();
        _pendingPlayCallbacks = 0;
        _pendingCutOutCallbacks = 0;
        _pendingStopAllCallbacks = 0;
        var player = _player;
        _player = null;
        _outputChannel = null;
        var engine = _engine;
        _engine = null;
        var eventHandler = _eventHandler;
        _eventHandler = null;

        foreach (var scene in scenes)
        {
            ReleaseComObject(scene.Value);
        }

        ReleaseComObject(player);
        ReleaseComObject(engine);
        ReleaseComObject(eventHandler);
    }

    private void RetainRetiredScene(SceneReference? scene)
    {
        if (scene is not null &&
            !ReferenceEquals(scene.Value, _preparedScene?.Value) &&
            !ReferenceEquals(scene.Value, _currentScene?.Value) &&
            !_retiredScenes.Any(item => ReferenceEquals(item.Value, scene.Value)))
        {
            _retiredScenes.Add(scene);
        }
    }

    private static SceneReference? ReusableScene(
        SceneReference? scene,
        string sceneName) =>
        scene is not null &&
        string.Equals(scene.Name, sceneName, StringComparison.OrdinalIgnoreCase)
            ? scene
            : null;

    private void MoveAllScenesToPendingTakeOut()
    {
        AddDistinctScene(_pendingTakeOutScenes, _preparedScene);
        _preparedScene = null;
        AddDistinctScene(_pendingTakeOutScenes, _currentScene);
        _currentScene = null;
        foreach (var retiredScene in _retiredScenes)
        {
            AddDistinctScene(_pendingTakeOutScenes, retiredScene);
        }

        _retiredScenes.Clear();
    }

    private int UnloadAndRelease(List<SceneReference> scenes)
    {
        var unloaded = 0;
        var activeSceneNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        if (_preparedScene is not null)
        {
            activeSceneNames.Add(_preparedScene.Name);
        }

        if (_currentScene is not null)
        {
            activeSceneNames.Add(_currentScene.Name);
        }

        var unloadedSceneNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        while (scenes.Count > 0)
        {
            var scene = scenes[0];
            var shouldUnload =
                !activeSceneNames.Contains(scene.Name) &&
                unloadedSceneNames.Add(scene.Name);
            if (shouldUnload)
            {
                Invoke(scene.Value, "Unload");
            }

            scenes.RemoveAt(0);
            ReleaseComObject(scene.Value);
            if (shouldUnload)
            {
                unloaded++;
            }
        }

        return unloaded;
    }

    private bool MatchesOutput(K3dLifecycleCallback callback, int layoutIndex)
    {
        var expectedChannel = _outputChannel ?? 0;
        return callback.OutputChannelIndex == expectedChannel &&
            callback.LayerIndex == layoutIndex;
    }

    private static void AddDistinctScene(
        List<SceneReference> scenes,
        SceneReference? scene)
    {
        if (scene is not null &&
            !scenes.Any(item => ReferenceEquals(item.Value, scene.Value)))
        {
            scenes.Add(scene);
        }
    }

    private sealed record SceneReference(object Value, string Name);

    private object InvokeRequired(object target, string method, params object?[] arguments) =>
        Invoke(target, method, arguments)
        ?? throw new InvalidOperationException("The K3D operation returned no object.");

    private object? Invoke(object target, string method, params object?[] arguments) =>
        _invoker.Invoke(target, method, arguments);

    private void ReleaseComObject(object? value)
    {
        if (value is null)
        {
            return;
        }

        try
        {
            _releaser.Release(value);
        }
        catch
        {
            // Cleanup is best-effort and must not hide the SDK operation that failed.
        }
    }
}

internal sealed class K3dConnectSafetyException : Exception;
