/*
 * The MIT License
 *
 * Copyright 2011 Peter Kocsis <p. kocsis. 2. 7182 at gmail.com>.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.ftdi;

import com.sun.jna.Memory;
import com.sun.jna.ptr.ByteByReference;
import com.sun.jna.ptr.IntByReference;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Java class to communicate easily to a FTDI device.
 * @author Peter Kocsis <p. kocsis. 2. 7182 at gmail.com>
 */
public class FTDevice {

    static private final FTD2XX ftd2xx = FTD2XX.INSTANCE;
    private final int devID, devLocationID, flag;
    private final DeviceType devType;
    private int ftHandle;
    private final String devSerialNumber, devDescription;
    private FTDeviceInputStream fTDeviceInputStream = null;
    private FTDeviceOutputStream fTDeviceOutputStream = null;
    private boolean open = false;

    private FTDevice(DeviceType devType, int devID, int devLocationID,
            String devSerialNumber, String devDescription, int ftHandle,
            int flag) {
        this.devType = devType;
        this.devID = devID;
        this.devLocationID = devLocationID;
        this.devSerialNumber = devSerialNumber;
        this.devDescription = devDescription;
        this.ftHandle = ftHandle;
        this.flag = flag;
        this.open = false;
    }

    /**
     * Get device description.
     * @return device description
     */
    public String getDevDescription() {
        return devDescription;
    }

    /**
     * Get device ID.
     * @return device ID
     */
    public int getDevID() {
        return devID;
    }

    /**
     * Get device serial number.
     * @return device serial number
     */
    public String getDevSerialNumber() {
        return devSerialNumber;
    }

    /**
     * Get device type.
     * @return device type.
     */
    public DeviceType getDevType() {
        return devType;
    }

    /**
     * Get device location.
     * @return device location.
     */
    public int getDevLocationID() {
        return devLocationID;
    }

    /**
     * Get device flag.
     * @return flag.
     */
    public int getFlag() {
        return flag;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof FTDevice)) {
            return false;
        }
        FTDevice eq = (FTDevice) obj;
        return eq.ftHandle == this.ftHandle;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 97 * hash + this.ftHandle;
        return hash;
    }

    @Override
    public String toString() {
        return "FTDevice{" + "devDescription=" + devDescription
                + ", devSerialNumber=" + devSerialNumber + '}';
    }

    private static void ensureFTStatus(int ftstatus) throws FTD2XXException {
        if (!(ftstatus == FT_STATUS.OK.constant())) {
            throw new FTD2XXException(ftstatus);
        }
    }

    private static FTDevice getXthDevice(int Xth) throws FTD2XXException {
        IntByReference flag = new IntByReference();
        IntByReference devType = new IntByReference();
        IntByReference devID = new IntByReference();
        IntByReference locID = new IntByReference();
        IntByReference ftHandle = new IntByReference();
        Memory devSerNum = new Memory(16);
        Memory devDesc = new Memory(64);

        ensureFTStatus(ftd2xx.FT_GetDeviceInfoDetail(Xth, flag, devType, devID,
                locID, devSerNum, devDesc, ftHandle));

        return new FTDevice(DeviceType.values()[devType.getValue()],
                devID.getValue(), locID.getValue(), devSerNum.getString(0),
                devDesc.getString(0), ftHandle.getValue(), flag.getValue());
    }

    /**
     * Get the connected FTDI devices. It will not contain opened devices.
     * @return List contain available FTDI devices.
     * @throws FTD2XXException If something goes wrong.
     */
    public static List<FTDevice> getDevices() throws FTD2XXException {
        return getDevices(false);
    }

    /**
     * Get the connected FTDI devices.
     * @param isIncludeOpenedDevices Would you like to see opened devices?
     * @return List contain available FTDI devices.
     * @throws FTD2XXException If something goes wrong.
     */
    public static List<FTDevice> getDevices(boolean isIncludeOpenedDevices)
            throws FTD2XXException {
        IntByReference devNum = new IntByReference();

        ensureFTStatus(ftd2xx.FT_CreateDeviceInfoList(devNum));

        ArrayList<FTDevice> devs = new ArrayList<FTDevice>(devNum.getValue());

        for (int i = 0; i < devNum.getValue(); i++) {
            FTDevice device = getXthDevice(i);
            //device is occupied?
            if (isIncludeOpenedDevices) {
                devs.add(device);
            } else {
                if ((device.flag & FTD2XX.FT_FLAGS_OPENED) == 0) {
                    devs.add(device);
                }
            }

        }

        Logger.getLogger(FTDevice.class.getName()).log(Level.INFO,
                "Found devs: {0} (All:{1})",
                new Object[]{devs.size(), devNum.getValue()});

        return devs;
    }

    /**
     * Get the connected FTDI devices. It will not contain opened devices.
     * @param description Filtering option, exact match need.
     * @return List contain available FTDI devices.
     * @throws FTD2XXException If something goes wrong.
     */
    public static List<FTDevice> getDevicesByDescription(String description)
            throws FTD2XXException {
        IntByReference devNum = new IntByReference();

        ensureFTStatus(ftd2xx.FT_CreateDeviceInfoList(devNum));

        ArrayList<FTDevice> devs = new ArrayList<FTDevice>(devNum.getValue());

        for (int i = 0; i < devNum.getValue(); i++) {
            FTDevice device = getXthDevice(i);

            if (((device.flag & FTD2XX.FT_FLAGS_OPENED) == 0)
                    && description.equals(device.devDescription)) {
                devs.add(device);
            }

        }

        Logger.getLogger(FTDevice.class.getName()).log(Level.INFO,
                "Found devs: {0} (All:{1})",
                new Object[]{devs.size(), devNum.getValue()});

        return devs;
    }

    /**
     * Get the connected FTDI devices. It will not contain opened devices.
     * @param serialNumber Filtering option, exact match need.
     * @return List contain available FTDI devices.
     * @throws FTD2XXException If something goes wrong.
     */
    public static List<FTDevice> getDevicesBySerialNumber(String serialNumber)
            throws FTD2XXException {
        IntByReference devNum = new IntByReference();

        ensureFTStatus(ftd2xx.FT_CreateDeviceInfoList(devNum));

        ArrayList<FTDevice> devs = new ArrayList<FTDevice>(devNum.getValue());

        for (int i = 0; i < devNum.getValue(); i++) {
            FTDevice device = getXthDevice(i);

            if (((device.getFlag() & FTD2XX.FT_FLAGS_OPENED) == 0)
                    && serialNumber.equals(device.devSerialNumber)) {
                devs.add(device);
            }

        }

        Logger.getLogger(FTDevice.class.getName()).log(Level.INFO,
                "Found devs: {0} (All:{1})",
                new Object[]{devs.size(), devNum.getValue()});

        return devs;
    }

    /**
     * Get the connected FTDI devices. It will not contain opened devices.
     * @param deviceType Filtering option.
     * @return List contain available FTDI devices.
     * @throws FTD2XXException If something goes wrong.
     */
    public static List<FTDevice> getDevicesByDeviceType(DeviceType deviceType)
            throws FTD2XXException {
        IntByReference devNum = new IntByReference();

        ensureFTStatus(ftd2xx.FT_CreateDeviceInfoList(devNum));

        ArrayList<FTDevice> devs = new ArrayList<FTDevice>(devNum.getValue());

        for (int i = 0; i < devNum.getValue(); i++) {
            FTDevice device = getXthDevice(i);

            if (((device.flag & FTD2XX.FT_FLAGS_OPENED) == 0)
                    && device.devType.equals(deviceType)) {
                devs.add(device);
            }

        }

        Logger.getLogger(FTDevice.class.getName()).log(Level.INFO,
                "Found devs: {0} (All:{1})",
                new Object[]{devs.size(), devNum.getValue()});

        return devs;
    }

    /**
     * Open connection with device.
     * @throws FTD2XXException If something goes wrong.
     */
    public void open() throws FTD2XXException {
        Memory memory = new Memory(16);
        memory.setString(0, devSerialNumber);
        IntByReference handle = new IntByReference();
        ensureFTStatus(ftd2xx.FT_OpenEx(memory, FTD2XX.FT_OPEN_BY_SERIAL_NUMBER,
                handle));
        this.ftHandle = handle.getValue();
        open = true;
    }

    /**
     * Close connection with device.
     * @throws FTD2XXException If something goes wrong.
     */
    public void close() throws FTD2XXException {
    	if (!open) return;
    	open = false;
        ensureFTStatus(ftd2xx.FT_Close(ftHandle));
    }

    /**
     * Set desired baud rate.
     * @param baudRate The baud rate.
     * @throws FTD2XXException If something goes wrong.
     */
    public void setBaudRate(long baudRate) throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_SetBaudRate(ftHandle, (int) baudRate));
    }

    /**
     * This function sets the data characteristics for the device
     * @param wordLength Number of bits per word 
     * @param stopBits Number of stop bits
     * @param parity Parity
     * @throws FTD2XXException If something goes wrong.
     */
    public void setDataCharacteristics(WordLength wordLength, StopBits stopBits,
            Parity parity) throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_SetDataCharacteristics(ftHandle,
                (byte) wordLength.constant(), (byte) stopBits.constant(),
                (byte) parity.constant()));
    }

    /**
     * Set the read and write timeouts for the device.
     * @param readTimeout Read timeout in milliseconds.
     * @param writeTimeout Write timeout in milliseconds.
     * @throws FTD2XXException If something goes wrong.
     */
    public void setTimeouts(long readTimeout, long writeTimeout)
            throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_SetTimeouts(ftHandle, (int) readTimeout,
                (int) writeTimeout));
    }

    /**
     * Sets the flow control for the device.
     * @param flowControl Flow control type.
     * @throws FTD2XXException If something goes wrong.
     */
    public void setFlowControl(FlowControl flowControl) throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_SetFlowControl(ftHandle,
                (short) flowControl.constant(), (byte) 0, (byte) 0));
    }

    /**
     * Sets the flow control for the device.
     * @param flowControl Flow control type.
     * @param uXon Character used to signal Xon. Only used if flow control is 
     * FT_FLOW_XON_XOFF
     * @param uXoff Character used to signal Xoff.  Only used if flow control is 
     * FT_FLOW_XON_XOFF
     * @throws FTD2XXException If something goes wrong.
     */
    public void setFlowControl(FlowControl flowControl, byte uXon, byte uXoff)
            throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_SetFlowControl(ftHandle,
                (short) flowControl.constant(), uXon, uXoff));
    }

    /**
     * Set the Data Terminal Ready (DTR) control signal.
     * @param status Status of DTR signal.
     * @throws FTD2XXException If something goes wrong.
     */
    public void setDtr(boolean status) throws FTD2XXException {
        if (status) {
            ensureFTStatus(ftd2xx.FT_SetDtr(ftHandle));
        } else {
            ensureFTStatus(ftd2xx.FT_ClrDtr(ftHandle));
        }
    }

    /**
     * Set the Request To Send (RTS) control signal
     * @param status Status of RTS signal.
     * @throws FTD2XXException If something goes wrong.
     */
    public void setRts(boolean status) throws FTD2XXException {
        if (status) {
            ensureFTStatus(ftd2xx.FT_SetRts(ftHandle));
        } else {
            ensureFTStatus(ftd2xx.FT_ClrRts(ftHandle));
        }
    }

    /**
     * Gets the modem status and line status from the device.
     * @return Modem and line statuses
     * @throws FTD2XXException If something goes wrong.
     */
    public EnumSet<DeviceStatus> getDeviceStatus() throws FTD2XXException {
        IntByReference modstat = new IntByReference();
        ensureFTStatus(ftd2xx.FT_GetModemStatus(ftHandle, modstat));
        return DeviceStatus.parseToEnumset(modstat.getValue());
    }

    /**
     * Gets the number of bytes in the receive queue.
     * @return The number of bytes in the receive queue
     * @throws FTD2XXException If something goes wrong.
     */
    public int getQueueStatus() throws FTD2XXException {
        IntByReference reference = new IntByReference();
        ensureFTStatus(ftd2xx.FT_GetQueueStatus(ftHandle, reference));
        return reference.getValue();
    }

    /**
     * Purge receive or transmit buffers in the device.
     * @param rxBuffer Will rxBuffer be purged?
     * @param txBuffer Will txBuffer be purged?
     * @throws FTD2XXException If something goes wrong.
     */
    public void purgeBuffer(boolean rxBuffer, boolean txBuffer)
            throws FTD2XXException {
        int mask = 0;
        if (rxBuffer) {
            mask |= Purge.PURGE_RX.constant();
        }
        if (txBuffer) {
            mask |= Purge.PURGE_TX.constant();
        }
        ensureFTStatus(ftd2xx.FT_Purge(ftHandle, mask));
    }

    /**
     * Send a reset command to the device.
     * @throws FTD2XXException If something goes wrong.
     */
    public void resetDevice() throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_ResetDevice(ftHandle));
    }

    /**
     * Set the latency timer value.
     * @param timer Latency timer value in milliseconds. 
     * Valid range is 2 – 255.
     * @throws FTD2XXException If something goes wrong.
     * @throws IllegalArgumentException If timer was not in range 2 - 255.
     */
    public void setLatencyTimer(short timer) throws FTD2XXException,
            IllegalArgumentException {
        if (!((timer > 2) && (timer < 255))) {
            throw new IllegalArgumentException("Valid range is 2 – 255!");
        }
        ensureFTStatus(ftd2xx.FT_SetLatencyTimer(ftHandle, (byte) timer));
    }

    /**
     * Get the current value of the latency timer.
     * @return latency timer value.
     * @throws FTD2XXException If something goes wrong.
     */
    public short getLatencyTimer() throws FTD2XXException {
        ByteByReference byReference = new ByteByReference();
        ensureFTStatus(ftd2xx.FT_GetLatencyTimer(ftHandle, byReference));
        return (short) ((short) byReference.getValue() & 0xFF);
    }

    /**
     * Enables different chip modes.
     * @param ucMask Required value for bit mode mask.  This sets up which bits
     * are  inputs and outputs.  A bit value of 0 sets the corresponding pin to 
     * an input, a bit value of 1 sets the corresponding pin to an output. In 
     * the case of CBUS Bit Bang, the upper nibble of this value controls which 
     * pins are inputs and outputs, while the lower nibble controls which of the
     * outputs are high and low.
     * @param bitMode Mode value.
     * @throws FTD2XXException If something goes wrong.
     */
    public void setBitMode(byte ucMask, BitModes bitMode)
            throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_SetBitMode(ftHandle, ucMask,
                (byte) bitMode.constant()));
    }

    /**
     * Gets the instantaneous value of the data bus.
     * @return instantaneous data bus value
     * @throws FTD2XXException If something goes wrong.
     */
    public BitModes getBitMode() throws FTD2XXException {
        ByteByReference byt = new ByteByReference();
        ensureFTStatus(ftd2xx.FT_GetBitmode(ftHandle, byt));
        return BitModes.parse(byt.getValue());
    }

    /**
     * Set the USB request transfer size.
     * This function can be used to change the transfer sizes from the default 
     * transfer size of 4096 bytes to better suit the application requirements.
     * Transfer sizes must be set to a multiple of 64 bytes between 64 bytes and
     * 64k bytes.
     * When FT_SetUSBParameters is called, the change comes into effect
     * immediately and any data that was held in the driver at the time of the
     * change is lost. Note that, at present, only dwInTransferSize is 
     * supported.
     * @param inTransferSize Transfer size for USB IN request
     * @param outTransferSize Transfer size for USB OUT request
     * @throws FTD2XXException If something goes wrong.
     */
    public void setUSBParameters(int inTransferSize, int outTransferSize)
            throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_SetUSBParameters(ftHandle, inTransferSize,
                outTransferSize));
    }

    /**
     * Program the EEPROM data
     * @param programData EEPROM to program
     * @throws FTD2XXException If something goes wrong.
     */
    public void writeEEPROM(EEPROMData programData)
            throws FTD2XXException {
        ensureFTStatus(ftd2xx.FT_EE_Program(ftHandle,
                programData.ft_program_data));
    }

    /**
     * Read device EEPROM data
     * @return EEPROM data
     * @throws FTD2XXException If something goes wrong.
     */
    public EEPROMData readEEPROM() throws FTD2XXException {
        FTD2XX.FT_PROGRAM_DATA.ByReference ftByReference =
                new FTD2XX.FT_PROGRAM_DATA.ByReference();
        ensureFTStatus(ftd2xx.FT_EE_Read(ftHandle, ftByReference));
        return new EEPROMData(ftByReference);
    }

    /**
     * Get the available size of the EEPROM user area
     * @return available size in bytes, of the EEPROM user area
     * @throws FTD2XXException If something goes wrong.
     */
    public int getEEPROMUserAreaSize() throws FTD2XXException {
        IntByReference size = new IntByReference();
        ensureFTStatus(ftd2xx.FT_EE_UASize(ftHandle, size));
        return size.getValue();
    }

    /**
     * Read the contents of the EEPROM user area
     * @param numberOfBytes Size in bytes, to be read
     * @return User EEPROM content
     * @throws FTD2XXException If something goes wrong.
     */
    public byte[] readEEPROMUserArea(int numberOfBytes)
            throws FTD2XXException {
        IntByReference actually = new IntByReference();
        Memory dest = new Memory(numberOfBytes);
        ensureFTStatus(ftd2xx.FT_EE_UARead(ftHandle, dest, numberOfBytes,
                actually));
        return dest.getByteArray(0, actually.getValue());
    }

    /**
     * Read all contents of the EEPROM user area
     * @return User EEPROM content
     * @throws FTD2XXException If something goes wrong.
     */
    public byte[] readFullEEPROMUserArea()
            throws FTD2XXException {
        int numberOfBytes = getEEPROMUserAreaSize();
        return readEEPROMUserArea(numberOfBytes);
    }

    /**
     * Read all contents of the EEPROM user area as String
     * @return User EEPROM content as String
     * @throws FTD2XXException If something goes wrong.
     */
    public String readFullEEPROMUserAreaAsString() throws IOException {
        IntByReference actually = new IntByReference();
        int numberOfBytes = getEEPROMUserAreaSize();
        Memory dest = new Memory(numberOfBytes);
        ensureFTStatus(ftd2xx.FT_EE_UARead(ftHandle, dest, numberOfBytes,
                actually));
        return dest.getString(0);
    }

    /**
     * Write data into the EEPROM user area
     * @param data byte[] to write
     * @throws FTD2XXException If something goes wrong.
     */
    public void writeEEPROMUserArea(byte[] data) throws FTD2XXException {
        Memory source = new Memory(data.length);
        source.write(0, data, 0, data.length);
        ensureFTStatus(ftd2xx.FT_EE_UAWrite(ftHandle, source, data.length));
    }

    /**
     * Write string into the EEPROM user area
     * @param data byte[] to write
     * @throws FTD2XXException If something goes wrong.
     */
    public void writeEEPROMUserArea(String data) throws FTD2XXException {
        Memory source = new Memory(data.length());
        source.setString(0, data);
        ensureFTStatus(ftd2xx.FT_EE_UAWrite(ftHandle, source, data.length()));
    }

    /**
     * Write bytes to device.
     * @param bytes Byte array to send
     * @param offset Start index
     * @param length Amount of bytes to write
     * @return Number of bytes actually written
     * @throws FTD2XXException If something goes wrong.
     */
    public int write(byte[] bytes, int offset, int length)
            throws FTD2XXException {
        Memory memory = new Memory(length);
        memory.write(0, bytes, offset, length);
        IntByReference wrote = new IntByReference();

        ensureFTStatus(ftd2xx.FT_Write(ftHandle, memory, length, wrote));

        return wrote.getValue();
    }

    /**
     * Write bytes to device.
     * @param bytes Byte array to send
     * @return Number of bytes actually written
     * @throws FTD2XXException If something goes wrong.
     */
    public int write(byte[] bytes) throws FTD2XXException {
        return write(bytes, 0, bytes.length);
    }

    /**
     * Write byte to device.
     * @param b Byte to send (0..255)
     * @return It was success?
     * @throws FTD2XXException  
     */
    public boolean write(int b) throws FTD2XXException {
        byte[] c = new byte[1];
        c[0] = (byte) b;
        return (write(c) == 1) ? true : false;
    }

    /**
     * Read bytes from device.
     * @param bytes Bytes array to store read bytes
     * @param offset Start index.
     * @param lenght Amount of bytes to read
     * @return Number of bytes actually read
     * @throws FTD2XXException If something goes wrong.
     */
    public int read(byte[] bytes, int offset, int lenght)
            throws FTD2XXException {
        Memory memory = new Memory(lenght);
        IntByReference read = new IntByReference();

        ensureFTStatus(ftd2xx.FT_Read(ftHandle, memory, lenght, read));

        memory.read(0, bytes, offset, lenght);

        return read.getValue();
    }

    /**
     * Read bytes from device.
     * @param bytes Bytes array to store read bytes
     * @return Number of bytes actually read
     * @throws FTD2XXException If something goes wrong.
     */
    public int read(byte[] bytes) throws FTD2XXException {
        return read(bytes, 0, bytes.length);
    }

    /**
     * Read a byte from device.
     * @return The byte what read or -1;
     * @throws FTD2XXException 
     */
    public int read() throws FTD2XXException {
        byte[] c = new byte[1];
        int ret = read(c);
        return (ret == 1) ? ((int) c[0] & 0xFF) : -1;
    }

    /**
     * Read given bytes from device.
     * @param number How many bytes do you want to read?
     * @return Read bytes
     * @throws FTD2XXException If something goes wrong.
     */
    public byte[] read(int number)
            throws FTD2XXException {
        byte[] ret = new byte[number];
        int actually = read(ret);
        if (actually != number) {
            byte[] shrink = new byte[actually];
            System.arraycopy(ret, 0, shrink, 0, actually);
            return shrink;
        } else {
            return ret;
        }
    }

    /**
     * Get an InputStream to device.
     * @return InputStream
     */
    public InputStream getInputStream() {
        if (fTDeviceInputStream == null) {
            fTDeviceInputStream = new FTDeviceInputStream(this);
        }
        return fTDeviceInputStream;
    }

    /**
     * Get an OutputStream to device.
     * @return OutputStream
     */
    public OutputStream getOutputStream() {
        if (fTDeviceOutputStream == null) {
            fTDeviceOutputStream = new FTDeviceOutputStream(this);
        }
        return fTDeviceOutputStream;
    }

    @Override
    @SuppressWarnings("FinalizeDeclaration")
    protected void finalize() throws Throwable {
        try {
            close();
        } catch (FTD2XXException ex) {
        }
        super.finalize();
    }
}
