/* -*-mode:c++; c-basic-offset:4; -*- */
/**********************************************************************************************
Copyright (C) 2007 Oliver Eichler oliver.eichler@gmx.de

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., 59 Temple Place - Suite 330, Boston, MA 02111 USA

USB Interface for MacOS X
Written by (C) Albrecht Dre <albrecht.dress@arcor.de> 2008

Based upon the CUSB.cpp source file and the examples provided by Apple with XCode

Garmin and MapSource are registered trademarks or trademarks of Garmin Ltd.
or one of its subsidiaries.

Apple is a registered trademark by Apple, Inc.
**********************************************************************************************/
#include <mach/mach.h>
#include <CoreFoundation/CFNumber.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/usb/IOUSBLib.h>

#include "CUSB.h"
#include "IDevice.h"
#include "Platform.h"

#include <iostream>
#include <sstream>
#include <assert.h>
#include <errno.h>
#include <sys/time.h>

using namespace Garmin;
using namespace std;

#define GUSB_DATA_AVAILABLE     2
#define GUSB_SESSION_START      5
#define GUSB_SESSION_STARTED    6

//#undef DBG
#define DBG

#define DBG_LINE_SIZE 16

namespace Garmin
{

    struct readIntrCmnd_t
    {
        void * data;
        UInt32 * size;
        IOReturn * result;
    };

    void * readIntrPipe(void * ptr) {
        CUSB * usb = (CUSB *) ptr;

        do {
            // wait for a command
            pthread_mutex_lock(&usb->ir_mutex);
            while (usb->readIntrCmnd == 0)
                pthread_cond_wait(&usb->ir_cond, &usb->ir_mutex);

            // eject from the thread if requested
            if (usb->readIntrCmnd == (struct readIntrCmnd_t *) (-1)) {
                pthread_mutex_unlock(&usb->ir_mutex);
                return NULL;
            }

            // perform the read
            pthread_mutex_unlock(&usb->ir_mutex);
            *usb->readIntrCmnd->result =
                (*(usb->usbIntf))->ReadPipe(usb->usbIntf, usb->epIntrIn, usb->readIntrCmnd->data,
                usb->readIntrCmnd->size);

            // notify caller
            pthread_mutex_lock(&usb->ir_mutex);
            usb->readIntrCmnd = 0;
            pthread_cond_broadcast(&usb->ir_cond);
            pthread_mutex_unlock(&usb->ir_mutex);
        } while (1);
        // never reached
    }

    // Helper: create an IOKit error string
    class MacErrStr: public string
    {
        public:
            MacErrStr(IOReturn val) {
                if (val == kIOReturnSuccess) {
                    assign("no error");
                    return;
                }

                assign("[");

                // get the system
                switch (val & system_emask) {
                    case sys_iokit:
                        append("IOKit"); break;
                    default:
                        stringstream msg;
                        msg << "system " << hex << err_get_system(val);
                        append(msg.str());
                }

                // get the subsystem
                switch (val & sub_emask) {
                    case sub_iokit_common:
                        append("/common"); break;
                    case sub_iokit_usb:
                        append("/usb"); break;
                    default:
                        stringstream msg;
                        msg << "/sub " << hex << err_get_sub(val);
                        append(msg.str());
                }

                // append the error code (too many to decode them - there should be a system function!)
                stringstream msg;
                msg << "] code 0x" << hex << err_get_code(val);
                append(msg.str());
            }

            ~MacErrStr() {}
    };
}


CUSB::CUSB()
: epBulkIn(0),
epBulkOut(0),
epIntrIn(0),
max_tx_size(-1),
doBulkRead(false),
productId(0),
softwareVersion(0),
protocolArraySize(-1),
masterPort(0),
usbDevRef(0),
usbDev(0),
usbInterfaceRef(0),
usbIntf(0),
ir_thread(0),
readIntrCmnd(0)
{
    kern_return_t err;

    // create the mutex and the condition for the interrupt pipe
    if (pthread_mutex_init(&ir_mutex, NULL) != 0)
        throw exce_t(errOpen, "Failed to initialise mutex.");
    if (pthread_cond_init(&ir_cond, NULL) != 0)
        throw exce_t(errOpen, "Failed to initialise condition variable.");

    // open the master port
    if ((err = IOMasterPort(MACH_PORT_NULL, &masterPort))) {
        stringstream msg;
        msg << "Error " << MacErrStr(err) << " creating master port.";
        throw exce_t(errOpen, msg.str());
    }

    // launch the thread for reading from the interrupt pipe
    if (pthread_create(&ir_thread, NULL, &readIntrPipe, this) != 0)
        throw exce_t(errOpen, "Failed to launch read thread.");
}


CUSB::~CUSB()
{
    close();

    // terminate the interrupt pipe read thread
    if (ir_thread) {
        pthread_mutex_lock(&ir_mutex);
        readIntrCmnd = (struct readIntrCmnd_t *) (-1);
        pthread_cond_signal(&ir_cond);
        pthread_mutex_unlock(&ir_mutex);
        pthread_join(ir_thread, NULL);
    }
    pthread_cond_destroy(&ir_cond);
    pthread_mutex_destroy(&ir_mutex);
    if (masterPort)
        mach_port_deallocate(mach_task_self(), masterPort);
    masterPort = 0;
}


void CUSB::open()
{
    CFMutableDictionaryRef matchingDict;
    SInt32 idVal;
    CFNumberRef numberRef;

    // create a matching dictionary
    if (!(matchingDict = IOServiceMatching(kIOUSBDeviceClassName)))
        throw exce_t(errOpen, "Cannot create matching dictionary.");

    // configure the dictionary
    idVal = GARMIN_VID;
    if (!(numberRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &idVal))) {
        CFRelease(matchingDict);
        throw exce_t(errOpen, "Cannot configure matching dictionary.");
    }
    CFDictionaryAddValue(matchingDict, CFSTR(kUSBVendorID), numberRef);
    CFRelease(numberRef);
    idVal = G60CSX_PID;
    if (!(numberRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &idVal))) {
        CFRelease(matchingDict);
        throw exce_t(errOpen, "Cannot configure matching dictionary.");
    }
    CFDictionaryAddValue(matchingDict, CFSTR(kUSBProductID), numberRef);
    CFRelease(numberRef);

    // get matching device (note: call consumes the matchingDict, so we must not unref it)
    // FIXME - at this point we get only the first Garmin...
    if ((usbDevRef = IOServiceGetMatchingService(masterPort, matchingDict)))
        start();
    else
        throw exce_t(errOpen, "No Garmin device found, is the unit connected?");
}


void CUSB::close()
{
    // release interface
    if (usbIntf) {
        IOReturn err;

        if ((err = (*usbIntf)->USBInterfaceClose(usbIntf)) != kIOReturnSuccess) {
#ifdef DBG
            cout << "Error " << MacErrStr(err) << " closing USB interface." << endl;
#endif
        }
        if ((err = (*usbIntf)->Release(usbIntf)) != kIOReturnSuccess) {
#ifdef DBG
            cout << "Error " << MacErrStr(err) << " releasing USB interface." << endl;
#endif
        }
    }
    usbIntf = 0;

    if (usbInterfaceRef)
        IOObjectRelease(usbInterfaceRef);
    usbInterfaceRef = 0;

    // release USB device
    if (usbDev) {
        IOReturn err;

        if ((err = (*usbDev)->USBDeviceClose(usbDev)) != kIOReturnSuccess) {
#ifdef DBG
            cout << "Error " << MacErrStr(err) << " closing USB device." << endl;
#endif
        }
        if ((err = (*usbDev)->Release(usbDev)) != kIOReturnSuccess) {
#ifdef DBG
            cout << "Error " << MacErrStr(err) << " releasing USB device." << endl;
#endif
        }
    }
    usbDev = 0;

    // release matching device
    if (usbDevRef)
        IOObjectRelease(usbDevRef);
    usbDevRef = 0;
}


void CUSB::close2()
{
    close();
}


void CUSB::debug(const char * mark, const Packet_t& data)
{
#ifndef DBG
    return;
#endif
    unsigned i;
    uint32_t size;
    unsigned bytes = DBG_LINE_SIZE;
    char buf[DBG_LINE_SIZE + 1];

    memset(buf, 0x20, sizeof(buf));
    buf[DBG_LINE_SIZE] = 0;

    cout << mark << endl << "     ";

    const uint8_t * pData = (const uint8_t*) &data;

    size = gar_endian(uint32_t, data.size);
    if (size > GUSB_MAX_BUFFER_SIZE) {
        cerr << "WARNING! Data size " << data.size << " exceeds buffer size." << endl;
        cerr << "Truncate to " << GUSB_MAX_BUFFER_SIZE << "." << endl;
        size = GUSB_PAYLOAD_SIZE;
    }

    for (i = 0; i < (size + GUSB_HEADER_SIZE); ++i) {
        if (i && !(i % DBG_LINE_SIZE)) {
            cout << " " << buf << endl << "     ";
            memset(buf, 0x20, sizeof(buf));
            buf[DBG_LINE_SIZE] = 0;
            bytes = DBG_LINE_SIZE;
        }

        cout.width(2);
        cout.fill('0');
        cout << hex << (unsigned) pData[i] << " ";

        if (isprint(pData[i]))
            buf[i % DBG_LINE_SIZE] = pData[i];
        else
            buf[i % DBG_LINE_SIZE] = '.';

        --bytes;

    }
    for (i = 0; i < bytes; i++) cout << "   ";
    cout << " " << buf << dec << endl;

}


int CUSB::read(Packet_t& data)
{
    IOReturn res;
    UInt32 size = sizeof(Packet_t);

    data.type = 0;
    data.id   = 0;
    data.size = 0;

    if (doBulkRead)
        res = (*usbIntf)->ReadPipeTO(usbIntf, epBulkIn, (void *) &data, &size, 3000, 4000);
    else {
        // tell the thread to read from the interrupt pipe
        pthread_mutex_lock(&ir_mutex);
        struct readIntrCmnd_t readCmnd;
        readCmnd.data = (void *) &data;
        readCmnd.result = &res;
        readCmnd.size = &size;
        readIntrCmnd = &readCmnd;
        pthread_cond_broadcast(&ir_cond);
        pthread_mutex_unlock(&ir_mutex);

        // set the timeout - MacOS X doesn't have clock_gettime?!? That's against the POSIX standard!
        struct timeval tv;
        gettimeofday(&tv, 0);
        struct timespec wait_until;
        wait_until.tv_sec = tv.tv_sec + 3;
        wait_until.tv_nsec = tv.tv_usec * 1000;

        // wait for the thread, abort on timeout
        pthread_mutex_lock(&ir_mutex);
        while (readIntrCmnd != 0 && pthread_cond_timedwait(&ir_cond, &ir_mutex, &wait_until) == 0);
        if (readIntrCmnd != 0) {
            (*usbIntf)->AbortPipe(usbIntf, epIntrIn);
            (*usbIntf)->ClearPipeStall(usbIntf, epIntrIn);
            pthread_cond_wait(&ir_cond, &ir_mutex);
        }
        pthread_mutex_unlock(&ir_mutex);
    }

    if (res == kIOReturnSuccess && size > 0) {
        debug(doBulkRead ? "b >>" : "i >>", data);

#if defined(HAVE_BIGENDIAN)
        // endian fix for id and size
        data.id = gar_endian(uint16_t, data.id);
        data.size = gar_endian(uint32_t, data.size);
#endif                   // big endian platform

        // switch to bulk pipe
        if (size > 0 && data.id == GUSB_DATA_AVAILABLE)
            doBulkRead = true;
    }

    // switch to interrupt pipe on errors or zero size packages
    if (size == 0 || res != kIOReturnSuccess)
        doBulkRead = false;

    if (res != kIOReturnSuccess) {
        stringstream msg;
        msg << "USB read failed: error " << MacErrStr(res);
        throw exce_t(errRead, msg.str());
    }

    return size;
}


void CUSB::write(const Packet_t& data)
{
    UInt32 size = GUSB_HEADER_SIZE + data.size;
    IOReturn res;
    char * src;

#if defined(HAVE_BIGENDIAN)
    // make a local copy for swapping the header
    Packet_t real_cmnd(data.type, gar_endian(uint16_t, data.id));
    real_cmnd.size = gar_endian(uint32_t, data.size);

    // copy payload (if any)
    if (data.size > 0)
        memcpy(real_cmnd.payload, data.payload, data.size);

    // send the tweaked packet
    src = (char *) &real_cmnd;
#else
    src = (char *) &data;
#endif                       // big endian platform

    res = (*usbIntf)->WritePipeTO(usbIntf, epBulkOut, (void *) src, size, 3000, 4000);

    debug("b <<", (Packet_t &) *src);

    if (res != kIOReturnSuccess) {
        stringstream msg;
        msg << "USB bulk write failed: error " << MacErrStr(res);
        throw exce_t(errWrite, msg.str());
    }

    /*
      The Garmin protocol requires that packets that are exactly
      a multiple of the max tx size be followed by a zero length
      packet.
    */
    if (size && !(size % max_tx_size)) {
        (*usbIntf)->WritePipeTO(usbIntf, epBulkOut, (void *) src, 0, 1000, 2000);
#ifdef DBG
        cout << "b << zero size packet to terminate" << endl;
#endif
    }
}


void CUSB::start(void)
{
    if (usbDev)
        return;

    // deal with the device: create plug-in
    IOReturn err;
    IOCFPlugInInterface **iodev;
    SInt32 score;

    err = IOCreatePlugInInterfaceForService(usbDevRef, kIOUSBDeviceUserClientTypeID,
        kIOCFPlugInInterfaceID, &iodev, &score);
    if ((err != kIOReturnSuccess) || !iodev) {
        stringstream msg;
        msg << "Garmin USB device: error " << MacErrStr(err) << " creating plugin.";
        throw exce_t(errOpen, msg.str());
    }

    // query the interface
    err = (*iodev)->QueryInterface(iodev, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID),
        (void **) &usbDev);
    IODestroyPlugInInterface(iodev);
    if ((err != kIOReturnSuccess) || !usbDev) {
        stringstream msg;
        msg << "Garmin USB device: error " << MacErrStr(err) << " creating device interface.";
        throw exce_t(errOpen, msg.str());
    }

    // open the interface
    if ((err = (*usbDev)->USBDeviceOpen(usbDev)) != kIOReturnSuccess) {
        (*usbDev)->Release(usbDev);
        usbDev = 0;
        stringstream msg;
        msg << "Garmin USB device: error " << MacErrStr(err) << " opening USB device.";
        throw exce_t(errOpen, msg.str());
    }

    // get configurations
    UInt8 numConf;

    err = (*usbDev)->GetNumberOfConfigurations(usbDev, &numConf);
    if ((err != kIOReturnSuccess) || !numConf) {
        stringstream msg;
        msg << "Garmin USB device: error " << MacErrStr(err) << " reading configurations.";
        throw exce_t(errOpen, msg.str());
    }
#ifdef DBG
    cout << "Garmin USB device: found " << unsigned(numConf) << " configurations" << endl;
#endif

    // get the first configuration
    IOUSBConfigurationDescriptorPtr confDesc;

    if ((err = (*usbDev)->GetConfigurationDescriptorPtr(usbDev, 0, &confDesc)) !=
    kIOReturnSuccess) {
        stringstream msg;
        msg << "Garmin USB device: error " << MacErrStr(err) << " reading first configuration.";
        throw exce_t(errOpen, msg.str());
    }

    // set the first configuration
    if ((err = (*usbDev)->SetConfiguration(usbDev, confDesc->bConfigurationValue)) !=
    kIOReturnSuccess) {
        stringstream msg;
        msg << "Garmin USB device: error " << MacErrStr(err) << " setting configuration.";
        throw exce_t(errOpen, msg.str());
    }

    // find the interface
    IOUSBFindInterfaceRequest interfaceRequest;

                                 // requested class
    interfaceRequest.bInterfaceClass = kIOUSBFindInterfaceDontCare;
                                 // requested subclass
    interfaceRequest.bInterfaceSubClass = kIOUSBFindInterfaceDontCare;
                                 // requested protocol
    interfaceRequest.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
                                 // requested alt setting
    interfaceRequest.bAlternateSetting = kIOUSBFindInterfaceDontCare;

    io_iterator_t iterator;

    if ((err = (*usbDev)->CreateInterfaceIterator(usbDev, &interfaceRequest, &iterator)) !=
    kIOReturnSuccess) {
        stringstream msg;
        msg << "Garmin USB device: error " << MacErrStr(err) << " creating interface iterator.";
        throw exce_t(errOpen, msg.str());
    }

    if (!(usbInterfaceRef = IOIteratorNext(iterator))) {
        IOObjectRelease(iterator);
        throw exce_t(errOpen, "Garmin USB device: cannot find USB interface.");
    }
    IOObjectRelease(iterator);

    // deal with the interface: create interface plug-in
    err = IOCreatePlugInInterfaceForService(usbInterfaceRef, kIOUSBInterfaceUserClientTypeID,
        kIOCFPlugInInterfaceID, &iodev, &score);
    if ((err != kIOReturnSuccess) || !iodev) {
        stringstream msg;
        msg << "Garmin USB device interface: error " << MacErrStr(err) << " creating plugin.";
        throw exce_t(errOpen, msg.str());
    }

    // query interface
    err = (*iodev)->QueryInterface(iodev, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID),
        (void **) &usbIntf);
    IODestroyPlugInInterface(iodev);
    if ((err != kIOReturnSuccess) || !usbIntf) {
        stringstream msg;
        msg << "Garmin USB device interface: error " << MacErrStr(err) << " creating device interface.";
        throw exce_t(errOpen, msg.str());
    }

    // open interface
    if ((err = (*usbIntf)->USBInterfaceOpen(usbIntf)) != kIOReturnSuccess) {
        (*usbIntf)->Release(usbIntf);
        usbIntf = 0;
        stringstream msg;
        msg << "Garmin USB device interface: error " << MacErrStr(err) << " opening USB device interface.";
        throw exce_t(errOpen, msg.str());
    }

    // get endpoints
    UInt8 numEP;

    if ((err = (*usbIntf)->GetNumEndpoints(usbIntf, &numEP)) != kIOReturnSuccess) {
        stringstream msg;
        msg << "Garmin USB device interface: error " << MacErrStr(err) << " reading number of endpoints.";
        throw exce_t(errOpen, msg.str());
    }
#ifdef DBG
    cout << "Garmin USB device interface: found " << unsigned(numEP) << " endpoints." << endl;
#endif

    // identify bulk in, bulk out and interrupt in endpoints
    int i;

    for (i = 1; i <= numEP; i++) {
        UInt8 direction;
        UInt8 number;
        UInt8 transferType;
        UInt8 interval;
        UInt16 maxPacketSize;

        if ((err =
            (*usbIntf)->GetPipeProperties(usbIntf, i, &direction, &number,
            &transferType, &maxPacketSize, &interval)) !=
        kIOReturnSuccess) {
            stringstream msg;
            msg << "Garmin USB device interface: error " << MacErrStr(err) << " reading properties of endpoint "
                << i << ".";
            throw exce_t(errOpen, msg.str());
        }
#ifdef DBG
        cout << "endpoint " << i << ": dir 0x" << hex << unsigned(direction) << ", number 0x" <<
            hex << unsigned(number) << ", type 0x" << hex << unsigned(transferType) << ", max size " <<
            dec << maxPacketSize << ", interval 0x" << hex << unsigned(interval);
#endif
        if (transferType == kUSBBulk) {
            switch (direction) {
                case kUSBIn:
#ifdef DBG
                    cout << " ---> use as BULK IN pipe" << endl;
#endif
                    epBulkIn = i;
                    break;

                case kUSBOut:
#ifdef DBG
                    cout << " ---> use as BULK OUT pipe" << endl;
#endif
                    epBulkOut = i;
                    max_tx_size = maxPacketSize;
                    break;

                default:
#ifdef DBG
                    cout << " ---> no idea what I should do with it" << endl;
#endif
                    break;
            }
        }
        else if (transferType == kUSBInterrupt && (direction == kUSBIn)) {
#ifdef DBG
            cout << " ---> use as INTR IN pipe" << endl;
#endif
            epIntrIn = i;
        }
        else {
#ifdef DBG
            cout << " ---> no idea what I should do with it" << endl;
#endif
        }
    }

    if (!epBulkIn || !epBulkOut || !epIntrIn || max_tx_size <= 0)
        throw exce_t(errOpen,"Garmin USB device interface: failed to identify USB endpoints.");
}


void CUSB::syncup(void)
{
    static const Packet_t gpack_session_start(GUSB_PROTOCOL_LAYER, GUSB_SESSION_START);
    Packet_t response;

    write(gpack_session_start);
    read(response);
    if (response.id == GUSB_SESSION_STARTED) {
        Packet_t command;
        Packet_t response;

        command.type = GUSB_APPLICATION_LAYER;
        command.id   = Pid_Product_Rqst;
        command.size = 0;

        write(command);

        protocolArraySize = 0;
        while (read(response)) {
            if (response.id == Pid_Product_Data) {
                //TODO read data
                Product_Data_t * pData = (Product_Data_t*)response.payload;
                productId       = gar_load(uint16_t, pData->product_id);
                softwareVersion = gar_load(int16_t, pData->software_version);
                productString   = pData->str;
#ifdef DBG
                cout << "Product: " << hex << productId << " " << dec << softwareVersion <<
                    " " << productString << endl;
#endif
            }

            if (response.id == Pid_Ext_Product_Data) {
                //TODO read data
            }

            if (response.id == Pid_Protocol_Array) {
                // note: we cannot use a Protocol_Data_t here due to alignment issues
                // on some platforms...
                uint8_t * p = response.payload;
                for (uint32_t i = 0; i < response.size; i += sizeof(Protocol_Data_t)) {
                    uint8_t  pr_tag = *p++;
                    uint16_t pr_data = gar_ptr_load(uint16_t, p);
                    p += 2;
#ifdef DBG
                    cout << "Protocol: "<< (char)pr_tag <<  dec << pr_data << endl;
#endif
                    ++protocolArraySize;
                    protocolArray[protocolArraySize].tag = pr_tag;
                    protocolArray[protocolArraySize].data = pr_data;
                }
                ++protocolArraySize;
#ifdef DBG
                cout << "protocolArraySize:" << protocolArraySize << endl;
#endif
                //
                if (!doBulkRead) return;
            }
        }
        return;
    }

    throw exce_t(errSync,"Failed to sync. up with device");
}


uint16_t CUSB::getDataType(int data_no, char tag, uint16_t protocol)
{
    // Find the right tag D<Data_no> for <tag><protocol>
    for (uint32_t i = 0; i < protocolArraySize - 1 - data_no; i++) {
        if ((char) protocolArray[i].tag == tag) {
            if (protocolArray[i].data == protocol) {
                // accept data_no=-1 as a protocol verification only
                if (data_no == -1)
                    return (uint16_t) 1;
                if ((char) protocolArray[i + 1 + data_no].tag == 'D')
                    return protocolArray[i + 1 + data_no].data;
            }
        }
    }
    return (uint16_t) 0;
}
