/*
 * CashDrawerImpl.java
 *
 * Created on April 18 2008, 16:41
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package com.shtrih.jpos.cashdrawer;

/**
 *
 * @author V.Kravtsov
 */
// java
import gnu.io.*;
import java.util.Vector;
import java.util.Enumeration;
// jpos
import jpos.*;
import jpos.events.*;
import jpos.services.*;
import jpos.config.*;
// apache
import org.apache.log4j.*;
// this
import com.shtrih.jpos.*;
import com.shtrih.util.*;
import com.shtrih.jpos.events.*;
import com.shtrih.fiscalprinter.*;
import com.shtrih.fiscalprinter.port.*;
import com.shtrih.jpos.fiscalprinter.*;
import com.shtrih.fiscalprinter.command.*;

public class CashDrawerImpl extends DeviceService
        implements CashDrawerService113, CashDrawerConst, JposConst, JposEntryConst {
    // private data
    /////////////////////////////////////////////////////////////////////////
    // internal state

    private boolean connected = false;
    private boolean claimed = false;
    private EventCallbacks cb = null;
    private int state = JPOS_S_CLOSED;
    private String checkHealthText = "";
    private boolean drawerOpened = false;
    private int powerState = JPOS_PS_UNKNOWN;
    private int powerNotify = JPOS_PN_DISABLED;
    private String physicalDeviceName = "SHTRIM-M cash drawer";
    private String physicalDeviceDescription = "SHTRIM-M fiscal printer cash drawer";
    // internal parameters
    private int drawerNumber = 0;
    private int pollInterval = 500;
    private String statisticFileName = "ShtrihCashDrawer.xml";
    private boolean deviceEnabled = false;
    private boolean capStatus = true;
    // internal classes
    private Thread deviceThread = null;     // device poll thread
    private Thread eventThread = null;      // event delivery thread
    private Vector events = new Vector();
    private PrinterPort port;
    private SMPrinterDeviceImpl device;
    private SMFiscalPrinterImpl printer;
    private static Logger logger = Logger.getLogger(CashDrawerImpl.class);
    private CashDrawerStatistics statistics = new CashDrawerStatistics();
    private ConnectParams connectParams = new ConnectParams();
    private final FiscalPrinterParams printerParams = new FiscalPrinterParams();

    public CashDrawerImpl() {
        statistics.unifiedPOSVersion = "1.13";
        statistics.deviceCategory = "CashDrawer";
        statistics.manufacturerName = "SHTRIH-M";
        statistics.modelName = "SHTRIH-M fiscal printer cash drawer";
        statistics.serialNumber = "";
        statistics.firmwareRevision = "";
        statistics.physicalInterface = RS232_DEVICE_BUS;
        statistics.installationDate = "";

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

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

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

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

    private void checkEnabled()
            throws JposException {
        if (!getDeviceEnabled()) {
            throw new JposException(JPOS_E_DISABLED);
        }
    }

    private void checkOnLine()
            throws JposException {
        if (!connected) {
            throw new JposException(JPOS_E_OFFLINE);
        }
    }

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

    private void statusUpdateEvent(int value) {
        addEvent(
                new StatusUpdateEventRequest(cb,
                new StatusUpdateEvent(this, value)));
    }

    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();
        }
    }

    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 setDrawerOpened(boolean drawerOpened) {
        if (drawerOpened != this.drawerOpened) {
            if (drawerOpened) {
                statusUpdateEvent(CASH_SUE_DRAWEROPEN);
            } else {
                statusUpdateEvent(CASH_SUE_DRAWERCLOSED);
            }
            this.drawerOpened = drawerOpened;
        }
    }

    public void deviceProc() {
        try {
            Thread thisThread = Thread.currentThread();
            while (deviceThread == thisThread) {
                synchronized (printer) {
                    try {
                        connect();
                        // update cash drawer state
                        LongPrinterStatus status = printer.readLongStatus();
                        setDrawerOpened(status.getPrinterFlags().isDrawerOpened());
                        // update device parameters
                        statistics.serialNumber = status.getSerial();
                        statistics.firmwareRevision = status.getFirmwareRevision();
                    } catch (Exception e) {
                        logger.error(e);
                        setPowerState(JPOS_PS_OFFLINE);
                        connected = false;
                    }
                    printer.wait(pollInterval);
                }
            }
        } catch (InterruptedException e) {
            // Restore the interrupted status
            logger.error("InterruptedException", e);
            Thread.currentThread().interrupt();
        }
    }

    // Properties
    public String getCheckHealthText()
            throws JposException {
        logger.debug("getCheckHealthText: " + checkHealthText);
        return checkHealthText;
    }

    public boolean getClaimed()
            throws JposException {
        logger.debug("getClaimed: " + String.valueOf(claimed));

        checkOpened();
        return claimed;
    }

    public boolean getDeviceEnabled()
            throws JposException {
        logger.debug("getDeviceEnabled: " + String.valueOf(deviceEnabled));
        return deviceEnabled;
    }

    public void setDeviceEnabled(boolean deviceEnabled)
            throws JposException {
        logger.debug("setDeviceEnabled(" + String.valueOf(deviceEnabled) + ")");
        try {
            if (deviceEnabled != getDeviceEnabled()) {
                if (deviceEnabled) {
                    claim(0);
                    connect();

                    deviceThread = new Thread(new DeviceTarget(this));
                    deviceThread.start();
                } else {
                    deviceThread = null;
                    setPowerState(JPOS_PS_UNKNOWN);
                }
                this.deviceEnabled = deviceEnabled;
            }
        } catch (Exception e) {
            handleException(e);
        }
    }

    public String getDeviceServiceDescription()
            throws JposException {
        String deviceServiceDescription = "Cash drawer service driver, 2008 SHTRIH-M";
        logger.debug("getDeviceServiceDescription: " + deviceServiceDescription);
        return deviceServiceDescription;
    }

    public int getDeviceServiceVersion()
            throws JposException {
        int deviceServiceVersion = deviceVersion113 + 118;
        logger.debug("getDeviceServiceVersion: " + String.valueOf(deviceServiceVersion));
        return deviceServiceVersion;
    }

    public boolean getFreezeEvents()
            throws JposException {
        boolean freezeEvents = eventThread == null;
        logger.debug("getFreezeEvents: " + String.valueOf(freezeEvents));

        checkOpened();
        return freezeEvents;
    }

    public void setFreezeEvents(boolean freezeEvents)
            throws JposException {
        logger.debug("setFreezeEvents(" + String.valueOf(freezeEvents) + ")");

        checkOpened();
        if (freezeEvents != getFreezeEvents()) {
            if (freezeEvents) {
                eventThread = null;
            } else {
                eventThread = new Thread(new EventTarget(this));
                eventThread.start();
            }
        }
    }

    public String getPhysicalDeviceDescription()
            throws JposException {
        logger.debug("getPhysicalDeviceDescription: " + physicalDeviceDescription);

        checkOpened();
        return physicalDeviceDescription;
    }

    public String getPhysicalDeviceName()
            throws JposException {
        logger.debug("getPhysicalDeviceName: " + physicalDeviceName);

        checkOpened();
        return physicalDeviceName;
    }

    public int getState()
            throws JposException {
        logger.debug("getState: " + String.valueOf(state));
        return state;
    }

    // Methods supported by all device services.
    public void claim(int timeout)
            throws JposException {
        logger.debug("claim(" + String.valueOf(timeout) + ")");
        try {
            checkOpened();
            if (!claimed) {
                port.setPortName(connectParams.portName);
                port.setBaudRate(connectParams.baudRate);
                port.setOpenTimeout(timeout);
                port.open();
                claimed = true;
            }
        } catch (Exception e) {
            handleException(e);
        }
        logger.debug("claim: OK");
    }

    public void close()
            throws JposException {
        logger.debug("close");
        checkOpened();
        if (claimed) {
            release();
        }
        state = JPOS_S_CLOSED;
        statistics.save(statisticFileName);
        logger.debug("close: OK");
    }

    public void checkHealth(int level)
            throws JposException {
        logger.debug("checkHealth(" + String.valueOf(level) + ")");

        try {
            checkEnabled();
            if (level == JPOS_CH_EXTERNAL) {
                checkHealthText = "";
                openDrawer();
                return;
            }
            if (level == JPOS_CH_INTERACTIVE) {
                checkHealthText = "";
                //new CheckHealthDialog(this)
                return;
            }
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.InvalidParameterValue + "level"));

        } catch (Exception e) {
            handleException(e);
        }
        logger.debug("checkHealth: OK");
    }

    public void directIO(int command, int[] data, Object object)
            throws JposException {
        logger.debug("directIO("
                + String.valueOf(command) + ", "
                + String.valueOf(data) + ", "
                + String.valueOf(object) + ")");

        throw new JposException(JPOS_E_ILLEGAL,
                Localizer.getString(Localizer.NotImplemented));
    }

    public void open(String logicalName, EventCallbacks cb)
            throws JposException {
        logger.debug("open");
        logger.debug("SHTRIH-M JavaPos CashDrawer service");
        logger.debug("DeviceServiceVersion: " + String.valueOf(getDeviceServiceVersion()));
        try {
            this.cb = cb;
            if (jposEntry != null) {
                port = createPort(jposEntry);
                device = new SMPrinterDeviceImpl(port);
                printer = new SMFiscalPrinterImpl(device, printerParams);

                // reading
                JposPropertyReader reader = new JposPropertyReader(jposEntry);

                printer.getParams().statusCommand = reader.readInteger("statusCommand",
                        PrinterConst.SMFP_STATUS_COMMAND_DS);

                connectParams.portName =
                        reader.readString(RS232Const.RS232_PORT_NAME_PROP_NAME, "");

                connectParams.baudRate =
                        reader.readInteger(RS232Const.RS232_BAUD_RATE_PROP_NAME, 4800);

                connectParams.byteTimeout =
                        reader.readInteger("byteTimeout", 200);

                connectParams.deviceByteTimeout =
                        reader.readInteger("deviceByteTimeout", 1000);

                //connectParams.searchByPortEnabled =
                //    reader.readBoolean("searchByPortEnabled", false);

                connectParams.searchByBaudRateEnabled =
                        reader.readBoolean("searchByBaudRateEnabled", true);

                int operatorPassword = reader.readInteger("operatorPassword", 1);
                pollInterval = reader.readInteger("pollInterval", 500);
                capStatus = reader.readBoolean("capStatus", true);

                statisticFileName =
                        reader.readString("statisticFileName", "ShtrihCashDrawer.xml");

                // initialization
                statistics.load(statisticFileName);
                device.setByteTimeout(connectParams.byteTimeout);
            }
            state = JPOS_S_IDLE;
        } catch (Exception e) {
            handleException(e);
        }
        logger.debug("open: OK");
    }

    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);
    }

    public void release()
            throws JposException {
        logger.debug("release");
        try {
            checkClaimed();
            setDeviceEnabled(false);
            claimed = false;
            synchronized (printer) {
                printer.closePort();
            }
        } catch (Exception e) {
            JposExceptionHandler.handleException(e);
        }
        logger.debug("release: OK");
    }

    // Capabilities
    public boolean getCapStatus()
            throws JposException {
        logger.debug("getCapStatus: " + String.valueOf(capStatus));
        return capStatus;
    }

    public boolean getCapStatusMultiDrawerDetect()
            throws JposException {
        boolean value = false;
        logger.debug("getCapStatusMultiDrawerDetect: " + String.valueOf(value));
        return value;
    }

    public boolean getCapStatisticsReporting()
            throws JposException {
        boolean value = true;
        logger.debug("getCapStatisticsReporting: " + String.valueOf(value));
        return value;
    }

    public boolean getCapUpdateStatistics()
            throws JposException {
        boolean value = true;
        logger.debug("getCapUpdateStatistics: " + String.valueOf(value));
        return value;
    }

    public int getCapPowerReporting()
            throws JposException {
        logger.debug("getCapPowerReporting: JPOS_PR_STANDARD");
        return JPOS_PR_STANDARD;
    }

    public boolean getCapCompareFirmwareVersion()
            throws JposException {
        logger.debug("getCapCompareFirmwareVersion: false");
        return false;
    }

    public boolean getCapUpdateFirmware()
            throws JposException {
        logger.debug("getCapUpdateFirmware: false");
        return false;
    }

    // Properties
    public boolean getDrawerOpened()
            throws JposException {
        logger.debug("getDrawerOpened: " + String.valueOf(drawerOpened));
        checkEnabled();
        return drawerOpened;
    }

    public int getPowerNotify()
            throws JposException {
        logger.debug("getPowerNotify: " + String.valueOf(powerNotify));
        return powerNotify;
    }

    public void setPowerNotify(int powerNotify)
            throws JposException {
        logger.debug("setPowerNotify(" + String.valueOf(powerNotify) + ")");

        if (deviceEnabled) {
            throw new JposException(JPOS_E_ILLEGAL,
                    Localizer.getString(Localizer.DeviceIsEnabled));
        }
        this.powerNotify = powerNotify;
    }

    public int getPowerState()
            throws JposException {
        logger.debug("getPowerState: " + String.valueOf(powerState));
        return powerState;
    }

    // Methods
    public void openDrawer()
            throws JposException {
        logger.debug("openDrawer()");
        try {
            checkEnabled();
            checkOnLine();
            synchronized (printer) {
                printer.openCashDrawer(drawerNumber);
            }
        } catch (Exception e) {
            statistics.drawerOpenFailed();
            JposExceptionHandler.handleException(e);
        }
    }

    public void waitForDrawerClose(int beepTimeout, int beepFrequency,
            int beepDuration, int beepDelay)
            throws JposException {
        logger.debug("waitForDrawerClose("
                + String.valueOf(beepTimeout) + ", "
                + String.valueOf(beepFrequency) + ", "
                + String.valueOf(beepDuration) + ", "
                + String.valueOf(beepDelay) + ")");

        checkEnabled();
        checkOnLine();
        try {
            long timeLeft = 0;
            long currentTime = 0;
            long startTime = System.currentTimeMillis();
            while (drawerOpened) {
                currentTime = System.currentTimeMillis();
                timeLeft = startTime - currentTime;
                if (timeLeft > beepTimeout) {
                    Sound.beep(beepFrequency, beepDuration);
                    Thread.sleep(beepDelay);
                }
            }
        } catch (Exception e) {
            logger.error(e);
            throw new JposException(JPOS_E_FAILURE, e.getMessage(), e);
        }
        logger.debug("waitForDrawerClose: OK");
    }

    public void resetStatistics(String statisticsBuffer)
            throws JposException {
        logger.debug("resetStatistics(" + statisticsBuffer + ")");

        checkEnabled();
        statistics.reset(statisticsBuffer);
    }

    public void retrieveStatistics(String[] statisticsBuffer)
            throws JposException {
        logger.debug("retrieveStatistics(" + statisticsBuffer[0] + ")");

        checkEnabled();
        statistics.retrieve(statisticsBuffer);
    }

    public void updateStatistics(String statisticsBuffer)
            throws JposException {
        logger.debug("updateStatistics(" + statisticsBuffer + ")");

        checkEnabled();
        statistics.update(statisticsBuffer);
    }

    public void compareFirmwareVersion(String firmwareFileName, int[] result)
            throws JposException {
        logger.debug("compareFirmwareVersion(" + firmwareFileName + ")");
        checkEnabled();
        throw new JposException(JPOS_E_ILLEGAL);
    }

    public void updateFirmware(String firmwareFileName)
            throws JposException {
        logger.debug("updateFirmware(" + firmwareFileName + ")");

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

    public boolean searchByBaudRates(
            int deviceBaudRate,
            int deviceByteTimeout,
            boolean searchByBaudRateEnabled)
            throws Exception {
        if (searchByBaudRateEnabled) {
            int[] baudRates = printer.getSupportedBaudRates();
            for (int j = 0; j < baudRates.length; j++) {
                if (printer.connectDevice(baudRates[j], deviceBaudRate, deviceByteTimeout)) {
                    return true;
                }
            }
        }
        return false;
    }

    public void connect()
            throws Exception {
        // connect, if not yet connected
        if (!connected) {
            searchDevice(connectParams);
            setPowerState(JPOS_PS_ONLINE);
            connected = true;
        }
    }

    public void searchDevice(ConnectParams cp)
            throws Exception {
        logger.debug("connect");
        int[] baudRates = printer.getSupportedBaudRates();
        if (printer.connectDevice(cp.baudRate, cp.baudRate, cp.deviceByteTimeout)) {
            return;
        }

        if (cp.searchByPortEnabled) {
            Enumeration e = CommPortIdentifier.getPortIdentifiers();
            while (e.hasMoreElements()) {
                CommPortIdentifier portIdentifier = (CommPortIdentifier) e.nextElement();
                if (portIdentifier.getPortType() == portIdentifier.PORT_SERIAL) {
                    if (!cp.portName.equalsIgnoreCase(portIdentifier.getName())) {
                        port.setPortName(portIdentifier.getName());
                        port.setBaudRate(cp.baudRate);
                        port.setOpenTimeout(0);
                        port.open();
                        if (searchByBaudRates(
                                cp.baudRate,
                                cp.deviceByteTimeout,
                                cp.searchByBaudRateEnabled)) {
                            return;
                        }
                    }
                }
            }
        } else {
            if (searchByBaudRates(
                    cp.baudRate,
                    cp.deviceByteTimeout,
                    cp.searchByBaudRateEnabled)) {
                return;
            }
        }
        throw new JposException(JPOS_E_NOHARDWARE);
    }

    class DeviceTarget implements Runnable {

        private final CashDrawerImpl cashDrawer;

        public DeviceTarget(CashDrawerImpl cashDrawer) {
            this.cashDrawer = cashDrawer;
        }

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

    class EventTarget implements Runnable {

        private final CashDrawerImpl cashDrawer;

        public EventTarget(CashDrawerImpl cashDrawer) {
            this.cashDrawer = cashDrawer;
        }

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