/*
 * Copyright (C) 2005-2008 by Daniel Wagner
 *
 * This file is part of FFADO
 * FFADO = Free FireWire (pro-)audio drivers for Linux
 *
 * FFADO is based upon FreeBoB
 *
 * 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) version 3 of the License.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "bebob_dl_mgr.h"
#include "bebob_dl_codes.h"
#include "bebob_dl_bcd.h"

#include "libieee1394/configrom.h"
#include "libieee1394/ieee1394service.h"

#include "libutil/cmd_serialize.h"
#include "libutil/Time.h"

#include "libutil/ByteSwap.h"

#include "ffadodevice.h"

#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <memory>

namespace BeBoB {
    enum {
        AddrRegInfo    = 0xffffc8020000ULL,
        AddrRegReq     = 0xffffc8021000ULL,
        AddrRegReqBuf  = 0xffffc8021040ULL,
        AddrRegResp    = 0xffffc8029000ULL,
        AddrRegRespBuf = 0xffffc8029040ULL,
    };

    enum {
        RegInfoManufactorIdOffset =      0x00,
        RegInfoProtocolVersionOffset =   0x08,
        RegInfoBootloaderVersionOffset = 0x0c,
        RegInfoGUID =                    0x10,
        RegInfoHardwareModelId =         0x18,
        RegInfoHardwareRevision =        0x1c,
        RegInfoSoftwareDate =            0x20,
        RegInfoSoftwareTime =            0x28,
        RegInfoSoftwareId =              0x30,
        RegInfoSoftwareVersion =         0x34,
        RegInfoBaseAddress =             0x38,
        RegInfoMaxImageLen =             0x3c,
        RegInfoBootloaderDate =          0x40,
        RegInfoBootloaderTime =          0x48,
        RegInfoDebuggerDate =            0x50,
        RegInfoDebuggerTime =            0x58,
        RegInfoDebuggerId =              0x60,
        RegInfoDebuggerVersion =         0x64
    };

    enum {
        MaxRetries = 10,
    };

    IMPL_DEBUG_MODULE( BootloaderManager, BootloaderManager, DEBUG_LEVEL_NORMAL );
}

BeBoB::BootloaderManager::BootloaderManager(Ieee1394Service& ieee1349service,
                                            fb_nodeid_t nodeId )
    : m_ieee1394service( &ieee1349service )
    , m_protocolVersion( eBPV_Unknown )
    , m_isAppRunning( false )
    , m_forceEnabled( false )
    , m_bStartBootloader( true )
{
    memset( &m_cachedInfoRegs, 0, sizeof( m_cachedInfoRegs ) );

    m_configRom = new ConfigRom( *m_ieee1394service, nodeId );
    // XXX throw exception if initialize fails!
    m_configRom->initialize();
    if ( !cacheInfoRegisters() ) {
        debugError( "BootloaderManager: could not cache info registers\n" );
    }

    switch(  m_cachedInfoRegs.m_protocolVersion ) {
    case 1:
        m_protocolVersion = eBPV_V1;
        break;
    case 3:
        m_protocolVersion = eBPV_V3;
        break;
    default:
        // exception?
        break;
    }

    pthread_mutex_init( &m_mutex, 0 );
    pthread_cond_init( &m_cond, 0 );

    m_functor = new MemberFunctor0< BeBoB::BootloaderManager*,
                void (BeBoB::BootloaderManager::*)() >
                ( this, &BeBoB::BootloaderManager::busresetHandler, false );
    m_ieee1394service->addBusResetHandler( m_functor );
}

BeBoB::BootloaderManager::~BootloaderManager()
{
    m_ieee1394service->remBusResetHandler( m_functor );
    delete( m_functor );

    delete m_configRom;

    pthread_cond_destroy( &m_cond );
    pthread_mutex_destroy( &m_mutex );
}

bool
BeBoB::BootloaderManager::cacheInfoRegisters()
{
    if ( !m_configRom->updatedNodeId() ) {
        debugError( "cacheInfoRegisters: did not find device anymore\n" );
        return false;
    }

    if ( !m_ieee1394service->read(
             0xffc0 | m_configRom->getNodeId(),
             AddrRegInfo,
             sizeof( m_cachedInfoRegs )/4,
             reinterpret_cast<fb_quadlet_t*>( &m_cachedInfoRegs ) ) )
    {
        return false;
    }

    if ( m_cachedInfoRegs.m_bootloaderVersion != 0x0 ) {
        m_isAppRunning = false;
    } else {
        m_isAppRunning = true;
    }

    m_cachedInfoRegs.m_guid = ( m_cachedInfoRegs.m_guid >> 32 )
                              | ( m_cachedInfoRegs.m_guid << 32 );

    return true;
}

bool
BeBoB::BootloaderManager::cacheInfoRegisters( int retries )
{
    for ( int i = 0; i < retries; ++i ) {
        if ( cacheInfoRegisters() ) {
            return true;
        }
        sleep( 1 );
        printf(".");
        fflush(stdout);
    }

    return false;
}

std::string
BeBoB::BootloaderManager::getSoftwareDate()
{
    return makeDate( m_cachedInfoRegs.m_softwareDate );
}

std::string
BeBoB::BootloaderManager::getSoftwareTime()
{
    return makeDate( m_cachedInfoRegs.m_softwareTime );
}

void
BeBoB::BootloaderManager::printInfoRegisters()
{
    using namespace std;

    if ( !cacheInfoRegisters() ) {
        debugError( "Could not read info registers\n" );
        return;
    }

    printf( "Info Registers\n" );
    printf( "\tManufactors Id:\t\t%s\n",
            makeString( m_cachedInfoRegs.m_manId ).c_str() );
    printf( "\tProtocol Version:\t0x%08x\n",
            m_cachedInfoRegs.m_protocolVersion );
    printf( "\tBootloader Version:\t0x%08x\n",
            m_cachedInfoRegs.m_bootloaderVersion );
    printf( "\tGUID:\t\t\t0x%08x%08x\n",
            ( unsigned int )( m_cachedInfoRegs.m_guid >> 32 ),
            ( unsigned int )( m_cachedInfoRegs.m_guid & 0xffffffff ) );
    printf( "\tHardware Model ID:\t0x%08x\n",
            m_cachedInfoRegs.m_hardwareModelId );
    printf( "\tHardware Revision:\t0x%08x\n",
            m_cachedInfoRegs.m_hardwareRevision );
    if (  m_cachedInfoRegs.m_softwareDate
          && m_cachedInfoRegs.m_softwareTime )
    {
        printf( "\tSoftware Date:\t\t%s, %s\n",
                makeDate( m_cachedInfoRegs.m_softwareDate ).c_str(),
                makeTime( m_cachedInfoRegs.m_softwareTime ).c_str() );
    }
    printf( "\tSoftware Id:\t\t0x%08x\n", m_cachedInfoRegs.m_softwareId );
    printf( "\tSoftware Version:\t0x%08x\n",
            m_cachedInfoRegs.m_softwareVersion );
    printf( "\tBase Address:\t\t0x%08x\n", m_cachedInfoRegs.m_baseAddress );
    printf( "\tMax. Image Len:\t\t0x%08x\n", m_cachedInfoRegs.m_maxImageLen );
    if ( m_cachedInfoRegs.m_bootloaderDate
         && m_cachedInfoRegs.m_bootloaderTime )
    {
        printf( "\tBootloader Date:\t%s, %s\n",
                makeDate( m_cachedInfoRegs.m_bootloaderDate ).c_str(),
                makeTime( m_cachedInfoRegs.m_bootloaderTime ).c_str() );
    }
    if ( m_cachedInfoRegs.m_debuggerDate
         && m_cachedInfoRegs.m_debuggerTime )
    {
        printf( "\tDebugger Date:\t\t%s, %s\n",
                makeDate( m_cachedInfoRegs.m_debuggerDate ).c_str(),
                makeTime( m_cachedInfoRegs.m_debuggerTime ).c_str() );
    }
    printf( "\tDebugger Id:\t\t0x%08x\n", m_cachedInfoRegs.m_debuggerId );
    printf( "\tDebugger Version:\t0x%08x\n",
            m_cachedInfoRegs.m_debuggerVersion );
}

bool
BeBoB::BootloaderManager::downloadFirmware( std::string filename )
{
    using namespace std;

    printf( "parse BCD file\n" );
    ffado_smartptr<BCD> bcd = ffado_smartptr<BCD>( new BCD( filename ) );
    if ( !bcd.get() ) {
        debugError( "downloadFirmware: Could not open or parse BCD '%s'\n",
                    filename.c_str() );
        return false;
    }
    if ( !bcd->parse() ) {
        debugError( "downloadFirmware: BCD parsing failed\n" );
        return false;
    }

    printf( "check firmware device compatibility... " );
    if ( !m_forceEnabled ) {
        if ( !checkDeviceCompatibility( *bcd ) ) {
            printf( "failed.\n" );
            return false;
        }
        printf( "ok\n" );
    } else {
        printf( "forced\n" );
    }

    if ( m_bStartBootloader ) {
        printf( "prepare for download (start bootloader)\n" );
        if ( !startBootloaderCmd() ) {
            debugError( "downloadFirmware: Could not start bootloader\n" );
            return false;
        }
    }

    printf( "start downloading protocol for application image\n" );
    if ( !downloadObject( *bcd, eOT_Application ) ) {
        debugError( "downloadFirmware: Firmware download failed\n" );
        return false;
    }

    printf( "start downloading protocol for CnE\n" );
    if ( !downloadObject( *bcd, eOT_CnE ) ) {
        debugError( "downloadFirmware: CnE download failed\n" );
        return false;
    }

    printf( "setting CnE to factory default settings\n" );
    if ( !initializeConfigToFactorySettingCmd() ) {
        debugError( "downloadFirmware: Could not reinitalize CnE\n" );
        return false;
    }

    printf( "start application\n" );
    if ( !startApplicationCmd() ) {
        debugError( "downloadFirmware: Could not restart application\n" );
        return false;
    }

    return true;
}

bool
BeBoB::BootloaderManager::downloadCnE( std::string filename )
{
    using namespace std;

    printf( "parse BCD file\n" );
    ffado_smartptr<BCD> bcd = ffado_smartptr<BCD>( new BCD( filename ) );
    if ( !bcd.get() ) {
        debugError( "downloadCnE: Could not open or parse BCD '%s'\n",
                    filename.c_str() );
        return false;
    }
    if ( !bcd->parse() ) {
        debugError( "downloadCnE: BCD parsing failed\n" );
        return false;
    }

    printf( "check firmware device compatibility... " );
    if ( !m_forceEnabled ) {
        if ( !checkDeviceCompatibility( *bcd ) ) {
            printf( "failed.\n" );
            return false;
        }
        printf( "ok\n" );
    } else {
        printf( "forced\n" );
    }

    if ( m_bStartBootloader ) {
        printf( "prepare for download (start bootloader)\n" );
        if ( !startBootloaderCmd() ) {
            debugError( "downloadCnE: Could not start bootloader\n" );
            return false;
        }
    }

    printf( "start downloading protocol for CnE\n" );
    if ( !downloadObject( *bcd, eOT_CnE ) ) {
        debugError( "downloadCnE: CnE download failed\n" );
        return false;
    }

    printf( "setting CnE to factory default settings\n" );
    if ( !initializeConfigToFactorySettingCmd() ) {
        debugError( "downloadFirmware: Could not reinitalize CnE\n" );
        return false;
    }

    printf( "start application\n" );
    if ( !startApplicationCmd() ) {
        debugError( "downloadCnE: Could not restart application\n" );
        return false;
    }

    return true;
}


bool
BeBoB::BootloaderManager::downloadObject( BCD& bcd, EObjectType eObject )
{
    using namespace std;

    CommandCodesDownloadStart::EObject eCCDSObject;
    fb_quadlet_t baseAddress;
    fb_quadlet_t imageLength;
    fb_quadlet_t crc;
    fb_quadlet_t offset;

    switch ( eObject ) {
    case eOT_Application:
        eCCDSObject = CommandCodesDownloadStart::eO_Application;
        baseAddress = bcd.getImageBaseAddress();
        imageLength = bcd.getImageLength();
        crc = bcd.getImageCRC();
        offset = bcd.getImageOffset();
        break;
    case eOT_CnE:
        eCCDSObject = CommandCodesDownloadStart::eO_Config;
        baseAddress = 0;
        imageLength = bcd.getCnELength();
        crc = bcd.getCnECRC();
        offset = bcd.getCnEOffset();
        break;
    default:
        return false;
    }

    CommandCodesDownloadStart ccDStart ( m_protocolVersion, eCCDSObject );
    ccDStart.setDate( bcd.getSoftwareDate() );
    ccDStart.setTime( bcd.getSoftwareTime() );
    ccDStart.setId( bcd.getSoftwareId() );
    ccDStart.setVersion( bcd.getSoftwareVersion() );
    ccDStart.setBaseAddress( baseAddress );
    ccDStart.setLength( imageLength );
    ccDStart.setCRC( crc );

    if ( !writeRequest( ccDStart ) ) {
        debugError( "downloadObject: start command write request failed\n" );
        return false;
    }

    // bootloader erases the flash, have to wait until is ready
    // to answer our next request
    printf( "wait until flash erasing has terminated\n" );
    int cnt = 30;
    while(cnt--) {
        sleep( 1 );
        printf(".");
        fflush(stdout);
    }
    printf("\n");

    if ( !readResponse( ccDStart ) ) {
        debugError( "downloadObject: (start) command read request failed\n" );
        return false;
    }

    if ( ccDStart.getMaxBlockSize() < 0 ) {
        debugError( "downloadObject: (start) command reported error %d\n",
                    ccDStart.getMaxBlockSize() );
        return false;
    }

    unsigned int maxBlockSize = m_configRom->getAsyMaxPayload();
    unsigned int i = 0;
    fb_quadlet_t address = 0;
    bool result = true;
    int totalBytes = imageLength;
    int downloadedBytes = 0;
    while ( imageLength > 0 ) {
        unsigned int blockSize = imageLength > maxBlockSize ?
                        maxBlockSize  : imageLength;

        fb_byte_t block[blockSize];
        if ( !bcd.read( offset, block, blockSize ) ) {
            result = false;
            break;
        }

        if ( !get1394Serivce()->write(
                 0xffc0 | getConfigRom()->getNodeId(),
                 AddrRegReqBuf,
                 ( blockSize + 3 ) / 4,
                 reinterpret_cast<fb_quadlet_t*>( block ) ) )
        {
            debugError( "downloadObject: Could not write to node %d\n",
                        getConfigRom()->getNodeId() );
            return false;
        }

        CommandCodesDownloadBlock ccBlock( m_protocolVersion );
        ccBlock.setSeqNumber( i );
        ccBlock.setAddress( baseAddress + address );
        ccBlock.setNumberBytes( blockSize );

        if ( !writeRequest( ccBlock ) ) {
            debugError( "downloadObject: (block) write request failed\n" );
            result = false;
            break;
        }
        SleepRelativeUsec( 100 );

        if ( !readResponse( ccBlock ) ) {
            debugError( "downloadObject: (block) read request failed\n" );
            result = false;
            break;
        }

        if ( i != ccBlock.getRespSeqNumber() ) {
            debugError( "downloadObject: (block) wrong sequence number "
                        "reported. %d expected, %d reported\n",
                        i, ccBlock.getRespSeqNumber() );
            result = false;
            break;
        }
        if ( ccBlock.getRespErrorCode() != 0 ) {
            debugError( "downloadObject: (block) failed download with "
                        "error code 0x%08x\n", ccBlock.getRespErrorCode() );
            result = false;
            break;
        }

        downloadedBytes += blockSize;
        if ( ( i % 100 ) == 0 ) {
           printf( "%10d/%d bytes downloaded\r",
                   downloadedBytes, totalBytes );
           fflush(stdout);
        }

        imageLength -= blockSize;
        address += blockSize;
        offset += blockSize;
        i++;
    }
    printf( "%10d/%d bytes downloaded\n",
            downloadedBytes, totalBytes );

    if ( !result ) {
        debugError( "downloadObject: seqNumber = %d, "
                    "address = 0%08x, offset = 0x%08x, "
                    "restImageLength = 0x%08x\n",
                    i, address, offset, imageLength );
    }

    CommandCodesDownloadEnd ccEnd( m_protocolVersion );
    if ( !writeRequest( ccEnd ) ) {
        debugError( "downloadObject: (end) command write failed\n" );
    }

    printf( "wait for transaction completion\n" );
    cnt = 10;
    while(cnt--) {
        sleep( 1 );
        printf(".");
        fflush(stdout);
    }
    printf("\n");

    if ( !readResponse( ccEnd ) ) {
        debugError( "downloadObject: (end) command read failed\n" );
    }

    if ( result ) {
        if ( ccEnd.getRespIsValid() ) {
            if ( ccEnd.getRespCrc32() == crc ) {
                debugOutput( DebugModule::eDL_Normal,
                             "downloadObject: CRC match\n" );
            } else {
                debugError( "downloadObject: CRC mismatch. 0x%08x expected, "
                            "0x%08x reported",
                            crc, ccEnd.getRespCrc32() );
                result = false;
            }
        } else {
            debugError( "downloadObject: (end) object is not valid\n" );
            result = false;
        }
    }
    printf( "download protocol successfully completed\n" );
    return result;
}

bool
BeBoB::BootloaderManager::programGUID( fb_octlet_t guid )
{
    if ( m_bStartBootloader ) {
        if ( !startBootloaderCmd() ) {
            debugError( "programGUID: Could not start bootloader\n" );
            return false;
        }
    }

    if ( !programGUIDCmd( guid ) ) {
        debugError( "programGUID: Could not program guid\n" );
        return false;
    }

    if ( !startApplicationCmd() ) {
        debugError( "Could not restart application\n");
        return false;
    }

    return true;
}

void
BeBoB::BootloaderManager::busresetHandler()
{
    pthread_cond_signal( &m_cond );
}

void
BeBoB::BootloaderManager::waitForBusReset()
{
    struct timespec timeout;
    int retcode;
    // pthread_cond_timedwait() uses CLOCK_REALTIME to evaluate its
    // timeout argument.
    clock_gettime(CLOCK_REALTIME, &timeout);
    do {
        printf(".");
        fflush(stdout);
        timeout.tv_sec = timeout.tv_sec + 1;
        retcode = pthread_cond_timedwait( &m_cond, &m_mutex, &timeout );
    } while (retcode == ETIMEDOUT);
}

bool
BeBoB::BootloaderManager::writeRequest( CommandCodes& cmd )
{
    unsigned char buf[ ( ( cmd.getMaxSize()+3 )/4 ) * 4 ];
    memset( buf, 0, sizeof( buf ) );

    Util::Cmd::BufferSerialize se( buf,  sizeof( buf ) );
    if ( !cmd.serialize( se ) ) {
        debugError( "writeRequest: Could not serialize command code %d\n",
                    cmd.getCommandCode() );
        return false;
    }

    if ( !get1394Serivce()->write(
             0xffc0 | getConfigRom()->getNodeId(),
             AddrRegReq,
             sizeof( buf )/4,
             reinterpret_cast<fb_quadlet_t*>( buf ) ) )
    {
        debugError( "writeRequest: Could not ARM write to node %d\n",
                    getConfigRom()->getNodeId() );
        return false;
    }

    return true;
}

bool
BeBoB::BootloaderManager::readResponse( CommandCodes& writeRequestCmd )
{
    const size_t buf_length = 0x40;
    unsigned char raw[buf_length];
    if ( !get1394Serivce()->read(
             0xffc0 | getConfigRom()->getNodeId(),
             AddrRegResp,
             writeRequestCmd.getRespSizeInQuadlets(),
             reinterpret_cast<fb_quadlet_t*>( raw ) ) )
    {
        return false;
    }

    Util::Cmd::BufferDeserialize de( raw, buf_length );
    if ( !writeRequestCmd.deserialize( de ) ) {
        debugError( "readResponse: deserialize failed\n" );
        return false;
    }

    bool result =
        writeRequestCmd.getProtocolVersion()
        == writeRequestCmd.getRespProtocolVersion();
    result &=
        writeRequestCmd.getCommandId()
        == writeRequestCmd.getRespCommandId();
    result &=
        writeRequestCmd.getCommandCode()
        == writeRequestCmd.getRespCommandCode();
    #ifdef DEBUG
       if ( !result ) {
           debugError( "readResponse: protocol version: %d expected, "
                       " %d reported\n",
                       writeRequestCmd.getProtocolVersion(),
                       writeRequestCmd.getRespProtocolVersion() );
           debugError( "readResponse: command id: %d expected, "
                       " %d reported\n",
                       writeRequestCmd.getCommandId(),
                       writeRequestCmd.getRespCommandId() );
           debugError( "readResponse: command code: %d expected, "
                       " %d reported\n",
                       writeRequestCmd.getCommandCode(),
                       writeRequestCmd.getRespCommandCode() );
       }
    #endif
    return result;
}

bool
BeBoB::BootloaderManager::startBootloaderCmd()
{
    CommandCodesReset cmd( m_protocolVersion,
                           CommandCodesReset::eSM_Bootloader ) ;
    if ( !writeRequest( cmd ) ) {
        debugError( "startBootloaderCmd: writeRequest failed\n" );
        return false;
    }

    waitForBusReset();
    if ( !cacheInfoRegisters( MaxRetries ) ) {
        debugError( "startBootloaderCmd: Could not read info registers\n" );
        return false;
    }

    // wait for bootloader finish startup sequence
    // there is no way to find out when it has finished
    sleep( 10 );
    int cnt = 10;
    while(cnt--) {
        sleep( 1 );
        printf(".");
        fflush(stdout);
    }
    printf("\n");

    return true;
}

bool
BeBoB::BootloaderManager::startApplicationCmd()
{
    CommandCodesGo cmd( m_protocolVersion,
                           CommandCodesGo::eSM_Application ) ;
    if ( !writeRequest( cmd ) ) {
        debugError( "startApplicationCmd: writeRequest failed\n" );
        return false;
    }

    return true;
}

bool
BeBoB::BootloaderManager::programGUIDCmd( fb_octlet_t guid )
{
    CommandCodesProgramGUID cmd( m_protocolVersion, guid );
    if ( !writeRequest( cmd ) ) {
        debugError( "programGUIDCmd: writeRequest failed\n" );
        return false;
    }

    sleep( 1 );

    return true;
}

bool
BeBoB::BootloaderManager::initializePersParamCmd()
{
    CommandCodesInitializePersParam cmd( m_protocolVersion );
    if ( !writeRequest( cmd ) ) {
        debugError( "initializePersParamCmd: writeRequest failed\n" );
        return false;
    }

    sleep( 1 );

    return true;
}

bool
BeBoB::BootloaderManager::initializeConfigToFactorySettingCmd()
{
    CommandCodesInitializeConfigToFactorySetting cmd( m_protocolVersion );
    if ( !writeRequest( cmd ) ) {
        debugError( "initializeConfigToFactorySettingCmd: writeRequest failed\n" );
        return false;
    }

    sleep( 5 );
    int cnt = 5;
    while(cnt--) {
        sleep( 1 );
        printf(".");
        fflush(stdout);
    }
    printf("\n");

    return true;
}

bool
BeBoB::BootloaderManager::checkDeviceCompatibility( BCD& bcd )
{
    fb_quadlet_t vendorOUI = (  m_cachedInfoRegs.m_guid >> 40 );


    if ( ( vendorOUI == bcd.getVendorOUI() )
         && ( m_cachedInfoRegs.m_softwareId == bcd.getSoftwareId() ) )
    {
        return true;
    }

    printf( "vendorOUI = 0x%08x\n", vendorOUI );
    printf( "BCD vendorOUI = 0x%08x\n", bcd.getVendorOUI() );
    printf( "software ID = 0x%08x\n", m_cachedInfoRegs.m_softwareId );
    printf( "BCD software ID = 0x%08x\n", bcd.getSoftwareId() );

    return false;
}
