/**********************************************************************************************
    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 <iostream>
#include <sstream>
#include <assert.h>
#include <errno.h>

//#include <windows.h>
#include <initguid.h>
#include <setupapi.h>            // You may need to explicitly link with setupapi.lib
#include <winioctl.h>

// {2C9C45C2-8E7D-4C08-A12D-816BBAE722C0}
DEFINE_GUID(GARMIN_GUID,               0x2c9c45c2L, 0x8e7d, 0x4c08, 0xa1, 0x2d, 0x81, 0x6b, 0xba, 0xe7, 0x22, 0xc0);
DEFINE_GUID(GUID_DEVINTERFACE_GRMNUSB, 0x2c9c45c2L, 0x8e7d, 0x4c08, 0xa1, 0x2d, 0x81, 0x6b, 0xba, 0xe7, 0x22, 0xc0);

#define IOCTL_ASYNC_IN        CTL_CODE (FILE_DEVICE_UNKNOWN, 0x850, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_USB_PACKET_SIZE CTL_CODE (FILE_DEVICE_UNKNOWN, 0x851, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define MAX_BUFFER_SIZE 4096
#define ASYNC_DATA_SIZE 64

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_LINE_SIZE 16

CUSB::CUSB()
: busses(0)
, udev(0)
//, theInterface(-1)
, max_tx_size(0)
, epBulkIn(-1)
, epBulkOut(-1)
, epIntrIn(-1)
, doBulkRead(false)
, productId(0)
, softwareVersion(0)
{
}


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


void CUSB::open()
{
    // Make all the necessary Windows calls to get a handle
    // to our USB device
    DWORD theBytesReturned = 0;

    PSP_INTERFACE_DEVICE_DETAIL_DATA theDevDetailData = 0;
    SP_DEVINFO_DATA theDevInfoData = { sizeof( SP_DEVINFO_DATA ) };

    Packet_t theStartSessionPacket = Packet_t(0,5);
    Packet_t thePacket = Packet_t();

    HDEVINFO theDevInfo;
    //	SP_DEVICE_INTERFACE_DATA devinterface;

    theDevInfo = ::SetupDiGetClassDevs( (GUID *) &GARMIN_GUID, NULL, NULL,
        DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);

    if (theDevInfo == INVALID_HANDLE_VALUE) {
        throw exce_t(errOpen,"Is the Garmin USB driver installed?");
    }
    SP_DEVICE_INTERFACE_DATA theInterfaceData;
    theInterfaceData.cbSize = sizeof( theInterfaceData );

    if( !SetupDiEnumDeviceInterfaces( theDevInfo,
        NULL,
        (GUID*) &GUID_DEVINTERFACE_GRMNUSB,
        0,
        &theInterfaceData ) &&
    GetLastError() == ERROR_NO_MORE_ITEMS ) {
        udev = 0;
        return;
    }

    SetupDiGetDeviceInterfaceDetail(
        theDevInfo,
        &theInterfaceData,
        NULL,
        0,
        &theBytesReturned,
        NULL );

    theDevDetailData =
        (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc( theBytesReturned );
    theDevDetailData->cbSize = sizeof( SP_INTERFACE_DEVICE_DETAIL_DATA );

    SetupDiGetDeviceInterfaceDetail( theDevInfo,
        &theInterfaceData,
        theDevDetailData,
        theBytesReturned,
        NULL,
        &theDevInfoData );

    udev = CreateFile(
        theDevDetailData->DevicePath,
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL );

    free( theDevDetailData );

    if(udev == NULL) {
        throw exce_t(errOpen,"Is the unit connected?");
    }
    if(udev == INVALID_HANDLE_VALUE) {
        throw exce_t(errOpen,"The unit seems to be connected with something else?");
    }

    // Get the USB packet size, which we need for sending packets
    gUSBPacketSize = 0;
    DeviceIoControl( udev,
        IOCTL_USB_PACKET_SIZE,
        0,
        0,
        &gUSBPacketSize,
        sizeof( gUSBPacketSize ),
        &theBytesReturned,
        NULL );

    // Tell the device that we are starting a session.
    /*	write( theStartSessionPacket );

        // Wait until the device is ready to start the session

        for( ; ; )
            {
            read( thePacket );

            if( thePacket.type == 0 &&
                thePacket.id == 6 )
                {
                break;
                }

    //	    free( thePacket );
            }

    //	free( thePacket );

    */
}


void CUSB::close()
{
    if(udev) {
        CloseHandle(udev);
        udev = 0;
    }
}


void CUSB::close2()
{
    if(udev) {
        CloseHandle(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 = 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=0;

    data.type = 0;
    data.id   = 0;
    data.size = 0;
    DWORD theBytesReturned = 0;
    BOOL ok = true;

    if(doBulkRead) {
        ok = ReadFile( udev, &data, MAX_BUFFER_SIZE, &theBytesReturned, NULL );
        if ( theBytesReturned <= 0 ) doBulkRead = false;
#ifdef WIN_DEBUG
        FILE *fp = NULL;
        fp = fopen( "report.txt","a");
        if (fp != NULL) {
            fprintf(fp,"CUSB::read bulk type=%d, id=%d, ret=%d\n",data.type,data.id,theBytesReturned);
            fclose(fp);
        }
#endif                   // WIN_DEBUG
    }
    else {
        DeviceIoControl( udev, IOCTL_ASYNC_IN, 0, 0, &data,
            ASYNC_DATA_SIZE, &theBytesReturned, NULL );
        if( data.type == 0 && data.id == GUSB_DATA_AVAILABLE ) {
            doBulkRead = true;
            data.type = 0;
            data.id   = 0;
            ok = ReadFile( udev, &data, MAX_BUFFER_SIZE, &theBytesReturned, NULL );
        }

    }

    if ( !ok ) {
        stringstream msg;
        msg << "USB ReadFile failed:" ;
        throw exce_t(errRead,msg.str());
    }

    /*
        // 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;
        }
    */
    res = 1;
    return res;
}


void CUSB::write(const Packet_t& data)
{
    //	unsigned l = sizeof(data);
    DWORD theBytesToWrite = GUSB_HEADER_SIZE + data.size;
    DWORD theBytesReturned = 0;

    WriteFile( udev, &data, theBytesToWrite, &theBytesReturned, NULL );
    if ( theBytesReturned != theBytesToWrite ) {
        stringstream msg;
        msg << "USB bulk write failed:" ;
        throw exce_t(errWrite,msg.str());
    }

    // If the packet size was an exact multiple of the USB packet
    // size, we must make a final write call with no data

    if( theBytesToWrite % gUSBPacketSize == 0 ) {
        WriteFile( udev, 0, 0, &theBytesReturned, NULL );
    }
}


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

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

}


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

    protocolArraySize = 0;

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

        command.type = GUSB_APPLICATION_LAYER;
        command.b1 = 0; command.b2 = 0; command.b3 = 0;
        command.id   = Pid_Product_Rqst;
        command.b6 = 0; command.b7 = 0;
        command.size = 0;

        write(command);

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

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

                                 // This should mark the end
            if(response.id == Pid_Protocol_Array) {
                //TODO read data

                Protocol_Data_t * pData = (Protocol_Data_t*)response.payload;
                for(uint32_t i = 0; i < response.size; i += sizeof(Protocol_Data_t)) {
#ifdef WIN_DEBUG
                    qDebug() << "Protocol: "<< (char)pData->tag <<  dec << pData->data << endl;
#endif
                    ++protocolArraySize;
                    protocolArray[protocolArraySize].tag = pData->tag;
                    protocolArray[protocolArraySize].data = pData->data;
                    ++pData;
                }
                ++protocolArraySize;
#ifdef WIN_DEBUG
                qDebug() << "protocolArraySize:" << protocolArraySize << endl;
#endif

                return;          // let's leave, even so it is a strange ending
            }
        }
        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;
}
