package com.binho.cosmicsdk;

import com.sun.jna.Memory;
import com.sun.jna.Pointer;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import static com.binho.cosmicsdk.Types.*;

public final class CosmicSdk implements AutoCloseable {
    public record StatusResult<T>(int status, T value) {
    }

    private Pointer handle;
    private Consumer<Notification> notificationHandler;
    private NativeMethods.NotificationCallback nativeNotificationCallback;

    public CosmicSdk(int timeoutMs) {
        this.handle = NativeMethods.INSTANCE.CosmicSDK_Create(timeoutMs);
        if (this.handle == null || Pointer.nativeValue(this.handle) == 0) {
            throw new IllegalStateException("CosmicSDK_Create returned a null handle.");
        }
    }

    public boolean connect() {
        return nativeBool(NativeMethods.INSTANCE.CosmicSDK_Connect(handle, null));
    }

    public boolean connect(String devicePath) {
        return nativeBool(NativeMethods.INSTANCE.CosmicSDK_Connect(handle, devicePath));
    }

    public boolean connectWithSerialNumber(String serialNumber) {
        if (serialNumber == null || serialNumber.isBlank()) {
            throw new IllegalArgumentException("serialNumber cannot be null or blank.");
        }
        return nativeBool(NativeMethods.INSTANCE.CosmicSDK_ConnectWithSerialNumber(handle, serialNumber));
    }

    public void disconnect() {
        if (handle != null && Pointer.nativeValue(handle) != 0) {
            NativeMethods.INSTANCE.CosmicSDK_Disconnect(handle);
        }
    }

    public StatusResult<DeviceInfo> getDeviceInfo() {
        NativeMethods.NativeDeviceInfo info = new NativeMethods.NativeDeviceInfo();
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_GetDeviceInfo(handle, info));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }

        DeviceInfo value = new DeviceInfo(
                readUtf8(info.manufacturer, info.manufacturerLength),
                readUtf8(info.productName, info.productNameLength),
                readUtf8(info.serialNumber, info.serialNumberLength),
                readUtf8(info.fwVersion, info.fwVersionLength),
                readUtf8(info.hwVersion, info.hwVersionLength)
        );
        return new StatusResult<>(status, value);
    }

    public int enterBootMode() {
        return u16(NativeMethods.INSTANCE.CosmicSDK_EnterBootMode(handle));
    }

    public int i2cSetBusVoltage(int voltageMv, boolean persist) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I2cSetBusVoltage(handle, u16Checked(voltageMv, "voltageMv"), b(persist)));
    }

    public int i2cControllerInit(I2cBus bus, int frequencyHz, I2cPullUp pullUp) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I2cControllerInit(handle, (byte) bus.value(), frequencyHz, (byte) pullUp.value()));
    }

    public int i2cSetParameters(I2cBus bus, int frequencyHz, I2cPullUp pullUp) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I2cSetParameters(handle, (byte) bus.value(), frequencyHz, (byte) pullUp.value()));
    }

    public int i2cSetPullUpResistors(I2cBus bus, I2cPullUp pullUp) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I2cSetPullUpResistors(handle, (byte) bus.value(), (byte) pullUp.value()));
    }

    public int i2cWrite(I2cBus bus, int address, byte[] subAddress, byte[] data, boolean nonStop) {
        Memory subAddressMem = toMemory(subAddress);
        Memory dataMem = toMemory(data);
        int subLen = subAddress == null ? 0 : subAddress.length;
        int dataLen = data == null ? 0 : data.length;
        return u16(NativeMethods.INSTANCE.CosmicSDK_I2cWrite(
                handle,
                (byte) bus.value(),
                u8Checked(address, "address"),
                subAddressMem == null ? Pointer.NULL : subAddressMem,
                u8Checked(subLen, "subAddressLength"),
                dataMem == null ? Pointer.NULL : dataMem,
                dataLen,
                b(nonStop)
        ));
    }

    public StatusResult<byte[]> i2cRead(I2cBus bus, int address, byte[] subAddress, int readLength, boolean nonStop) {
        if (readLength < 0) {
            throw new IllegalArgumentException("readLength must be >= 0.");
        }
        Memory subAddressMem = toMemory(subAddress);
        Memory out = new Memory(readLength);
        int subLen = subAddress == null ? 0 : subAddress.length;
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I2cRead(
                handle,
                (byte) bus.value(),
                u8Checked(address, "address"),
                subAddressMem == null ? Pointer.NULL : subAddressMem,
                u8Checked(subLen, "subAddressLength"),
                u16Checked(readLength, "readLength"),
                out,
                b(nonStop)
        ));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, out.getByteArray(0, readLength));
    }

    public StatusResult<I2cScanResult> i2cScanBus(I2cBus bus, boolean include10BitAddresses) {
        Memory addresses = new Memory(32);
        byte[] count7 = new byte[1];
        byte[] count10 = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I2cScanBus(
                handle,
                addresses,
                (byte) 32,
                count7,
                count10,
                (byte) bus.value(),
                b(include10BitAddresses)
        ));
        if (status != ResultCodes.SUCCESS && status != ResultCodes.FW_I2C_BUS_WITH_NO_TARGETS_CONNECTED) {
            return new StatusResult<>(status, null);
        }

        int count7Bit = u8(count7[0]);
        int count10Bit = u8(count10[0]);
        byte[] raw = addresses.getByteArray(0, 32);
        int[] addresses7Bit = new int[count7Bit];
        int[] addresses10Bit = new int[count10Bit];

        for (int i = 0; i < count7Bit; i++) {
            addresses7Bit[i] = u8(raw[i]);
        }
        for (int i = 0; i < count10Bit; i++) {
            int idx = count7Bit + i * 2;
            if (idx + 1 >= raw.length) {
                break;
            }
            addresses10Bit[i] = u8(raw[idx]) | (u8(raw[idx + 1]) << 8);
        }

        return new StatusResult<>(status, new I2cScanResult(addresses7Bit, addresses10Bit));
    }

    public int i3cSetBusVoltage(int voltageMv) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cSetBusVoltage(handle, u16Checked(voltageMv, "voltageMv")));
    }

    public int i3cControllerInit(I3cPushPullRate pushPullRate, I3cOpenDrainTransferRate openDrainRate, I2cTransferRate i2cRate, I3cDriveStrength driveStrength) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cControllerInit(handle, pushPullRate.value(), openDrainRate.value(), i2cRate.value(), driveStrength.value()));
    }

    public int i3cSetParameters(I3cPushPullRate pushPullRate, I3cOpenDrainTransferRate openDrainRate, I2cTransferRate i2cRate, I3cDriveStrength driveStrength) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cSetParameters(handle, pushPullRate.value(), openDrainRate.value(), i2cRate.value(), driveStrength.value()));
    }

    public int i3cInitBus(List<I3cTargetInfo> targets) {
        if (targets == null || targets.isEmpty()) {
            return u16(NativeMethods.INSTANCE.CosmicSDK_I3cInitBus(handle, null, (byte) 0));
        }

        NativeMethods.NativeI3cTargetInfo[] nativeTargets = (NativeMethods.NativeI3cTargetInfo[]) new NativeMethods.NativeI3cTargetInfo().toArray(targets.size());
        for (int i = 0; i < targets.size(); i++) {
            I3cTargetInfo target = targets.get(i);
            nativeTargets[i].staticAddress = u8Checked(target.staticAddress(), "targets[" + i + "].staticAddress");
            nativeTargets[i].dynamicAddress = u8Checked(target.dynamicAddress(), "targets[" + i + "].dynamicAddress");
            System.arraycopy(target.pid(), 0, nativeTargets[i].pid, 0, 6);
            nativeTargets[i].bcr = target.bcr().raw();
            nativeTargets[i].dcr = u8Checked(target.dcr(), "targets[" + i + "].dcr");
            nativeTargets[i].mwl = u16Checked(target.mwl(), "targets[" + i + "].mwl");
            nativeTargets[i].mrl = u16Checked(target.mrl(), "targets[" + i + "].mrl");
            nativeTargets[i].maxIbiPayloadLength = u8Checked(target.maxIbiPayloadLength(), "targets[" + i + "].maxIbiPayloadLength");
            nativeTargets[i].configuration = target.configuration().toByte();
            nativeTargets[i].write();
        }
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cInitBus(handle, nativeTargets, u8Checked(nativeTargets.length, "targets.length")));
    }

    public int i3cInitBus() {
        return i3cInitBus(null);
    }

    public int i3cResetBus() {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cResetBus(handle));
    }

    public StatusResult<List<I3cTargetInfo>> i3cGetDeviceTable(int maxTargets) {
        if (maxTargets < 0) {
            throw new IllegalArgumentException("maxTargets must be >= 0.");
        }
        NativeMethods.NativeI3cTargetInfo[] nativeTargets = (NativeMethods.NativeI3cTargetInfo[]) new NativeMethods.NativeI3cTargetInfo().toArray(maxTargets);
        byte[] count = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cGetDeviceTable(handle, nativeTargets, u8Checked(maxTargets, "maxTargets"), count));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }

        List<I3cTargetInfo> targets = new ArrayList<>();
        int targetCount = u8(count[0]);
        for (int i = 0; i < targetCount; i++) {
            nativeTargets[i].read();
            targets.add(new I3cTargetInfo(
                    u8(nativeTargets[i].staticAddress),
                    u8(nativeTargets[i].dynamicAddress),
                    Arrays.copyOf(nativeTargets[i].pid, 6),
                    I3cBcr.fromByte(nativeTargets[i].bcr),
                    u8(nativeTargets[i].dcr),
                    u16(nativeTargets[i].mwl),
                    u16(nativeTargets[i].mrl),
                    u8(nativeTargets[i].maxIbiPayloadLength),
                    I3cTargetConfiguration.fromByte(nativeTargets[i].configuration)
            ));
        }
        return new StatusResult<>(status, targets);
    }

    public StatusResult<List<I3cTargetInfo>> i3cGetDeviceTable() {
        return i3cGetDeviceTable(CosmicConstants.I3C_MAX_TARGETS);
    }

    public int i3cSetTargetDeviceConfiguration(int targetAddress, I3cTargetConfiguration configuration) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cSetTargetDeviceConfiguration(handle, u8Checked(targetAddress, "targetAddress"), configuration.toByte()));
    }

    public int i3cLegacyI2cWrite(int address, byte[] subAddress, byte[] data, int flags) {
        Memory subAddressMem = toMemory(subAddress);
        Memory dataMem = toMemory(data);
        int subLen = subAddress == null ? 0 : subAddress.length;
        int dataLen = data == null ? 0 : data.length;
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cLegacyI2CWrite(
                handle,
                u8Checked(address, "address"),
                subAddressMem == null ? Pointer.NULL : subAddressMem,
                u8Checked(subLen, "subAddressLength"),
                dataMem == null ? Pointer.NULL : dataMem,
                u16Checked(dataLen, "dataLength"),
                flags
        ));
    }

    public StatusResult<byte[]> i3cLegacyI2cRead(int address, byte[] subAddress, int readLength, int flags) {
        if (readLength < 0) {
            throw new IllegalArgumentException("readLength must be >= 0.");
        }
        Memory subAddressMem = toMemory(subAddress);
        Memory out = new Memory(readLength);
        int subLen = subAddress == null ? 0 : subAddress.length;
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cLegacyI2CRead(
                handle,
                u8Checked(address, "address"),
                subAddressMem == null ? Pointer.NULL : subAddressMem,
                u8Checked(subLen, "subAddressLength"),
                u16Checked(readLength, "readLength"),
                out,
                flags
        ));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, out.getByteArray(0, readLength));
    }

    public int i3cSdrWrite(int address, byte[] subAddress, byte[] data, int flags) {
        Memory subAddressMem = toMemory(subAddress);
        Memory dataMem = toMemory(data);
        int subLen = subAddress == null ? 0 : subAddress.length;
        int dataLen = data == null ? 0 : data.length;
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cSdrWrite(
                handle,
                u8Checked(address, "address"),
                subAddressMem == null ? Pointer.NULL : subAddressMem,
                u8Checked(subLen, "subAddressLength"),
                dataMem == null ? Pointer.NULL : dataMem,
                u16Checked(dataLen, "dataLength"),
                flags
        ));
    }

    public StatusResult<byte[]> i3cSdrRead(int address, byte[] subAddress, int readLength, int flags) {
        if (readLength < 0) {
            throw new IllegalArgumentException("readLength must be >= 0.");
        }
        Memory subAddressMem = toMemory(subAddress);
        Memory out = new Memory(readLength);
        short[] outLength = new short[1];
        int subLen = subAddress == null ? 0 : subAddress.length;
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cSdrRead(
                handle,
                u8Checked(address, "address"),
                subAddressMem == null ? Pointer.NULL : subAddressMem,
                u8Checked(subLen, "subAddressLength"),
                u16Checked(readLength, "readLength"),
                out,
                outLength,
                flags
        ));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        int length = u16(outLength[0]);
        return new StatusResult<>(status, out.getByteArray(0, length));
    }

    public int i3cHdrDdrWrite(int address, int command, byte[] data) {
        Memory dataMem = toMemory(data);
        int dataLen = data == null ? 0 : data.length;
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cHdrDdrWrite(
                handle,
                u8Checked(address, "address"),
                u8Checked(command, "command"),
                dataMem == null ? Pointer.NULL : dataMem,
                u16Checked(dataLen, "dataLength")
        ));
    }

    public StatusResult<byte[]> i3cHdrDdrRead(int address, int command, int readLength) {
        if (readLength < 0) {
            throw new IllegalArgumentException("readLength must be >= 0.");
        }
        Memory out = new Memory(readLength);
        short[] outLength = new short[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cHdrDdrRead(
                handle,
                u8Checked(address, "address"),
                u8Checked(command, "command"),
                u16Checked(readLength, "readLength"),
                outLength,
                out
        ));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        int length = u16(outLength[0]);
        return new StatusResult<>(status, out.getByteArray(0, length));
    }

    public int i3cTargetResetPattern() {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cTargetResetPattern(handle));
    }

    public int i3cHdrExitPattern() {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cHdrExitPattern(handle));
    }

    public int i3cCccEntdaa() {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccENTDAA(handle));
    }

    public int i3cCccSetdasa(int staticAddress, int dynamicAddress) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccSETDASA(handle, u8Checked(staticAddress, "staticAddress"), u8Checked(dynamicAddress, "dynamicAddress")));
    }

    public int i3cCccSetaasa(byte[] staticAddresses) {
        Memory mem = toMemory(staticAddresses);
        int count = staticAddresses == null ? 0 : staticAddresses.length;
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccSETAASA(handle, mem == null ? Pointer.NULL : mem, new NativeMethods.SizeT(count)));
    }

    public int i3cCccSetnewda(int oldAddress, int newAddress) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccSETNEWDA(handle, u8Checked(oldAddress, "oldAddress"), u8Checked(newAddress, "newAddress")));
    }

    public StatusResult<byte[]> i3cCccGetpid(int address) {
        Memory pid = new Memory(6);
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccGETPID(handle, u8Checked(address, "address"), pid));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, pid.getByteArray(0, 6));
    }

    public StatusResult<Integer> i3cCccGetbcr(int address) {
        byte[] out = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccGETBCR(handle, u8Checked(address, "address"), out));
        return status == ResultCodes.SUCCESS ? new StatusResult<>(status, u8(out[0])) : new StatusResult<>(status, null);
    }

    public StatusResult<Integer> i3cCccGetdcr(int address) {
        byte[] out = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccGETDCR(handle, u8Checked(address, "address"), out));
        return status == ResultCodes.SUCCESS ? new StatusResult<>(status, u8(out[0])) : new StatusResult<>(status, null);
    }

    public StatusResult<byte[]> i3cCccGetstatus(int targetAddress, Integer defByte, int bufferLength) {
        if (bufferLength < 0) {
            throw new IllegalArgumentException("bufferLength must be >= 0.");
        }
        Memory buffer = new Memory(bufferLength);
        byte[] outLen = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccGETSTATUS(
                handle,
                u8Checked(targetAddress, "targetAddress"),
                buffer,
                outLen,
                b(defByte != null),
                defByte == null ? 0 : u8Checked(defByte, "defByte")
        ));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, buffer.getByteArray(0, u8(outLen[0])));
    }

    public StatusResult<byte[]> i3cCccGetstatus(int targetAddress) {
        return i3cCccGetstatus(targetAddress, null, CosmicConstants.I3C_MAX_CCC_BUFFER_LENGTH);
    }

    public StatusResult<byte[]> i3cCccGetcaps(int targetAddress, Integer defByte, int bufferLength) {
        if (bufferLength < 0) {
            throw new IllegalArgumentException("bufferLength must be >= 0.");
        }
        Memory buffer = new Memory(bufferLength);
        byte[] outLen = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccGETCAPS(
                handle,
                u8Checked(targetAddress, "targetAddress"),
                buffer,
                outLen,
                b(defByte != null),
                defByte == null ? 0 : u8Checked(defByte, "defByte")
        ));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, buffer.getByteArray(0, u8(outLen[0])));
    }

    public StatusResult<byte[]> i3cCccGetcaps(int targetAddress) {
        return i3cCccGetcaps(targetAddress, null, CosmicConstants.I3C_MAX_CCC_BUFFER_LENGTH);
    }

    public StatusResult<I3cGetMrlResult> i3cCccGetmrl(int address) {
        short[] mrl = new short[1];
        byte[] ibiLength = new byte[1];
        byte[] ibiReturned = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccGETMRL(handle, u8Checked(address, "address"), mrl, ibiLength, ibiReturned));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, new I3cGetMrlResult(u16(mrl[0]), nativeBool(ibiReturned[0]) ? u8(ibiLength[0]) : null));
    }

    public int i3cCccSetmrlBroadcast(int mrl, Integer ibiLength) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccSETMRLBroadcast(handle, u16Checked(mrl, "mrl"), b(ibiLength != null), ibiLength == null ? 0 : u8Checked(ibiLength, "ibiLength")));
    }

    public int i3cCccSetmrlDirect(int address, int mrl, Integer ibiLength) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccSETMRLDirect(handle, u8Checked(address, "address"), u16Checked(mrl, "mrl"), b(ibiLength != null), ibiLength == null ? 0 : u8Checked(ibiLength, "ibiLength")));
    }

    public StatusResult<Integer> i3cCccGetmwl(int address) {
        short[] value = new short[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccGETMWL(handle, u8Checked(address, "address"), value));
        return status == ResultCodes.SUCCESS ? new StatusResult<>(status, u16(value[0])) : new StatusResult<>(status, null);
    }

    public int i3cCccSetmwlBroadcast(int mwl) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccSETMWLBroadcast(handle, u16Checked(mwl, "mwl")));
    }

    public int i3cCccSetmwlDirect(int address, int mwl) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccSETMWLDirect(handle, u8Checked(address, "address"), u16Checked(mwl, "mwl")));
    }

    public int i3cCccEnecBroadcast(int events) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccENECBroadcast(handle, u8Checked(events, "events")));
    }

    public int i3cCccEnecDirect(int address, int events) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccENECDirect(handle, u8Checked(address, "address"), u8Checked(events, "events")));
    }

    public int i3cCccDisecBroadcast(int events) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccDISECBroadcast(handle, u8Checked(events, "events")));
    }

    public int i3cCccDisecDirect(int address, int events) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccDISECDirect(handle, u8Checked(address, "address"), u8Checked(events, "events")));
    }

    public StatusResult<Integer> i3cCccRstactDirect(int targetAddress, I3cTargetResetDefByte defByte, I3cTransferDirection direction) {
        byte[] out = new byte[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccRSTACTDirect(handle, u8Checked(targetAddress, "targetAddress"), defByte.value(), direction.value(), out));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return direction == I3cTransferDirection.READ ? new StatusResult<>(status, u8(out[0])) : new StatusResult<>(status, null);
    }

    public int i3cCccRstactBroadcast(I3cTargetResetDefByte defByte) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_I3cCccRSTACTBroadcast(handle, defByte.value()));
    }

    public int uartSetBusVoltage(int voltageMv, boolean persist) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_UartSetBusVoltage(handle, u16Checked(voltageMv, "voltageMv"), b(persist)));
    }

    public int uartInit(UartBaudRate baudrate, UartHwHandshake handshake, UartParity parity, UartDataSize dataSize, UartStopBit stopBit) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_UartInit(handle, baudrate.value(), handshake.value(), parity.value(), dataSize.value(), stopBit.value()));
    }

    public int uartSetParameters(UartBaudRate baudrate, UartHwHandshake handshake, UartParity parity, UartDataSize dataSize, UartStopBit stopBit) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_UartSetParameters(handle, baudrate.value(), handshake.value(), parity.value(), dataSize.value(), stopBit.value()));
    }

    public int uartSendMessage(byte[] data) {
        Memory memory = toMemory(data);
        int len = data == null ? 0 : data.length;
        return u16(NativeMethods.INSTANCE.CosmicSDK_UartSendMessage(handle, memory == null ? Pointer.NULL : memory, u16Checked(len, "dataLength")));
    }

    public int gpioSetVoltage(int voltageMv, boolean persist) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_GpioSetVoltage(handle, u16Checked(voltageMv, "voltageMv"), b(persist)));
    }

    public int gpioConfigurePin(GpioPinNumber pinNumber, GpioFunctionality functionality, GpioLogicLevel initialOutputLogicLevel) {
        boolean setInitial = initialOutputLogicLevel != null;
        int level = initialOutputLogicLevel == null ? GpioLogicLevel.GPIO_LOW.value() : initialOutputLogicLevel.value();
        return u16(NativeMethods.INSTANCE.CosmicSDK_GpioConfigurePin(handle, pinNumber.value(), functionality.value(), b(setInitial), level));
    }

    public int gpioDigitalWrite(GpioPinNumber pinNumber, GpioLogicLevel logicLevel) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_GpioDigitalWrite(handle, pinNumber.value(), logicLevel.value()));
    }

    public StatusResult<GpioLogicLevel> gpioDigitalRead(GpioPinNumber pinNumber) {
        int[] out = new int[1];
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_GpioDigitalRead(handle, pinNumber.value(), out));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, GpioLogicLevel.fromValue(out[0]));
    }

    public int gpioConfigureInterrupt(GpioPinNumber pinNumber, GpioTriggerType trigger) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_GpioConfigureInterrupt(handle, pinNumber.value(), trigger.value()));
    }

    public int gpioDisableInterrupt(GpioPinNumber pinNumber) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_GpioDisableInterrupt(handle, pinNumber.value()));
    }

    public int spiSetBusVoltage(int voltageMv, boolean persist) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_SpiSetBusVoltage(handle, u16Checked(voltageMv, "voltageMv"), b(persist)));
    }

    public int spiControllerInit(SpiBitOrder bitOrder, SpiMode mode, SpiDataWidth dataWidth, SpiChipSelect chipSelect, SpiChipSelectPolarity chipSelectPolarity, int frequencyHz) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_SpiControllerInit(handle, bitOrder.value(), mode.value(), dataWidth.value(), chipSelect.value(), chipSelectPolarity.value(), frequencyHz));
    }

    public int spiSetParameters(SpiBitOrder bitOrder, SpiMode mode, SpiDataWidth dataWidth, SpiChipSelect chipSelect, SpiChipSelectPolarity chipSelectPolarity, int frequencyHz) {
        return u16(NativeMethods.INSTANCE.CosmicSDK_SpiSetParameters(handle, bitOrder.value(), mode.value(), dataWidth.value(), chipSelect.value(), chipSelectPolarity.value(), frequencyHz));
    }

    public StatusResult<byte[]> spiTransfer(byte[] data, int transferLength) {
        if (transferLength < 0) {
            throw new IllegalArgumentException("transferLength must be >= 0.");
        }
        Memory in = toMemory(data);
        Memory out = new Memory(transferLength);
        int inLen = data == null ? 0 : data.length;
        int status = u16(NativeMethods.INSTANCE.CosmicSDK_SpiTransfer(
                handle,
                in == null ? Pointer.NULL : in,
                u16Checked(inLen, "inDataLength"),
                out,
                u16Checked(transferLength, "transferLength")
        ));
        if (status != ResultCodes.SUCCESS) {
            return new StatusResult<>(status, null);
        }
        return new StatusResult<>(status, out.getByteArray(0, transferLength));
    }

    public void onNotification(Consumer<Notification> handler) {
        this.notificationHandler = handler;
        this.nativeNotificationCallback = this::dispatchNotification;
        NativeMethods.INSTANCE.CosmicSDK_onNotification(handle, nativeNotificationCallback);
    }

    private void dispatchNotification(Pointer notificationPtr) {
        if (notificationHandler == null || notificationPtr == null || Pointer.nativeValue(notificationPtr) == 0) {
            return;
        }

        NativeMethods.NativeNotification nativeNotification = new NativeMethods.NativeNotification(notificationPtr);
        NotificationType type = NotificationType.fromValue(nativeNotification.type);
        byte[] payload = readBytes(nativeNotification.payload, nativeNotification.payloadLength);
        I3cHotJoinNotification hotJoin = null;
        I3cIbiNotification ibi = null;
        GpioInterruptNotification gpio = null;
        UartRxNotification uart = null;

        if (type == NotificationType.NOTIF_I3C_HJ) {
            NativeMethods.NativeI3cHotJoinNotification parsed = new NativeMethods.NativeI3cHotJoinNotification();
            if (nativeBool(NativeMethods.INSTANCE.CosmicSDK_notificationAsI3cHotJoin(notificationPtr, parsed))) {
                parsed.read();
                hotJoin = new I3cHotJoinNotification(
                        Arrays.copyOf(parsed.pid, parsed.pid.length),
                        u8(parsed.bcr),
                        u8(parsed.dcr),
                        u8(parsed.dynamicAddress)
                );
            }
        } else if (type == NotificationType.NOTIF_I3C_IBI) {
            NativeMethods.NativeI3cIbiNotification parsed = new NativeMethods.NativeI3cIbiNotification();
            if (nativeBool(NativeMethods.INSTANCE.CosmicSDK_notificationAsI3cIbi(notificationPtr, parsed))) {
                parsed.read();
                ibi = new I3cIbiNotification(u8(parsed.address), readBytes(parsed.payload, parsed.payloadLength));
            }
        } else if (type == NotificationType.NOTIF_GPIO) {
            NativeMethods.NativeGpioInterruptNotification parsed = new NativeMethods.NativeGpioInterruptNotification();
            if (nativeBool(NativeMethods.INSTANCE.CosmicSDK_notificationAsGpio(notificationPtr, parsed))) {
                parsed.read();
                gpio = new GpioInterruptNotification(u8(parsed.pinNumber));
            }
        } else if (type == NotificationType.NOTIF_UART) {
            NativeMethods.NativeUartRxNotification parsed = new NativeMethods.NativeUartRxNotification();
            if (nativeBool(NativeMethods.INSTANCE.CosmicSDK_notificationAsUartRx(notificationPtr, parsed))) {
                parsed.read();
                uart = new UartRxNotification(readBytes(parsed.data, parsed.dataLength));
            }
        }

        notificationHandler.accept(new Notification(type, payload, hotJoin, ibi, gpio, uart));
    }

    private static Memory toMemory(byte[] data) {
        if (data == null || data.length == 0) {
            return null;
        }
        Memory memory = new Memory(data.length);
        memory.write(0, data, 0, data.length);
        return memory;
    }

    private static String readUtf8(Pointer pointer, NativeMethods.SizeT length) {
        if (pointer == null || Pointer.nativeValue(pointer) == 0 || length.longValue() <= 0) {
            return "";
        }
        return new String(pointer.getByteArray(0, (int) length.longValue()), StandardCharsets.UTF_8);
    }

    private static byte[] readBytes(Pointer pointer, NativeMethods.SizeT length) {
        if (pointer == null || Pointer.nativeValue(pointer) == 0 || length.longValue() <= 0) {
            return new byte[0];
        }
        return pointer.getByteArray(0, (int) length.longValue());
    }

    private static int u16(short value) {
        return Short.toUnsignedInt(value);
    }

    private static int u8(byte value) {
        return Byte.toUnsignedInt(value);
    }

    private static boolean nativeBool(byte value) {
        return value != 0;
    }

    private static byte b(boolean value) {
        return value ? (byte) 1 : (byte) 0;
    }

    private static byte u8Checked(int value, String name) {
        if (value < 0 || value > 0xFF) {
            throw new IllegalArgumentException(name + " must be in range [0, 255].");
        }
        return (byte) value;
    }

    private static short u16Checked(int value, String name) {
        if (value < 0 || value > 0xFFFF) {
            throw new IllegalArgumentException(name + " must be in range [0, 65535].");
        }
        return (short) value;
    }

    @Override
    public void close() {
        if (handle == null || Pointer.nativeValue(handle) == 0) {
            return;
        }
        NativeMethods.INSTANCE.CosmicSDK_Disconnect(handle);
        NativeMethods.INSTANCE.CosmicSDK_Destroy(handle);
        handle = null;
    }
}
