package ru.upos;

import jssc.SerialPort;
import jssc.SerialPortException;
import jssc.SerialPortTimeoutException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

import static ru.upos.AsciiCharacters.*;
//import java.util.logging.Logger;

/**
 * Обмен пакетами с терминалом UPOS 1.0
 */
public class TerminalConnection {

    //private static Logger log = Logger.getLogger(TerminalConnection.class.getName());

    private static final String sendHeader = ">";
    private static final String recvHeader = "<";

    private final static int maxFrameLen = 180;

    private final static int operationTimeout = 140000;
    private final static int etxTimeout = 1500;
    private final static int repeatTimeout = 2500;

    private byte[] request;
    private byte[] response;
    private SerialPort terminal;

    public void setRequest(byte[] request) {
        this.request = Arrays.copyOf(request, request.length);
    }

    public byte[] getRequest() {
        return request;
    }

    public boolean initializeTerminal(String portName) throws SerialPortException {
        terminal = new SerialPort(portName);
        if (!terminal.openPort()) {
            return false;
        }
        if (!terminal.setParams(SerialPort.BAUDRATE_115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE)) {
            terminal.closePort();
            return false;
        }
        terminal.readBytes();
        terminal.purgePort(SerialPort.PURGE_RXCLEAR);
        terminal.purgePort(SerialPort.PURGE_TXCLEAR);
        terminal.purgePort(SerialPort.PURGE_RXABORT);
        terminal.purgePort(SerialPort.PURGE_TXABORT);
        return true;
    }

    public void closeTerminal() throws SerialPortException {
        terminal.closePort();
    }

    private boolean checkResponseSymbol(byte frameId, boolean isFinal, byte responseSymbol) {
        boolean isOdd = ((frameId % 2) == 1);

        return  (isFinal && responseSymbol == eot.getValue()
                || !isFinal && isOdd && responseSymbol == bel.getValue()
                || !isFinal && !isOdd && responseSymbol == ack.getValue());
    }

    public boolean sendRequest() throws IOException, SerialPortException, SerialPortTimeoutException {
        int blockNum = 1;
        byte frameId;
        int finalBlockLen = request.length % maxFrameLen;
        int currentLen = maxFrameLen;
        boolean isFinalBlock = false;

        if (request.length > maxFrameLen) {
            blockNum += request.length / maxFrameLen;
        }
        for (int i = 0; i < blockNum; ++i) {
            frameId = (byte)(i | 0x80);
            if (i == (blockNum - 1)) {
                currentLen = finalBlockLen;
                frameId = (byte)i;
                isFinalBlock = true;
            }
            byte[] block = new byte[currentLen];
            System.arraycopy(request, i * maxFrameLen, block, 0, currentLen);
            UposMsbFrame uposMsbFrame = new UposMsbFrame(frameId);
            byte[] frame = uposMsbFrame.createFrame(block);
            //log.info(UposUtils.getDataHex(sendHeader, frame));
            //System.out.println(UposUtils.getDataHex(sendHeader, frame));
            if (!terminal.writeBytes(frame)) {
                return false;
            }
            byte[] b = terminal.readBytes(1, 1000);
            //log.info(UposUtils.getDataHex(recvHeader, b));
            //System.out.println(UposUtils.getDataHex(recvHeader, b));
            if (!checkResponseSymbol(frameId, isFinalBlock, b[0])) {
                return false;
            }
        }
        return true;
    }

    private byte receiveStx(boolean isRepeat) throws SerialPortException {
        byte[] block;
        long startTime = System.currentTimeMillis();
        int timeout = operationTimeout;

        if (isRepeat) {
            timeout = repeatTimeout;
        }
        for (;;) {
            block = terminal.readBytes(1);
            if (block.length == 1 && block[0] == stx.getValue()) {
                return block[0];
            }
            if (System.currentTimeMillis() - startTime > timeout) {
                return 0;
            }
        }
    }

    private byte[] receiveEtx(boolean isRepeat) throws IOException, SerialPortException  {
        byte[] block;
        long startTime = System.currentTimeMillis();
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        int timeout = etxTimeout;

        if (isRepeat) {
            timeout = repeatTimeout;
        }
        for (;;) {
            block = terminal.readBytes();
            bytes.write(block);
            if (System.currentTimeMillis() - startTime > timeout) {
                break;
            }
            if (block[block.length - 1] == etx.getValue()) {
                break;
            }
        }
        return bytes.toByteArray();
    }

    private byte getAckSymbol(byte frameId, boolean isFinal) {
        boolean isOdd = (((frameId & 0xFF) % 2) == 1);

        if (isFinal) {
            return eot.getValue();
        }
        if (isOdd) {
            return bel.getValue();
        }
        return ack.getValue();
    }

    public boolean receiveResponse() throws IOException, SerialPortException, SerialPortTimeoutException {
        ByteArrayOutputStream frame = new ByteArrayOutputStream();
        ByteArrayOutputStream message = new ByteArrayOutputStream();
        byte[] block;
        boolean isRepeat = false;

        for (;;) {
            byte firstByte = receiveStx(isRepeat);
            if (firstByte != stx.getValue()) {
                return false;
            }
            frame.reset();
            frame.write(firstByte);
            block = receiveEtx(isRepeat);
            if (block[block.length - 1] != etx.getValue()) {
                terminal.writeByte(nak.getValue());
                isRepeat = true;
                continue;
            }
            frame.write(block);
            //log.info(UposUtils.getDataHex(recvHeader, frame.toByteArray()));
            //System.out.println(UposUtils.getDataHex(recvHeader, frame.toByteArray()));
            UposMsbFrame uposMsbFrame = new UposMsbFrame();
            if (!uposMsbFrame.parseFrame(frame.toByteArray())) {
                return false;
            }
            byte[] b = new byte[]{getAckSymbol(uposMsbFrame.getId(), uposMsbFrame.isFinalFrame())};
            terminal.writeByte(b[0]);
            //log.info(UposUtils.getDataHex(sendHeader, b));
            //System.out.println(UposUtils.getDataHex(sendHeader, b));
            message.write(uposMsbFrame.getBody());
            if (uposMsbFrame.isFinalFrame()) {
                break;
            }
        }
        response = message.toByteArray();
        return true;
    }

    public byte[] getResponse() {
        return response;
    }
}
