/*
 * Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "D2XX.h"
#include <dlfcn.h>
#include <assert.h>

// D2XXWrapper is a wrapper around libftd2xx to make it amenable to loading
// with dlopen().

#define LOAD_SYM(type,var,name) \
    var = (type*) dlsym(handle, name); \
    if (!var) { \
        error = QString("could not load symbol ") + name; \
        return false; \
    }
#ifdef WIN32
#define WIN32_STDCALL __stdcall
#else
#define WIN32_STDCALL
#endif

typedef FT_STATUS WIN32_STDCALL FP_OpenEx(PVOID pArg1, DWORD Flags, FT_HANDLE *pHandle);
typedef FT_STATUS WIN32_STDCALL FP_Close(FT_HANDLE ftHandle);
typedef FT_STATUS WIN32_STDCALL FP_SetBaudRate(FT_HANDLE ftHandle, ULONG BaudRate);
typedef FT_STATUS WIN32_STDCALL FP_SetDataCharacteristics(FT_HANDLE ftHandle, UCHAR WordLength, UCHAR StopBits, UCHAR Parity);
typedef FT_STATUS WIN32_STDCALL FP_SetFlowControl(FT_HANDLE ftHandle, USHORT FlowControl, UCHAR XonChar, UCHAR XoffChar);
typedef FT_STATUS WIN32_STDCALL FP_GetQueueStatus(FT_HANDLE ftHandle, DWORD *dwRxBytes);
typedef FT_STATUS WIN32_STDCALL FP_SetTimeouts(FT_HANDLE ftHandle, ULONG ReadTimeout, ULONG WriteTimeout);
typedef FT_STATUS WIN32_STDCALL FP_Read(FT_HANDLE ftHandle, LPVOID lpBuffer, DWORD nBufferSize, LPDWORD lpBytesReturned);
typedef FT_STATUS WIN32_STDCALL FP_Write(FT_HANDLE ftHandle, LPVOID lpBuffer, DWORD nBufferSize, LPDWORD lpBytesWritten);
typedef FT_STATUS WIN32_STDCALL FP_CreateDeviceInfoList(LPDWORD lpdwNumDevs);
typedef FT_STATUS WIN32_STDCALL FP_GetDeviceInfoList(FT_DEVICE_LIST_INFO_NODE *pDest, LPDWORD lpdwNumDevs);

struct D2XXWrapper {
    void *handle;
    FP_OpenEx *open_ex;
    FP_Close *close;
    FP_SetBaudRate *set_baud_rate;
    FP_SetDataCharacteristics *set_data_characteristics;
    FP_SetFlowControl *set_flow_control;
    FP_GetQueueStatus *get_queue_status;
    FP_SetTimeouts *set_timeouts;
    FP_Read *read;
    FP_Write *write;
    FP_CreateDeviceInfoList *create_device_info_list;
    FP_GetDeviceInfoList *get_device_info_list;
    D2XXWrapper() : handle(NULL) {}
    ~D2XXWrapper() { if (handle) dlclose(handle); }
    bool init(QString &error)
    {
        #if defined(Q_OS_LINUX)
            const char *libname = "libftd2xx.so";
        #elif defined(Q_OS_WIN32)
            const char *libname = "ftd2xx.dll";
        #elif defined(Q_OS_DARWIN)
            const char *libname = "libftd2xx.dylib";
        #endif
        handle = dlopen(libname, RTLD_NOW);
        if (!handle) {
            error = QString("Couldn't load library ") + libname + ".";
            return false;
        }
        LOAD_SYM(FP_OpenEx, open_ex, "FT_OpenEx");
        LOAD_SYM(FP_Close, close, "FT_Close");
        LOAD_SYM(FP_SetBaudRate, set_baud_rate, "FT_SetBaudRate");
        LOAD_SYM(FP_SetDataCharacteristics, set_data_characteristics, "FT_SetDataCharacteristics");
        LOAD_SYM(FP_SetFlowControl, set_flow_control, "FT_SetFlowControl");
        LOAD_SYM(FP_GetQueueStatus, get_queue_status, "FT_GetQueueStatus");
        LOAD_SYM(FP_SetTimeouts, set_timeouts, "FT_SetTimeouts");
        LOAD_SYM(FP_Read, read, "FT_Read");
        LOAD_SYM(FP_Write, write, "FT_Write");
        LOAD_SYM(FP_CreateDeviceInfoList, create_device_info_list, "FT_CreateDeviceInfoList");
        LOAD_SYM(FP_GetDeviceInfoList, get_device_info_list, "FT_GetDeviceInfoList");
        return true;
    }
};

static D2XXWrapper *lib; // singleton lib instance

bool D2XXRegistered = CommPort::addListFunction(&D2XX::myListCommPorts);

D2XX::D2XX(const FT_DEVICE_LIST_INFO_NODE &info) :
    CommPort( "D2XX" ), info(info), _isOpen(false)
{
}

D2XX::~D2XX()
{
    if (_isOpen)
        close();
}

bool
D2XX::isOpen()
{
    return _isOpen;
}

bool
D2XX::open(QString &err)
{
    assert(!_isOpen);
    FT_STATUS ftStatus =
        lib->open_ex(info.Description, FT_OPEN_BY_DESCRIPTION, &ftHandle);
    if (ftStatus != FT_OK) {
        err = QString("FT_Open: %1").arg(ftStatus);
        return false;
    }
    _isOpen = true;
    ftStatus = lib->set_baud_rate(ftHandle, 9600);
    if (ftStatus != FT_OK) {
        err = QString("FT_SetBaudRate: %1").arg(ftStatus);
        close();
    }

    ftStatus = lib->set_data_characteristics(ftHandle,FT_BITS_8,FT_STOP_BITS_1,
                                             FT_PARITY_NONE);
    if (ftStatus != FT_OK) {
        err = QString("FT_SetDataCharacteristics: %1").arg(ftStatus);
        close();
    }

    ftStatus = lib->set_flow_control(ftHandle,FT_FLOW_NONE,
                                     '0','0'); //the 0's are ignored
    if (ftStatus != FT_OK) {
        err = QString("FT_SetFlowControl: %1").arg(ftStatus);
        close();
    }

    return true;
}

void
D2XX::close()
{
    assert(_isOpen);
    lib->close(ftHandle);
    _isOpen = false;
}

int
D2XX::read(void *buf, size_t nbyte, QString &err)
{
    assert(_isOpen);
    DWORD rxbytes;
    FT_STATUS ftStatus = lib->get_queue_status(ftHandle, &rxbytes);
    if (ftStatus != FT_OK) {
        err = QString("FT_GetQueueStatus: %1").arg(ftStatus);
        return -1;
    }
    // printf("rxbytes=%d\n", (int) rxbytes);
    // Return immediately whenever there's something to read.
    if (rxbytes > 0 && rxbytes < nbyte)
        nbyte = rxbytes;
    if (nbyte > rxbytes)
        lib->set_timeouts(ftHandle, 5000, 5000);
    DWORD n;
    ftStatus = lib->read(ftHandle, buf, nbyte, &n);
    if (ftStatus == FT_OK)
        return n;
    err = QString("FT_Read: %1").arg(ftStatus);
    return -1;
}

int
D2XX::write(void *buf, size_t nbyte, QString &err)
{
    assert(_isOpen);
    DWORD n;
    FT_STATUS ftStatus = lib->write(ftHandle, buf, nbyte, &n);
    if (ftStatus == FT_OK)
        return n;
    err = QString("FT_Write: %1").arg(ftStatus);
    return -1;
}

QString
D2XX::name() const
{
    return info.Description;
}

bool
D2XX::setBaudRate(int speed, QString &err)
{
    assert(_isOpen);
    FT_STATUS ftStatus = lib->set_baud_rate(ftHandle, speed);
    if (ftStatus != FT_OK) {
        err = QString("FT_SetBaudRate: %1").arg(ftStatus);
        return false;
    }

    return true;
}

QVector<CommPortPtr>
D2XX::myListCommPorts(QString &err)
{
    QVector<CommPortPtr> result;
    if (!lib) {
        lib = new D2XXWrapper;
        if (!lib->init(err)) {
            delete lib;
            lib = NULL;
            return result;
        }
    }
    DWORD numDevs;
    FT_STATUS ftStatus = lib->create_device_info_list(&numDevs);
    if(ftStatus != FT_OK) {
        err = QString("FT_CreateDeviceInfoList: %1").arg(ftStatus);
        return result;
    }
    FT_DEVICE_LIST_INFO_NODE *devInfo = new FT_DEVICE_LIST_INFO_NODE[numDevs];
    ftStatus = lib->get_device_info_list(devInfo, &numDevs);
    if (ftStatus != FT_OK)
        err = QString("FT_GetDeviceInfoList: %1").arg(ftStatus);
    else {
        for (DWORD i = 0; i < numDevs; i++)
            result.append(CommPortPtr(new D2XX(devInfo[i])));
    }
    delete [] devInfo;
    // If we can't open a D2XX device, it's usually because the VCP drivers
    // are installed, so it should also show up in the list of serial devices.
    for (int i = 0; i < result.size(); ++i) {
        CommPortPtr dev = result[i];
        QString tmp;
        if (dev->open(tmp))
            dev->close();
        else
            result.remove(i--);
    }
    return result;
}

