/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmHexFileConverter.h"

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#include "cmSystemTools.h"

#define INTEL_HEX_MIN_LINE_LENGTH (1 + 8 + 2)
#define INTEL_HEX_MAX_LINE_LENGTH (1 + 8 + (256 * 2) + 2)
#define MOTOROLA_SREC_MIN_LINE_LENGTH (2 + 2 + 4 + 2)
#define MOTOROLA_SREC_MAX_LINE_LENGTH (2 + 2 + 8 + (256 * 2) + 2)

static unsigned int
ChompStrlen(const char* line)
{
    if(line == nullptr)
    {
        return 0;
    }
    unsigned int length = static_cast<unsigned int>(strlen(line));
    if((line[length - 1] == '\n') || (line[length - 1] == '\r'))
    {
        length--;
    }
    if((line[length - 1] == '\n') || (line[length - 1] == '\r'))
    {
        length--;
    }
    return length;
}

static bool
OutputBin(FILE* file, const char* buf, unsigned int startIndex,
          unsigned int stopIndex)
{
    bool success = true;
    char hexNumber[3];
    hexNumber[2] = '\0';
    char         outBuf[256];
    unsigned int outBufCount = 0;
    for(unsigned int i = startIndex; i < stopIndex; i += 2)
    {
        hexNumber[0]               = buf[i];
        hexNumber[1]               = buf[i + 1];
        unsigned int convertedByte = 0;
        if(sscanf(hexNumber, "%x", &convertedByte) != 1)
        {
            success = false;
            break;
        }
        outBuf[outBufCount] = static_cast<char>(convertedByte & 0xff);
        outBufCount++;
    }
    if(success)
    {
        success = (fwrite(outBuf, 1, outBufCount, file) == outBufCount);
    }
    return success;
}

// see http://www.die.net/doc/linux/man/man5/srec.5.html
static bool
ConvertMotorolaSrecLine(const char* buf, FILE* outFile)
{
    unsigned int slen = ChompStrlen(buf);
    if((slen < MOTOROLA_SREC_MIN_LINE_LENGTH) ||
       (slen > MOTOROLA_SREC_MAX_LINE_LENGTH))
    {
        return false;
    }

    // line length must be even
    if(slen % 2 == 1)
    {
        return false;
    }

    if(buf[0] != 'S')
    {
        return false;
    }

    unsigned int dataStart = 0;
    // ignore extra address records
    if((buf[1] == '5') || (buf[1] == '7') || (buf[1] == '8') || (buf[1] == '9'))
    {
        return true;
    }
    if(buf[1] == '1')
    {
        dataStart = 8;
    } else if(buf[1] == '2')
    {
        dataStart = 10;
    } else if(buf[1] == '3')
    {
        dataStart = 12;
    } else  // unknown record type
    {
        return false;
    }

    // ignore the last two bytes  (checksum)
    return OutputBin(outFile, buf, dataStart, slen - 2);
}

// see http://en.wikipedia.org/wiki/Intel_hex
static bool
ConvertIntelHexLine(const char* buf, FILE* outFile)
{
    unsigned int slen = ChompStrlen(buf);
    if((slen < INTEL_HEX_MIN_LINE_LENGTH) || (slen > INTEL_HEX_MAX_LINE_LENGTH))
    {
        return false;
    }

    // line length must be odd
    if(slen % 2 == 0)
    {
        return false;
    }

    if((buf[0] != ':') || (buf[7] != '0'))
    {
        return false;
    }

    unsigned int dataStart = 0;
    if((buf[8] == '0') || (buf[8] == '1'))
    {
        dataStart = 9;
    }
    // ignore extra address records
    else if((buf[8] == '2') || (buf[8] == '3') || (buf[8] == '4') ||
            (buf[8] == '5'))
    {
        return true;
    } else  // unknown record type
    {
        return false;
    }

    // ignore the last two bytes  (checksum)
    return OutputBin(outFile, buf, dataStart, slen - 2);
}

cmHexFileConverter::FileType
cmHexFileConverter::DetermineFileType(const char* inFileName)
{
    char  buf[1024];
    FILE* inFile = cmsys::SystemTools::Fopen(inFileName, "rb");
    if(inFile == nullptr)
    {
        return Binary;
    }

    if(!fgets(buf, 1024, inFile))
    {
        buf[0] = 0;
    }
    fclose(inFile);
    FileType     type          = Binary;
    unsigned int minLineLength = 0;
    unsigned int maxLineLength = 0;
    if(buf[0] == ':')  // might be an intel hex file
    {
        type          = IntelHex;
        minLineLength = INTEL_HEX_MIN_LINE_LENGTH;
        maxLineLength = INTEL_HEX_MAX_LINE_LENGTH;
    } else if(buf[0] == 'S')  // might be a motorola srec file
    {
        type          = MotorolaSrec;
        minLineLength = MOTOROLA_SREC_MIN_LINE_LENGTH;
        maxLineLength = MOTOROLA_SREC_MAX_LINE_LENGTH;
    } else
    {
        return Binary;
    }

    unsigned int slen = ChompStrlen(buf);
    if((slen < minLineLength) || (slen > maxLineLength))
    {
        return Binary;
    }

    for(unsigned int i = 1; i < slen; i++)
    {
        if(!isxdigit(buf[i]))
        {
            return Binary;
        }
    }
    return type;
}

bool
cmHexFileConverter::TryConvert(const char* inFileName, const char* outFileName)
{
    FileType type = DetermineFileType(inFileName);
    if(type == Binary)
    {
        return false;
    }

    // try to open the file
    FILE* inFile  = cmsys::SystemTools::Fopen(inFileName, "rb");
    FILE* outFile = cmsys::SystemTools::Fopen(outFileName, "wb");
    if((inFile == nullptr) || (outFile == nullptr))
    {
        if(inFile != nullptr)
        {
            fclose(inFile);
        }
        if(outFile != nullptr)
        {
            fclose(outFile);
        }
        return false;
    }

    // convert them line by line
    bool success = false;
    char buf[1024];
    while(fgets(buf, 1024, inFile) != nullptr)
    {
        if(type == MotorolaSrec)
        {
            success = ConvertMotorolaSrecLine(buf, outFile);
        } else if(type == IntelHex)
        {
            success = ConvertIntelHexLine(buf, outFile);
        }
        if(!success)
        {
            break;
        }
    }

    // close them again
    fclose(inFile);
    fclose(outFile);
    return success;
}
