    package ru.upos;

//import java.util.logging.Logger;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

import static ru.upos.UposCommand.*;
import static ru.upos.UposRunScreenResult.*;

/**
 * Âûïîëíåíèå êîìàíäû íà òåðìèíàëå UPOS 1.0
 */
public class UposCommandProcess {

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

    private static final String sendHeader = "->";
    private static final String recvHeader = "<-";
    private static final String tlvHeader = "\ttlv";
    private static final String valHeader = "\t\tval";

    private Parameters parameters;
    private MessageId messageId;
    private DeviceInstructionProcess instruction;

    public UposCommandProcess(Parameters parameters) {
        this.parameters = new Parameters(parameters);
    }

    private byte[] createResultMessageLan() throws IOException {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();

        buf.write(instruction.getInstruction().getÑode());
        buf.write(instruction.getDeviceCode().getCode());
        buf.write(0x00);
        switch (instruction.getInstruction()) {
            case Open:
            case Close:
                buf.write(0x00);
                buf.write(0x00);
                break;
            case Read:
                buf.write(instruction.getData().length & 0xFF);
                buf.write((instruction.getData().length >> 8) & 0xFF);
                buf.write(instruction.getData());
                break;
            case Write:
                buf.write(0x02);
                buf.write(0x00);
                buf.write(instruction.getData().length & 0xFF);
                buf.write((instruction.getData().length >> 8) & 0xFF);
                break;
        }
        return buf.toByteArray();
    }

    private byte[] createResultMessagePingInfo() {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();

        buf.write(instruction.getInstruction().getÑode());
        buf.write(instruction.getDeviceCode().getCode());
        buf.write(0x00);
        // TODO:
        return buf.toByteArray();
    }

    private byte[] createResultMessagePrinter() {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();

        buf.write(instruction.getInstruction().getÑode());
        buf.write(instruction.getDeviceCode().getCode());
        buf.write(0x00);
        switch (instruction.getInstruction()) {
            case Open:
            case Write:
            case Close:
                buf.write(0x00);
                buf.write(0x00);
                break;
        }
        return buf.toByteArray();
    }

    private byte[] createMessageBodyRunScreen() throws IOException {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();

        buf.write(Continue.getResultMsb());
        return buf.toByteArray();
    }

    private UposMessage createResultMessage(UposMessage message) throws IOException {
        boolean result = instruction.runInstruction(); // TODO: add result using
        UposMessage messageSend = new UposMessage(Response, message.getMessageId() | 0x80000000);

        switch (instruction.getDeviceCode()) {
        case Lan:
            messageSend.setMessageBody(createResultMessageLan());
            break;
        case PingInfo:
            messageSend.setMessageBody(createResultMessagePingInfo());
            break;
        case Printer:
            messageSend.setMessageBody(createResultMessagePrinter());
            break;
        }
        return messageSend;
    }

    private UposMessage createResultMessageRunScreen(UposMessage message) throws IOException {
        UposMessage messageSend = new UposMessage(Response, message.getMessageId() | 0x80000000);

        messageSend.setMessageBody(createMessageBodyRunScreen());
        return messageSend;
    }

    private UposMessage createMessageEmptyGetReady() {
        UposMessage messageSend = new UposMessage(GetReady, 0);
        byte[] empty = {};

        messageSend.setMessageBody(empty);
        return messageSend;
    }

    private boolean runGetReadyCommand(TerminalConnection connection) throws Exception {
        // command GetReady (0x50)
        UposMessage messageSend = new UposMessage(GetReady, messageId.getMessageId());
        UposMessage messageRecv = new UposMessage(Response, 0);

        connection.setRequest(messageSend.createMessage(parameters));
        //log.info(UposUtils.getDataHex(sendHeader, connection.getRequest()));
        System.out.println(UposUtils.getTimeDataHex(sendHeader, connection.getRequest()));
        if (!connection.sendRequest()) {
            connection.closeTerminal();
            return false;
        }
        if (!connection.receiveResponse()) {
            connection.closeTerminal();
            return false;
        }
        messageRecv.parseMessage(connection.getResponse());
        //log.info(UposUtils.getDataHex(recvHeader, connection.getResponse()) + "\r\n" + UposUtils.getData(recvHeader, messageRecv.getMessageBody()));
        System.out.println(UposUtils.getTimeDataHex(recvHeader, connection.getResponse()));
        System.out.println(UposUtils.getData("\t" + recvHeader, messageRecv.getMessageBody()));
        return messageRecv.getCommand() == Response;
    }

    private boolean isResponseWithTags(byte[] data) {
        return ((data[0] & 0xFF) == 0xFF && (data[1] & 0xFE) == 0xFE);
    }

    private void logResponseTags(byte[] data) throws Exception {
        if (isResponseWithTags(data)) {
            System.out.println("Response format is TLV");
            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            buf.write(data, 2, data.length - 2);
            ArrayList<TLV> tlvList = TLV.parseBinaryBuffer(buf.toByteArray());
            for (TLV tlv: tlvList) {
                System.out.println(UposUtils.getDataHex(tlvHeader, tlv.getFullTLV()));
                System.out.println(String.format("\t\ttag: %X %s", tlv.getTag().getTag(), tlv.getTag().getName()));
                System.out.println(String.format("\t\tlen: %02X", tlv.getLength()));
                System.out.println(UposUtils.getDataHex(valHeader, tlv.getValue()));
            }
        } else {
            System.out.println(String.format("RequestId: 0x%02X%02X%02X%02X", data[3], data[2], data[1], data[0]));
            System.out.println(String.format("Error: 0x%02X%02X", data[5], data[4]));
        }
    }

    private void logMessage(String header, byte[] logData, String commandName) {
        System.out.println(UposUtils.getTimeDataHex(header, logData));
        System.out.print("\t" + header + ": command " + commandName);
        if (!"Response".equals(commandName) || sendHeader.equals(header)) {
            if (instruction.getInstruction() != null) {
                System.out.print(", instruction " + instruction.getInstruction().name());
            }
            if (instruction.getDeviceCode() != null) {
                System.out.print(", device " + instruction.getDeviceCode().name());
            }
        }
        System.out.println();
        if ("Response".equals(commandName)) {
            System.out.println();
        }
    }

    private boolean runTransactionCommand(TerminalConnection connection) throws Exception {
        // command Transaction (0x6D)
        UposMessage messageSend = new UposMessage(Transaction, messageId.getNextMesageId());
        UposMessage messageRecv = new UposMessage(Response, 0);
        UposCommand commandResp;
        boolean isSendEmptyGetReady = false;
        boolean isNeedSendRequest = true;

        connection.setRequest(messageSend.createMessage(parameters));
        do {
            if (isNeedSendRequest) {
                logMessage(sendHeader, connection.getRequest(), messageSend.getCommand().name());
            }
            if (isNeedSendRequest && !connection.sendRequest()) {
                return false;
            }
            isNeedSendRequest = true;
            if (!connection.receiveResponse()) {
                return false;
            }
            messageRecv.parseMessage(connection.getResponse());
            messageRecv.parseMessageBody(instruction);
            logMessage(recvHeader, connection.getResponse(), messageRecv.getCommand().name());
            commandResp = messageRecv.getCommand();
            switch (commandResp) {
            case MasterCall:
                if (instruction.getDeviceCode() == DeviceCode.WaitTimer) {
                    isNeedSendRequest = false;
                } else {
                    messageSend = createResultMessage(messageRecv);
                    connection.setRequest(messageSend.createMessage(parameters));
                }
                break;
            case RunScreen:
                messageSend = createResultMessageRunScreen(messageRecv);
                connection.setRequest(messageSend.createMessage(parameters));
                if (!isSendEmptyGetReady) {
                    //log.info(UposUtils.getDataHex(sendHeader, connection.getRequest()));
                    System.out.println(UposUtils.getTimeDataHex(sendHeader, connection.getRequest()));
                    if (!connection.sendRequest()) {
                        return false;
                    }
                    // wait & send cancel command (command GetReady without data)
                    TimeUnit.MILLISECONDS.sleep(10000);
                    messageSend = createMessageEmptyGetReady();
                    connection.setRequest(messageSend.createMessage(parameters));
                    isSendEmptyGetReady = true;
                }
                break;
            }
        } while (messageRecv.getCommand() != Response);
        // TODO: move tag parsing to another method or class
        logResponseTags(messageRecv.getMessageBody());
        return true;
    }

    private boolean runTransaction() throws Exception {
        messageId = new MessageId(System.currentTimeMillis());
        TerminalConnection connection = new TerminalConnection();
        String portName = "COM" + parameters.getSerialPort();

        if (!connection.initializeTerminal(portName)){
            return false;
        }
        if (!runGetReadyCommand(connection)) {
            connection.closeTerminal();
            return false;
        }
        if (!runTransactionCommand(connection)) {
            connection.closeTerminal();
            return false;
        }
        connection.closeTerminal();
        return true;
    }

    public boolean runCommand() throws Exception {
        instruction = new DeviceInstructionProcess();

        parameters.logParameters();
        switch (parameters.getOperation()) {
        case Purchase:
        case Cash:
        case Refund:
        case Settlement:
        case Reversal:
        case PurchaseWithCashback:
            return runTransaction();
        }
        return false;
    }
}
