package org.lidar;

import java.io.InputStream;
import java.io.OutputStream;
import javax.comm.*;
import java.util.*;
import org.apache.commons.configuration.XMLConfiguration;
import org.lidar.api.Management;

/**
 * Serial port communication
 * @author Andrej Cimpersek
 */
public class Serial {

    private boolean isOpened = false;
    private String deviceName;
    private String portName;
    private Enumeration portIdentifiers;
    private CommPortIdentifier portId;
    private SerialPort port;
    protected InputStream inputStream;
    protected OutputStream outputStream;
    private int baudRate, dataBits, stopBits, parity;
    private XMLConfiguration configuration;

    public Serial(String deviceName) {
        configuration = Config.getConfiguration();
        this.portName = configuration.getString(String.format("devices.%s.port", deviceName));
        this.baudRate = configuration.getInt(String.format("devices.%s.baudRate", deviceName));
        this.dataBits = configuration.getInt(String.format("devices.%s.dataBits", deviceName));
        this.stopBits = configuration.getInt(String.format("devices.%s.stopBits", deviceName));
        this.parity = configuration.getInt(String.format("devices.%s.parity", deviceName));
    }

    /**
     * Check each port identifier if
     *   (a) it indicates a serial (not a parallel) port, and
     *   (b) matches the desired name.
     * @return boolean
     */
    protected boolean identfy() {
        this.portIdentifiers = CommPortIdentifier.getPortIdentifiers();
        this.portId = null;  // will be set if port found
        while (portIdentifiers.hasMoreElements()) {
            CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
            if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL && pid.getName().equals(this.portName)) {
                this.portId = pid;
                return true;
            }
        }
        return false;
    }

    /**
     * Destructor - close port
     */
    @Override()
    protected void finalize() throws Throwable {
        closePort();
    }

    public boolean acquirePort() {
        if (this.portId == null) {
            return false;
        }
        // Use port identifier for acquiring the port
        try {
            this.port = (SerialPort) this.portId.open(
                    this.deviceName, // Name of the application asking for the port
                    10000 // Wait max. 10 sec. to acquire port
                    );
        } catch (PortInUseException e) {
            return false;
        }

        return true;
    }

    public boolean openPort() {
        if (!acquirePort()) {
            return false;
        }
        closePort();
        try {
            this.port.setSerialPortParams(this.baudRate, this.dataBits, this.stopBits, this.parity);
            this.port.removeEventListener();
            this.port.addEventListener(new SerialPortEventListener() {

                public void serialEvent(SerialPortEvent event) {
                    switch (event.getEventType()) {
                        case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
                            onOutputBufferEmpty(event);
                            break;

                        case SerialPortEvent.DATA_AVAILABLE:
                            onDataAvailable(event);
                            break;
                    }
                }
            });
            this.inputStream = this.port.getInputStream();
            this.outputStream = this.port.getOutputStream();
            this.isOpened = true;
        } catch (Exception e) {
            this.isOpened = false;
        } finally {
            return this.isOpened;
        }
    }

    public void closePort() {
        if (this.port == null) {
            return;
        }
        try {
            this.inputStream.close();
            this.outputStream.close();
            this.port.close();
        } catch (Exception e) {
        } finally {
            this.isOpened = false;
        }
    }

    /**
     * Write shortcut for string
     */
    public boolean sendCommand(String lockToken, String string) {
        return sendCommand(lockToken, string.getBytes());
    }

    /**
     * Write shortcut
     * @param bytes
     * @return boolean
     */
    public boolean sendCommand(String lockToken, byte[] bytes) {
        // TODO: worker, ki odklene po X času brez dostopa
        if (!Management.locked || lockToken != Management.lockToken) {
            return false;
        }
        Management.lockAccess = new Date();

        try {
            outputStream.write(bytes);
        } catch (Exception e) {
            return false;
        }

        return true;
    }

    /**
     * Handle output buffer empty events.
     * NOTE: The reception of this event is optional and not
     *       guaranteed by the API specification.
     * @param event The output buffer empty event
     */
    protected void onOutputBufferEmpty(SerialPortEvent event) {
        //TODO: implement
    }

    /**
     * Handle data available events.
     * @param event The data available event
     */
    protected void onDataAvailable(SerialPortEvent event) {
        //TODO: implement
    }
}
