using Brio.API;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Text.Json.Nodes;

namespace LightlessSync.Interop.Ipc;

public sealed class IpcCallerBrio : IpcServiceBase
{
    private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0));

    private readonly ILogger<IpcCallerBrio> _logger;
    private readonly DalamudUtilService _dalamudUtilService;

    private readonly ApiVersion _apiVersion;

    private readonly SpawnActor _spawnActor;
    private readonly DespawnActor _despawnActor;
    private readonly SetModelTransform _setModelTransform;
    private readonly GetModelTransform _getModelTransform;

    private readonly GetPoseAsJson _getPoseAsJson;
    private readonly LoadPoseFromJson _setPoseFromJson;

    private readonly FreezeActor _freezeActor;
    private readonly FreezePhysics _freezePhysics;

    public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface,
        DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator, dalamudPluginInterface, BrioDescriptor)
    {
        _logger = logger;
        _dalamudUtilService = dalamudUtilService;

        _apiVersion = new ApiVersion(dalamudPluginInterface);
        _spawnActor = new SpawnActor(dalamudPluginInterface);
        _despawnActor = new DespawnActor(dalamudPluginInterface);

        _setModelTransform = new SetModelTransform(dalamudPluginInterface);
        _getModelTransform = new GetModelTransform(dalamudPluginInterface);

        _getPoseAsJson = new GetPoseAsJson(dalamudPluginInterface);
        _setPoseFromJson = new LoadPoseFromJson(dalamudPluginInterface);

        _freezeActor = new FreezeActor(dalamudPluginInterface);
        _freezePhysics = new FreezePhysics(dalamudPluginInterface);

        CheckAPI();
    }

    public async Task<IGameObject?> SpawnActorAsync()
    {
        if (!APIAvailable) return null;
        _logger.LogDebug("Spawning Brio Actor");
        return await _dalamudUtilService.RunOnFrameworkThread(() => _spawnActor.Invoke(Brio.API.Enums.SpawnFlags.Default, true)).ConfigureAwait(false);
    }

    public async Task<bool> DespawnActorAsync(nint address)
    {
        if (!APIAvailable) return false;
        var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
        if (gameObject == null) return false;
        _logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
        return await _dalamudUtilService.RunOnFrameworkThread(() => _despawnActor.Invoke(gameObject)).ConfigureAwait(false);
    }

    public async Task<bool> ApplyTransformAsync(nint address, WorldData data)
    {
        if (!APIAvailable) return false;
        var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
        if (gameObject == null) return false;
        _logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);

        return await _dalamudUtilService.RunOnFrameworkThread(() => _setModelTransform.Invoke(gameObject,
            new Vector3(data.PositionX, data.PositionY, data.PositionZ),
            new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
            new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false);
    }

    public async Task<WorldData> GetTransformAsync(nint address)
    {
        if (!APIAvailable) return default;
        var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
        if (gameObject == null) return default;
        var data = await _dalamudUtilService.RunOnFrameworkThread(() => _getModelTransform.Invoke(gameObject)).ConfigureAwait(false);

        return new WorldData()
        {
            PositionX = data.Item1.Value.X,
            PositionY = data.Item1.Value.Y,
            PositionZ = data.Item1.Value.Z,
            RotationX = data.Item2.Value.X,
            RotationY = data.Item2.Value.Y,
            RotationZ = data.Item2.Value.Z,
            RotationW = data.Item2.Value.W,
            ScaleX = data.Item3.Value.X,
            ScaleY = data.Item3.Value.Y,
            ScaleZ = data.Item3.Value.Z
        };
    }

    public async Task<string?> GetPoseAsync(nint address)
    {
        if (!APIAvailable) return null;
        var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
        if (gameObject == null) return null;
        _logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue);

        return await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
    }

    public async Task<bool> SetPoseAsync(nint address, string pose)
    {
        if (!APIAvailable) return false;
        var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
        if (gameObject == null) return false;
        _logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);

        var applicablePose = JsonNode.Parse(pose)!;
        var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
        applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString());

        await _dalamudUtilService.RunOnFrameworkThread(() =>
        {
            _freezeActor.Invoke(gameObject);
            _freezePhysics.Invoke();
        }).ConfigureAwait(false);
        return await _dalamudUtilService.RunOnFrameworkThread(() => _setPoseFromJson.Invoke(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
    }

    protected override IpcConnectionState EvaluateState()
    {
        var state = base.EvaluateState();
        if (state != IpcConnectionState.Available)
        {
            return state;
        }

        try
        {
            var version = _apiVersion.Invoke();
            return version.Breaking == 3 && version.Feature >= 0
                ? IpcConnectionState.Available
                : IpcConnectionState.VersionMismatch;
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Failed to query Brio IPC version");
            return IpcConnectionState.Error;
        }
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
    }
}
