using System.Runtime.InteropServices;
using System.Text;

namespace CosmicDriver;

/// <summary>
/// Reference C# integration for the CosmicSDK C API.
/// This is example interop code, not an official C# SDK package.
/// </summary>
public sealed class CosmicSdk : IDisposable
{
    private nint _handle;
    private bool _disposed;
    private Action<Notification>? _notificationHandler;
    private NativeMethods.NotificationCallback? _nativeNotificationCallback;

    public CosmicSdk(int timeoutMs = 3000)
    {
        _handle = NativeMethods.CosmicSDK_Create(timeoutMs);
        if (_handle == nint.Zero)
        {
            throw new InvalidOperationException("CosmicSDK_Create returned a null handle.");
        }
    }

    public bool Connect(string? devicePath = null) => NativeMethods.CosmicSDK_Connect(_handle, devicePath);

    public bool ConnectWithSerialNumber(string serialNumber) => NativeMethods.CosmicSDK_ConnectWithSerialNumber(_handle, serialNumber);

    public void Disconnect() => NativeMethods.CosmicSDK_Disconnect(_handle);

    public (ResultCode Status, DeviceInfo? Info) GetDeviceInfo()
    {
        var native = new NativeDeviceInfo();
        var status = (ResultCode)NativeMethods.CosmicSDK_GetDeviceInfo(_handle, ref native);
        if (status != ResultCode.SUCCESS)
        {
            return (status, null);
        }

        return (status, new DeviceInfo(
            ReadUtf8(native.Manufacturer, native.ManufacturerLength),
            ReadUtf8(native.ProductName, native.ProductNameLength),
            ReadUtf8(native.SerialNumber, native.SerialNumberLength),
            ReadUtf8(native.FwVersion, native.FwVersionLength),
            ReadUtf8(native.HwVersion, native.HwVersionLength)
        ));
    }

    public ResultCode EnterBootMode() => (ResultCode)NativeMethods.CosmicSDK_EnterBootMode(_handle);

    public ResultCode I2cSetBusVoltage(ushort voltageMv, bool persist = false) =>
        (ResultCode)NativeMethods.CosmicSDK_I2cSetBusVoltage(_handle, voltageMv, persist);

    public ResultCode I2cControllerInit(I2cBus bus, uint frequencyHz, I2cPullUp pullUp) =>
        (ResultCode)NativeMethods.CosmicSDK_I2cControllerInit(_handle, (byte)bus, frequencyHz, (byte)pullUp);

    public ResultCode I2cSetParameters(I2cBus bus, uint frequencyHz, I2cPullUp pullUp) =>
        (ResultCode)NativeMethods.CosmicSDK_I2cSetParameters(_handle, (byte)bus, frequencyHz, (byte)pullUp);

    public ResultCode I2cSetPullUpResistors(I2cBus bus, I2cPullUp value) =>
        (ResultCode)NativeMethods.CosmicSDK_I2cSetPullUpResistors(_handle, (byte)bus, (byte)value);

    public ResultCode I2cWrite(I2cBus bus, byte address, byte[]? subAddress, byte[]? data, bool nonStop = false)
    {
        subAddress ??= Array.Empty<byte>();
        data ??= Array.Empty<byte>();
        return (ResultCode)NativeMethods.CosmicSDK_I2cWrite(_handle, (byte)bus, address, subAddress, (byte)subAddress.Length, data, (uint)data.Length, nonStop);
    }

    public (ResultCode Status, byte[]? Data) I2cRead(I2cBus bus, byte address, byte[]? subAddress, ushort readLength, bool nonStop = false)
    {
        subAddress ??= Array.Empty<byte>();
        var outData = new byte[readLength];
        var status = (ResultCode)NativeMethods.CosmicSDK_I2cRead(_handle, (byte)bus, address, subAddress, (byte)subAddress.Length, readLength, outData, nonStop);
        return status == ResultCode.SUCCESS ? (status, outData) : (status, null);
    }

    public (ResultCode Status, I2cScanResult? Result) I2cScanBus(I2cBus bus, bool include10BitAddresses = true)
    {
        var buffer = new byte[32];
        var status = (ResultCode)NativeMethods.CosmicSDK_I2cScanBus(_handle, buffer, (byte)buffer.Length, out var count7Bit, out var count10Bit, (byte)bus, include10BitAddresses);
        if (status != ResultCode.SUCCESS && status != ResultCode.FW_I2C_BUS_WITH_NO_TARGETS_CONNECTED)
        {
            return (status, null);
        }

        var addresses7 = buffer.Take(count7Bit).ToArray();
        var addresses10 = new ushort[count10Bit];
        for (int i = 0; i < count10Bit; i++)
        {
            int idx = count7Bit + (i * 2);
            if (idx + 1 >= buffer.Length)
            {
                break;
            }
            addresses10[i] = (ushort)(buffer[idx] | (buffer[idx + 1] << 8));
        }

        return (status, new I2cScanResult(addresses7, addresses10));
    }

    public ResultCode I3cSetBusVoltage(ushort voltageMv) => (ResultCode)NativeMethods.CosmicSDK_I3cSetBusVoltage(_handle, voltageMv);

    public ResultCode I3cControllerInit(I3cPushPullRate pushPullRate, I3cOpenDrainTransferRate openDrainRate, I2cTransferRate i2cRate, I3cDriveStrength driveStrength) =>
        (ResultCode)NativeMethods.CosmicSDK_I3cControllerInit(_handle, (int)pushPullRate, (int)openDrainRate, (int)i2cRate, (int)driveStrength);

    public ResultCode I3cSetParameters(I3cPushPullRate pushPullRate, I3cOpenDrainTransferRate openDrainRate, I2cTransferRate i2cRate, I3cDriveStrength driveStrength) =>
        (ResultCode)NativeMethods.CosmicSDK_I3cSetParameters(_handle, (int)pushPullRate, (int)openDrainRate, (int)i2cRate, (int)driveStrength);

    public ResultCode I3cInitBus(IReadOnlyList<I3cTargetInfo>? targets = null)
    {
        if (targets is null || targets.Count == 0)
        {
            return (ResultCode)NativeMethods.CosmicSDK_I3cInitBus(_handle, null, 0);
        }

        var native = targets.Select(t => t.ToNative()).ToArray();
        return (ResultCode)NativeMethods.CosmicSDK_I3cInitBus(_handle, native, (byte)native.Length);
    }

    public ResultCode I3cResetBus() => (ResultCode)NativeMethods.CosmicSDK_I3cResetBus(_handle);

    public (ResultCode Status, List<I3cTargetInfo>? Targets) I3cGetDeviceTable(byte maxTargets = CosmicConstants.I3C_MAX_TARGETS)
    {
        var native = Enumerable.Range(0, maxTargets)
            .Select(_ => new NativeI3cTargetInfo { Pid = new byte[6] })
            .ToArray();

        var status = (ResultCode)NativeMethods.CosmicSDK_I3cGetDeviceTable(_handle, native, maxTargets, out byte count);
        if (status != ResultCode.SUCCESS)
        {
            return (status, null);
        }

        var result = new List<I3cTargetInfo>(count);
        for (int i = 0; i < count; i++)
        {
            result.Add(I3cTargetInfo.FromNative(native[i]));
        }
        return (status, result);
    }

    public ResultCode I3cSetTargetDeviceConfiguration(byte targetAddress, I3cTargetConfiguration configuration) =>
        (ResultCode)NativeMethods.CosmicSDK_I3cSetTargetDeviceConfiguration(_handle, targetAddress, configuration.ToByte());

    public ResultCode I3cLegacyI2cWrite(byte address, byte[]? subAddress, byte[]? data, I3cTransferFlags flags = I3cTransferFlags.I3C_XFER_NONE)
    {
        subAddress ??= Array.Empty<byte>();
        data ??= Array.Empty<byte>();
        return (ResultCode)NativeMethods.CosmicSDK_I3cLegacyI2CWrite(_handle, address, subAddress, (byte)subAddress.Length, data, (ushort)data.Length, (int)flags);
    }

    public (ResultCode Status, byte[]? Data) I3cLegacyI2cRead(byte address, byte[]? subAddress, ushort readLength, I3cTransferFlags flags = I3cTransferFlags.I3C_XFER_NONE)
    {
        subAddress ??= Array.Empty<byte>();
        var outData = new byte[readLength];
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cLegacyI2CRead(_handle, address, subAddress, (byte)subAddress.Length, readLength, outData, (int)flags);
        return status == ResultCode.SUCCESS ? (status, outData) : (status, null);
    }

    public ResultCode I3cSdrWrite(byte address, byte[]? subAddress, byte[]? data, I3cTransferFlags flags = I3cTransferFlags.I3C_XFER_NONE)
    {
        subAddress ??= Array.Empty<byte>();
        data ??= Array.Empty<byte>();
        return (ResultCode)NativeMethods.CosmicSDK_I3cSdrWrite(_handle, address, subAddress, (byte)subAddress.Length, data, (ushort)data.Length, (int)flags);
    }

    public (ResultCode Status, byte[]? Data) I3cSdrRead(byte address, byte[]? subAddress, ushort readLength, I3cTransferFlags flags = I3cTransferFlags.I3C_XFER_NONE)
    {
        subAddress ??= Array.Empty<byte>();
        var outData = new byte[readLength];
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cSdrRead(_handle, address, subAddress, (byte)subAddress.Length, readLength, outData, out ushort outLength, (int)flags);
        return status == ResultCode.SUCCESS ? (status, outData.Take(outLength).ToArray()) : (status, null);
    }

    public ResultCode I3cHdrDdrWrite(byte address, byte command, byte[]? data)
    {
        data ??= Array.Empty<byte>();
        return (ResultCode)NativeMethods.CosmicSDK_I3cHdrDdrWrite(_handle, address, command, data, (ushort)data.Length);
    }

    public (ResultCode Status, byte[]? Data) I3cHdrDdrRead(byte address, byte command, ushort readLength)
    {
        var outData = new byte[readLength];
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cHdrDdrRead(_handle, address, command, readLength, out ushort outLength, outData);
        return status == ResultCode.SUCCESS ? (status, outData.Take(outLength).ToArray()) : (status, null);
    }

    public ResultCode I3cTargetResetPattern() => (ResultCode)NativeMethods.CosmicSDK_I3cTargetResetPattern(_handle);
    public ResultCode I3cHdrExitPattern() => (ResultCode)NativeMethods.CosmicSDK_I3cHdrExitPattern(_handle);
    public ResultCode I3cCccRstdaa() => (ResultCode)NativeMethods.CosmicSDK_I3cCccRSTDAA(_handle);
    public ResultCode I3cCccEntdaa() => (ResultCode)NativeMethods.CosmicSDK_I3cCccENTDAA(_handle);
    public ResultCode I3cCccSetdasa(byte staticAddress, byte dynamicAddress) => (ResultCode)NativeMethods.CosmicSDK_I3cCccSETDASA(_handle, staticAddress, dynamicAddress);

    public ResultCode I3cCccSetaasa(byte[] staticAddresses)
    {
        staticAddresses ??= Array.Empty<byte>();
        return (ResultCode)NativeMethods.CosmicSDK_I3cCccSETAASA(_handle, staticAddresses, (nuint)staticAddresses.Length);
    }

    public ResultCode I3cCccSetnewda(byte oldAddress, byte newAddress) => (ResultCode)NativeMethods.CosmicSDK_I3cCccSETNEWDA(_handle, oldAddress, newAddress);

    public (ResultCode Status, byte[]? Pid) I3cCccGetpid(byte address)
    {
        var pid = new byte[6];
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccGETPID(_handle, address, pid);
        return status == ResultCode.SUCCESS ? (status, pid) : (status, null);
    }

    public (ResultCode Status, byte? Value) I3cCccGetbcr(byte address)
    {
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccGETBCR(_handle, address, out byte value);
        return status == ResultCode.SUCCESS ? (status, value) : (status, null);
    }

    public (ResultCode Status, byte? Value) I3cCccGetdcr(byte address)
    {
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccGETDCR(_handle, address, out byte value);
        return status == ResultCode.SUCCESS ? (status, value) : (status, null);
    }

    public (ResultCode Status, byte[]? Data) I3cCccGetstatus(byte targetAddress, byte? defByte = null, int bufferLength = CosmicConstants.I3C_MAX_CCC_BUFFER_LENGTH)
    {
        var buffer = new byte[bufferLength];
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccGETSTATUS(_handle, targetAddress, buffer, out byte outLen, defByte.HasValue, defByte.GetValueOrDefault());
        return status == ResultCode.SUCCESS ? (status, buffer.Take(outLen).ToArray()) : (status, null);
    }

    public (ResultCode Status, byte[]? Data) I3cCccGetcaps(byte targetAddress, byte? defByte = null, int bufferLength = CosmicConstants.I3C_MAX_CCC_BUFFER_LENGTH)
    {
        var buffer = new byte[bufferLength];
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccGETCAPS(_handle, targetAddress, buffer, out byte outLen, defByte.HasValue, defByte.GetValueOrDefault());
        return status == ResultCode.SUCCESS ? (status, buffer.Take(outLen).ToArray()) : (status, null);
    }

    public ResultCode I3cCccRstactBroadcast(I3cTargetResetDefByte defByte) =>
        (ResultCode)NativeMethods.CosmicSDK_I3cCccRSTACTBroadcast(_handle, (int)defByte);

    public (ResultCode Status, byte? Value) I3cCccRstactDirect(byte targetAddress, I3cTargetResetDefByte defByte, I3cTransferDirection direction)
    {
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccRSTACTDirect(_handle, targetAddress, (int)defByte, (int)direction, out byte value);
        if (status != ResultCode.SUCCESS)
        {
            return (status, null);
        }
        return direction == I3cTransferDirection.READ ? (status, value) : (status, null);
    }

    public ResultCode I3cCccEnecBroadcast(I3cEvent events) => (ResultCode)NativeMethods.CosmicSDK_I3cCccENECBroadcast(_handle, (byte)events);
    public ResultCode I3cCccEnecDirect(byte address, I3cEvent events) => (ResultCode)NativeMethods.CosmicSDK_I3cCccENECDirect(_handle, address, (byte)events);
    public ResultCode I3cCccDisecBroadcast(I3cEvent events) => (ResultCode)NativeMethods.CosmicSDK_I3cCccDISECBroadcast(_handle, (byte)events);
    public ResultCode I3cCccDisecDirect(byte address, I3cEvent events) => (ResultCode)NativeMethods.CosmicSDK_I3cCccDISECDirect(_handle, address, (byte)events);

    public (ResultCode Status, ushort? Value) I3cCccGetmwl(byte address)
    {
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccGETMWL(_handle, address, out ushort value);
        return status == ResultCode.SUCCESS ? (status, value) : (status, null);
    }

    public ResultCode I3cCccSetmwlBroadcast(ushort mwl) => (ResultCode)NativeMethods.CosmicSDK_I3cCccSETMWLBroadcast(_handle, mwl);
    public ResultCode I3cCccSetmwlDirect(byte address, ushort mwl) => (ResultCode)NativeMethods.CosmicSDK_I3cCccSETMWLDirect(_handle, address, mwl);

    public (ResultCode Status, I3cGetMrlResult? Result) I3cCccGetmrl(byte address)
    {
        var status = (ResultCode)NativeMethods.CosmicSDK_I3cCccGETMRL(_handle, address, out ushort mrl, out byte ibiLength, out bool ibiLengthReturned);
        if (status != ResultCode.SUCCESS)
        {
            return (status, null);
        }
        return (status, new I3cGetMrlResult(mrl, ibiLengthReturned ? ibiLength : null));
    }

    public ResultCode I3cCccSetmrlBroadcast(ushort mrl, byte? ibiLength = null) =>
        (ResultCode)NativeMethods.CosmicSDK_I3cCccSETMRLBroadcast(_handle, mrl, ibiLength.HasValue, ibiLength.GetValueOrDefault());

    public ResultCode I3cCccSetmrlDirect(byte address, ushort mrl, byte? ibiLength = null) =>
        (ResultCode)NativeMethods.CosmicSDK_I3cCccSETMRLDirect(_handle, address, mrl, ibiLength.HasValue, ibiLength.GetValueOrDefault());

    public ResultCode UartSetBusVoltage(ushort voltageMv, bool persist = false) =>
        (ResultCode)NativeMethods.CosmicSDK_UartSetBusVoltage(_handle, voltageMv, persist);

    public ResultCode UartInit(UartBaudRate baudrate, UartHwHandshake handshake, UartParity parity, UartDataSize dataSize, UartStopBit stopBit) =>
        (ResultCode)NativeMethods.CosmicSDK_UartInit(_handle, (int)baudrate, (int)handshake, (int)parity, (int)dataSize, (int)stopBit);

    public ResultCode UartSetParameters(UartBaudRate baudrate, UartHwHandshake handshake, UartParity parity, UartDataSize dataSize, UartStopBit stopBit) =>
        (ResultCode)NativeMethods.CosmicSDK_UartSetParameters(_handle, (int)baudrate, (int)handshake, (int)parity, (int)dataSize, (int)stopBit);

    public ResultCode UartSendMessage(byte[] data) => (ResultCode)NativeMethods.CosmicSDK_UartSendMessage(_handle, data, (ushort)data.Length);

    public ResultCode GpioSetVoltage(ushort voltageMv, bool persist = false) =>
        (ResultCode)NativeMethods.CosmicSDK_GpioSetVoltage(_handle, voltageMv, persist);

    public ResultCode GpioConfigurePin(GpioPinNumber pinNumber, GpioFunctionality functionality, GpioLogicLevel? initialOutputLogicLevel = null)
    {
        bool setInitial = initialOutputLogicLevel.HasValue;
        var level = initialOutputLogicLevel ?? GpioLogicLevel.GPIO_LOW;
        return (ResultCode)NativeMethods.CosmicSDK_GpioConfigurePin(_handle, (int)pinNumber, (int)functionality, setInitial, (int)level);
    }

    public ResultCode GpioDigitalWrite(GpioPinNumber pinNumber, GpioLogicLevel logicLevel) =>
        (ResultCode)NativeMethods.CosmicSDK_GpioDigitalWrite(_handle, (int)pinNumber, (int)logicLevel);

    public (ResultCode Status, GpioLogicLevel? Value) GpioDigitalRead(GpioPinNumber pinNumber)
    {
        var status = (ResultCode)NativeMethods.CosmicSDK_GpioDigitalRead(_handle, (int)pinNumber, out int logicLevel);
        return status == ResultCode.SUCCESS ? (status, (GpioLogicLevel)logicLevel) : (status, null);
    }

    public ResultCode GpioConfigureInterrupt(GpioPinNumber pinNumber, GpioTriggerType trigger) =>
        (ResultCode)NativeMethods.CosmicSDK_GpioConfigureInterrupt(_handle, (int)pinNumber, (int)trigger);

    public ResultCode GpioDisableInterrupt(GpioPinNumber pinNumber) =>
        (ResultCode)NativeMethods.CosmicSDK_GpioDisableInterrupt(_handle, (int)pinNumber);

    public ResultCode SpiSetBusVoltage(ushort voltageMv, bool persist = false) =>
        (ResultCode)NativeMethods.CosmicSDK_SpiSetBusVoltage(_handle, voltageMv, persist);

    public ResultCode SpiControllerInit(SpiBitOrder bitOrder, SpiMode mode, SpiDataWidth dataWidth, SpiChipSelect chipSelect, SpiChipSelectPolarity chipSelectPolarity, uint frequencyHz) =>
        (ResultCode)NativeMethods.CosmicSDK_SpiControllerInit(_handle, (int)bitOrder, (int)mode, (int)dataWidth, (int)chipSelect, (int)chipSelectPolarity, frequencyHz);

    public ResultCode SpiSetParameters(SpiBitOrder bitOrder, SpiMode mode, SpiDataWidth dataWidth, SpiChipSelect chipSelect, SpiChipSelectPolarity chipSelectPolarity, uint frequencyHz) =>
        (ResultCode)NativeMethods.CosmicSDK_SpiSetParameters(_handle, (int)bitOrder, (int)mode, (int)dataWidth, (int)chipSelect, (int)chipSelectPolarity, frequencyHz);

    public (ResultCode Status, byte[]? Data) SpiTransfer(byte[]? data, ushort transferLength)
    {
        data ??= Array.Empty<byte>();
        var outData = new byte[transferLength];
        var status = (ResultCode)NativeMethods.CosmicSDK_SpiTransfer(_handle, data, (ushort)data.Length, outData, transferLength);
        return status == ResultCode.SUCCESS ? (status, outData) : (status, null);
    }

    public void OnNotification(Action<Notification> handler)
    {
        _notificationHandler = handler;
        _nativeNotificationCallback = NativeNotificationDispatcher;
        NativeMethods.CosmicSDK_onNotification(_handle, _nativeNotificationCallback);
    }

    private void NativeNotificationDispatcher(nint notificationPtr)
    {
        if (_notificationHandler is null || notificationPtr == nint.Zero)
        {
            return;
        }

        var native = Marshal.PtrToStructure<NativeNotification>(notificationPtr);
        var notificationType = Enum.IsDefined(typeof(NotificationType), native.Type)
            ? (NotificationType)native.Type
            : NotificationType.NOTIF_UNKNOWN;

        var payload = ReadBytes(native.Payload, native.PayloadLength);
        I3cHotJoinNotification? hotJoin = null;
        I3cIbiNotification? ibi = null;
        GpioInterruptNotification? gpio = null;
        UartRxNotification? uart = null;

        if (notificationType == NotificationType.NOTIF_I3C_HJ)
        {
            var parsed = new NativeI3cHotJoinNotification { Pid = new byte[6] };
            if (NativeMethods.CosmicSDK_notificationAsI3cHotJoin(notificationPtr, ref parsed))
            {
                hotJoin = new I3cHotJoinNotification(parsed.Pid.ToArray(), parsed.Bcr, parsed.Dcr, parsed.DynamicAddress);
            }
        }
        else if (notificationType == NotificationType.NOTIF_I3C_IBI)
        {
            var parsed = new NativeI3cIbiNotification();
            if (NativeMethods.CosmicSDK_notificationAsI3cIbi(notificationPtr, ref parsed))
            {
                ibi = new I3cIbiNotification(parsed.Address, ReadBytes(parsed.Payload, parsed.PayloadLength));
            }
        }
        else if (notificationType == NotificationType.NOTIF_GPIO)
        {
            var parsed = new NativeGpioInterruptNotification();
            if (NativeMethods.CosmicSDK_notificationAsGpio(notificationPtr, ref parsed))
            {
                gpio = new GpioInterruptNotification(parsed.PinNumber);
            }
        }
        else if (notificationType == NotificationType.NOTIF_UART)
        {
            var parsed = new NativeUartRxNotification();
            if (NativeMethods.CosmicSDK_notificationAsUartRx(notificationPtr, ref parsed))
            {
                uart = new UartRxNotification(ReadBytes(parsed.Data, parsed.DataLength));
            }
        }

        _notificationHandler(new Notification(notificationType, payload, hotJoin, ibi, gpio, uart));
    }

    private static byte[] ReadBytes(nint pointer, nuint length)
    {
        if (pointer == nint.Zero || length == 0)
        {
            return Array.Empty<byte>();
        }

        var bytes = new byte[(int)length];
        Marshal.Copy(pointer, bytes, 0, bytes.Length);
        return bytes;
    }

    private static string ReadUtf8(nint pointer, nuint length)
    {
        if (pointer == nint.Zero || length == 0)
        {
            return string.Empty;
        }

        return Encoding.UTF8.GetString(ReadBytes(pointer, length));
    }

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

        if (_handle != nint.Zero)
        {
            NativeMethods.CosmicSDK_Disconnect(_handle);
            NativeMethods.CosmicSDK_Destroy(_handle);
            _handle = nint.Zero;
        }

        _disposed = true;
        GC.SuppressFinalize(this);
    }

    ~CosmicSdk()
    {
        Dispose();
    }
}
