package me.koba1.led.device;

import com.fazecast.jSerialComm.SerialPort;
import com.fazecast.jSerialComm.SerialPortDataListener;
import com.fazecast.jSerialComm.SerialPortEvent;
import lombok.Getter;
import me.koba1.led.Main;
import me.koba1.led.device.packets.Packet;
import me.koba1.led.device.packets.TestConnectionPacket;
import me.koba1.led.device.packets.UpdateLedStripsPacket;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.*;

@Getter
public class DeviceManager {
    public class SerialPortListener implements SerialPortDataListener {
        @Override
        public int getListeningEvents() {
            return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
        }

        @Override
        public void serialEvent(SerialPortEvent event) {
            if ((event.getEventType() & SerialPort.LISTENING_EVENT_DATA_AVAILABLE) > 0) {
                if (serialPort.bytesAvailable() < 4) return;

                byte[] buffer = new byte[4];
                serialPort.readBytes(buffer, buffer.length);
                ByteBuffer bb = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
                short magic = bb.getShort();

                if (magic != Packet.MAGIC) return;

                short length = bb.getShort();
                buffer = new byte[length];

                serialPort.readBytes(buffer, buffer.length);
                bb = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);

                byte packetId = bb.get();
                byte data = bb.get();
                if (packetId == Packet.PacketType.ERROR.getId()) {
                    Main.getLogger().error("Device (" + serialPort.getSystemPortName() + ") responded with error: " + data);
                } else if (packetId != Packet.PacketType.READY.getId()) {
                    Main.getLogger().error("Device (" + serialPort.getSystemPortName() + ") responded with unknown packet ID: " + packetId);
                }

                if (deviceBusy.availablePermits() == 0)
                    deviceBusy.release();
            }
        }
    }

    private final ExecutorService senderExecutor = Executors.newSingleThreadExecutor();

    private boolean connected;
    private final Semaphore deviceBusy;
    private SerialPort serialPort;
    private final BlockingQueue<Packet> packetQueue;

    public DeviceManager() {
        this.serialPort = null;
        this.connected = false;
        this.deviceBusy = new Semaphore(1, true);
        this.serialPort = findDevice();
        this.connected = serialPort != null;
        this.packetQueue = new LinkedBlockingQueue<>();

        if (this.connected) {
            if (openPort(this.serialPort)) {
                this.serialPort.addDataListener(new SerialPortListener());
                System.out.println("Device communication successfully started");
                startPacketLoop();
            } else {
                Main.getLogger().error("Device (" + this.serialPort.getSystemPortName() + ") can't connected");
                //System.out.println("Device (" + this.serialPort.getSystemPortName() + ") can't connected");
            }
        } else {
            Main.getLogger().error("Can't connect to device.");
        }
    }

    public boolean reconnect() {
        if(this.connected) return true;

        this.serialPort = findDevice();
        this.connected = serialPort != null;
        this.deviceBusy.release();
        if (this.connected) {
            if (openPort(this.serialPort)) {
                this.serialPort.addDataListener(new SerialPortListener());
                System.out.println("Device communication successfully started");
                startPacketLoop();
                return true;
            } else {
                //Main.getLogger().error("Device (" + this.serialPort.getSystemPortName() + ") can't connected");
                //System.out.println("Device (" + this.serialPort.getSystemPortName() + ") can't connected");
            }
        } else {
            Main.getLogger().error("Can't connect to device.");
        }

        return false;
    }

    public SerialPort findDevice() {
        for (SerialPort port : SerialPort.getCommPorts()) {

            if (isBluetoothPort(port)) continue;
            if (!openPort(port)) continue;
            try {
                if (testConnection(port)) {
                    System.out.println("Device (" + port.getSystemPortName() + ") connected");
                    return port; // sikeres eszköz
                }
            } catch (Exception e) {
                System.out.println("Device (" + port.getSystemPortName() + ") did not respond");
                e.printStackTrace();
            } finally {
                port.closePort();
            }
        }
        return null;
    }

    public void sendPacket(Packet packet) {
        if (!this.connected) return;
        if (!this.packetQueue.isEmpty() && !(packet instanceof UpdateLedStripsPacket)) return;
        this.packetQueue.offer(packet);
    }

    public void startPacketLoop() {
        UpdateLedStripsPacket stripPacket = new UpdateLedStripsPacket();
        sendPacket(stripPacket);

        senderExecutor.submit(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Packet packet = packetQueue.take(); // ⬅ BLOCKOL, nem pörget
                    deviceBusy.acquire();
                    if (this.serialPort == null) {
                        deviceBusy.release();
                        Main.getLogger().error("Device disconnected runtime.");
                        Thread.currentThread().interrupt();
                        connected = false;
                        break;
                    }
                    byte[] data = packet.toBytes();
                    //System.out.println(java.util.HexFormat.of().formatHex(data));

                    serialPort.writeBytes(data, data.length);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private boolean openPort(SerialPort port) {
        if (!port.openPort()) return false;
        port.setComPortParameters(115200, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY);
        //port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 1000, 0);
        port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 250, 0);
        return true;
    }

    private boolean isBluetoothPort(SerialPort p) {
        String desc = String.valueOf(p.getPortDescription()).toLowerCase();
        String dname = String.valueOf(p.getDescriptivePortName()).toLowerCase();
        return desc.contains("bluetooth") || dname.contains("bluetooth");
    }

    private boolean testConnection(SerialPort port) {
        byte[] writeBuffer = new TestConnectionPacket().toBytes();
        port.writeBytes(writeBuffer, writeBuffer.length);
        byte[] buffer = new byte[4];
        port.readBytes(buffer, buffer.length);
        ByteBuffer bb = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
        // byte[] all = bb.array(); // wrap() esetén ez működik
        // System.out.println(java.util.HexFormat.of().formatHex(all));
        short magic = bb.getShort();

        if (magic != (short) 0xABCD) return false;

        short length = bb.getShort();
        buffer = new byte[length];
        port.readBytes(buffer, buffer.length);

        bb = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
        byte packetId = bb.get();
        byte data = bb.get();

        if (packetId != Packet.PacketType.READY.getId() || data != (byte) 1) {
            System.out.println("Device (" + port.getSystemPortName() + ") did not respond correctly");
            return false;
        }
        return true;
    }
}
