/////////////////////////////////////////////////////////////////////
//
// BaseJposService.java - Abstract base class for all JavaPOS services.
//
// Modification history
// ------------------------------------------------------------------
// 2007-07-24 JavaPOS Release 1.0                                  VK
//
/////////////////////////////////////////////////////////////////////
package com.shtrih.jpos.fiscalprinter;

import java.io.*;
import java.util.*;
import java.text.*;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.lang.Math;
import gnu.io.*;
// DOM
import org.w3c.dom.*;
import org.xml.sax.*;
// apache
import org.apache.log4j.*;
import org.xml.sax.InputSource;
import org.apache.xml.serialize.*;
import org.apache.xerces.dom.DocumentImpl;
import org.apache.xerces.parsers.DOMParser;

import jpos.*;
import jpos.events.*;
import jpos.config.*;
import jpos.services.*;
import jpos.config.simple.*;
import jpos.config.simple.xml.*;

import com.shtrih.ej.*;
import com.shtrih.util.*;
import com.shtrih.jpos.*;
import com.shtrih.barcode.*;
import com.shtrih.jpos.events.*;
import com.shtrih.fiscalprinter.*;
import com.shtrih.printer.ncr7167.*;
import com.shtrih.fiscalprinter.port.*;
import com.shtrih.fiscalprinter.model.*;
import com.shtrih.fiscalprinter.table.*;
import com.shtrih.fiscalprinter.command.*;
import com.shtrih.fiscalprinter.receipt.*;
import com.shtrih.jpos.fiscalprinter.receipt.*;
import com.shtrih.jpos.fiscalprinter.request.*;

public class FiscalPrinterImpl extends DeviceService
        implements PrinterConst, JposConst, JposEntryConst,
        FiscalPrinterConst, SmFptrConst, IPrinterEvents {

    private JposEntry entry;
    private static final int MaxStateCount = 3;
    private final FiscalPrinterFilters filters = new FiscalPrinterFilters();
    private Thread asyncThread = null;
    private Thread deviceThread = null;
    private Thread eventThread = null;
    private static Logger logger = Logger.getLogger(FiscalPrinterImpl.class);
    private boolean deviceEnabled = false;
    private int statusUpdateInterval = 500;
    private EventCallbacks cb = null;
    private final Vector events = new Vector();
    private int doubleWidthFont = 1;
    private final ConnectParams connectParams = new ConnectParams();
    private final FiscalDay fiscalDay = new FiscalDay();
    private final Vector requests = new Vector();
    private final Vector paymentNames = new Vector();
    private final ReceiptImages receiptImages = new ReceiptImages();
    private PrinterHeader header;
    private PrinterHeader trailer;
    private int[] vatValues = new int[4];
    private final PayTypes payTypes = new PayTypes();
    private PrinterPort port;
    private SMPrinterDeviceImpl device;
    public SMFiscalPrinter printer;
    private PrinterData params = new PrinterData();
    private final FiscalPrinterParams printerParams = new FiscalPrinterParams();
    private ReceiptPrinter receiptPrinter;
    private final FiscalPrinterStatistics statistics = new FiscalPrinterStatistics();
    //--------------------------------------------------------------------------
    // Variables
    //--------------------------------------------------------------------------
    // Instance Data Set in Derived Class
    public boolean claimed = false;
    public String checkHealthText = "";
    private String physicalDeviceDescription = "";
    private String deviceServiceDescription = "";
    private int deviceServiceVersion;
    private String physicalcription = "";
    private String physicalDeviceName = "";
    private int state;
    // Capabilities
    private boolean capAdditionalLines;
    private boolean capAmountAdjustment;
    private boolean capAmountNotPaid;
    private boolean capCheckTotal;
    private boolean capFixedOutput;
    private boolean capIndependentHeader;
    private boolean capItemList;
    private boolean capNonFiscalMode;
    private boolean capOrderAdjustmentFirst;
    private boolean capPercentAdjustment;
    private boolean capPositiveAdjustment;
    private boolean capPowerLossReport;
    private boolean capPredefinedPaymentLines;
    private boolean capReceiptNotPaid;
    private boolean capRemainingFiscalMemory;
    private boolean capReservedWord;
    private boolean capSetHeader;
    private boolean capSetPOSID;
    private boolean capSetStoreFiscalID;
    private boolean capSetTrailer;
    private boolean capSetVatTable;
    private boolean capSlpFiscalDocument;
    private boolean capSlpFullSlip;
    private boolean capSlpValidation;
    private boolean capSubAmountAdjustment;
    private boolean capSubPercentAdjustment;
    private boolean capSubtotal;
    private boolean capTrainingMode;
    private boolean capValidateJournal;
    private boolean capXReport;
    // 1.6
    public boolean capAdditionalHeader;
    public boolean capAdditionalTrailer;
    public boolean capChangeDue;
    private boolean capEmptyReceiptIsVoidable;
    private boolean capFiscalReceiptStation;
    private boolean capFiscalSlipStation;
    private boolean capFiscalReceiptType;
    private boolean capMultiContractor;
    private boolean capOnlyVoidLastItem;
    private boolean capPackageAdjustment;
    private boolean capPostPreLine;
    private boolean capSetCurrency;
    private boolean capTotalizerType;
    // 1.8
    private boolean capStatisticsReporting;
    private boolean capUpdateStatistics;
    // 1.9
    private boolean capCompareFirmwareVersion;
    private boolean capUpdateFirmware;
    // 1.11
    private boolean capPositiveSubtotalAdjustment;
    // State
    private boolean duplicateReceipt;
    private int outputID;
    private int powerNotify = JPOS_PN_DISABLED;
    private int powerState;
    private int countryCode;
    private int errorLevel;
    private int errorOutID;
    private int errorState;
    private int errorStation;
    private String errorString = "";
    private boolean flagWhenIdle;
    private String predefinedPaymentLines = "";
    public int printerState;
    private int quantityDecimalPlaces;
    private int quantityLength;
    private boolean recEmpty;
    private boolean recNearEnd;
    private boolean jrnEmpty;
    private boolean jrnNearEnd;
    private boolean slpEmpty;
    private boolean slpNearEnd;
    private int slipSelection;
    private boolean trainingModeActive;
    private boolean coverOpened = false;
    private boolean dayOpened = false;
    private int tableMode = SMFPTR_TABLE_MODE_AUTO;
    private int cutMode = SMFPTR_CUT_MODE_AUTO;
    private String reservedWord = "";
    // 1.6
    private int actualCurrency;
    private String additionalHeader = "";
    private String additionalTrailer = "";
    private String changeDue = "";
    private int contractorId;
    private int dateType;
    private int fiscalReceiptStation;
    private int fiscalReceiptType;
    private int messageType;
    private int totalizerType;
    private double amountFactor = 1;
    private double quantityFactor = 1;
    public boolean centerImage = true; // center BMP image
    public int graphicsLine = 1;
    private int pollInterval = 500;
    private boolean pollEnabled = true;
    private String statisticFileName = "ShtrihFiscalPrinter.xml";
    private boolean isAutomaticZReport = false;
    private boolean isReceiptOpened = false;
    private int receiptType = 0;
    private int reportDevice = SMFPTR_REPORT_DEVICE_EJ;
    private int reportType = SMFP_REPORT_TYPE_FULL;
    private String logicalName = "";
    private String fieldsFileName = "";
    private boolean asyncMode = false;
    private FlexCommands commands = null;
    private int numHeaderLines;
    private int numTrailerLines;
    private int recCloseWaitCount = 1;
    private int cutType = PrinterConst.SMFP_CUT_PARTIAL;
    private DeviceMetrics deviceMetrics = null;
    private boolean xmlZReportEnabled = false;
    private boolean csvZReportEnabled = false;
    private String xmlZReportFileName = "ZReport.xml";
    private String csvZReportFileName = "ZReport.csv";
    public static int defaultMaxReceiptNumber = 9999;
    private int maxReceiptNumber = defaultMaxReceiptNumber;
    private static final boolean defaultAutoOpenDrawer = false;
    private boolean autoOpenDrawer = defaultAutoOpenDrawer;
    private FiscalReceipt fiscalReceipt = new NullReceipt();
    private final Vector printItems = new Vector();
    private final PrinterReceipt receipt = new PrinterReceipt();

    class DeviceTarget implements Runnable {

        private final FiscalPrinterImpl fiscalPrinter;

        public DeviceTarget(FiscalPrinterImpl fiscalPrinter) {
            this.fiscalPrinter = fiscalPrinter;
        }

        public void run() {
            fiscalPrinter.deviceProc();
        }
    }

    class EventTarget implements Runnable {

        private final FiscalPrinterImpl fiscalPrinter;

        public EventTarget(FiscalPrinterImpl fiscalPrinter) {
            this.fiscalPrinter = fiscalPrinter;
        }

        public void run() {
            fiscalPrinter.eventProc();
        }
    }

    class AsyncTarget implements Runnable {

        private final FiscalPrinterImpl fiscalPrinter;

        public AsyncTarget(FiscalPrinterImpl fiscalPrinter) {
            this.fiscalPrinter = fiscalPrinter;
        }

        public void run() {
            fiscalPrinter.asyncProc();
        }
    }

    //--------------------------------------------------------------------------
    // Constructor
    //--------------------------------------------------------------------------
    public FiscalPrinterImpl() {
        statistics.unifiedPOSVersion = "1.13";
        statistics.deviceCategory = "CashDrawer";
        statistics.manufacturerName = "SHTRIH-M";
        statistics.modelName = "Fiscal printer";
        statistics.serialNumber = "";
        statistics.firmwareRevision = "";
        statistics.physicalInterface = RS232_DEVICE_BUS;
        statistics.installationDate = "";

        eventThread = new Thread(new EventTarget(this));
        eventThread.start();
        initializeData();
    }

    private void initializeData() {
        state = JPOS_S_CLOSED;
        capPositiveSubtotalAdjustment = true;
        deviceServiceDescription = "Fiscal Printer Service , SHTRIH-M, 2010";
        physicalDeviceDescription = "SHTRIH-M fiscal printer";
        physicalDeviceName = "SHTRIH-M fiscal printer";
        capAdditionalLines = true;
        capAmountAdjustment = true;
        capAmountNotPaid = false;
        capCheckTotal = true;
        capFixedOutput = false;
        capIndependentHeader = true;
        capItemList = false;
        capNonFiscalMode = true;
        capOrderAdjustmentFirst = false;
        capPercentAdjustment = true;
        capPositiveAdjustment = true;
        capPowerLossReport = false;
        capPredefinedPaymentLines = false;
        capReceiptNotPaid = false;
        capRemainingFiscalMemory = true;
        capReservedWord = false;
        capSetHeader = true;
        capSetPOSID = true;
        capSetStoreFiscalID = false;
        capSetTrailer = true;
        capSetVatTable = true;
        capSlpFiscalDocument = false;
        capSlpFullSlip = false;
        capSlpValidation = false;
        capSubAmountAdjustment = true;
        capSubPercentAdjustment = true;
        capSubtotal = true;
        capTrainingMode = true;
        capValidateJournal = false;
        capXReport = true;
        capAdditionalHeader = true;
        capAdditionalTrailer = true;
        capChangeDue = false;
        capEmptyReceiptIsVoidable = true;
        capFiscalReceiptStation = true;
        capFiscalSlipStation = false;
        capFiscalReceiptType = true;
        capMultiContractor = false;
        capOnlyVoidLastItem = false;
        capPackageAdjustment = true;
        capPostPreLine = true;
        capSetCurrency = false;
        capTotalizerType = true;
        capCompareFirmwareVersion = false;
        capUpdateFirmware = false;
        // state
        duplicateReceipt = false;
        outputID = 0;
        powerNotify = JPOS_PN_DISABLED;
        powerState = JPOS_PS_UNKNOWN;
        params.checkTotal = false;
        countryCode = FPTR_CC_RUSSIA;
        errorLevel = FPTR_EL_NONE;
        errorOutID = 0;
        errorState = 0;
        errorStation = 0;
        errorString = "";
        flagWhenIdle = false;
        jrnEmpty = false;
        jrnNearEnd = false;
        predefinedPaymentLines = "";
        printerState = FPTR_PS_MONITOR;
        quantityDecimalPlaces = 3;
        quantityLength = 10;
        recEmpty = false;
        recNearEnd = false;
        reservedWord = "";
        slpEmpty = false;
        slpNearEnd = false;
        slipSelection = FPTR_SS_FULL_LENGTH;
        trainingModeActive = false;
        actualCurrency = FPTR_AC_RUR;
        additionalHeader = "";
        additionalTrailer = "";
        changeDue = "";
        contractorId = FPTR_CID_SINGLE;
        dateType = FPTR_DT_RTC;
        fiscalReceiptStation = FPTR_RS_RECEIPT;
        fiscalReceiptType = FPTR_RT_SALES;
        messageType = FPTR_MT_FREE_TEXT;
        params.postLine = "";
        params.preLine = "";
        totalizerType = FPTR_TT_DAY;
        capUpdateStatistics = true;
        capStatisticsReporting = true;
        deviceServiceVersion = deviceVersion113 + 118;
        maxReceiptNumber = defaultMaxReceiptNumber;
    }

    public SMFiscalPrinter getPrinter() {
        if (printer == null) {
            logger.error("printer is null");
        }
        return printer;
    }

    private String[] parseText(String text)
            throws Exception {
        logger.debug("parseText: " + text);
        String data = getPrinter().processEscCommands(text);
        return getPrinter().splitText(data, printerParams.fontNumber);
    }

    private void printText(int station, String text, int fontNumber)
            throws Exception {
        String data = getPrinter().processEscCommands(text);
        if (data.length() > 0) {
            getPrinter().printText(station, data, fontNumber);
        }
    }

    public void setPrinter(SMFiscalPrinter printer) {
        this.printer = printer;
    }

    public void handleException(Exception e)
            throws JposException {
        logger.error("handleException: " + e.getMessage(), e);
        JposExceptionHandler.handleException(e);
    }

    public PrinterStatus readPrinterStatus()
            throws Exception {
        return getPrinter().readPrinterStatus();
    }

    public int getCommandTimeout(int code)
            throws Exception {
        FlexCommand command = commands.itemByCode(code);
        if (command != null) {
            return command.getTimeout();
        } else {
            return PrinterCommand.DefaultTimeout;
        }
    }

    public FlexCommands getCommands()
            throws Exception {
        if (commands == null) {
            commands = new FlexCommands();
            FlexCommandsReader reader = new FlexCommandsReader();
            reader.loadFromXml("commands.xml", commands);
        }
        return commands;
    }

    public void openReceipt(int receiptType)
            throws Exception {
        if ((!isReceiptOpened) && getModel().getCapOpenReceipt()) {
            getPrinter().beginFiscalReceipt(receiptType);
            getPrinter().waitForPrinting();
            this.receiptType = receiptType;
            isReceiptOpened = true;
        }
    }

    private PrinterModel getModel()
            throws Exception {
        return getPrinter().getModel();
    }

    private void setNumHeaderLines(int numHeaderLines)
            throws Exception {
        header = new PrinterHeader(numHeaderLines, this);
        this.numHeaderLines = numHeaderLines;
    }

    private void setNumTrailerLines(int numTrailerLines)
            throws Exception {
        trailer = new PrinterHeader(numTrailerLines, this);
        this.numTrailerLines = numTrailerLines;
    }

    private void setBaudRate(int value)
            throws Exception {
        getPrinter().checkBaudRate(value);
        connectParams.baudRate = value;
    }

    //--------------------------------------------------------------------------
    // Properties
    //--------------------------------------------------------------------------
    public String getCheckHealthText()
            throws Exception {
        checkOpened();
        return encodeText(checkHealthText);
    }

    public boolean getClaimed()
            throws Exception {
        checkOpened();
        return claimed;
    }

    public boolean getDeviceEnabled()
            throws Exception {
        return deviceEnabled;
    }

    private void setPowerState(int powerState) {
        if (powerNotify == JPOS_PN_ENABLED) {
            if (powerState != this.powerState) {
                switch (powerState) {
                    case JPOS_PS_ONLINE:
                        statusUpdateEvent(JPOS_SUE_POWER_ONLINE);
                        break;

                    case JPOS_PS_OFF:
                        statusUpdateEvent(JPOS_SUE_POWER_OFF);
                        break;

                    case JPOS_PS_OFFLINE:
                        statusUpdateEvent(JPOS_SUE_POWER_OFFLINE);
                        break;

                    case JPOS_PS_OFF_OFFLINE:
                        statusUpdateEvent(JPOS_SUE_POWER_OFF_OFFLINE);
                        break;
                }
            }
        }
        this.powerState = powerState;
    }

    private void setState(int value) {
        if (value != state) {
            state = value;
            if ((state == JPOS_S_IDLE) && (flagWhenIdle)) {
                statusUpdateEvent(FPTR_SUE_IDLE);
                flagWhenIdle = false;
            }
        }
    }

    private void addEvent(Runnable event) {
        synchronized (events) {
            events.add(event);
            events.notifyAll();
        }
    }

    private void statusUpdateEvent(int value) {
        logger.debug("statusUpdateEvent("
                + StatusUpdateEventHelper.getName(value) + ")");

        addEvent(new StatusUpdateEventRequest(cb,
                new StatusUpdateEvent(this, value)));
    }

    private void outputCompleteEvent(int outputID) {
        logger.debug("outputCompleteEvent("
                + String.valueOf(outputID) + ")");

        addEvent(new OutputCompleteEventRequest(cb,
                new OutputCompleteEvent(this, outputID)));
    }

    public void printerStatusRead(PrinterStatus status) {
        try {
            updateStatus(status);
        } catch (Exception e) {
            logger.error("printerStatusRead", e);
        }
    }

    public void commandExecuted(PrinterCommand command) {
        try {
            switch (command.getResultCode()) {
                case SMFP_EFPTR_NO_REC_PAPER:
                    setRecPaperState(true, recNearEnd);
                    break;

                case SMFP_EFPTR_NO_JRN_PAPER:
                    setJrnPaperState(true, jrnNearEnd);
                    break;

                case SMFP_EFPTR_NO_SLP_PAPER:
                    setSlpPaperState(true, slpNearEnd);
                    break;
            }
        } catch (Exception e) {
            logger.error("commandExecuted", e);
        }
    }

    public void setRecPaperState(boolean recEmpty, boolean recNearEnd)
            throws Exception {

        int state = getRecPaperState(recEmpty, recNearEnd);
        int curState = getRecPaperState(this.recEmpty, this.recNearEnd);
        if (state != curState) {
            statusUpdateEvent(state);
        }
        this.recEmpty = recEmpty;
        this.recNearEnd = recNearEnd;
    }

    public int getRecPaperState(boolean recEmpty, boolean recNearEnd)
            throws Exception {
        if (getCapRecPresent()) {
            if (!getCapRecEmptySensor()) {
                recEmpty = false;
            }
            if (!getCapRecNearEndSensor()) {
                recNearEnd = false;
            }
            if (recEmpty) {
                return FiscalPrinterConst.FPTR_SUE_REC_EMPTY;
            }
            if (recNearEnd) {
                return FiscalPrinterConst.FPTR_SUE_REC_NEAREMPTY;
            }
        }
        return FiscalPrinterConst.FPTR_SUE_REC_PAPEROK;
    }

    public int getJrnPaperState(boolean jrnEmpty, boolean jrnNearEnd)
            throws Exception {
        if (getCapJrnPresent()) {
            if (!getCapJrnEmptySensor()) {
                jrnEmpty = false;
            }
            if (!getCapJrnNearEndSensor()) {
                jrnNearEnd = false;
            }
            if (jrnEmpty) {
                return FiscalPrinterConst.FPTR_SUE_JRN_EMPTY;
            }
            if (jrnNearEnd) {
                return FiscalPrinterConst.FPTR_SUE_JRN_NEAREMPTY;
            }
        }
        return FiscalPrinterConst.FPTR_SUE_JRN_PAPEROK;
    }

    public int getSlpPaperState(boolean slpEmpty, boolean slpNearEnd)
            throws Exception {
        if (getCapSlpPresent()) {
            if (!getCapSlpEmptySensor()) {
                slpEmpty = false;
            }
            if (!getCapSlpNearEndSensor()) {
                slpNearEnd = false;
            }
            if (slpEmpty) {
                return FiscalPrinterConst.FPTR_SUE_SLP_EMPTY;
            }
            if (slpNearEnd) {
                return FiscalPrinterConst.FPTR_SUE_SLP_NEAREMPTY;
            }
        }
        return FiscalPrinterConst.FPTR_SUE_SLP_PAPEROK;
    }

    public void setJrnPaperState(boolean jrnEmpty, boolean jrnNearEnd)
            throws Exception {

        int state = getJrnPaperState(jrnEmpty, jrnNearEnd);
        int curState = getJrnPaperState(this.jrnEmpty, this.jrnNearEnd);
        if (state != curState) {
            statusUpdateEvent(state);
        }
        this.jrnEmpty = jrnEmpty;
        this.jrnNearEnd = jrnNearEnd;
    }

    public void setSlpPaperState(boolean slpEmpty, boolean slpNearEnd)
            throws Exception {

        int state = getSlpPaperState(slpEmpty, slpNearEnd);
        int curState = getSlpPaperState(this.slpEmpty, this.slpNearEnd);
        if (state != curState) {
            statusUpdateEvent(state);
        }
        this.slpEmpty = slpEmpty;
        this.slpNearEnd = slpNearEnd;
    }

    void setCoverState(boolean isCoverOpened)
            throws Exception {
        if (getCapCoverSensor()) {
            if (isCoverOpened != coverOpened) {
                if (isCoverOpened) {
                    statusUpdateEvent(FPTR_SUE_COVER_OPEN);
                } else {
                    statusUpdateEvent(FPTR_SUE_COVER_OK);
                }
                coverOpened = isCoverOpened;
            }
        }
    }

    public void execute(FiscalPrinterRequest request)
            throws Exception {
        if (asyncMode) {
            setState(JPOS_S_BUSY);
            synchronized (requests) {
                requests.add(request);
                requests.notifyAll();
                outputID = request.getId();
            }
        } else {
            if (state == JPOS_S_BUSY) {
                throw new JposException(JPOS_E_BUSY);
            }

            checkOnLine();
            synchronized (printer) {
                request.execute(this);
            }
        }
    }

    private void executePrinterRequest(FiscalPrinterRequest request) {
        while (true) {
            try {
                request.execute(this);
                setPowerState(JPOS_PS_ONLINE);
                outputCompleteEvent(request.getId());
                break;
            } catch (Exception e) {
                JposException jpose = JposExceptionHandler.getJposException(e);

                setState(JPOS_S_ERROR);
                switch (jpose.getErrorCode()) {
                    case JPOS_E_TIMEOUT:
                        setPowerState(JPOS_PS_OFFLINE);
                        break;
                }

                ErrorEvent event = new ErrorEvent(this, jpose.getErrorCode(),
                        jpose.getErrorCodeExtended(), JPOS_EL_OUTPUT, JPOS_ER_CLEAR);

                /*
                if (getFreezeEvents())
                {
                events.add(new ErrorEventRequest(cb, event));
                } else
                {
                if (cb != null) cb.fireErrorEvent(event);
                if (event.getErrorResponse() != JPOS_ER_RETRY) break;
                }
                 */
            }
        }
    }

    // event delivery routine
    public void eventProc() {
        try {
            Thread thisThread = Thread.currentThread();
            while (eventThread == thisThread) {
                synchronized (events) {
                    while (!events.isEmpty()) {
                        ((Runnable) events.remove(0)).run();

                    }
                    events.wait();
                }
            }
        } catch (InterruptedException e) {
            // Restore the interrupted status
            logger.error("InterruptedException", e);
            Thread.currentThread().interrupt();
        }
    }

    public void asyncProc() {
        try {
            Thread thisThread = Thread.currentThread();
            while (asyncThread == thisThread) {
                synchronized (requests) {
                    while (!requests.isEmpty()) {
                        setState(JPOS_S_BUSY);
                        executePrinterRequest((FiscalPrinterRequest) requests.remove(0));
                    }
                    setState(JPOS_S_IDLE);
                    requests.wait();
                }
            }
        } catch (InterruptedException e) {
            // Restore the interrupted status
            logger.error("InterruptedException", e);
            Thread.currentThread().interrupt();
        }
    }

    public void updateStatus(PrinterStatus status)
            throws Exception {
        PrinterFlags flags = status.getPrinterFlags();

        setRecPaperState(
                flags.isRecEmpty(),
                flags.isRecNearEnd());

        setJrnPaperState(
                flags.isJrnEmpty(),
                flags.isJrnNearEnd());

        setSlpPaperState(
                flags.isSlpEmpty(),
                flags.isSlpNearEnd());

        setCoverState(flags.isCoverOpened());
        if (status.getPrinterMode().getDayOpened()) {
            dayOpened = true;
        }
    }

    private void startPoll() {
        deviceThread = new Thread(new DeviceTarget(this));
        deviceThread.start();
    }

    private void stopPoll() {
        deviceThread = null;
        setPowerState(JPOS_PS_UNKNOWN);
    }

    public void setDeviceEnabled(boolean deviceEnabled)
            throws Exception {
        logger.debug("setDeviceEnabled(" + String.valueOf(deviceEnabled) + ")");

        checkClaimed();
        if (this.deviceEnabled != deviceEnabled) {
            if (deviceEnabled) {
                searchDevice(0);
                setPowerState(JPOS_PS_ONLINE);
                setJrnPaperState(true, true); // !!!

                updateDeviceMetrics();
                checkEcrMode();
                cancelReceipt();
                writeTables();
                readPrinterStatus();

                // if polling enabled - create device thread
                if (pollEnabled) {
                    startPoll();
                }
            } else {
                stopPoll();
            }
            this.deviceEnabled = deviceEnabled;
        }
    }

    private void updateDeviceMetrics()
            throws Exception {
        LongPrinterStatus longStatus = null;
        ShortPrinterStatus shortStatus = null;

        deviceMetrics = getPrinter().readDeviceMetrics();
        try {
            if (getModel().getCapShortStatus()) {
                shortStatus = getPrinter().readShortStatus();
            }
        } catch (SmFiscalPrinterException e) {
            logger.error("readShortStatus error", e);
        }
        longStatus = getPrinter().readLongStatus();

        if (shortStatus != null) {
            LogWriter.write(shortStatus);
        }
        LogWriter.write(longStatus);
        LogWriter.write(deviceMetrics);
        LogWriter.writeSeparator();

        physicalDeviceName =
                deviceMetrics.getDeviceName() + ", ¹ "
                + longStatus.getSerial();

        String formatString = Localizer.getString(
                Localizer.PhysicalDeviceDescription);

        // iceDescription = "%s,  %s, ÏÎ ÔÐ: %s.%d, %s, ÏÎ ÔÏ: %s.%d, %s"
        physicalDeviceDescription =
                deviceMetrics.getDeviceName() + ", "
                + longStatus.getSerial() + ", ÏÎ ÔÐ: "
                + longStatus.getFirmwareVersion() + "."
                + String.valueOf(longStatus.getFirmwareBuild()) + ", "
                + longStatus.getFirmwareDate().toString() + ", ÏÎ ÔÏ: "
                + longStatus.getFMFirmwareVersion() + "."
                + String.valueOf(longStatus.getFMFirmwareBuild()) + ", "
                + longStatus.getFMFirmwareDate().toString();

        logger.debug("PhysicalDeviceName: " + physicalDeviceName);
        logger.debug("PhysicalDeviceDescription: " + physicalDeviceDescription);

        // update device parameters
        statistics.serialNumber = longStatus.getSerial();
        statistics.firmwareRevision = longStatus.getFirmwareRevision();
    }

    public void setPollEnabled(boolean value)
            throws Exception {
        logger.debug("setPollEnabled(" + String.valueOf(value) + ")");
        if (value != pollEnabled) {
            if (value) {
                if (deviceEnabled) {
                    startPoll();
                }
            } else {
                stopPoll();
            }
        }
        pollEnabled = value;
    }

    public String getDeviceServiceDescription()
            throws Exception {
        checkOpened();
        return encodeText(deviceServiceDescription);
    }

    public int getDeviceServiceVersion()
            throws Exception {
        checkOpened();
        return deviceServiceVersion;
    }

    public boolean getFreezeEvents()
            throws Exception {
        checkOpened();
        return eventThread == null;
    }

    public void setFreezeEvents(boolean freezeEvents)
            throws Exception {
        checkOpened();
        if (freezeEvents != getFreezeEvents()) {
            if (freezeEvents) {
                eventThread = null;
            } else {
                eventThread = new Thread(new EventTarget(this));
                eventThread.start();
            }
        }
    }

    public String getPhysicalDeviceDescription()
            throws Exception {
        checkOpened();
        return encodeText(physicalDeviceDescription);
    }

    public String getPhysicalDeviceName()
            throws Exception {
        checkOpened();
        return encodeText(physicalDeviceName);
    }

    public int getState()
            throws Exception {
        return state;
    }

    //--------------------------------------------------------------------------
    // Properties
    //--------------------------------------------------------------------------
    // Capabilities
    public boolean getCapAdditionalLines()
            throws Exception {
        return capAdditionalLines;
    }

    public boolean getCapAmountAdjustment()
            throws Exception {
        return capAmountAdjustment;
    }

    public boolean getCapAmountNotPaid()
            throws Exception {
        return capAmountNotPaid;
    }

    public boolean getCapCheckTotal()
            throws Exception {
        return capCheckTotal;
    }

    public boolean getCapCoverSensor()
            throws Exception {
        return getModel().getCapCoverSensor();
    }

    public boolean getCapDoubleWidth()
            throws Exception {
        return getModel().getCapDoubleWidth();
    }

    public boolean getCapDuplicateReceipt()
            throws Exception {
        return getModel().getCapDuplicateReceipt();
    }

    public void setDuplicateReceipt(boolean aduplicateReceipt)
            throws Exception {
        if (!getCapDuplicateReceipt()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.ReceiptDuplicationNotSupported));
        }
        duplicateReceipt = aduplicateReceipt;
    }

    public boolean getCapFixedOutput()
            throws Exception {
        return capFixedOutput;
    }

    public boolean getCapHasVatTable()
            throws Exception {
        return getModel().getCapHasVatTable();
    }

    public boolean getCapIndependentHeader()
            throws Exception {
        return capIndependentHeader;
    }

    public boolean getCapItemList()
            throws Exception {
        return capItemList;
    }

    public boolean getCapJrnEmptySensor()
            throws Exception {
        return getModel().getCapJrnEmptySensor();
    }

    public boolean getCapJrnNearEndSensor()
            throws Exception {
        return getModel().getCapJrnNearEndSensor();
    }

    public boolean getCapJrnPresent()
            throws Exception {
        return getModel().getCapJrnPresent();
    }

    public boolean getCapNonFiscalMode()
            throws Exception {
        return capNonFiscalMode;
    }

    public boolean getCapOrderAdjustmentFirst()
            throws Exception {
        return capOrderAdjustmentFirst;
    }

    public boolean getCapPercentAdjustment()
            throws Exception {
        return capPercentAdjustment;
    }

    public boolean getCapPositiveAdjustment()
            throws Exception {
        return capPositiveAdjustment;
    }

    public boolean getCapPowerLossReport()
            throws Exception {
        return capPowerLossReport;
    }

    public int getCapPowerReporting()
            throws Exception {
        return JPOS_PR_STANDARD;
    }

    public boolean getCapPredefinedPaymentLines()
            throws Exception {
        return capPredefinedPaymentLines;
    }

    public boolean getCapReceiptNotPaid()
            throws Exception {
        return capReceiptNotPaid;
    }

    public boolean getCapRecEmptySensor()
            throws Exception {
        return getModel().getCapRecEmptySensor();
    }

    public boolean getCapRecNearEndSensor()
            throws Exception {
        return getModel().getCapRecNearEndSensor();
    }

    public boolean getCapRecPresent()
            throws Exception {
        return getModel().getCapRecPresent();
    }

    public boolean getCapRemainingFiscalMemory()
            throws Exception {
        return capRemainingFiscalMemory;
    }

    public boolean getCapReservedWord()
            throws Exception {
        return capReservedWord;
    }

    public boolean getCapSetHeader()
            throws Exception {
        return capSetHeader;
    }

    public boolean getCapSetPOSID()
            throws Exception {
        return capSetPOSID;
    }

    public boolean getCapSetStoreFiscalID()
            throws Exception {
        return capSetStoreFiscalID;
    }

    public boolean getCapSetTrailer()
            throws Exception {
        return capSetTrailer;
    }

    public boolean getCapSetVatTable()
            throws Exception {
        return capSetVatTable;
    }

    public boolean getCapSlpEmptySensor()
            throws Exception {
        return getModel().getCapSlpEmptySensor();
    }

    public boolean getCapSlpFiscalDocument()
            throws Exception {
        return capSlpFiscalDocument;
    }

    public boolean getCapSlpFullSlip()
            throws Exception {
        return capSlpFullSlip;
    }

    public boolean getCapSlpNearEndSensor()
            throws Exception {
        return getModel().getCapSlpNearEndSensor();
    }

    public boolean getCapSlpPresent()
            throws Exception {
        return getModel().getCapSlpPresent();
    }

    public boolean getCapSlpValidation()
            throws Exception {
        return capSlpValidation;
    }

    public boolean getCapSubAmountAdjustment()
            throws Exception {
        return capSubAmountAdjustment;
    }

    public boolean getCapSubPercentAdjustment()
            throws Exception {
        return capSubPercentAdjustment;
    }

    public boolean getCapSubtotal()
            throws Exception {
        return capSubtotal;
    }

    public boolean getCapTrainingMode()
            throws Exception {
        return capTrainingMode;
    }

    public boolean getCapValidateJournal()
            throws Exception {
        return capValidateJournal;
    }

    public boolean getCapXReport()
            throws Exception {
        return capXReport;
    }

    public int getOutputID()
            throws Exception {
        checkOpened();
        return outputID;
    }

    public int getPowerNotify()
            throws Exception {
        checkOpened();
        return powerNotify;
    }

    public void setPowerNotify(int powerNotify)
            throws Exception {
        if (deviceEnabled) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.DeviceIsEnabled));
        }
        this.powerNotify = powerNotify;
    }

    public int getPowerState()
            throws Exception {
        checkOpened();
        return powerState;
    }

    public int getAmountDecimalPlace()
            throws Exception {
        return getModel().getAmountDecimalPlace();
    }

    public boolean getAsyncMode()
            throws Exception {
        return asyncMode;
    }

    public void setAsyncMode(boolean asyncMode)
            throws Exception {
        if (asyncMode != this.asyncMode) {
            if (asyncMode) {
                asyncThread = new Thread(new AsyncTarget(this));
                asyncThread.start();
            } else {
                asyncThread = null;
            }
            this.asyncMode = asyncMode;
        }
    }

    public boolean getCheckTotal()
            throws Exception {
        return params.checkTotal;
    }

    public void setCheckTotal(boolean value)
            throws Exception {
        params.checkTotal = value;
    }

    public int getCountryCode()
            throws Exception {
        checkEnabled();
        return countryCode;
    }

    public boolean getCoverOpen()
            throws Exception {
        checkEnabled();
        if (!getCapCoverSensor()) {
            return false;
        } else {
            return coverOpened;
        }
    }

    public boolean getDayOpened()
            throws Exception {
        checkEnabled();
        return dayOpened;
    }

    public int getDescriptionLength()
            throws Exception {
        return getModel().getTextLength(printer.getParams().fontNumber);
    }

    public boolean getDuplicateReceipt()
            throws Exception {
        if (!getCapDuplicateReceipt()) {
            return false;
        }
        return duplicateReceipt;
    }

    public int getErrorLevel()
            throws Exception {
        return errorLevel;
    }

    public int getErrorOutID()
            throws Exception {
        checkEnabled();
        return errorOutID;
    }

    public int getErrorState()
            throws Exception {
        return errorState;
    }

    public int getErrorStation()
            throws Exception {
        return errorStation;
    }

    public String getErrorString()
            throws Exception {
        return encodeText(errorString);
    }

    public boolean getFlagWhenIdle()
            throws Exception {
        return flagWhenIdle;
    }

    public void setFlagWhenIdle(boolean value)
            throws Exception {
        if (value != flagWhenIdle) {
            flagWhenIdle = value;

            // If the State is already set to S_IDLE
            // when this property is set to true, then a
            // StatusUpdateEvent is enqueued immediately.

            if ((state == JPOS_S_IDLE) && (flagWhenIdle)) {
                statusUpdateEvent(FPTR_SUE_IDLE);

                // This property is automatically reset to false when
                // the status event is delivered.

                // We reset this property when status event is enqueued.
                // I think that it is a feature, not a code bug.

                flagWhenIdle = false;
            }
        }
    }

    public boolean getJrnEmpty()
            throws Exception {
        checkEnabled();
        return jrnEmpty;
    }

    public boolean getJrnNearEnd()
            throws Exception {
        checkEnabled();
        return jrnNearEnd;
    }

    public int getMessageLength()
            throws Exception {
        return getModel().getTextLength(printer.getParams().fontNumber);
    }

    public int getNumHeaderLines()
            throws Exception {
        return numHeaderLines;
    }

    public int getNumTrailerLines()
            throws Exception {
        return numTrailerLines;
    }

    public int getNumVatRates()
            throws Exception {
        return getModel().getNumVatRates();
    }

    public String getPredefinedPaymentLines()
            throws Exception {
        return encodeText(predefinedPaymentLines);
    }

    public int getPrinterState()
            throws Exception {
        checkEnabled();
        return printerState;
    }

    public int getQuantityDecimalPlaces()
            throws Exception {
        checkEnabled();
        return quantityDecimalPlaces;
    }

    public int getQuantityLength()
            throws Exception {
        checkEnabled();
        return quantityLength;
    }

    public boolean getRecEmpty()
            throws Exception {
        checkEnabled();
        if (getCapRecEmptySensor()) {
            return recEmpty;
        } else {
            return false;
        }
    }

    public boolean getRecNearEnd()
            throws Exception {
        checkEnabled();
        if (getCapRecNearEndSensor()) {
            return recNearEnd;
        }
        {
            return false;
        }
    }

    public int getRemainingFiscalMemory()
            throws Exception {
        checkEnabled();
        return getPrinter().readLongStatus().getFMFreeRecords();
    }

    public String getReservedWord()
            throws Exception {
        return encodeText(reservedWord);
    }

    public boolean getSlpEmpty()
            throws Exception {
        checkEnabled();
        return slpEmpty;
    }

    public boolean getSlpNearEnd()
            throws Exception {
        checkEnabled();
        return slpNearEnd;
    }

    public int getSlipSelection()
            throws Exception {
        checkEnabled();
        return slipSelection;
    }

    public void setSlipSelection(int value)
            throws Exception {
        checkEnabled();
        if (value == FPTR_SS_FULL_LENGTH) {
            slipSelection = value;
        } else {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue) + "SlipSelection");
        }
    }

    public boolean getTrainingModeActive()
            throws Exception {
        checkEnabled();
        return trainingModeActive;
    }

    public boolean getCapAdditionalHeader()
            throws Exception {
        return capAdditionalHeader;
    }

    public boolean getCapAdditionalTrailer()
            throws Exception {
        return capAdditionalTrailer;
    }

    public boolean getCapChangeDue()
            throws Exception {
        return capChangeDue;
    }

    public boolean getCapEmptyReceiptIsVoidable()
            throws Exception {
        return capEmptyReceiptIsVoidable;
    }

    public boolean getCapFiscalReceiptStation()
            throws Exception {
        switch (fiscalReceiptStation) {
            case FPTR_RS_RECEIPT:
                return capFiscalReceiptStation;

            case FPTR_RS_SLIP:
                return capFiscalSlipStation;

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidPropertyValue)
                        + "fiscalReceiptStation");
        }
    }

    public boolean getCapFiscalReceiptType()
            throws Exception {
        return capFiscalReceiptType;
    }

    public boolean getCapMultiContractor()
            throws Exception {
        return capMultiContractor;
    }

    public boolean getCapOnlyVoidLastItem()
            throws Exception {
        return capOnlyVoidLastItem;
    }

    public boolean getCapPackageAdjustment()
            throws Exception {
        return capPackageAdjustment;
    }

    public boolean getCapPostPreLine()
            throws Exception {
        return capPostPreLine;
    }

    public boolean getCapSetCurrency()
            throws Exception {
        return capSetCurrency;
    }

    public boolean getCapTotalizerType()
            throws Exception {
        return capTotalizerType;
    }

    // Properties
    public int getActualCurrency()
            throws Exception {
        checkEnabled();
        return actualCurrency;
    }

    public String getAdditionalHeader()
            throws Exception {
        checkEnabled();
        if (!getCapAdditionalHeader()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.AdditionalHeaderNotSupported));
        }
        return encodeText(additionalHeader);
    }

    public void setAdditionalHeader(String value)
            throws Exception {
        checkEnabled();
        if (!getCapAdditionalHeader()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.AdditionalHeaderNotSupported));
        }
        additionalHeader = decodeText(value);
    }

    public String getAdditionalTrailer()
            throws Exception {
        checkEnabled();
        if (!getCapAdditionalTrailer()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.AdditionalTrailerNotSupported));
        }
        return encodeText(additionalTrailer);
    }

    public void setAdditionalTrailer(String value)
            throws Exception {
        checkEnabled();
        if (!getCapAdditionalTrailer()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.AdditionalTrailerNotSupported));
        }
        additionalTrailer = decodeText(value);
    }

    public String getChangeDue()
            throws Exception {
        if (!getCapChangeDue()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.ChangeDueTextNotSupported));
        }
        return encodeText(changeDue);
    }

    public void setChangeDue(String value)
            throws Exception {
        if (!getCapChangeDue()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.ChangeDueTextNotSupported));
        }
        changeDue = decodeText(value);
    }

    public int getContractorId()
            throws Exception {
        checkEnabled();
        return contractorId;
    }

    public void setContractorId(int value)
            throws Exception {
        checkEnabled();
        if (!getCapMultiContractor()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.MultipleContractorsNotSupported));
        }
        contractorId = value;
    }

    public int getDateType()
            throws Exception {
        checkEnabled();
        return dateType;
    }

    public void setDateType(int value)
            throws Exception {
        checkEnabled();
        switch (value) {
            case FPTR_DT_EOD:
            case FPTR_DT_RTC:

                dateType = value;
                break;
            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue) + "DateType");
        }
    }

    public int getFiscalReceiptStation()
            throws Exception {
        checkEnabled();
        return fiscalReceiptStation;
    }

    public void setFiscalReceiptStation(int value)
            throws Exception {
        checkEnabled();
        // Check if the Fiscal Printer is currently in the Monitor State
        checkPrinterState(FPTR_PS_MONITOR);

        switch (value) {
            case FPTR_RS_RECEIPT:

                fiscalReceiptStation = value;
                break;

            case FPTR_RS_SLIP:
                if (!capFiscalSlipStation) {
                    throw new JposException(JPOS_E_ILLEGAL,
                            Localizer.getString(Localizer.SlipStationNotPresent));
                }
                fiscalReceiptStation = value;
                break;
            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue));
        }
    }

    public int getFiscalReceiptType()
            throws Exception {
        checkEnabled();
        return fiscalReceiptType;
    }

    public int getMessageType()
            throws Exception {
        return messageType;
    }

    public void setMessageType(int value)
            throws Exception {
        if (value == FPTR_MT_FREE_TEXT) {
            messageType = value;
        } else {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.MessageTypeNotSupported));
        }
    }

    public String getPostLine()
            throws Exception {
        checkEnabled();
        return encodeText(params.postLine);
    }

    public void setPostLine(String value)
            throws Exception {
        checkEnabled();
        params.postLine = decodeText(value);
    }

    public String getPreLine()
            throws Exception {
        checkEnabled();
        return encodeText(params.preLine);
    }

    public void setPreLine(String value)
            throws Exception {
        checkEnabled();
        params.preLine = decodeText(value);
    }

    public int getTotalizerType()
            throws Exception {
        checkEnabled();
        return totalizerType;
    }

    public void setTotalizerType(int value)
            throws Exception {
        checkEnabled();
        switch (value) {
            case FPTR_TT_DOCUMENT: // Document totalizer
            case FPTR_TT_DAY: // Day totalizer
            case FPTR_TT_RECEIPT: // Receipt totalizer
            case FPTR_TT_GRAND: // Grand totalizer
                totalizerType = value;
                break;

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue));
        }
    }

    private void checkOpened()
            throws Exception {
        if (state == JPOS_S_CLOSED) {
            throw new JposException(JPOS_E_CLOSED);
        }
    }

    // write fields if file specified
    private void writeFieldsFile() {
        if (fieldsFileName.length() != 0) {
            try {
                File file = new File(fieldsFileName);
                if (file.exists()) {
                    logger.debug("File exists: " + fieldsFileName);
                    writeTables(fieldsFileName);
                } else {
                    logger.debug("File not exists: " + fieldsFileName);
                }
            } catch (Exception e) {
                logger.error(e.getMessage());
            }
        }
    }

    private void writePaymentNames()
            throws Exception {
        // payment names
        logger.debug("write payment names");
        for (int i = 0; i < paymentNames.size(); i++) {
            FptrPaymentName paymentName = (FptrPaymentName) paymentNames.get(i);
            if (paymentName.getCode() != PrinterConst.SMFP_TABLE_TAX_ROW_CASH) {
                int result = getPrinter().writeTable(PrinterConst.SMFP_TABLE_PAYTYPE,
                        paymentName.getCode(), 1, paymentName.getName());
                if (printer.failed(result)) {
                    break;
                }
            }
        }
    }

    // clear table SMFP_TABLE_TEXT
    private void clearTextTable()
            throws Exception {
        ReadTableInfo tableStructure = getPrinter().readTableInfo(
                PrinterConst.SMFP_TABLE_TEXT);
        int rowCount = tableStructure.getRowCount();
        for (int i = 1; i <= rowCount; i++) {
            int result = getPrinter().writeTable(PrinterConst.SMFP_TABLE_TEXT, i, 1, "");
            if (printer.failed(result)) {
                break;
            }
        }
    }

    private void writeTables()
            throws Exception {
        if (tableMode == SMFPTR_TABLE_MODE_AUTO) {
            clearTextTable();
            writePaymentNames();
            getPrinter().writeParameter(SMFP_PARAMID_AUTO_CUT, false);
            getPrinter().writeParameter(SMFP_PARAMID_AUTO_DRAWER, autoOpenDrawer);
        }
        writeFieldsFile();
    }

    public void claim(int timeout)
            throws Exception {
        checkOpened();
        if (!claimed) {
            port.setPortName(connectParams.portName);
            port.setBaudRate(connectParams.baudRate);
            port.setOpenTimeout(timeout);
            port.open();
            claimed = true;
        }
    }

    public void close()
            throws Exception {
        checkOpened();
        if (claimed) {
            release();
        }
        setState(JPOS_S_CLOSED);
        statistics.save(statisticFileName);
    }

    public void checkHealth(int level)
            throws Exception {
        checkEnabled();
        checkOnLine();
        switch (level) {
            case JPOS_CH_INTERNAL:
                checkHealthText = InternalCheckHelthReport.getReport(printer);
                break;

            case JPOS_CH_EXTERNAL:
                checkHealthText = checkHealthExternal();
                break;

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue)
                        + ", level");
        }
    }

    public PrinterImages getPrinterImages() {
        return getPrinter().getPrinterImages();
    }

    public void printImage(int index)
            throws Exception {
        getPrinterImages().printImage(index, this);
    }

    /** Write tables from CSV file and don't check **/
    public void writeTables(String fileName)
            throws Exception {
        logger.debug("writeTables: " + fileName);
        PrinterFields fields = new PrinterFields();
        CsvTablesReader reader = new CsvTablesReader();
        reader.load(fileName, fields);

        String[] fieldValue = new String[1];
        for (int i = 0; i < fields.size(); i++) {
            fieldValue[0] = "";
            PrinterField field = fields.get(i);
            // Do not write tables SMFP_TABLE_TEXT and SMFP_TABLE_PAYTYPE
            // to not break correct driver behavior
            if ((field.getTable() != PrinterConst.SMFP_TABLE_TEXT)
                    && (field.getTable() != PrinterConst.SMFP_TABLE_PAYTYPE)) {
                // if field have correct number
                if (printer.isValidField(field.getTable(),
                        field.getRow(), field.getNumber())) {
                    getPrinter().check(printer.readTable(
                            field.getTable(),
                            field.getRow(),
                            field.getNumber(),
                            fieldValue));

                    if (fieldValue[0].compareTo(field.getValue()) != 0) {
                        getPrinter().writeTable(
                                field.getTable(),
                                field.getRow(),
                                field.getNumber(),
                                field.getValue());
                    }
                }
            }
        }
        logger.debug("writeTables: OK");
    }

    /** Read tables from CSV file **/
    public void readTables(String fileName)
            throws Exception {
        logger.debug("readTables: " + fileName);
        PrinterTables tables = new PrinterTables();
        CsvTablesWriter writer = new CsvTablesWriter();

        getPrinter().readTables(tables);
        writer.save(fileName, tables);
        logger.debug("readTables: OK");
    }

    public void directIO(int command, int[] data, Object object)
            throws Exception {
        switch (command) {
            case SMFPTR_DIO_COMMAND_OBJECT:
                PrinterCommand printerCommand = (PrinterCommand) object;
                device.execute(printerCommand);
                getPrinter().check(printerCommand.getResultCode());
                break;

            case SMFPTR_DIO_COMMAND:
                new DIOExecuteCommand(this).execute(data, object);
                break;

            case SMFPTR_DIO_PRINT_BARCODE_OBJECT:
                printBarcode((PrinterBarcode) object);
                break;

            case SMFPTR_DIO_SET_DEPARTMENT:
                params.department = ((int[]) object)[0];
                break;

            case SMFPTR_DIO_GET_DEPARTMENT:
                ((int[]) object)[0] = params.department;
                break;

            case SMFPTR_DIO_STRCOMMAND:
                new DIOExecuteCommandStr(this).execute(data, object);
                break;

            case SMFPTR_DIO_READTABLE:
                new DIOReadTable(this).execute(data, object);
                break;

            case SMFPTR_DIO_WRITETABLE:
                new DIOWriteTable(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_PAYMENT_NAME:
                new DIOReadPaymentName(this).execute(data, object);
                break;

            case SMFPTR_DIO_WRITE_PAYMENT_NAME:
                new DIOWritePaymentName(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_DAY_END:
                PrinterStatus status = getPrinter().readLongPrinterStatus();
                if (status.getPrinterMode().isDayEndRequired()) {
                    data[0] = 1;
                } else {
                    data[0] = 0;
                }
                break;

            case SMFPTR_DIO_PRINT_BARCODE:
                new DIOPrintBarcode(this).execute(data, object);
                break;

            case SMFPTR_DIO_LOAD_IMAGE:
                String fileName = ((String[]) (object))[0];
                data[0] = getPrinterImages().loadImage(fileName, this);
                saveProperties();
                break;

            case SMFPTR_DIO_PRINT_IMAGE:
                getPrinterImages().printImage(data[0], this);
                getPrinter().waitForPrinting();
                break;

            case SMFPTR_DIO_CLEAR_IMAGES:
                getPrinterImages().clear();
                saveProperties();
                break;

            case SMFPTR_DIO_ADD_LOGO:
                int logoImageIndex = data[0];
                int logoPosition = data[1];
                getPrinterImages().checkIndex(logoImageIndex);
                ReceiptImage logo = new ReceiptImage(true, logoImageIndex, logoPosition);
                receiptImages.add(logo);
                saveProperties();
                break;

            // clear logo
            case SMFPTR_DIO_CLEAR_LOGO:
                receiptImages.clear();
                saveProperties();
                break;

            case SMFPTR_DIO_PRINT_LINE:
                byte[] lineData = new byte[getModel().getPrintWidth() / 8];
                Arrays.fill(lineData, (byte) 0xFF);
                int lineHeight = data[0];
                getPrinter().printGraphicLine(lineHeight, lineData);
                getPrinter().waitForPrinting();
                break;

            // get driver parameter
            case SMFPTR_DIO_GET_DRIVER_PARAMETER:
                switch (data[0]) {
                    case SMFPTR_DIO_PARAM_REPORT_DEVICE:
                        ((int[]) object)[0] = reportDevice;
                        break;

                    case SMFPTR_DIO_PARAM_REPORT_TYPE:
                        ((int[]) object)[0] = reportType;
                        break;

                    case SMFPTR_DIO_PARAM_NUMHEADERLINES:
                        ((int[]) object)[0] = numHeaderLines;
                        break;

                    case SMFPTR_DIO_PARAM_NUMTRAILERLINES:
                        ((int[]) object)[0] = numTrailerLines;
                        break;

                    case SMFPTR_DIO_PARAM_POLL_ENABLED:
                        ((boolean[]) object)[0] = pollEnabled;
                        break;


                }
                break;

            // set driver parameter
            case SMFPTR_DIO_SET_DRIVER_PARAMETER:
                switch (data[0]) {
                    case SMFPTR_DIO_PARAM_REPORT_DEVICE:
                        reportDevice = ((int[]) object)[0];
                        break;

                    case SMFPTR_DIO_PARAM_REPORT_TYPE:
                        reportType = ((int[]) object)[0];
                        break;

                    case SMFPTR_DIO_PARAM_NUMHEADERLINES:
                        setNumHeaderLines(((int[]) object)[0]);
                        break;

                    case SMFPTR_DIO_PARAM_NUMTRAILERLINES:
                        setNumTrailerLines(((int[]) object)[0]);
                        break;

                    case SMFPTR_DIO_PARAM_POLL_ENABLED:
                        setPollEnabled(((boolean[]) object)[0]);
                        break;
                }
                break;

            case SMFPTR_DIO_PRINT_TEXT:
                new DIOPrintText(this).execute(data, object);
                break;

            case SMFPTR_DIO_WRITE_TABLES:
                writeTables((String) object);
                break;

            case SMFPTR_DIO_READ_TABLES:
                readTables((String) object);
                break;

            case SMFPTR_DIO_READ_SERIAL:
                new DIOReadSerial(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_EJ_SERIAL:
                new DIOReadEJSerial(this).execute(data, object);
                break;

            case SMFPTR_DIO_OPEN_DRAWER:
                new DIOOpenDrawer(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_DRAWER_STATE:
                new DIOReadDrawerState(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_PRINTER_STATUS:
                new DIOReadPrinterStatus(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_CASH_REG:
                new DIOReadCashReg(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_OPER_REG:
                new DIOReadOperReg(this).execute(data, object);
                break;

            case SMFPTR_DIO_XML_ZREPORT:
                new DIOXMLZReport(this).execute(data, object);
                break;

            case SMFPTR_DIO_CSV_ZREPORT:
                new DioCsvZReport(this).execute(data, object);
                break;


            case SMFPTR_DIO_WRITE_DEVICE_PARAMETER:
                new DIOWriteParameter(this).execute(data, object);
                break;

            case SMFPTR_DIO_READ_DEVICE_PARAMETER:
                new DIOReadParameter(this).execute(data, object);
                break;

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue)
                        + ", command");
        }
    }

    // try to connect to device
    private boolean connectDevice(
            String searchPortName,
            int searchBaudRate,
            int searchTimeout)
            throws Exception {
        logger.debug("connectDevice");
        try {
            port.setPortName(searchPortName);
            port.setBaudRate(searchBaudRate);
            port.setOpenTimeout(searchTimeout);
            port.open();

            logger.debug("printer.connect");
            getPrinter().connect();

            // always set port parameters to update byte
            // receive timeout in fiscal printer
            getPrinter().writePortParams(0,
                    getPrinter().getBaudRateIndex(connectParams.baudRate),
                    connectParams.deviceByteTimeout);

            // if baudrate changed - reopen port
            if (searchBaudRate != connectParams.baudRate) {
                port.setPortName(searchPortName);
                port.setBaudRate(connectParams.baudRate);
                port.setOpenTimeout(searchTimeout);
                port.open();
            }
            return true;
        } catch (IOException e) {
            logger.error(e);
            return false;
        }
    }

    // search device on ports and baudrates
    private void searchDevice(int timeout)
            throws Exception {
        logger.debug("searchDevice");

        int[] deviceBaudRates = {2400, 4800, 9600, 19200, 38400, 57600, 115200};
        if (connectDevice(connectParams.portName, connectParams.baudRate, timeout)) {
            return;
        }

        if (connectParams.searchByPortEnabled) {
            Enumeration e = CommPortIdentifier.getPortIdentifiers();
            while (e.hasMoreElements()) {
                CommPortIdentifier port = (CommPortIdentifier) e.nextElement();
                if (port.getPortType() == port.PORT_SERIAL) {
                    if (!connectParams.portName.equalsIgnoreCase(port.getName())) {
                        if (connectParams.searchByBaudRateEnabled) {
                            for (int j = 0; j < deviceBaudRates.length; j++) {
                                if (connectDevice(port.getName(), deviceBaudRates[j],
                                        timeout)) {
                                    return;
                                }
                            }
                        } else {
                            if (connectDevice(port.getName(), connectParams.baudRate,
                                    timeout)) {
                                return;
                            }
                        }
                    }
                }
            }
        } else {
            if (connectParams.searchByBaudRateEnabled) {
                for (int j = 0; j < deviceBaudRates.length; j++) {
                    if (connectDevice(
                            connectParams.portName,
                            deviceBaudRates[j],
                            timeout)) {
                        return;
                    }
                }
            }
        }
        throw new JposException(JPOS_E_NOHARDWARE);
    }

    public void setEventCallbacks(EventCallbacks cb) {
        this.cb = cb;
    }

    void setJposEntry(JposEntry entry) {
        jposEntry = entry;
    }

    public void open(String logicalName, EventCallbacks cb)
            throws Exception {
        // debug messages
        logger.debug("SHTRIH-M JavaPos FiscalPrinter service");
        logger.debug("DeviceServiceVersion: " + String.valueOf(deviceServiceVersion));

        logicalName = decodeText(logicalName);
        this.logicalName = logicalName;

        setEventCallbacks(cb);
        initializeData();

        if (jposEntry != null) {
            JposPropertyReader reader = new JposPropertyReader(jposEntry);

            port = createPort(jposEntry);
            device = new SMPrinterDeviceImpl(port);
            printer = new SMFiscalPrinterImpl(device, printerParams);
            getPrinter().setEvents(this);
            receiptPrinter = new ReceiptPrinterImpl(printer, params);

            connectParams.portName = reader.readString(RS232Const.RS232_PORT_NAME_PROP_NAME, "");
            //!!! setBaudRate(reader.readInteger(RS232Const.RS232_BAUD_RATE_PROP_NAME, 4800));
            connectParams.baudRate = reader.readInteger(RS232Const.RS232_BAUD_RATE_PROP_NAME, 4800);
            params.department = reader.readInteger("department", 1);
            printerParams.fontNumber = reader.readInteger("fontNumber", 1);
            params.closeReceiptText = reader.readString("closeReceiptText", "");
            params.subtotalText = reader.readString("subtotalText", "SUBTOTAL");
            connectParams.byteTimeout = reader.readInteger("byteTimeout", 200);
            connectParams.deviceByteTimeout = reader.readInteger("deviceByteTimeout", 1000);
            getPrinter().setByteTimeout(connectParams.byteTimeout);

            getPrinter().setTaxPassword(reader.readInteger("taxPassword", 0));
            getPrinter().setUsrPassword(reader.readInteger("operatorPassword", 1));
            getPrinter().setSysPassword(reader.readInteger("sysAdminPassword", 30));

            connectParams.searchByPortEnabled = reader.readBoolean("searchByPortEnabled", false);
            connectParams.searchByBaudRateEnabled = reader.readBoolean("searchByBaudRateEnabled", true);
            pollInterval = reader.readInteger("pollInterval", 500);
            pollEnabled = reader.readBoolean("pollEnabled", true);

            payTypes.loadFromJposEntry(jposEntry);
            amountFactor = reader.readDouble("amountFactor", 1);
            quantityFactor = reader.readDouble("quantityFactor", 1);

            printerParams.stringEncoding = reader.readString("stringEncoding",
                    printerParams.stringEncoding);
            if (printer.getParams().stringEncoding == "") {
                printerParams.stringEncoding = System.getProperty("file.encoding");
            }
            printerParams.escCommandsEnabled = reader.readBoolean("escCommandsEnabled", false);

            logger.debug("stringEncoding: " + printerParams.stringEncoding);
            statisticFileName = reader.readString("statisticFileName", "ShtrihFiscalPrinter.xml");
            fieldsFileName = reader.readString("fieldsFileName", "");
            printerParams.barcodePrintTime = reader.readInteger("barcodePrintTime",
                    printerParams.defaultBarcodePrintTime);
            setNumHeaderLines(reader.readInteger("numHeaderLines", 5));
            setNumTrailerLines(reader.readInteger("numTrailerLines", 5));
            // paymentNames
            String paymentName;
            String propertyName;
            paymentNames.clear();
            for (int i = 1; i <= 4; i++) {
                propertyName = "paymentName" + String.valueOf(i);
                if (jposEntry.hasPropertyWithName(propertyName)) {
                    paymentName = (String) jposEntry.getPropertyValue(propertyName);
                    paymentNames.add(new FptrPaymentName(i, paymentName));
                }
            }

            statistics.load(statisticFileName);

            reportDevice = reader.readInteger("reportDevice",
                    SMFPTR_REPORT_DEVICE_EJ);
            reportType = reader.readInteger("reportType", SMFP_REPORT_TYPE_FULL);
            printerParams.statusCommand = reader.readInteger("statusCommand", SMFP_STATUS_COMMAND_DS);
            String messagesFileName = reader.readString("messagesFileName", "");
            Localizer.init(messagesFileName);

            boolean wrapText = reader.readBoolean("wrapText", true);
            getPrinter().setWrapText(wrapText);

            params.recCloseSleepTime = reader.readInteger("recCloseSleepTime", 0);
            recCloseWaitCount = reader.readInteger("recCloseWaitCount", 1);
            cutType = reader.readInteger("cutType", PrinterConst.SMFP_CUT_PARTIAL);

            int maxEnqNumber = reader.readInteger("maxEnqNumber", 10);
            device.setMaxEnqNumber(maxEnqNumber);

            int maxNakCommandNumber = reader.readInteger("maxNakCommandNumber", 3);
            device.setMaxNakCommandNumber(maxNakCommandNumber);

            int maxNakAnswerNumber = reader.readInteger("maxNakAnswerNumber", 3);
            device.setMaxNakAnswerNumber(maxNakAnswerNumber);

            int maxAckNumber = reader.readInteger("maxAckNumber", 3);
            device.setMaxAckNumber(maxAckNumber);

            int maxRepeatCount = reader.readInteger("maxRepeatCount", 3);
            device.setMaxRepeatCount(maxRepeatCount);

            createFilters(reader);

            xmlZReportEnabled = reader.readBoolean("XmlZReportEnabled", false);
            xmlZReportFileName = reader.readString("XmlZReportFileName", "ZReport.xml");

            csvZReportEnabled = reader.readBoolean("CsvZReportEnabled", false);
            csvZReportFileName = reader.readString("CsvZReportFileName", "ZReport.csv");
            maxReceiptNumber = reader.readInteger("maxReceiptNumber", defaultMaxReceiptNumber);
            autoOpenDrawer = reader.readBoolean("autoOpenDrawer", defaultAutoOpenDrawer);
            tableMode = reader.readInteger("tableMode", SMFPTR_TABLE_MODE_AUTO);
            cutMode = reader.readInteger("cutMode", SMFPTR_CUT_MODE_AUTO);
        }
        loadProperties(logicalName);
        state = JPOS_S_IDLE;
    }

    private PrinterPort createPort(JposEntry entry)
            throws Exception {
        logger.debug("createPort");
        String portClass = "com.shtrih.fiscalprinter.port.SerialPrinterPort";
        if (entry.hasPropertyWithName("portClass")) {
            portClass = (String) entry.getPropertyValue("portClass");
        }
        return PrinterPortFactory.createInstance(portClass);
    }

    private void createFilters(JposPropertyReader reader)
            throws Exception {
        boolean enabled = reader.readBoolean("ZeroPriceFilterEnabled", false);
        String time1Text = reader.readString("ZeroPriceFilterTime1", "21:00");
        String time2Text = reader.readString("ZeroPriceFilterTime2", "11:00");
        String errorText = reader.readString("ZeroPriceFilterErrorText", "");

        SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm");
        Date time1 = timeFormat.parse(time1Text);
        Date time2 = timeFormat.parse(time2Text);

        FiscalPrinterFilter113 filter = new ZeroPriceFilter(enabled,
                time1, time2, errorText);
        filters.clear();
        filters.add(filter);

        // Discount filter
        enabled = reader.readBoolean("DiscountFilterEnabled", false);
        if (enabled) {
            filter = new DiscountFilter(this);
            filters.add(filter);
        }
    }

    private JposEntry findEntry(JposRegPopulator populator, String logicalName) {
        JposEntry entry = null;
        Enumeration entries = populator.getEntries();
        while (entries.hasMoreElements()) {
            try {
                entry = (JposEntry) entries.nextElement();
                if (entry.getLogicalName().equalsIgnoreCase(logicalName)) {
                    break;
                }
            } catch (Exception e) {
                logger.error(e);
            }
        }

        if (entry == null) {
            entry = new SimpleEntry(logicalName, populator);
        }
        return entry;
    }

    public void release()
            throws Exception {
        saveProperties();
        checkClaimed();
        setDeviceEnabled(false);
        claimed = false;
        getPrinter().closePort();
    }

//////////////////////////////////////////////////////////////////////////////
//  Fiscal Document
//////////////////////////////////////////////////////////////////////////////
    private void noSlipStationError()
            throws Exception {
        throw new JposException(JPOS_E_ILLEGAL,
                Localizer.getString(Localizer.SlipStationNotPresent));
    }

    public void beginFiscalDocument(int documentAmount)
            throws Exception {
        checkEnabled();
        noSlipStationError();
    }

    public void printFiscalDocumentLine(String documentLine)
            throws Exception {
        checkEnabled();
        noSlipStationError();
    }

    public void endFiscalDocument()
            throws Exception {
        checkEnabled();
        noSlipStationError();
    }

//////////////////////////////////////////////////////////////////////////////
//  Fixed Output
//////////////////////////////////////////////////////////////////////////////
    public void beginFixedOutput(int station, int documentType)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void printFixedOutput(int documentType, int lineNumber,
            String data)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void endFixedOutput()
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

//////////////////////////////////////////////////////////////////////////////
//  Slip
//////////////////////////////////////////////////////////////////////////////
    public void beginInsertion(int timeout)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void beginItemList(int vatID)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void beginRemoval(int timeout)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void endInsertion()
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void endItemList()
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void endRemoval()
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

//////////////////////////////////////////////////////////////////////////////
//  NonFiscal
//////////////////////////////////////////////////////////////////////////////
    public void beginNonFiscal()
            throws Exception {
        checkOnLine();
        checkPrinterState(FPTR_PS_MONITOR);
        setPrinterState(FPTR_PS_NONFISCAL);
    }

    public void printNormalAsync(int station, String data)
            throws Exception {
        checkOnLine();
        data = decodeText(data);

        if ((printerState == FPTR_PS_NONFISCAL)
                || (isReceiptOpened() && ((fiscalReceiptType == FPTR_RT_CASH_IN)
                || (fiscalReceiptType == FPTR_RT_CASH_OUT)))) {
            receiptPrinter.printText(getStation(station), data,
                    printerParams.fontNumber);
        } else {
            throw new JposException(JPOS_E_ILLEGAL);
        }
    }

    public void printNormal(int station, String data)
            throws Exception {
        checkOnLine();
        execute(new PrintNormalRequest(station, data));
    }

    private void printHeaderLine(HeaderLine line)
            throws Exception {
        int lineFont = printerParams.fontNumber;
        if (line.getDoubleWidth()) {
            lineFont = 2;
        }
        getPrinter().printLine(SMFP_STATION_RECJRN, line.getText(), lineFont);
    }

    /** print minimum required header lines, cut and print others **/
    private void printHeader()
            throws Exception {
        int minHeaderLines = getModel().getMinHeaderLines();
        for (int i = 0; i < minHeaderLines; i++) {
            printHeaderLine(header.get(i));
        }
        cutPaper();
        for (int i = minHeaderLines; i < header.getNumLines(); i++) {
            printHeaderLine(header.get(i));
        }
    }

    private void cutPaper() throws Exception
    {
        if ((cutMode == SMFPTR_CUT_MODE_AUTO)&&(getModel().getCapCutter()))
        {
            getPrinter().cutPaper(cutType);
        }
    }
    
    private void printTrailer()
            throws Exception {
        for (int i = 0; i < trailer.getNumLines(); i++) {
            printHeaderLine(trailer.get(i));
        }
    }

    private void printDocEnd()
            throws Exception {
        for (int i = 0; i < recCloseWaitCount; i++) {
            getPrinter().waitForPrinting();
        }
        receiptImages.print(SMFPTR_LOGO_BEFORE_TRAILER, this);
        printTrailer();
        receiptImages.print(SMFPTR_LOGO_AFTER_TRAILER, this);
        if (additionalTrailer.length() > 0) {
            printText(SMFP_STATION_REC, additionalTrailer, getFontNumber());
        }
        receiptImages.print(SMFPTR_LOGO_AFTER_ADDTRAILER, this);
        printText(SMFP_STATION_REC, " ", getFontNumber());
        printHeader();
        receiptImages.print(SMFPTR_LOGO_AFTER_HEADER, this);
        header.printImage(this);
    }

    private void printReportEnd()
            throws Exception {
        getPrinter().waitForPrinting();
        receiptImages.print(SMFPTR_LOGO_BEFORE_TRAILER, this);
        printTrailer();
        receiptImages.print(SMFPTR_LOGO_AFTER_TRAILER, this);
        receiptImages.print(SMFPTR_LOGO_AFTER_ADDTRAILER, this);
        printText(SMFP_STATION_REC, " ", getFontNumber());
        printText(SMFP_STATION_REC, " ", getFontNumber());
        printHeader();
        receiptImages.print(SMFPTR_LOGO_AFTER_HEADER, this);
    }

    public void endNonFiscal()
            throws Exception {
        checkOnLine();
        checkPrinterState(FPTR_PS_NONFISCAL);
        setPrinterState(FPTR_PS_MONITOR);
        printDocEnd();
    }

//////////////////////////////////////////////////////////////////////////////
//  Training
//////////////////////////////////////////////////////////////////////////////
    public void beginTraining()
            throws Exception {
        checkEnabled();
        checkPrinterState(FPTR_PS_MONITOR);
        trainingModeActive = true;
    }

    public void endTraining()
            throws Exception {
        checkEnabled();
        if (trainingModeActive) {
            trainingModeActive = false;
        } else {
            throwWrongStateError();
        }
    }

//////////////////////////////////////////////////////////////////////////////
    public void clearError()
            throws Exception {
        checkEnabled();
        // !!!
    }

    public void clearOutput()
            throws Exception {
        checkClaimed();
        // Clears all buffered output data, including all asynchronous output
        // Also, when possible, halts outputs that are in progress
        synchronized (requests) {
            requests.clear();
        }
        // Any output error events that are enqueued –
        // usually waiting for FreezeEvents to
        // be set to false – are also cleared.
        synchronized (events) {
            for (int i = events.size() - 1; i >= 0; i--) {
                if (events.get(i) instanceof ErrorEventRequest) {
                    events.remove(i);
                }
            }
        }
        setState(JPOS_S_IDLE);
    }

    private String getDataFirmware(int[] optArgs)
            throws Exception {
        LongPrinterStatus status = getPrinter().readLongStatus();
        String result = "";
        switch (optArgs[0]) {
            // printer firmware version number
            case 0:
                result = status.getFirmwareVersion();
                break;

            // printer firmware build number
            case 1:
                result = Long.toString(status.getFirmwareBuild());
                break;

            // fiscal memory firmware version number
            case 2:
                result = status.getFMFirmwareVersion();
                break;

            // fiscal memory firmware build number
            case 3:
                result = Long.toString(status.getFMFirmwareBuild());
                break;

            default:
                result += "Printer firmware : " + status.getFirmwareVersion();
                result += ", build " + String.valueOf(status.getFirmwareBuild());
                result += " from " + status.getFirmwareDate().toString();
                result += " Fiscal memory firmware: " + status.getFMFirmwareVersion();
                result += ", build " + String.valueOf(status.getFMFirmwareBuild());
                result += " from " + status.getFMFirmwareDate().toString();
        }
        return result;
    }

    private long getSubtotal()
            throws Exception {
        long total = 0;
        PrinterStatus status = readPrinterStatus();
        if (status.getPrinterMode().isReceiptOpened()) {
            total = getPrinter().getSubtotal();
        }
        return total;
    }

    public String getTenderData(int optArg)
            throws Exception {
        switch (optArg) {
            // Cash
            case FPTR_PDL_CASH:
            // Cheque.
            case FPTR_PDL_CHEQUE:
            // Chitty.
            case FPTR_PDL_CHITTY:
            // Coupon.
            case FPTR_PDL_COUPON:
            // Currency.
            case FPTR_PDL_CURRENCY:
            case FPTR_PDL_DRIVEN_OFF:
            // Printer EFT.
            case FPTR_PDL_EFT_IMPRINTER:
            // Terminal EFT.
            case FPTR_PDL_EFT_TERMINAL:
            case FPTR_PDL_TERMINAL_IMPRINTER:
            // Gift.
            case FPTR_PDL_FREE_GIFT:
            // Giro.
            case FPTR_PDL_GIRO:
            // Home.
            case FPTR_PDL_HOME:
            case FPTR_PDL_IMPRINTER_WITH_ISSUER:
            // Local account.
            case FPTR_PDL_LOCAL_ACCOUNT:
            // Local card account.
            case FPTR_PDL_LOCAL_ACCOUNT_CARD:
            // Pay card.
            case FPTR_PDL_PAY_CARD:
            // Manual pay card.
            case FPTR_PDL_PAY_CARD_MANUAL:
            // Prepay.
            case FPTR_PDL_PREPAY:
            // Pump test.
            case FPTR_PDL_PUMP_TEST:
            // Credit.
            case FPTR_PDL_SHORT_CREDIT:
            // Staff.
            case FPTR_PDL_STAFF:
            // Voucher.
            case FPTR_PDL_VOUCHER:
                return "0";

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue)
                        + ", OptArgs");
        }
    }

    public int getLineCountData(int optArg)
            throws Exception {
        switch (optArg) {
            // Number of item lines.
            case FPTR_LC_ITEM:
                return 2;
            // Number of voided item lines.
            case FPTR_LC_ITEM_VOID:
                return 2;
            // Number of printDiscount lines.
            case FPTR_LC_DISCOUNT:
                return 2;
            // Number of voided printDiscount lines.
            case FPTR_LC_DISCOUNT_VOID:
                return 2;
            // Number of surcharge lines.
            case FPTR_LC_SURCHARGE:
                return 2;
            // Number of voided surcharge lines.
            case FPTR_LC_SURCHARGE_VOID:
                return 2;
            // Number of refund lines.
            case FPTR_LC_REFUND:
                return 2;
            // Number of voided refund lines.
            case FPTR_LC_REFUND_VOID:
                return 2;
            // Number of subtotal printDiscount lines.
            case FPTR_LC_SUBTOTAL_DISCOUNT:
                return 2;
            // Number of voided subtotal printDiscount lines.
            case FPTR_LC_SUBTOTAL_DISCOUNT_VOID:
                return 2;
            // Number of subtotal surcharge lines.
            case FPTR_LC_SUBTOTAL_SURCHARGE:
                return 2;
            // Number of voided subtotal surcharge lines.
            case FPTR_LC_SUBTOTAL_SURCHARGE_VOID:
                return 2;
            // Number of comment lines.
            case FPTR_LC_COMMENT:
                return 0;
            // Number of subtotal lines.
            case FPTR_LC_SUBTOTAL:
                return 1;
            // Number of total lines.
            case FPTR_LC_TOTAL:
                return 2;

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue)
                        + ", OptArgs");
        }
    }

    public int getDataDescriptionLength(int optArg)
            throws Exception {
        switch (optArg) {
            // printRecItem method
            case FPTR_DL_ITEM:
            // printRecItemAdjustment method.
            case FPTR_DL_ITEM_ADJUSTMENT:
            // printRecItemFuel method.
            case FPTR_DL_ITEM_FUEL:
            // printRecItemFuelVoid method.
            case FPTR_DL_ITEM_FUEL_VOID:
            // printRecNotPaid method.
            case FPTR_DL_NOT_PAID:
            // printRecPackageAdjustment method.
            case FPTR_DL_PACKAGE_ADJUSTMENT:
            // printRecRefund method.
            case FPTR_DL_REFUND:
            // printRecRefundVoid method.
            case FPTR_DL_REFUND_VOID:
            // printRecSubtotalAdjustment method.
            case FPTR_DL_SUBTOTAL_ADJUSTMENT:
            // printRecTotal method.
            case FPTR_DL_TOTAL:
            // printRecVoid method.
            case FPTR_DL_VOID:
            // printRecItemVoid and printRecItemAdjustmentVoid methods.
            case FPTR_DL_VOID_ITEM:
                return getModel().getTextLength(getFontNumber());

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue)
                        + ", OptArgs");
        }
    }

    public void getData(int dataItem, int[] optArgs, String[] data)
            throws Exception {
        checkOnLine();
        String result = "";
        LongPrinterStatus status;

        long number;
        switch (dataItem) {
            // Get the Fiscal Printer’s firmware release number.
            case FPTR_GD_FIRMWARE:
                result = getDataFirmware(optArgs);
                break;

            // Get the Fiscal Printer’s fiscal ID.
            case FPTR_GD_PRINTER_ID:
                status = getPrinter().readLongStatus();
                result = status.getFiscalIDText();
                break;

            // Get the Fiscal Printer’s fiscal ID.
            case FPTR_GD_CURRENT_TOTAL:
                result = amountToString(getSubtotal());
                break;

            // Get the daily total.
            case FPTR_GD_DAILY_TOTAL:

                result = amountToString(printer.readCashRegister(241));
                break;

            // Get the Fiscal Printer’s grand total.
            case FPTR_GD_GRAND_TOTAL:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue) + "dataItem");

            // Get the total number of voided receipts.
            case FPTR_GD_MID_VOID:
                result = Long.toString(printer.readOperationRegister(166));
                break;

            // Get the current total of not paid receipts.
            case FPTR_GD_NOT_PAID:
                result = "0";
                break;

            // Get the number of fiscal receipts printed
            case FPTR_GD_RECEIPT_NUMBER:
                status = getPrinter().readLongStatus();
                PrinterMode mode = status.getPrinterMode();
                int documentNumber = status.getDocumentNumber();
                result = Long.toString(documentNumber);
                break;

            // Get the current total of refunds.
            case FPTR_GD_REFUND:
                result = Long.toString(printer.readOperationRegister(146));
                break;

            // Get the current total of voided refunds.
            case FPTR_GD_REFUND_VOID:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue) + "dataItem");

            // Get the number of daily fiscal documents.
            case FPTR_GD_FISCAL_DOC:
                result = Long.toString(fiscalDay.getFiscalDocNumber());
                break;

            // Get the number of daily voided fiscal documents
            case FPTR_GD_FISCAL_DOC_VOID:
                result = Long.toString(fiscalDay.getFiscalDocVoidNumber());
                break;

            // Get the number of daily fiscal sales receipts
            case FPTR_GD_FISCAL_REC:
                number =
                        getPrinter().readOperationRegister(144)
                        + getPrinter().readOperationRegister(145)
                        + getPrinter().readOperationRegister(146)
                        + getPrinter().readOperationRegister(147);

                result = Long.toString(number);
                break;

            case FPTR_GD_FISCAL_REC_VOID:
                number =
                        getPrinter().readOperationRegister(179)
                        + getPrinter().readOperationRegister(180)
                        + getPrinter().readOperationRegister(181)
                        + getPrinter().readOperationRegister(182);

                result = Long.toString(number);
                break;

            case FPTR_GD_NONFISCAL_DOC:
                result = Long.toString(fiscalDay.getNonFiscalDocNumber());
                break;

            case FPTR_GD_NONFISCAL_DOC_VOID:
                result = Long.toString(fiscalDay.getNonFiscalDocVoidNumber());
                break;

            case FPTR_GD_NONFISCAL_REC:
                result = Long.toString(fiscalDay.getNonFiscalRecNumber());
                break;

            case FPTR_GD_SIMP_INVOICE:
                result = Long.toString(fiscalDay.getSimpInvoiceNumber());
                break;

            case FPTR_GD_Z_REPORT:
                status = getPrinter().readLongStatus();
                // Printer is not fiscalized
                if (status.getRegistrationNumber() == 0) {
                    // Z-report number before fiscalization
                    result = Long.toString(printer.readOperationRegister(159));
                } else {
                    result = Long.toString(status.getDayNumber());
                }
                break;

            case FPTR_GD_TENDER:
                result = getTenderData(optArgs[0]);
                break;

            case FPTR_GD_LINECOUNT:
                result = String.valueOf(getLineCountData(optArgs[0]));
                break;

            case FPTR_GD_DESCRIPTION_LENGTH:
                result = String.valueOf(getDataDescriptionLength(optArgs[0]));
                break;


            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue) + "dataItem");
        }
        data[0] = encodeText(result);
    }

    public void getDate(String[] Date)
            throws Exception {
        checkOnLine();
        String result = "";
        if (Date.length < 1) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue) + "Date");
        }

        switch (dateType) {
            // Date of last end of day.
            case FPTR_DT_EOD:
                ReadFMLastRecordDate lastFmRecordDate = getPrinter().readFMLastRecordDate();
                if (lastFmRecordDate.getRecordType() == 1) {
                    PrinterDate date = lastFmRecordDate.getRecordDate();
                    JposFiscalPrinterDate jposDate =
                            new JposFiscalPrinterDate(
                            date.getDay(),
                            date.getMonth(),
                            date.getYear() + 2000,
                            0,
                            0);
                    result = jposDate.toString();
                }
                break;

            // Real time clock of the Fiscal Printer.
            case FPTR_DT_RTC:
                LongPrinterStatus status = getPrinter().readLongStatus();
                PrinterDate printerDate = status.getDate();
                PrinterTime printerTime = status.getTime();

                JposFiscalPrinterDate jposDate = new JposFiscalPrinterDate(
                        printerDate.getDay(),
                        printerDate.getMonth(),
                        printerDate.getYear() + 2000,
                        printerTime.getHour(),
                        printerTime.getMin());
                result = jposDate.toString();
                break;

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue) + "DateType");
        }
        Date[0] = encodeText(result);
    }

    public void getTotalizer(int vatID, int optArgs, String[] data)
            throws Exception {
        checkOnLine();
        String result = "";
        /*
        switch (totalizerType)
        {
        case FPTR_TT_DAY:
        case FPTR_TT_DOCUMENT:
        case FPTR_TT_RECEIPT:
        case FPTR_TT_GRAND:
        
        switch (optArgs)
        {
        case FPTR_GT_GROSS:
        data[0] = String.valueOf(printer.getTotal());
        break;
        
        case FPTR_GT_DISCOUNT:
        GetDiscountTotalizer(data);
        break;
        
        case FPTR_GT_ITEM:
        GetItemTotalizer(data);
        break;
        
        case FPTR_GT_REFUND:
        GetRefundTotalizer(data);
        break;
        
        case FPTR_GT_SURCHARGE:
        GetSurchargeTotalizer(data);
        break;
        
        default:
        throw new JposException(JPOS_E_ILLEGAL,
        "Invalid optArgs parameter value" );
        }
        }
         */
    }

    private boolean isSalesReceipt() {
        switch (fiscalReceiptType) {
            case FPTR_RT_SALES:
            case FPTR_RT_GENERIC:
            case FPTR_RT_SERVICE:
            case FPTR_RT_SIMPLE_INVOICE:
            case FPTR_RT_REFUND:
                return true;
            default:
                return false;
        }
    }

    private void dayEndRequiredError()
            throws Exception {
        //throw new JposException(JPOS_E_FAILURE, "Day end required");
        throw new JposException(JPOS_E_EXTENDED, JPOS_EFPTR_DAY_END_REQUIRED,
                "Day end required");
    }

    private void checkDayEnd()
            throws Exception {
        PrinterStatus status = readPrinterStatus();
        if (status.getPrinterMode().isDayEndRequired()) {
            dayEndRequiredError();
        }
    }

//////////////////////////////////////////////////////////////////////////////
//  Fiscal Receipt
//////////////////////////////////////////////////////////////////////////////
    public void beginFiscalReceipt(boolean printHeader)
            throws Exception {
        checkOnLine();
        checkPrinterState(FPTR_PS_MONITOR);
        // Cancel receipt if it opened
        cancelReceipt();
        // check end of day
        if (isSalesReceipt()) {
            checkDayEnd();
        }

        fiscalDay.open();

        if (additionalHeader.length() > 0) {
            printText(SMFP_STATION_REC, additionalHeader, getFontNumber());
        }

        setPrinterState(FPTR_PS_FISCAL_RECEIPT);
        dayOpened = true;
        printItems.clear();
        fiscalReceipt.beginFiscalReceipt(printHeader);
    }

    public void endFiscalReceipt(boolean printHeader)
            throws Exception {
        logger.debug("endFiscalReceipt");

        checkOnLine();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT_ENDING);

        if (fiscalReceipt.isCancelled()) {
            receiptPrinter.printText(params.TEXT_REC_CANCEL);
        } else {
            fiscalReceipt.endFiscalReceipt(printHeader);
        }
        try {
            printRecMessages();
            printDocEnd();
        } catch (Exception e) {
            // ignore print errors because cashin is succeeded
            logger.error("printCashIn: " + e.getMessage());
        }
        setPrinterState(FPTR_PS_MONITOR);
    }

    private void printRecMessages()
            throws Exception {
        while (printItems.size() > 0) {
            PrintItem item = (PrintItem) printItems.get(0);
            item.print(this);
            printItems.remove(0);
        }
    }

    public void printDuplicateReceipt()
            throws Exception {
        checkOnLine();
        checkPrinterState(FPTR_PS_MONITOR);
        if (!getCapDuplicateReceipt()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.ReceiptDuplicationNotSupported));
        }
        getPrinter().duplicateReceipt();
        printDocEnd();
        duplicateReceipt = false;
    }

    /**************************************************************************
    Prints a report of totals for a range of dates on the receipt.
    This method is always performed synchronously.
    The dates are strings in the format “ddmmyyyyhhmm”, where:
    dd day of the month (1 - 31)
    mm month (1 - 12)
    yyyy year (1997-)
    hh hour (0-23)
    mm minutes (0-59)
     ***************************************************************************/
    public void printPeriodicTotalsReport(String date1, String date2)
            throws Exception {
        checkOnLine();
        checkStateBusy();
        checkPrinterState(FPTR_PS_MONITOR);

        date1 = decodeText(date1);
        date2 = decodeText(date2);

        PrinterDate printerDate1 =
                JposFiscalPrinterDate.valueOf(date1).getPrinterDate();
        PrinterDate printerDate2 =
                JposFiscalPrinterDate.valueOf(date2).getPrinterDate();

        if (reportDevice == SMFPTR_REPORT_DEVICE_EJ) {
            EJDate d1 = new EJDate(printerDate1);
            EJDate d2 = new EJDate(printerDate2);
            getPrinter().printEJDayReportOnDates(d1, d2, reportType);
        } else {
            getPrinter().printFMReportDates(printerDate1, printerDate2, reportType);
        }
        printReportEnd();
    }

    public void printPowerLossReport()
            throws Exception {
        checkEnabled();
        checkOnLine();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    private void checkQuantity(int value)
            throws Exception {
        if (value < 0) {
            throw new JposException(JPOS_E_EXTENDED,
                    JPOS_EFPTR_BAD_ITEM_QUANTITY);
        }
    }

    private void checkPrice(long value)
            throws Exception {
        if (value < 0) {
            throw new JposException(JPOS_E_EXTENDED,
                    JPOS_EFPTR_BAD_PRICE);
        }
    }

    private void checkSalesReceipt()
            throws Exception {
        if (!isSalesReceipt()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.MethodNotSupported));
        }
    }

    private void checkLongParam(long Value, long minValue, long maxValue,
            String propName)
            throws Exception {
        if (Value < minValue) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue) + propName);
        }
        if (Value > maxValue) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue) + propName);
        }
    }

    private void checkVatInfo(long value)
            throws Exception {
        checkLongParam(value, 0, 4, "VatInfo");
    }

    private void checkReceiptStation()
            throws Exception {
        if (fiscalReceiptStation != FPTR_RS_RECEIPT) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue) + "fiscalReceiptStation");
        }
    }

    private String printDescription(String description)
            throws Exception {
        String result = "";
        checkOnLine();
        String[] lines = parseText(description);
        if (lines.length == 1) {
            result = lines[0];
        } else {
            for (int i = 0; i < lines.length; i++) {
                getPrinter().printLine(SMFP_STATION_REC, lines[i], getFontNumber());
            }
        }
        return result;
    }

    private long convertAmount(long value) {
        return Math.abs((long) (value * amountFactor));
    }

    private int convertQuantity(int value) {
        return (int) (value * quantityFactor);
    }

    public void printRecItemAsync(String description, long price, int quantity,
            int vatInfo, long unitPrice, String unitName)
            throws Exception {
        unitName = decodeText(unitName);
        description = decodeText(description);
        price = convertAmount(price);
        unitPrice = convertAmount(unitPrice);
        quantity = convertQuantity(quantity);

        checkEnabled();
        checkSalesReceipt();
        checkReceiptStation();
        checkQuantity(quantity);
        checkPrice(price);
        checkPrice(unitPrice);
        checkVatInfo(vatInfo);

        fiscalReceipt.printRecItem(
                description,
                price,
                quantity,
                vatInfo,
                unitPrice,
                unitName);
    }

    public void printRecItem(
            String description,
            long price,
            int quantity,
            int vatInfo,
            long unitPrice,
            String unitName)
            throws Exception {
        filters.printRecItem(description, price, quantity,
                vatInfo, unitPrice, unitName);

        checkOnLine();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        execute(new PrintRecItemRequest(description, price, quantity, vatInfo,
                unitPrice, unitName));
    }

    public void doPrintRecMessage(
            int station,
            int font,
            String message)
            throws Exception {
        fiscalReceipt.printRecMessage(station, font, message);
    }

    public void printRecMessageAsync(
            int station,
            int font,
            String message)
            throws Exception {
        message = decodeText(message);
        if (isReceiptEnding()) {
            printItems.add(new TextLine(station, font, message));
        } else {
            doPrintRecMessage(station, font, message);
        }
    }

    public boolean isReceiptEnding() {
        return printerState == FiscalPrinterConst.FPTR_PS_FISCAL_RECEIPT_ENDING;
    }

    public void printRecMessageAsync(String message)
            throws Exception {
        printRecMessageAsync(PrinterConst.SMFP_STATION_RECJRN,
                getFontNumber(), message);
    }

    public void printRecMessage(String message)
            throws Exception {
        checkOnLine();
        execute(new PrintRecMessageRequest(message));
    }

    private void checkPercents(long amount)
            throws Exception {
        if ((amount < 0) || (amount > 10000)) {
            throw new JposException(JPOS_E_EXTENDED,
                    JPOS_EFPTR_BAD_ITEM_AMOUNT);
        }
    }

    public void checkAdjustment(int adjustmentType, long amount)
            throws Exception {
        switch (adjustmentType) {
            case FPTR_AT_AMOUNT_DISCOUNT:
            case FPTR_AT_AMOUNT_SURCHARGE:
                break;

            case FPTR_AT_PERCENTAGE_DISCOUNT:
            case FPTR_AT_PERCENTAGE_SURCHARGE:
                checkPercents(amount);
                break;

            default: {
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue) + "adjustmentType");
            }
        }
    }

    public void printRecItemAdjustmentAsync(int adjustmentType,
            String description, long amount, int vatInfo)
            throws Exception {
        description = decodeText(description);
        amount = convertAmount(amount);

        checkEnabled();
        checkSalesReceipt();
        checkVatInfo(vatInfo);
        checkAdjustment(adjustmentType, amount);

        // filter request
        PrintRecItemAdjustmentRequest request =
                new PrintRecItemAdjustmentRequest(adjustmentType,
                description, amount, vatInfo);
        request = filters.printRecItemAdjustment(request);
        adjustmentType = request.getAdjustmentType();
        description = request.getDescription();
        amount = request.getAmount();
        vatInfo = request.getVatInfo();

        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecItemAdjustment(adjustmentType,
                description, amount, vatInfo);
    }

    public void printRecItemAdjustment(int adjustmentType,
            String description, long amount, int vatInfo)
            throws Exception {
        checkOnLine();
        execute(new PrintRecItemAdjustmentRequest(adjustmentType, description,
                amount, vatInfo));
    }

    private String amountToString(double amount) {
        DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.getDefault());
        symbols.setDecimalSeparator('.');
        DecimalFormat formatter = new DecimalFormat("0.00", symbols);
        return formatter.format(amount / 100.0);
    }

    private void printCurrency(String text, long amount)
            throws Exception {
        printStrings(text, amountToString(amount));
    }

    private String formatStrings(String line1, String line2)
            throws Exception {
        int len;
        String S = "";
        len = getModel().getTextLength(getFontNumber()) - line2.length();

        for (int i = 0; i < len; i++) {
            if (i < line1.length()) {
                S = S + line1.charAt(i);
            } else {
                S = S + " ";
            }
        }
        return S + line2;
    }

    private void printStrings(String line1, String line2)
            throws Exception {
        checkOnLine();
        printText(SMFP_STATION_REC, formatStrings(line1, line2),
                getFontNumber());
    }

    public void printRecItemFuelAsync(String description, long price,
            int quantity, int vatInfo, long unitPrice, String unitName,
            long specialTax, String specialTaxName)
            throws Exception {
    }

    public void printRecItemFuelVoidAsync(String description, long price,
            int vatInfo, long specialTax)
            throws Exception {
    }

    public void printRecNotPaidAsync(String description, long amount)
            throws Exception {
    }

    public void printRecNotPaid(String description, long amount)
            throws Exception {
        checkOnLine();
        description = decodeText(description);
        amount = convertAmount(amount);
        throw new JposException(JPOS_E_ILLEGAL,
                Localizer.getString(Localizer.NotPaidReceiptsNotSupported));
    }

    public void printRecRefund(String description, long amount, int vatInfo)
            throws Exception {
        checkOnLine();
        execute(new PrintRecRefundRequest(description, amount, vatInfo));
    }

    public void printRecRefundAsync(String description, long amount, int vatInfo)
            throws Exception {
        description = decodeText(description);
        amount = convertAmount(amount);

        checkEnabled();
        checkSalesReceipt();
        checkVatInfo(vatInfo);

        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecRefund(description, amount, vatInfo);
    }

    private void checkTotal(long total)
            throws Exception {
        // update receipt sum
        long receiptSubtotal = getSubtotal();
        if (params.checkTotal && isReceiptOpened) {
            if (total < 0) {
                // negative receipt total
                throw new JposException(JPOS_E_EXTENDED,
                        JPOS_EFPTR_NEGATIVE_TOTAL);
            }
            // compare totals
            if (receiptSubtotal != total) {
                logger.error("Totals compare failed!");
                logger.debug("Receipt total is " + String.valueOf(receiptSubtotal));
                logger.debug("Application total is " + String.valueOf(total));

                cancelReceipt();
                setPrinterState(FPTR_PS_MONITOR);
                throw new JposException(JPOS_E_EXTENDED,
                        JPOS_EFPTR_BAD_ITEM_AMOUNT);
            }
        }
    }

    public void printRecSubtotal(long amount)
            throws Exception {
        checkOnLine();
        execute(new PrintRecSubtotalRequest(amount));
    }

    public void printRecSubtotalAsync(long amount)
            throws Exception {
        amount = convertAmount(amount);

        checkEnabled();
        checkSalesReceipt();

        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        checkTotal(amount);
        fiscalReceipt.printRecSubtotal(amount);
    }

    public void printRecSubtotalAdjustment(int adjustmentType,
            String description, long amount)
            throws Exception {
        checkOnLine();
        execute(new PrintRecSubtotalAdjustmentRequest(adjustmentType,
                description, amount));
    }

    public void printRecSubtotalAdjustmentAsync(int adjustmentType,
            String description, long amount)
            throws Exception {
        description = decodeText(description);
        amount = convertAmount(amount);

        checkEnabled();
        checkAdjustment(adjustmentType, amount);
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecSubtotalAdjustment(
                adjustmentType, description, amount);
    }

    public void printRecTotal(long total, long payment, String description)
            throws Exception {
        checkOnLine();
        execute(new PrintRecTotalRequest(total, payment, description));
    }

    public void printRecTotalAsync(long total, long payment, String description)
            throws Exception {
        logger.debug("printRecTotal");

        total = convertAmount(total);
        payment = convertAmount(payment);
        description = decodeText(description);

        checkEnabled();

        if ((printerState != FPTR_PS_FISCAL_RECEIPT)
                & (printerState != FPTR_PS_FISCAL_RECEIPT_TOTAL)) {
            throwWrongStateError();
        }
        checkTotal(total);

        long payType;
        try {
            payType = Integer.parseInt(description);
        } catch (NumberFormatException e) {
            throw new JposException(JposConst.JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue)
                    + "description");
        }
        payType = getPayType(payType);
        fiscalReceipt.printRecTotal(total, payment, payType, description);
        if (fiscalReceipt.isPayed()) {
            setPrinterState(FPTR_PS_FISCAL_RECEIPT_ENDING);
        } else {
            setPrinterState(FPTR_PS_FISCAL_RECEIPT_TOTAL);
        }
    }

    public void printRecVoidAsync(String description)
            throws Exception {
        description = decodeText(description);
        if ((printerState == FPTR_PS_FISCAL_RECEIPT)
                || (printerState == FPTR_PS_FISCAL_RECEIPT_ENDING)) {
            fiscalReceipt.printRecVoid(description);
            setPrinterState(FPTR_PS_FISCAL_RECEIPT_ENDING);
        } else {
            throwWrongStateError();
        }
    }

    public void printRecVoid(String description)
            throws Exception {
        checkOnLine();
        execute(new PrintRecVoidRequest(description));
    }

    public void printRecVoidItem(String description, long amount,
            int quantity, int adjustmentType, long adjustment, int vatInfo)
            throws Exception {
        checkOnLine();
        description = decodeText(description);
        amount = convertAmount(amount);
        quantity = convertQuantity(quantity);

        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        checkQuantity(quantity);
        checkVatInfo(vatInfo);
        fiscalReceipt.printRecVoidItem(description, amount, quantity,
                adjustmentType, adjustment, vatInfo);
    }

    /*
    Remarks: Prints a report of the fiscal EPROM contents on the
    receipt that occurred between two end points.
    This method is always performed synchronously
     */
    public void printReport(int reportType, String startNum, String endNum)
            throws Exception {
        checkOnLine();
        startNum = decodeText(startNum);
        endNum = decodeText(endNum);

        checkEnabled();
        checkPrinterState(FPTR_PS_MONITOR);

        int day1 = 0;
        int day2 = 0;
        switch (reportType) {
            case FPTR_RT_ORDINAL:
            case FPTR_RT_EOD_ORDINAL:
                day1 = stringParamToInt(startNum, "startNum");
                day2 = stringParamToInt(endNum, "endNum");
                if (reportDevice == SMFPTR_REPORT_DEVICE_EJ) {
                    getPrinter().printEJReportDays(day1, day2, this.reportType);
                } else {
                    getPrinter().printFMReportDays(day1, day2, this.reportType);
                }
                printReportEnd();
                break;

            case FPTR_RT_DATE:
                PrinterDate date1;
                PrinterDate date2;
                // pase dates
                date1 = JposFiscalPrinterDate.valueOf(startNum).getPrinterDate();
                date2 = JposFiscalPrinterDate.valueOf(endNum).getPrinterDate();
                // print report
                if (reportDevice == SMFPTR_REPORT_DEVICE_EJ) {
                    getPrinter().printEJDayReportOnDates(new EJDate(date1),
                            new EJDate(date2), this.reportType);
                } else {
                    getPrinter().printFMReportDates(date1, date2, this.reportType);
                }
                printReportEnd();
                break;

            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue)
                        + "reportType");
        }
    }

    public void printXReport()
            throws Exception {
        checkOnLine();
        checkStateBusy();
        checkPrinterState(FPTR_PS_MONITOR);
        getPrinter().printXReport();
        printReportEnd();
    }

    public void printZReport()
            throws Exception {
        checkOnLine();
        checkStateBusy();
        checkPrinterState(FPTR_PS_MONITOR);

        printEmptyReceipt();
        saveZReportXml();

        PrinterStatus status = readPrinterStatus();
        if (status.getPrinterMode().canPrintZReport()) {
            getPrinter().printZReport();
            fiscalDay.close();
            printReportEnd();
            dayOpened = false;
        } else {
            throw new JposException(JPOS_E_ILLEGAL);
        }
    }

    private void saveZReportXml()
            throws Exception {
        if (xmlZReportEnabled || csvZReportEnabled) {
            try {
                RegisterReport report = new RegisterReport();
                RegisterReportReader.execute(report, printer);
                if (xmlZReportEnabled) {
                    try {
                        XmlRegisterReportWriter.execute(report, xmlZReportFileName);
                    } catch (Exception e) {
                        logger.error("Error saving file", e);
                    }
                }
                if (csvZReportEnabled) {
                    try {
                        CsvRegisterReportWriter.execute(report, csvZReportFileName);
                    } catch (Exception e) {
                        logger.error("Error saving file", e);
                    }
                }
            } catch (Exception e) {
                logger.error("Error saving file", e);
                throw new JposException(JPOS_E_FAILURE, e.getMessage());
            }
        }
    }

    // If day is closed - print empty receipt
    private void printEmptyReceipt()
            throws Exception {
        PrinterStatus status = readPrinterStatus();
        if (status.getPrinterMode().isDayClosed()) {
            getPrinter().printSale(0, 0, params.department, 0, 0, 0, 0, "");
            getPrinter().closeReceipt(0, 0, 0, 0, 0, 0, 0, 0, 0, "");
            printDocEnd();
        }
    }

    public void resetPrinter()
            throws Exception {
        checkOnLine();
        cancelReceipt();
        receiptType = 0;
        isReceiptOpened = false;
    }

    public void setDate(String date)
            throws Exception {
        checkOnLine();
        date = decodeText(date);

        checkEnabled();
        PrinterStatus status = readPrinterStatus();
        if (!status.getPrinterMode().isDayClosed()) {
            dayEndRequiredError();
        }

        JposFiscalPrinterDate jposDate = JposFiscalPrinterDate.valueOf(date);
        PrinterDate printerDate = jposDate.getPrinterDate();
        PrinterTime printerTime = jposDate.getPrinterTime();

        getPrinter().check(printer.writeDate(printerDate));
        int resultCode = getPrinter().confirmDate(printerDate);
        if (resultCode != 0) {
            // try to set date back
            printerDate = getPrinter().readLongStatus().getDate();
            getPrinter().check(printer.confirmDate(printerDate));

            throw new Exception(
                    Localizer.getString(Localizer.FailedConfirmDate)
                    + PrinterError.getFullText(resultCode));
        }
        getPrinter().writeTime(printerTime);
        // check if date and time was set correctly
        LongPrinterStatus fullStatus = getPrinter().readLongStatus();
        if (!PrinterDate.compare(printerDate, fullStatus.getDate())) {
            logger.error("Failed to set printer date: "
                    + PrinterDate.toText(printerDate) + " <> "
                    + PrinterDate.toText(fullStatus.getDate()));
        }
        PrinterTime time = new PrinterTime(
                fullStatus.getTime().getHour(),
                fullStatus.getTime().getMin(),
                0);
        if (!PrinterTime.compare(printerTime, time)) {
            logger.error("Failed to set printer time: "
                    + PrinterTime.toString(printerTime) + " <> "
                    + PrinterTime.toString(fullStatus.getTime()));
        }
    }

    public void setHeaderLine(int lineNumber, String text,
            boolean doubleWidth)
            throws Exception {
        checkOnLine();
        text = decodeText(text);
        logger.debug("setHeaderLine: " + text);

        checkEnabled();
        // reset graphic info
        if (lineNumber == 1) {
            graphicsLine = 1;
        }
        header.setLine(lineNumber, text, doubleWidth);
        saveProperties();
    }

    public void setPOSID(String POSID, String cashierID)
            throws Exception {
        checkOnLine();
        POSID = decodeText(POSID);
        cashierID = decodeText(cashierID);

        checkEnabled();
        // write cashier ID
        getPrinter().writeTable(SMFP_TABLE_CASHIER,
                readPrinterStatus().getOperatorNumber(), 2, cashierID);
    }

    public void setStoreFiscalID(String ID)
            throws Exception {
        checkOnLine();
        ID = decodeText(ID);
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void setTrailerLine(int lineNumber, String text,
            boolean doubleWidth)
            throws Exception {
        checkOnLine();
        text = decodeText(text);
        checkEnabled();
        trailer.setLine(lineNumber, text, doubleWidth);
        saveProperties();
    }

    /**************************************************************************
    
    Gets the rate associated with a given VAT identifier.
    
    Parameter Description
    
    vatID   - VAT identifier of the required rate.
    optArgs - For some countries, this additional argument may be
    needed. Consult the Fiscal Printer Service vendor's
    documentation for details.
    vatRate - The rate associated with the VAT identifier
    
     **************************************************************************/
    public void getVatEntry(int vatID, int optArgs, int[] vatRate)
            throws Exception {
        checkOnLine();
        // 4 tax rates available in SHTRIH-M fiscal printers
        checkParamValue(vatID, 1, vatValues.length, "vatID");
        String[] vatValue = new String[1];
        vatValue[0] = "";

        getPrinter().check(printer.readTable(SMFP_TABLE_TAX, vatID, 1, vatValue));
        vatRate[0] = Integer.parseInt(vatValue[0]);
    }

    public void setVatTable()
            throws Exception {
        checkOnLine();
        for (int i = 0; i < vatValues.length; i++) {
            getPrinter().check(printer.writeTable(SMFP_TABLE_TAX, i + 1, 1,
                    String.valueOf(vatValues[i])));
        }
    }

    public void setVatValue(int vatID, String vatValue)
            throws Exception {
        checkOnLine();
        vatValue = decodeText(vatValue);
        // 4 tax rates available in SHTRIH-M fiscal printers
        checkParamValue(vatID, 1, vatValues.length, "vatID");
        int intVatValue = Integer.parseInt(vatValue);
        checkParamValue(intVatValue, 0, 10000, "vatValue");
        vatValues[vatID - 1] = intVatValue;
    }

    public void verifyItem(String itemName, int vatID)
            throws Exception {
        checkOnLine();
        itemName = decodeText(itemName);
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void setCurrency(int newCurrency)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void printRecCashAsync(long amount)
            throws Exception {
        amount = convertAmount(amount);
        checkEnabled();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecCash(amount);
    }

    public void printRecCash(long amount)
            throws Exception {
        checkOnLine();
        execute(new PrintRecCashRequest(amount));
    }

    public void printRecItemFuel(
            String description,
            long price,
            int quantity,
            int vatInfo,
            long unitPrice,
            String unitName,
            long specialTax,
            String specialTaxName)
            throws Exception {
        unitName = decodeText(unitName);
        description = decodeText(description);
        specialTaxName = decodeText(specialTaxName);

        price = convertAmount(price);
        quantity = convertQuantity(quantity);
        unitPrice = convertAmount(unitPrice);

        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void printRecItemFuelVoid(
            String description,
            long price,
            int vatInfo,
            long specialTax)
            throws Exception {
        price = convertAmount(price);
        description = decodeText(description);

        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    private void checkAdjustments(int adjustmentType,
            PackageAdjustments adjustments)
            throws Exception {
        PackageAdjustment adjustment;
        for (int i = 0; i < adjustments.size(); i++) {
            adjustment = adjustments.getItem(i);
            checkAdjustment(adjustmentType, adjustment.amount);
        }
    }

    public void printRecPackageAdjustment(int adjustmentType,
            String description, String vatAdjustment)
            throws Exception {
        checkOnLine();
        execute(new PrintRecPackageAdjustmentRequest(
                adjustmentType, description, vatAdjustment));
    }

    public void printRecPackageAdjustmentAsync(int adjustmentType,
            String description, String vatAdjustment)
            throws Exception {
        description = decodeText(description);
        vatAdjustment = decodeText(vatAdjustment);

        checkEnabled();
        checkSalesReceipt();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecPackageAdjustment(adjustmentType,
                description, vatAdjustment);
    }

    public void printRecPackageAdjustVoid(int adjustmentType,
            String vatAdjustment)
            throws Exception {
        execute(new PrintRecPackageAdjustVoidRequest(adjustmentType,
                vatAdjustment));
    }

    public void printRecPackageAdjustVoidAsync(int adjustmentType,
            String vatAdjustment)
            throws Exception {
        vatAdjustment = decodeText(vatAdjustment);
        checkEnabled();
        checkSalesReceipt();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecPackageAdjustVoid(adjustmentType, vatAdjustment);
    }

    public void printRecRefundVoid(String description, long amount,
            int vatInfo)
            throws Exception {
        execute(new PrintRecRefundVoidRequest(description, amount, vatInfo));
    }

    public void printRecRefundVoidAsync(String description, long amount,
            int vatInfo)
            throws Exception {
        description = decodeText(description);
        amount = convertAmount(amount);

        checkEnabled();
        checkSalesReceipt();
        checkVatInfo(vatInfo);
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecRefundVoid(description, amount, vatInfo);
    }

    public void printRecSubtotalAdjustVoid(int adjustmentType, long amount)
            throws Exception {
        execute(new PrintRecSubtotalAdjustVoidRequest(adjustmentType, amount));
    }

    public void printRecSubtotalAdjustVoidAsync(int adjustmentType, long amount)
            throws Exception {
        amount = convertAmount(amount);
        checkEnabled();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        fiscalReceipt.printRecSubtotalAdjustVoid(adjustmentType, amount);
    }

    public void printRecTaxID(String taxID)
            throws Exception {
        execute(new PrintRecTaxIDRequest(taxID));
    }

    public void printRecTaxIDAsync(String taxID)
            throws Exception {
        checkEnabled();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT_ENDING);
        printText(SMFP_STATION_REC, decodeText(taxID), getFontNumber());
    }

    public int getAmountDecimalPlaces()
            throws Exception {
        checkEnabled();
        return getModel().getAmountDecimalPlace();
    }

    public boolean getCapStatisticsReporting()
            throws Exception {
        checkOpened();
        return capStatisticsReporting;
    }

    public boolean getCapUpdateStatistics()
            throws Exception {
        checkOpened();
        return capUpdateStatistics;
    }

    public void resetStatistics(String statisticsBuffer)
            throws Exception {
        statisticsBuffer = decodeText(statisticsBuffer);
        checkEnabled();
        statistics.reset(statisticsBuffer);
    }

    public void retrieveStatistics(String[] statisticsBuffer)
            throws Exception {
        checkEnabled();
        statistics.retrieve(statisticsBuffer);
    }

    public void updateStatistics(String statisticsBuffer)
            throws Exception {
        checkEnabled();
        statistics.update(statisticsBuffer);
    }

    public boolean getCapCompareFirmwareVersion()
            throws Exception {
        checkOpened();
        return capCompareFirmwareVersion;
    }

    public boolean getCapUpdateFirmware()
            throws Exception {
        checkOpened();
        return capUpdateFirmware;
    }

    public void compareFirmwareVersion(String firmwareFileName, int[] result)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void updateFirmware(String firmwareFileName)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    // 1.11
    // Capabilities
    public boolean getCapPositiveSubtotalAdjustment()
            throws Exception {
        return capPositiveSubtotalAdjustment;
    }

    // Methods
    public void printRecItemVoidAsync(String description, long price,
            int quantity, int vatInfo, long unitPrice, String unitName)
            throws Exception {
        price = convertAmount(price);
        quantity = convertQuantity(quantity);
        unitPrice = convertAmount(unitPrice);
        description = decodeText(description);
        unitName = decodeText(unitName);

        checkEnabled();
        checkQuantity(quantity);
        checkVatInfo(vatInfo);
        fiscalReceipt.printRecItemVoid(description, price,
                quantity, vatInfo, unitPrice, unitName);
    }

    public void printRecItemVoid(
            String description,
            long price,
            int quantity,
            int vatInfo,
            long unitPrice,
            String unitName)
            throws Exception {
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        PrintRecItemVoidRequest request = new PrintRecItemVoidRequest(
                description, price, quantity, vatInfo, unitPrice, unitName);
        request = filters.printRecItemVoid(request);
        execute(request);
    }

    public void printRecItemAdjustmentVoidAsync(int adjustmentType,
            String description, long amount, int vatInfo)
            throws Exception {
        description = decodeText(description);
        amount = convertAmount(amount);

        checkEnabled();
        checkSalesReceipt();
        checkVatInfo(vatInfo);
        fiscalReceipt.printRecItemAdjustmentVoid(adjustmentType,
                description, amount, vatInfo);
    }

    public void printRecItemAdjustmentVoid(int adjustmentType,
            String description, long amount, int vatInfo)
            throws Exception {
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);
        execute(new PrintRecItemAdjustmentVoidRequest(
                adjustmentType, description, amount, vatInfo));
    }
    ////////////////////////////

    private void checkPrinterState(int value)
            throws Exception {
        if (printerState != value) {
            throwWrongStateError();
        }
    }

    private void setPrinterState(int newState) {
        printerState = newState;
    }

    private boolean isReceiptOpened() {
        return ((printerState == FPTR_PS_FISCAL_RECEIPT)
                || (printerState == FPTR_PS_FISCAL_RECEIPT_ENDING)
                || (printerState == FPTR_PS_FISCAL_RECEIPT_TOTAL));
    }

    private int getStation(int station)
            throws Exception {
        // check valid stations
        JposPrinterStation printerStation = new JposPrinterStation(station);
        if (printerStation.isRecStation() && (!getCapRecPresent())) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.ReceiptStationNotPresent));
        }
        if (printerStation.isJrnStation() && (!getCapJrnPresent())) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.JournalStationNotPresent));
        }
        if (printerStation.isSlpStation() && (!getCapSlpPresent())) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.SlipStationNotPresent));
        }

        return printerStation.getStation();
    }

    private void checkParamValue(int value, int minValue, int maxValue,
            String paramText)
            throws Exception {
        if ((value < minValue) || (value > maxValue)) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue)
                    + paramText);
        }
    }

    private int stringParamToInt(String s, String paramName)
            throws Exception {
        try {
            int result = Integer.parseInt(s);
            return result;
        } catch (Exception e) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue)
                    + paramName);
        }
    }

    private String checkHealthExternal()
            throws Exception {
        PrinterStatus status = getPrinter().waitForPrinting();
        if (!status.getPrinterMode().isTestMode()) {
            getPrinter().startTest(1);
            getPrinter().waitForPrinting();
        }
        getPrinter().stopTest();
        getPrinter().waitForPrinting();
        return "External HCheck: OK";
    }

    private void printPreLine()
            throws Exception {
        if (params.preLine.length() > 0) {
            printText(SMFP_STATION_REC, params.preLine, getFontNumber());
            params.preLine = "";
        }
    }

    private void printPostLine()
            throws Exception {
        if (params.postLine.length() > 0) {
            printText(SMFP_STATION_REC, params.postLine, getFontNumber());
            params.postLine = "";
        }
    }

    private void checkStateBusy()
            throws Exception {
        if (state == JPOS_S_BUSY) {
            throw new JposException(JPOS_E_BUSY);
        }
    }

    private void cancelReceipt()
            throws Exception {
        setPrinterState(FPTR_PS_MONITOR);
        PrinterStatus status = getPrinter().waitForPrinting();
        if (status.getPrinterMode().isReceiptOpened()) {
            getPrinter().sysAdminCancelReceipt();
            printDocEnd();
        }
    }

    public int getPayType(long userPayType)
            throws Exception {
        //logger.debug("getPayType(" + String.valueOf(userPayType) + ")");

        PayType payType = payTypes.itemById(userPayType);
        if (payType == null) {
            return 0;
        } else {
            return payType.printerPayType;
        }
    }

    public int getFont(boolean doubleWidth) {
        if (doubleWidth) {
            return doubleWidthFont;
        } else {
            return getFontNumber();
        }
    }

    public String encodeText(String text) {
        if (getStringEncoding().length() == 0) {
            return text;
        } else {
            try {
                return new String(text.getBytes(getStringEncoding()));
            } catch (UnsupportedEncodingException e) {
                logger.error(e);
                return text;
            }
        }
    }

    public String decodeText(String text) {
        if (getStringEncoding().length() == 0) {
            return text;
        } else {
            try {
                return new String(text.getBytes(), getStringEncoding());
            } catch (UnsupportedEncodingException e) {
                logger.error(e);
                return text;








            }
        }
    }

    class PackageAdjustment {

        public int vat;
        public long amount;
    }

    class PackageAdjustments extends Vector {

        public PackageAdjustment addItem(int vat, long amount) {
            PackageAdjustment result = new PackageAdjustment();
            result.vat = vat;
            result.amount = amount;
            add(result);
            return result;
        }

        public PackageAdjustment getItem(int Index) {
            return (PackageAdjustment) get(Index);
        }

        public void parse(String s) {
            String[] items = StringUtils.split(s, ';');
            for (int i = 0; i < items.length; i++) {
                String[] fields = StringUtils.split(items[i], ',');
                if (fields.length >= 2) {
                    addItem(Integer.parseInt(fields[0]), Long.parseLong(fields[1]));
                }
            }
        }
    }

    private void checkClaimed()
            throws Exception {
        if (!claimed) {
            throw new JposException(JPOS_E_NOTCLAIMED);
        }
    }

    private void checkEnabled()
            throws Exception {
        checkClaimed();
        if (!deviceEnabled) {
            throw new JposException(JPOS_E_DISABLED);
        }
    }

    private void checkOnLine()
            throws Exception {
        checkEnabled();
    }

    public void printFiscalDocumentLineAsync(String documentLine)
            throws Exception {
        checkEnabled();
        noSlipStationError();
    }

    public void printFixedOutputAsync(int documentType, int lineNumber,
            String data)
            throws Exception {
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public int getMaxGraphicsHeight()
            throws Exception {
        return getModel().getMaxGraphicsHeight();
    }

    public void printGraphics(int line1, int line2)
            throws Exception {
        if (PrintGraphics.validLines(line1, line2) && getModel().getCapGraphics()) {
            getPrinter().printGraphics(line1, line2);
        } else {
            if (getModel().getCapGraphicsEx() && PrintGraphicsEx.validLines(line1, line2)) {
                getPrinter().printGraphicsEx(line1, line2);
            }
        }
    }

    public void loadGraphics(int lineNumber, int lineCount, byte[] data)
            throws Exception {
        if ((lineNumber + lineCount) > getMaxGraphicsHeight()) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidImageHeight) + ", "
                    + String.valueOf(lineCount) + " > "
                    + String.valueOf(getMaxGraphicsHeight()));
        }

        if (getModel().getCapGraphicsEx()) {
            getPrinter().loadGraphicsEx(lineNumber, data);
        } else {
            if (getModel().getCapGraphics()) {
                getPrinter().loadGraphics(lineNumber, data);
            }
        }
    }
    private static final int TimeToSleep = 100;

    private void checkEcrMode()
            throws Exception {
        logger.debug("checkEcrMode");
        int endDumpCount = 0;
        int confirmDateCount = 0;
        int writePointCount = 0;
        int stopTestCount = 0;

        for (;;) {
            PrinterStatus status = getPrinter().waitForPrinting();
            switch (status.getPrinterMode().getValue()) {
                case MODE_DUMPMODE: {
                    getPrinter().endDump();
                    endDumpCount++;
                    if (endDumpCount >= MaxStateCount) {
                        throw new Exception(
                                Localizer.getString(Localizer.EndDumpFailed));
                    }
                    break;
                }

                case MODE_LOCKED: {
                    throw new Exception(
                            Localizer.getString(Localizer.LockedTaxPassword));
                }

                case MODE_WAITDATE: {
                    PrinterDate date = getPrinter().readLongStatus().getDate();
                    getPrinter().confirmDate(date);
                    confirmDateCount++;
                    if (confirmDateCount >= MaxStateCount) {
                        throw new Exception(
                                Localizer.getString(Localizer.ConfirmDateFailed));
                    }
                    break;
                }

                case MODE_POINTPOS: {
                    getPrinter().writeDecimalPoint(SMFP_POINT_POSITION_2);
                    writePointCount++;
                    if (writePointCount >= MaxStateCount) {
                        throw new Exception(
                                Localizer.getString(Localizer.WriteDecimalPointFailed));
                    }
                    break;
                }

                case MODE_TECH: {
                    Calendar now = Calendar.getInstance();
                    PrinterDate date = new PrinterDate();
                    PrinterTime time = new PrinterTime();

                    getPrinter().resetFM();
                    getPrinter().writeDate(date);
                    getPrinter().confirmDate(date);
                    getPrinter().writeTime(time);
                    break;
                }

                case MODE_TEST: {
                    getPrinter().stopTest();
                    stopTestCount++;
                    if (stopTestCount >= MaxStateCount) {
                        throw new Exception(
                                Localizer.getString(Localizer.StopTestFailed));
                    }
                    break;
                }

                case MODE_FULLREPORT:
                case MODE_EJREPORT:
                case MODE_SLPPRINT: {
                    getPrinter().sleep(TimeToSleep);
                    break;
                }

                default:
                    return;
            }
        }
    }

    public void checkDeviceStatus() {
        try {
            PrinterStatus status = readPrinterStatus();
            setPowerState(JPOS_PS_ONLINE);
            if (status.getSubmode() == ECR_SUBMODE_AFTER) {
                getPrinter().continuePrint();
                getPrinter().waitForPrinting();
            }

        } catch (Exception e) {
            logger.error(e);
            setPowerState(JPOS_PS_OFFLINE);
        }
    }

    public void deviceProc() {
        try {
            Thread thisThread = Thread.currentThread();
            while (deviceThread == thisThread) {
                synchronized (printer) {
                    checkDeviceStatus();
                    getPrinter().wait(pollInterval);
                }
            }
        } catch (InterruptedException e) {
            logger.error("InterruptedException", e);
            Thread.currentThread().interrupt();
        }
    }

    public int getMaxGraphicsWidth()
            throws Exception {
        return getModel().getMaxGraphicsWidth();
    }

    private DeviceMetrics getDeviceMetrics()
            throws Exception {
        if (deviceMetrics == null) {
            deviceMetrics = getPrinter().readDeviceMetrics();
        }
        return deviceMetrics;
    }

    public void printBarcode(PrinterBarcode barcode)
            throws Exception {
        if (isReceiptEnding()) {
            printItems.add(barcode);
        } else {
            getPrinter().printBarcode(barcode);
        }
    }

    public void printRecItemRefund(
            String description,
            long amount,
            int quantity,
            int vatInfo,
            long unitAmount,
            String unitName)
            throws Exception {
        checkOnLine();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);

        filters.printRecItemRefund(description, amount,
                quantity, vatInfo, unitAmount, unitName);

        execute(new PrintRecItemRefundRequest(description, amount,
                quantity, vatInfo, unitAmount, unitName));
    }

    public void printRecItemRefundAsync(
            String description,
            long amount,
            int quantity,
            int vatInfo,
            long unitAmount,
            String unitName)
            throws Exception {
        unitName = decodeText(unitName);
        description = decodeText(description);
        amount = convertAmount(amount);
        unitAmount = convertAmount(unitAmount);
        quantity = convertQuantity(quantity);

        checkEnabled();
        checkSalesReceipt();
        checkReceiptStation();
        checkQuantity(quantity);
        checkPrice(amount);
        checkPrice(unitAmount);
        checkVatInfo(vatInfo);

        fiscalReceipt.printRecItemRefund(description, amount,
                quantity, vatInfo, unitAmount, unitName);
    }

    public void printRecItemRefundVoid(String description,
            long amount,
            int quantity,
            int vatInfo,
            long unitAmount,
            String unitName)
            throws Exception {
        checkOnLine();
        checkPrinterState(FPTR_PS_FISCAL_RECEIPT);

        filters.printRecItemRefundVoid(description, amount,
                quantity, vatInfo, unitAmount, unitName);

        execute(new PrintRecItemRefundVoidRequest(description, amount,
                quantity, vatInfo, unitAmount, unitName));
    }

    public void printRecItemRefundVoidAsync(String description,
            long amount,
            int quantity,
            int vatInfo,
            long unitAmount,
            String unitName)
            throws Exception {
        amount = convertAmount(amount);
        quantity = convertQuantity(quantity);
        unitAmount = convertAmount(unitAmount);
        description = decodeText(description);
        unitName = decodeText(unitName);

        checkEnabled();
        checkQuantity(quantity);
        checkVatInfo(vatInfo);

        fiscalReceipt.printRecItemRefundVoid(description,
                amount, quantity, vatInfo, unitAmount, unitName);
    }

    public void saveXmlZReport(String fileName)
            throws JposException {
        try {
            RegisterReport report = new RegisterReport();
            RegisterReportReader.execute(report, printer);
            XmlRegisterReportWriter.execute(report, fileName);
        } catch (Exception e) {
            logger.error("Error saving file", e);
            throw new JposException(JPOS_E_FAILURE, e.getMessage());
        }
    }

    public void saveCsvZReport(String fileName)
            throws JposException {
        try {
            RegisterReport report = new RegisterReport();
            RegisterReportReader.execute(report, printer);
            CsvRegisterReportWriter.execute(report, fileName);
        } catch (Exception e) {
            logger.error("Error saving file", e);
            throw new JposException(JPOS_E_FAILURE, e.getMessage());
        }
    }

    private void loadProperties(String logicalName)
            throws Exception {
        JposRegPopulator populator = new SimpleXmlRegPopulator();
        populator.load("shtrihjavapos.xml");
        JposEntry entry = findEntry(populator, logicalName);

        header.load(entry, "Header", this);
        trailer.load(entry, "Trailer", this);
        getPrinterImages().load(entry);
        receiptImages.load(entry);
    }

    private String getPropsFileName() {
        return "shtrihjavapos.xml";
    }

    protected JposEntry createJposEntry(
            String logicalName,
            String factoryClass,
            String serviceClass,
            String vendorName,
            String vendorURL,
            String deviceCategory,
            String jposVersion,
            String productName,
            String productDescription,
            String productURL) {
        JposEntry jposEntry = new SimpleEntry();

        jposEntry.addProperty(JposEntry.LOGICAL_NAME_PROP_NAME, logicalName);
        jposEntry.addProperty(JposEntry.SI_FACTORY_CLASS_PROP_NAME, factoryClass);
        jposEntry.addProperty(JposEntry.SERVICE_CLASS_PROP_NAME, serviceClass);
        jposEntry.addProperty(JposEntry.VENDOR_NAME_PROP_NAME, vendorName);
        jposEntry.addProperty(JposEntry.VENDOR_URL_PROP_NAME, vendorURL);
        jposEntry.addProperty(JposEntry.DEVICE_CATEGORY_PROP_NAME, deviceCategory);
        jposEntry.addProperty(JposEntry.JPOS_VERSION_PROP_NAME, jposVersion);
        jposEntry.addProperty(JposEntry.PRODUCT_NAME_PROP_NAME, productName);
        jposEntry.addProperty(JposEntry.PRODUCT_DESCRIPTION_PROP_NAME, productDescription);
        jposEntry.addProperty(JposEntry.PRODUCT_URL_PROP_NAME, productURL);

        return jposEntry;
    }

    private void saveProperties()
            throws Exception {
        logger.debug("saveProperties");
        try {
            JposEntry entry = createJposEntry(
                    logicalName,
                    "com.shtrih.jpos.ShtrihJposServiceInstanceFactory",
                    "com.shtrih.jpos.fiscalprinter.FiscalPrinterService",
                    "SHTRIH-M",
                    "http://www.shtrih-m.com",
                    "FiscalPrinter",
                    "1.9",
                    "Fiscal printer service",
                    "ShtrihFptr",
                    "http://www.javapos.com");

            logger.debug("getPrinterImages().save");
            getPrinterImages().save(entry);

            logger.debug("receiptImages.save");
            receiptImages.save(entry);

            logger.debug("header.save");
            header.save(entry, "Header");

            logger.debug("trailer.save");
            trailer.save(entry, "Trailer");

            JposRegPopulator populator = new XercesRegPopulator();
            Vector v = new Vector();
            v.add(entry);
            populator.save(v.elements(), getPropsFileName());
        } catch (Exception e) {
            logger.error("saveProperties", e);
        }
    }

    private void throwWrongStateError()
            throws Exception {
        throw new JposException(JPOS_E_EXTENDED, JPOS_EFPTR_WRONG_STATE,
                Localizer.getString(Localizer.WrongPrinterState)
                + "(" + String.valueOf(printerState) + ", "
                + "\"" + PrinterState.getText(printerState) + "\"");
    }

    public void writeParameter(int parameterID, int value)
            throws Exception {
        getPrinter().writeParameter(parameterID, value);
    }

    public void writeParameter(int parameterID, boolean value)
            throws Exception {
        getPrinter().writeParameter(parameterID, value);
    }

    public void writeParameter(int parameterID, String value)
            throws Exception {
        getPrinter().writeParameter(parameterID, value);
    }

    public String readParameter(int parameterID)
            throws Exception {
        return getPrinter().readParameter(parameterID);
    }

    public void setFiscalReceiptType(int value)
            throws Exception {
        checkEnabled();
        checkPrinterState(FPTR_PS_MONITOR);
        switch (value) {
            case FPTR_RT_CASH_IN: {
                fiscalReceipt = new CashInReceipt(
                        receiptPrinter,
                        params,
                        fiscalDay,
                        receipt);
                break;
            }
            case FPTR_RT_CASH_OUT: {
                fiscalReceipt = new CashOutReceipt(
                        receiptPrinter,
                        params,
                        fiscalDay,
                        receipt);
                break;
            }
            case FPTR_RT_GENERIC:
            case FPTR_RT_SALES:
            case FPTR_RT_SERVICE:
            case FPTR_RT_SIMPLE_INVOICE:
            case FPTR_RT_REFUND:
                fiscalReceipt = new SalesReceipt(
                        receiptPrinter,
                        params,
                        fiscalDay,
                        receipt);
                break;
            default:
                throw new JposException(JPOS_E_ILLEGAL,
                        Localizer.getString(Localizer.InvalidParameterValue));
        }
        fiscalReceiptType = value;
    }

    public PrinterData getParams() {
        return params;
    }

    public PrinterReceipt getReceipt() {
        return receipt;
    }

    public int getFontNumber() {
        return printerParams.fontNumber;
    }

    public String getStringEncoding() {
        return printerParams.stringEncoding;
    }
}
