/*
 * SmPrinterDevice.java
 *
 * Created on July 31 2007, 16:41
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
/**
 *
 * @author V.Kravtsov
 */
package com.shtrih.fiscalprinter;

import org.apache.log4j.*;
import java.io.IOException;
import java.io.ByteArrayOutputStream;

import com.shtrih.util.*;
import com.shtrih.fiscalprinter.*;
import com.shtrih.fiscalprinter.port.*;
import com.shtrih.fiscalprinter.command.*;

public class SMPrinterDeviceImpl implements SMPrinterDevice {
    // serial port interface

    private PrinterPort printerPort;
    // byte receive timeotu
    private int byteTimeout = 100;
    // constants
    private final static byte STX = 0x02;
    private final static byte ENQ = 0x05;
    private final static byte ACK = 0x06;
    private final static byte NAK = 0x15;
    // maximum counters
    private int maxEnqNumber = 3;
    private int maxNakCommandNumber = 3;
    private int maxNakAnswerNumber = 3;
    private int maxAckNumber = 3;
    private int maxRepeatCount = 3;
    private byte[] txData = new byte[0];
    private byte[] rxData = new byte[0];
    private static Logger logger = Logger.getLogger(SMPrinterDeviceImpl.class);

    public SMPrinterDeviceImpl(PrinterPort printerPort) {
        this.printerPort = printerPort;
    }

    public PrinterPort getPrinterPort() {
        return printerPort;
    }

    public void setPrinterPort(PrinterPort printerPort) {
        this.printerPort = printerPort;
    }

    public int getByteTimeout() {
        return byteTimeout;
    }

    public void setByteTimeout(int value) {
        byteTimeout = value;
    }

    private static byte[] copyOf(byte[] original, int newLength) {
        byte[] copy = new byte[newLength];
        System.arraycopy(original, 0, copy, 0,
                Math.min(original.length, newLength));
        return copy;

    }

    private byte[] readAnswer(int timeout)
            throws Exception {
        int enqNumber = 0;
        for (;;) {
            printerPort.setTimeout(timeout + byteTimeout);
            // STX
            while (printerPort.readByte() != STX) {
            }
            // set byte timeout
            printerPort.setTimeout(byteTimeout);
            // data length
            int dataLength = printerPort.readByte() + 1;
            // command data
            byte[] commandData = printerPort.readBytes(dataLength);
            // check CRC
            byte crc = commandData[commandData.length - 1];
            commandData = copyOf(commandData, commandData.length - 1);
            if (SmPrinterFrame.getCrc(commandData) == crc) {
                printerPort.write(ACK);
                return commandData;
            } else {
                printerPort.write(NAK);
                printerPort.write(ENQ);
                int B = printerPort.readByte();
                switch (B) {
                    case ACK:
                        break;
                    case NAK:
                        return commandData;
                    default:
                        enqNumber++;
                        if (enqNumber >= maxEnqNumber) {
                            throw new IOException(
                                    Localizer.getString(
                                    Localizer.NoConnection));
                        }
                }
            }
        }
    }

    private void writeCommand(byte[] data)
            throws Exception {
        byte nakCommandNumber = 0;

        {
            printerPort.write(data);
            switch (printerPort.readByte()) {
                case ACK:
                    return;
                case NAK: {
                    nakCommandNumber++;
                    break;
                }
                default:
                    return;
            }
            if (nakCommandNumber >= maxNakCommandNumber) {
                throw new IOException(
                        Localizer.getString(
                        Localizer.NoConnection));
            }
        }
    }

    public byte[] send(byte[] data, int timeout)
            throws Exception {
        int ackNumber = 0;
        int enqNumber = 0;

        for (;;) {
            printerPort.setTimeout(byteTimeout);
            printerPort.write(ENQ);

            int B = 0;
            try {
                B = printerPort.readByte();
            } catch (IOException e) {
            }

            switch (B) {
                case ACK: {
                    readAnswer(timeout);
                    ackNumber++;
                    break;
                }
                case NAK: {
                    writeCommand(data);
                    return readAnswer(timeout);
                }
                default: {
                    Thread.sleep(100);
                    enqNumber++;
                }
            }

            if (ackNumber >= maxAckNumber) {
                throw new IOException(
                        Localizer.getString(
                        Localizer.NoConnection));
            }

            if (enqNumber >= maxEnqNumber) {
                throw new IOException(
                        Localizer.getString(
                        Localizer.NoConnection));
            }
        }
    }

    public synchronized byte[] sendCommand(byte[] data, int timeout)
            throws Exception {
        synchronized (printerPort.getConnection()) {
            txData = SmPrinterFrame.encode(data);
            byte[] rx = send(txData, timeout);
            rxData = SmPrinterFrame.encode(rx);
            return rx;
        }
    }

    public synchronized void connect()
            throws Exception {
        synchronized (printerPort.getConnection()) {
            int ackNumber = 0;
            int enqNumber = 0;

            for (;;) {
                printerPort.setTimeout(byteTimeout);
                printerPort.write(ENQ);

                int B = 0;
                try {
                    B = printerPort.readByte();
                } catch (IOException e) {
                }

                switch (B) {
                    case ACK: {
                        readAnswer(0);
                        ackNumber++;
                        break;
                    }
                    case NAK: {
                        return;
                    }

                    default: {
                        Thread.sleep(100);
                        enqNumber++;
                    }
                }

                if (ackNumber >= maxAckNumber) {
                    throw new IOException(
                            Localizer.getString(
                            Localizer.NoConnection));
                }

                if (enqNumber >= maxEnqNumber) {
                    throw new IOException(
                            Localizer.getString(
                            Localizer.NoConnection));
                }
            }
        }
    }

    public synchronized void execute(PrinterCommand command)
            throws Exception {
        int repeatCount = 0;
        while (true) {
            try {
                repeatCount++;
                logger.debug(command.getText());
                byte[] tx = command.encodeData();
                byte[] rx = sendCommand(tx, command.getTimeout());
                command.decodeData(rx);
                if (command.getResultCode() != 0) {
                    String text = PrinterError.getFullText(command.getResultCode());
                    logger.error(text);
                }
                break;
            } catch (IOException e) {
                if (!command.getIsRepeatable()) {
                    throw e;
                }
                if (repeatCount >= maxRepeatCount) {
                    throw e;
                }
            }
        }
    }

    public void setMaxEnqNumber(int value) {
        maxEnqNumber = value;
    }

    public int getMaxEnqNumber() {
        return maxEnqNumber;
    }

    public int getMaxNakCommandNumber() {
        return maxNakCommandNumber;
    }

    public int getMaxNakAnswerNumber() {
        return maxNakAnswerNumber;
    }

    public int getMaxAckNumber() {
        return maxAckNumber;
    }

    public int getMaxRepeatCount() {
        return maxRepeatCount;
    }

    public void setMaxNakCommandNumber(int value) {
        maxNakCommandNumber = value;
    }

    public void setMaxNakAnswerNumber(int value) {
        maxNakAnswerNumber = value;
    }

    public void setMaxAckNumber(int value) {
        maxAckNumber = value;
    }

    public void setMaxRepeatCount(int value) {
        maxRepeatCount = value;
    }

    public byte[] getTxData() {
        return txData;
    }

    public byte[] getRxData() {
        return rxData;
    }
}