/******************************************************************************
 *
 * Project:  ISO 8211 Access
 * Purpose:  Implements the DDFModule class.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 1999, Frank Warmerdam
 * Copyright (c) 2011-2013, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"
#include "iso8211.h"

#include <algorithm>
#include <array>
#include <cstdio>
#include <cstring>

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_vsi.h"

/************************************************************************/
/*                             DDFModule()                              */
/************************************************************************/

/**
 * The constructor.
 */

DDFModule::DDFModule() = default;

/************************************************************************/
/*                             ~DDFModule()                             */
/************************************************************************/

/**
 * The destructor.
 */

DDFModule::~DDFModule()

{
    Close();
}

/************************************************************************/
/*                               Close()                                */
/*                                                                      */
/*      Note that closing a file also destroys essentially all other    */
/*      module datastructures.                                          */
/************************************************************************/

/**
 * Close an ISO 8211 file.
 */

void DDFModule::Close()

{
    /* -------------------------------------------------------------------- */
    /*      Close the file.                                                 */
    /* -------------------------------------------------------------------- */
    if (fpDDF != nullptr)
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpDDF));
        fpDDF = nullptr;
    }

    poRecord.reset();

    apoFieldDefns.clear();
}

/************************************************************************/
/*                                Open()                                */
/*                                                                      */
/*      Open an ISO 8211 file, and read the DDR record to build the     */
/*      field definitions.                                              */
/************************************************************************/

/**
 * Open a ISO 8211 (DDF) file for reading.
 *
 * If the open succeeds the data descriptive record (DDR) will have been
 * read, and all the field and subfield definitions will be available.
 *
 * @param pszFilename   The name of the file to open.
 * @param bFailQuietly If FALSE a CPL Error is issued for non-8211 files,
 * otherwise quietly return NULL.
 * @param fpDDFIn The open file, or nullptr. Ownership is transferred to the
 * DDFModule.
 *
 * @return FALSE if the open fails or TRUE if it succeeds.  Errors messages
 * are issued internally with CPLError().
 */

int DDFModule::Open(const char *pszFilename, int bFailQuietly,
                    VSILFILE *fpDDFIn)

{
    constexpr int nLeaderSize = 24;

    /* -------------------------------------------------------------------- */
    /*      Close the existing file if there is one.                        */
    /* -------------------------------------------------------------------- */
    if (fpDDF != nullptr)
        Close();

    /* -------------------------------------------------------------------- */
    /*      Open the file.                                                  */
    /* -------------------------------------------------------------------- */
    VSIStatBufL sStat;
    if (fpDDFIn)
    {
        fpDDF = fpDDFIn;
        CPL_IGNORE_RET_VAL(VSIFSeekL(fpDDF, 0, SEEK_SET));
    }
    else
    {
        if (VSIStatL(pszFilename, &sStat) == 0 && !VSI_ISDIR(sStat.st_mode))
            fpDDF = VSIFOpenL(pszFilename, "rb");

        if (fpDDF == nullptr)
        {
            if (!bFailQuietly)
                CPLError(CE_Failure, CPLE_OpenFailed,
                         "Unable to open DDF file `%s'.", pszFilename);
            return FALSE;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Read the 24 byte leader.                                        */
    /* -------------------------------------------------------------------- */
    char achLeader[nLeaderSize];

    if (static_cast<int>(VSIFReadL(achLeader, 1, nLeaderSize, fpDDF)) !=
        nLeaderSize)
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpDDF));
        fpDDF = nullptr;

        if (!bFailQuietly)
            CPLError(CE_Failure, CPLE_FileIO,
                     "Leader is short on DDF file `%s'.", pszFilename);

        return FALSE;
    }

    /* -------------------------------------------------------------------- */
    /*      Verify that this appears to be a valid DDF file.                */
    /* -------------------------------------------------------------------- */
    int i, bValid = TRUE;

    for (i = 0; i < nLeaderSize; i++)
    {
        if (achLeader[i] < 32 || achLeader[i] > 126)
            bValid = FALSE;
    }

    if (achLeader[5] != '1' && achLeader[5] != '2' && achLeader[5] != '3')
        bValid = FALSE;

    if (achLeader[6] != 'L')
        bValid = FALSE;
    if (achLeader[8] != '1' && achLeader[8] != ' ')
        bValid = FALSE;

    /* -------------------------------------------------------------------- */
    /*      Extract information from leader.                                */
    /* -------------------------------------------------------------------- */

    if (bValid)
    {
        _recLength = DDFScanInt(achLeader + 0, 5);
        _interchangeLevel = achLeader[5];
        _leaderIden = achLeader[6];
        _inlineCodeExtensionIndicator = achLeader[7];
        _versionNumber = achLeader[8];
        _appIndicator = achLeader[9];
        _fieldControlLength = DDFScanInt(achLeader + 10, 2);
        _fieldAreaStart = DDFScanInt(achLeader + 12, 5);
        _extendedCharSet[0] = achLeader[17];
        _extendedCharSet[1] = achLeader[18];
        _extendedCharSet[2] = achLeader[19];
        _sizeFieldLength = DDFScanInt(achLeader + 20, 1);
        _sizeFieldPos = DDFScanInt(achLeader + 21, 1);
        _sizeFieldTag = DDFScanInt(achLeader + 23, 1);

        if (_recLength < nLeaderSize || _fieldControlLength <= 0 ||
            _fieldAreaStart < 24 || _sizeFieldLength <= 0 ||
            _sizeFieldPos <= 0 || _sizeFieldTag <= 0)
        {
            bValid = FALSE;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      If the header is invalid, then clean up, report the error       */
    /*      and return.                                                     */
    /* -------------------------------------------------------------------- */
    if (!bValid)
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpDDF));
        fpDDF = nullptr;

        if (!bFailQuietly)
            CPLError(CE_Failure, CPLE_AppDefined,
                     "File `%s' does not appear to have\n"
                     "a valid ISO 8211 header.\n",
                     pszFilename);
        return FALSE;
    }

    /* -------------------------------------------------------------------- */
    /*      Read the whole record info memory.                              */
    /* -------------------------------------------------------------------- */
    std::string osRecord;
    osRecord.assign(achLeader, nLeaderSize);
    osRecord.resize(_recLength);

    if (static_cast<int>(VSIFReadL(osRecord.data() + nLeaderSize, 1,
                                   _recLength - nLeaderSize, fpDDF)) !=
        _recLength - nLeaderSize)
    {
        if (!bFailQuietly)
            CPLError(CE_Failure, CPLE_FileIO,
                     "Header record is short on DDF file `%s'.", pszFilename);

        return FALSE;
    }

    /* -------------------------------------------------------------------- */
    /*      First make a pass counting the directory entries.               */
    /* -------------------------------------------------------------------- */
    int nFieldEntryWidth, nFDCount = 0;

    nFieldEntryWidth = _sizeFieldLength + _sizeFieldPos + _sizeFieldTag;

    for (i = nLeaderSize; i + nFieldEntryWidth <= _recLength;
         i += nFieldEntryWidth)
    {
        if (osRecord[i] == DDF_FIELD_TERMINATOR)
            break;

        nFDCount++;
    }

    /* -------------------------------------------------------------------- */
    /*      Allocate, and read field definitions.                           */
    /* -------------------------------------------------------------------- */
    for (i = 0; i < nFDCount; i++)
    {
        char szTag[128];
        int nEntryOffset = nLeaderSize + i * nFieldEntryWidth;
        int nFieldLength, nFieldPos;

        strncpy(szTag, osRecord.c_str() + nEntryOffset, _sizeFieldTag);
        szTag[_sizeFieldTag] = '\0';

        nEntryOffset += _sizeFieldTag;
        nFieldLength =
            DDFScanInt(osRecord.c_str() + nEntryOffset, _sizeFieldLength);

        nEntryOffset += _sizeFieldLength;
        nFieldPos = DDFScanInt(osRecord.c_str() + nEntryOffset, _sizeFieldPos);

        if (nFieldPos < 0 || nFieldPos > INT_MAX - _fieldAreaStart ||
            nFieldLength <
                2 ||  // DDFFieldDefn::Initialize() assumes at least 2 bytes
            _recLength - (_fieldAreaStart + nFieldPos) < nFieldLength)
        {
            if (!bFailQuietly)
                CPLError(CE_Failure, CPLE_FileIO,
                         "Header record invalid on DDF file `%s'.",
                         pszFilename);

            return FALSE;
        }

        auto poFDefn = std::make_unique<DDFFieldDefn>();
        if (poFDefn->Initialize(this, szTag, nFieldLength,
                                osRecord.c_str() + _fieldAreaStart + nFieldPos))
            AddField(std::move(poFDefn));
    }

    /* -------------------------------------------------------------------- */
    /*      Record the current file offset, the beginning of the first      */
    /*      data record.                                                    */
    /* -------------------------------------------------------------------- */
    nFirstRecordOffset = static_cast<long>(VSIFTellL(fpDDF));

    return TRUE;
}

/************************************************************************/
/*                             Initialize()                             */
/************************************************************************/

int DDFModule::Initialize(char chInterchangeLevel, char chLeaderIden,
                          char chCodeExtensionIndicator, char chVersionNumber,
                          char chAppIndicator,
                          const std::array<char, 3> &achExtendedCharSet,
                          int nSizeFieldLength, int nSizeFieldPos,
                          int nSizeFieldTag)

{
    _interchangeLevel = chInterchangeLevel;
    _leaderIden = chLeaderIden;
    _inlineCodeExtensionIndicator = chCodeExtensionIndicator;
    _versionNumber = chVersionNumber;
    _appIndicator = chAppIndicator;
    _extendedCharSet = achExtendedCharSet;
    _sizeFieldLength = nSizeFieldLength;
    _sizeFieldPos = nSizeFieldPos;
    _sizeFieldTag = nSizeFieldTag;

    return TRUE;
}

/************************************************************************/
/*                               Create()                               */
/************************************************************************/

int DDFModule::Create(const char *pszFilename)

{
    CPLAssert(fpDDF == nullptr);

    /* -------------------------------------------------------------------- */
    /*      Create the file on disk.                                        */
    /* -------------------------------------------------------------------- */
    fpDDF = VSIFOpenL(pszFilename, "wb+");
    if (fpDDF == nullptr)
    {
        CPLError(CE_Failure, CPLE_OpenFailed,
                 "Failed to create file %s, check path and permissions.",
                 pszFilename);
        return FALSE;
    }

    bReadOnly = FALSE;

    /* -------------------------------------------------------------------- */
    /*      Prepare all the field definition information.                   */
    /* -------------------------------------------------------------------- */
    _recLength =
        24 +
        GetFieldCount() * (_sizeFieldLength + _sizeFieldPos + _sizeFieldTag) +
        1;

    _fieldAreaStart = _recLength;

    for (auto &poFieldDefn : apoFieldDefns)
    {
        int nLength;

        poFieldDefn->GenerateDDREntry(this, nullptr, &nLength);
        _recLength += nLength;
    }

    /* -------------------------------------------------------------------- */
    /*      Setup 24 byte leader.                                           */
    /* -------------------------------------------------------------------- */
    char achLeader[25];

    snprintf(achLeader + 0, sizeof(achLeader) - 0, "%05d", _recLength);
    achLeader[5] = _interchangeLevel;
    achLeader[6] = _leaderIden;
    achLeader[7] = _inlineCodeExtensionIndicator;
    achLeader[8] = _versionNumber;
    achLeader[9] = _appIndicator;
    snprintf(achLeader + 10, sizeof(achLeader) - 10, "%02d",
             _fieldControlLength);
    snprintf(achLeader + 12, sizeof(achLeader) - 12, "%05d", _fieldAreaStart);
    memcpy(achLeader + 17, _extendedCharSet.data(), 3);
    snprintf(achLeader + 20, sizeof(achLeader) - 20, "%1d", _sizeFieldLength);
    snprintf(achLeader + 21, sizeof(achLeader) - 21, "%1d", _sizeFieldPos);
    achLeader[22] = '0';
    snprintf(achLeader + 23, sizeof(achLeader) - 23, "%1d", _sizeFieldTag);
    int bRet = VSIFWriteL(achLeader, 24, 1, fpDDF) > 0;

    /* -------------------------------------------------------------------- */
    /*      Write out directory entries.                                    */
    /* -------------------------------------------------------------------- */
    int nOffset = 0;
    for (auto &poFieldDefn : apoFieldDefns)
    {
        char achDirEntry[255];
        char szFormat[32];
        int nLength;

        CPLAssert(_sizeFieldLength + _sizeFieldPos + _sizeFieldTag <
                  static_cast<int>(sizeof(achDirEntry)));

        poFieldDefn->GenerateDDREntry(this, nullptr, &nLength);

        CPLAssert(static_cast<int>(strlen(poFieldDefn->GetName())) ==
                  _sizeFieldTag);
        snprintf(achDirEntry, sizeof(achDirEntry), "%s",
                 poFieldDefn->GetName());
        snprintf(szFormat, sizeof(szFormat), "%%0%dd", _sizeFieldLength);
        snprintf(achDirEntry + _sizeFieldTag,
                 sizeof(achDirEntry) - _sizeFieldTag, szFormat, nLength);
        snprintf(szFormat, sizeof(szFormat), "%%0%dd", _sizeFieldPos);
        snprintf(achDirEntry + _sizeFieldTag + _sizeFieldLength,
                 sizeof(achDirEntry) - _sizeFieldTag - _sizeFieldLength,
                 szFormat, nOffset);
        nOffset += nLength;

        bRet &= VSIFWriteL(achDirEntry,
                           _sizeFieldLength + _sizeFieldPos + _sizeFieldTag, 1,
                           fpDDF) > 0;
    }

    char chUT = DDF_FIELD_TERMINATOR;
    bRet &= VSIFWriteL(&chUT, 1, 1, fpDDF) > 0;

    /* -------------------------------------------------------------------- */
    /*      Write out the field descriptions themselves.                    */
    /* -------------------------------------------------------------------- */
    for (auto &poFieldDefn : apoFieldDefns)
    {
        char *pachData = nullptr;
        int nLength = 0;

        poFieldDefn->GenerateDDREntry(this, &pachData, &nLength);
        bRet &= VSIFWriteL(pachData, nLength, 1, fpDDF) > 0;
        CPLFree(pachData);
    }

    return bRet ? TRUE : FALSE;
}

/************************************************************************/
/*                                Dump()                                */
/************************************************************************/

/**
 * Write out module info to debugging file.
 *
 * A variety of information about the module is written to the debugging
 * file.  This includes all the field and subfield definitions read from
 * the header.
 *
 * @param fp The standard IO file handle to write to.  i.e. stderr.
 */

void DDFModule::Dump(FILE *fp, int nNestingLevel) const

{
    std::string osIndent;
    for (int i = 0; i < nNestingLevel; ++i)
        osIndent += "  ";

#define Print(...)                                                             \
    do                                                                         \
    {                                                                          \
        fprintf(fp, "%s", osIndent.c_str());                                   \
        fprintf(fp, __VA_ARGS__);                                              \
    } while (0)

    Print("DDFModule:\n");
    Print("    _recLength = %d\n", _recLength);
    Print("    _interchangeLevel = %c\n", _interchangeLevel);
    Print("    _leaderIden = %c\n", _leaderIden);
    Print("    _inlineCodeExtensionIndicator = %c\n",
          _inlineCodeExtensionIndicator);
    Print("    _versionNumber = %c\n", _versionNumber);
    Print("    _appIndicator = %c\n", _appIndicator);
    Print("    _extendedCharSet = `%c%c%c'\n", _extendedCharSet[0],
          _extendedCharSet[1], _extendedCharSet[2]);
    Print("    _fieldControlLength = %d\n", _fieldControlLength);
    Print("    _fieldAreaStart = %d\n", _fieldAreaStart);
    Print("    _sizeFieldLength = %d\n", _sizeFieldLength);
    Print("    _sizeFieldPos = %d\n", _sizeFieldPos);
    Print("    _sizeFieldTag = %d\n", _sizeFieldTag);

    for (const auto &poFieldDefn : apoFieldDefns)
    {
        poFieldDefn->Dump(fp, nNestingLevel + 1);
    }
}

/************************************************************************/
/*                           FindFieldDefn()                            */
/************************************************************************/

/**
 * Fetch the definition of the named field.
 *
 * This function will scan the DDFFieldDefn's on this module, to find
 * one with the indicated field name.
 *
 * @param pszFieldName The name of the field to search for.  The comparison is
 *                     case insensitive.
 *
 * @return A pointer to the request DDFFieldDefn object is returned, or NULL
 * if none matching the name are found.  The return object remains owned by
 * the DDFModule, and should not be deleted by application code.
 */

const DDFFieldDefn *DDFModule::FindFieldDefn(const char *pszFieldName) const

{
    /* -------------------------------------------------------------------- */
    /*      This pass tries to reduce the cost of comparing strings by      */
    /*      first checking the first character, and by using strcmp()       */
    /* -------------------------------------------------------------------- */
    for (const auto &poFieldDefn : apoFieldDefns)
    {
        const char *pszThisName = poFieldDefn->GetName();

        if (*pszThisName == *pszFieldName && *pszFieldName != '\0' &&
            strcmp(pszFieldName + 1, pszThisName + 1) == 0)
            return poFieldDefn.get();
    }

    /* -------------------------------------------------------------------- */
    /*      Now do a more general check.  Application code may not          */
    /*      always use the correct name case.                               */
    /* -------------------------------------------------------------------- */
    for (const auto &poFieldDefn : apoFieldDefns)
    {
        if (EQUAL(pszFieldName, poFieldDefn->GetName()))
            return poFieldDefn.get();
    }

    return nullptr;
}

/************************************************************************/
/*                             ReadRecord()                             */
/*                                                                      */
/*      Read one record from the file, and return to the                */
/*      application.  The returned record is owned by the module,       */
/*      and is reused from call to call in order to preserve headers    */
/*      when they aren't being re-read from record to record.           */
/************************************************************************/

/**
 * Read one record from the file.
 *
 * @return A pointer to a DDFRecord object is returned, or NULL if a read
 * error, or end of file occurs.  The returned record is owned by the
 * module, and should not be deleted by the application.  The record is
 * only valid until the next ReadRecord() at which point it is overwritten.
 */

DDFRecord *DDFModule::ReadRecord()

{
    if (poRecord == nullptr)
        poRecord = std::make_unique<DDFRecord>(this);

    if (poRecord->Read())
        return poRecord.get();
    else
        return nullptr;
}

/************************************************************************/
/*                              AddField()                              */
/************************************************************************/

/**
 * Add new field definition.
 *
 * Field definitions may only be added to DDFModules being used for
 * writing, not those being used for reading.  Ownership of the
 * DDFFieldDefn object is taken by the DDFModule.
 *
 * @param poNewFDefn definition to be added to the module.
 */

void DDFModule::AddField(std::unique_ptr<DDFFieldDefn> poNewFDefn)

{
    apoFieldDefns.push_back(std::move(poNewFDefn));
}

/************************************************************************/
/*                              GetField()                              */
/************************************************************************/

/**
 * Fetch a field definition by index.
 *
 * @param i (from 0 to GetFieldCount() - 1.
 * @return the returned field pointer or NULL if the index is out of range.
 */

DDFFieldDefn *DDFModule::GetField(int i)

{
    if (i < 0 || static_cast<size_t>(i) >= apoFieldDefns.size())
        return nullptr;
    else
        return apoFieldDefns[i].get();
}

/************************************************************************/
/*                               Rewind()                               */
/************************************************************************/

/**
 * Return to first record.
 *
 * The next call to ReadRecord() will read the first data record in the file.
 *
 * @param nOffset the offset in the file to return to.  By default this is
 * -1, a special value indicating that reading should return to the first
 * data record.  Otherwise it is an absolute byte offset in the file.
 */

void DDFModule::Rewind(vsi_l_offset nOffset)

{
    if (nOffset == static_cast<vsi_l_offset>(-1))
        nOffset = nFirstRecordOffset;

    if (fpDDF == nullptr)
        return;

    if (VSIFSeekL(fpDDF, nOffset, SEEK_SET) < 0)
        return;

    if (nOffset == nFirstRecordOffset && poRecord != nullptr)
        poRecord->Clear();
}
