/**********************************************************************************************
    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

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

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

#include <iostream>
#include <sstream>
#include <assert.h>
#include <errno.h>

using namespace Garmin;
using namespace std;

#ifndef ETIMEDOUT                // included for windows by dr, copied from pthread.h
#  define ETIMEDOUT 10060        /* This is the value in winsock.h. */
#endif

#define EA(x) (x & USB_ENDPOINT_ADDRESS_MASK)

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

#define USB_INTERRUPT_TIMEOUT  3000
#define USB_BULK_TIMEOUT      30000

#undef DBG

#define DBG_LINE_SIZE 16

CUSB::CUSB()
: busses(0)
, udev(0)
, theInterface(-1)
, epBulkIn(-1)
, epBulkOut(-1)
, epIntrIn(-1)
, max_tx_size(0)
, doBulkRead(false)
, productId(0)
, softwareVersion(0)
, protocolArraySize(-1)
{
    usb_init();
    usb_find_busses();
    usb_find_devices();
    busses = usb_get_busses();
}


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


void CUSB::open()
{
    assert(busses);

    usb_bus *bus = 0;

    for(bus = busses; bus; bus = bus->next) {
        struct usb_device * dev = 0;
        for (dev = bus->devices; dev; dev = dev->next) {
#ifdef DBG
            cout << hex << dev->descriptor.idVendor << " " << dev->descriptor.idProduct << endl;
#endif
            if(dev->descriptor.idVendor == GARMIN_VID) {

                if(dev->descriptor.idProduct == G60CSX_PID) {
                    start(dev);
                    break;
                }
            }
        }
    }

    if(udev == 0) {
        throw exce_t(errOpen,"Is the unit connected?");
    }
}


void CUSB::close()
{
    if(udev) {
        usb_release_interface(udev, theInterface);
        usb_close(udev);
        udev = 0;
    }
}


void CUSB::close2()
{
    if(udev) {
        usb_release_interface(udev, theInterface);
        usb_reset(udev);
        usb_close(udev);
        udev = 0;
    }
}


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)
{
    int res;

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

    if(doBulkRead) {
        res = ::usb_bulk_read(udev,epBulkIn,(char*)&data,sizeof(data),USB_BULK_TIMEOUT);

        if (res > 0)
            debug("b >>", 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
    }
    else {
        res = ::usb_interrupt_read(udev,epIntrIn,(char*)&data,sizeof(data),USB_INTERRUPT_TIMEOUT);

        if (res > 0)
            debug("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
    }

    // Some devices sending data on the interrupt pipe seem
    // to timeout occasionally. It seems to be save to ignore this
    // timeout.
    if(res == -ETIMEDOUT && !doBulkRead) {
        res = 0;
    }

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

    // switch to interrupt pipe on errors or zero size packages
    if(res <= 0) {
        doBulkRead = false;
    }

    if(res < 0) {
        stringstream msg;
        msg << "USB read failed:" << usb_strerror();
        throw exce_t(errRead,msg.str());
    }

    return res;
}


void CUSB::write(const Packet_t& data)
{
    unsigned size = GUSB_HEADER_SIZE + data.size;
    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

    int res = ::usb_bulk_write(udev,epBulkOut,src,size,USB_BULK_TIMEOUT);
    debug("b <<", (Packet_t &) *src);

    if(res < 0) {
        stringstream msg;
        msg << "USB bulk write failed:" << usb_strerror();
        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)) {
        ::usb_bulk_write(udev,epBulkOut,(char*)&data,0,USB_BULK_TIMEOUT);
#ifdef DBG
        cout << "b << zero size packet to terminate" << endl;
#endif
    }
}


void CUSB::start(struct usb_device *dev)
{
    int i;
    if(udev) return;

    udev = usb_open(dev);
    if(udev == 0) {
        stringstream msg;
        msg << "Failed to open USB device: " << usb_strerror();
        throw exce_t(errOpen,msg.str());
    }

    if (dev->config == 0) {
        stringstream msg;
        msg << "USB device has no configuration: " << usb_strerror();
        throw exce_t(errOpen,msg.str());
    }

    if (usb_set_configuration(udev, dev->config->bConfigurationValue) < 0) {
        stringstream msg;
#if __linux__
        char drvnm[128];
        drvnm[0] = 0;
        msg << "Failed to configure USB: " << usb_strerror();

        usb_get_driver_np(udev, 0, drvnm, sizeof(drvnm)-1);

        if(strlen(drvnm) != 0) {
            msg << "\n\nThe kernel driver '" << drvnm << "' is blocking. "
                << "Please use 'rmmod " << drvnm << "' as root to remove it temporarily. "
                << "You might consider to add 'blacklist " << drvnm << "' to your "
                << "modeprobe.conf, to remove the module permanently.";
        }
        throw exce_t(errOpen,msg.str());
#endif

        msg << "Failed to configure USB: " << usb_strerror();
        throw exce_t(errOpen,msg.str());
    }

    theInterface = dev->config->interface->altsetting->bInterfaceNumber;
    if (usb_claim_interface(udev, theInterface) < 0) {
        stringstream msg;
        msg << "Failed to claim USB interface: " << usb_strerror();
        throw exce_t(errOpen,msg.str());
    }

    max_tx_size = dev->descriptor.bMaxPacketSize0;
#ifdef DBG
    cout << "  max. packet size:" << max_tx_size << endl;
#endif

    for (i = 0; i < dev->config->interface->altsetting->bNumEndpoints; i++) {
        struct usb_endpoint_descriptor * ep;
        ep = &dev->config->interface->altsetting->endpoint[i];

        switch (ep->bmAttributes & USB_ENDPOINT_TYPE_MASK) {

            case USB_ENDPOINT_TYPE_BULK:
                if (ep->bEndpointAddress & USB_ENDPOINT_DIR_MASK) {
#ifdef DBG
                    cout << "  epBulkIn " << hex << EA(ep->bEndpointAddress) << endl;
#endif
                    epBulkIn = EA(ep->bEndpointAddress);
                }
                else {
#ifdef DBG
                    cout << "  epBulkOut " << hex << EA(ep->bEndpointAddress) << endl;
#endif
                    epBulkOut = EA(ep->bEndpointAddress);
                }
                break;

            case USB_ENDPOINT_TYPE_INTERRUPT:
                if (ep->bEndpointAddress & USB_ENDPOINT_DIR_MASK) {
#ifdef DBG
                    cout << "  epIntrIn " << hex << EA(ep->bEndpointAddress) << endl;
#endif
                    epIntrIn = EA(ep->bEndpointAddress);
                }
                break;
        }
    }

    if ((epBulkIn > 0) && (epBulkOut > 0) && (epIntrIn > 0)) {
        return;
    }

    throw exce_t(errOpen,"Failed to identify USB endpoints for this device.");
}


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

    int i, res;
    for(i=0; i<10; ++i) {
        write(gpack_session_start);
        if((res = read(response)) > 0) break;
    }
    if(res == 0) {
        throw exce_t(errSync,"Failed to sync. up with device");
    }

    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)
{

    if (protocolArraySize == 0)
    {
        return 0;
    }

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