/**********************************************************************
 *
 * Name:     mitab_rawbinblock.cpp
 * Project:  MapInfo TAB Read/Write library
 * Language: C++
 * Purpose:  Implementation of the TABRawBinBlock class used to handle
 *           reading/writing blocks in the .MAP files
 * Author:   Daniel Morissette, dmorissette@dmsolutions.ca
 *
 **********************************************************************
 * Copyright (c) 1999, 2000, Daniel Morissette
 * Copyright (c) 2014, Even Rouault <even.rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 **********************************************************************/

#include "cpl_port.h"
#include "mitab.h"

#include <cctype>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <algorithm>

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

/*=====================================================================
 *                      class TABRawBinBlock
 *====================================================================*/

/**********************************************************************
 *                   TABRawBinBlock::TABRawBinBlock()
 *
 * Constructor.
 **********************************************************************/
TABRawBinBlock::TABRawBinBlock(TABAccess eAccessMode /*= TABRead*/,
                               GBool bHardBlockSize /*= TRUE*/)
    : m_fp(nullptr), m_eAccess(eAccessMode), m_nBlockType(0),
      m_pabyBuf(nullptr), m_nBlockSize(0), m_nSizeUsed(0),
      m_bHardBlockSize(bHardBlockSize), m_nFileOffset(0), m_nCurPos(0),
      m_nFirstBlockPtr(0), m_nFileSize(-1), m_bModified(FALSE)
{
}

/**********************************************************************
 *                   TABRawBinBlock::~TABRawBinBlock()
 *
 * Destructor.
 **********************************************************************/
TABRawBinBlock::~TABRawBinBlock()
{
    if (m_pabyBuf)
        CPLFree(m_pabyBuf);
}

/**********************************************************************
 *                   TABRawBinBlock::ReadFromFile()
 *
 * Load data from the specified file location and initialize the block.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::ReadFromFile(VSILFILE *fpSrc, int nOffset, int nSize)
{
    if (fpSrc == nullptr || nSize == 0)
    {
        CPLError(CE_Failure, CPLE_AssertionFailed,
                 "TABRawBinBlock::ReadFromFile(): Assertion Failed!");
        return -1;
    }

    m_fp = fpSrc;

    VSIFSeekL(fpSrc, 0, SEEK_END);
    m_nFileSize = static_cast<int>(VSIFTellL(m_fp));

    m_nFileOffset = nOffset;
    m_nCurPos = 0;
    m_bModified = FALSE;

    /*----------------------------------------------------------------
     * Alloc a buffer to contain the data
     *---------------------------------------------------------------*/
    GByte *pabyBuf = static_cast<GByte *>(CPLMalloc(nSize * sizeof(GByte)));

    /*----------------------------------------------------------------
     * Read from the file
     *---------------------------------------------------------------*/
    if (VSIFSeekL(fpSrc, nOffset, SEEK_SET) != 0 ||
        (m_nSizeUsed = static_cast<int>(
             VSIFReadL(pabyBuf, sizeof(GByte), nSize, fpSrc))) == 0 ||
        (m_bHardBlockSize && m_nSizeUsed != nSize))
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "ReadFromFile() failed reading %d bytes at offset %d.", nSize,
                 nOffset);
        CPLFree(pabyBuf);
        return -1;
    }

    /*----------------------------------------------------------------
     * Init block with the data we just read
     *---------------------------------------------------------------*/
    return InitBlockFromData(pabyBuf, nSize, m_nSizeUsed, FALSE, fpSrc,
                             nOffset);
}

/**********************************************************************
 *                   TABRawBinBlock::CommitToFile()
 *
 * Commit the current state of the binary block to the file to which
 * it has been previously attached.
 *
 * Derived classes may want to (optionally) reimplement this method if
 * they need to do special processing before committing the block to disk.
 *
 * For files created with bHardBlockSize=TRUE, a complete block of
 * the specified size is always written, otherwise only the number of
 * used bytes in the block will be written to disk.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::CommitToFile()
{
    int nStatus = 0;

    if (m_fp == nullptr || m_nBlockSize <= 0 || m_pabyBuf == nullptr ||
        m_nFileOffset < 0)
    {
        CPLError(CE_Failure, CPLE_AssertionFailed,
                 "TABRawBinBlock::CommitToFile(): Block has not been "
                 "initialized yet!");
        return -1;
    }

    /*----------------------------------------------------------------
     * If block has not been modified, then just return... nothing to do.
     *---------------------------------------------------------------*/
    if (!m_bModified)
        return 0;

    /*----------------------------------------------------------------
     * Move the output file pointer to the right position...
     *---------------------------------------------------------------*/
    if (VSIFSeekL(m_fp, m_nFileOffset, SEEK_SET) != 0)
    {
        /*------------------------------------------------------------
         * Moving pointer failed... we may need to pad with zeros if
         * block destination is beyond current end of file.
         *-----------------------------------------------------------*/
        int nCurPos = static_cast<int>(VSIFTellL(m_fp));

        if (nCurPos < m_nFileOffset && VSIFSeekL(m_fp, 0L, SEEK_END) == 0 &&
            (nCurPos = static_cast<int>(VSIFTellL(m_fp))) < m_nFileOffset)
        {
            const GByte cZero = 0;

            while (nCurPos < m_nFileOffset && nStatus == 0)
            {
                if (VSIFWriteL(&cZero, 1, 1, m_fp) != 1)
                {
                    CPLError(CE_Failure, CPLE_FileIO,
                             "Failed writing 1 byte at offset %d.", nCurPos);
                    nStatus = -1;
                    break;
                }
                nCurPos++;
            }
        }

        if (nCurPos != m_nFileOffset)
            nStatus = -1;  // Error message will follow below
    }

    /*----------------------------------------------------------------
     * At this point we are ready to write to the file.
     *
     * If m_bHardBlockSize==FALSE, then we do not write a complete block;
     * we write only the part of the block that was used.
     *---------------------------------------------------------------*/
    int numBytesToWrite = m_bHardBlockSize ? m_nBlockSize : m_nSizeUsed;

    /*CPLDebug("MITAB", "Committing to offset %d", m_nFileOffset);*/

    if (nStatus != 0 ||
        VSIFWriteL(m_pabyBuf, sizeof(GByte), numBytesToWrite, m_fp) !=
            static_cast<size_t>(numBytesToWrite))
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "Failed writing %d bytes at offset %d.", numBytesToWrite,
                 m_nFileOffset);
        return -1;
    }
    if (m_nFileOffset + numBytesToWrite > m_nFileSize)
    {
        m_nFileSize = m_nFileOffset + numBytesToWrite;
    }

    VSIFFlushL(m_fp);

    m_bModified = FALSE;

    return 0;
}

/**********************************************************************
 *                   TABRawBinBlock::CommitAsDeleted()
 *
 * Commit current block to file using block type 4 (garbage block)
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::CommitAsDeleted(GInt32 nNextBlockPtr)
{
    CPLErrorReset();

    if (m_pabyBuf == nullptr)
    {
        CPLError(CE_Failure, CPLE_AssertionFailed,
                 "CommitAsDeleted(): Block has not been initialized yet!");
        return -1;
    }

    /*-----------------------------------------------------------------
     * Create deleted block header
     *----------------------------------------------------------------*/
    GotoByteInBlock(0x000);
    WriteInt16(TABMAP_GARB_BLOCK);  // Block type code
    WriteInt32(nNextBlockPtr);

    int nStatus = CPLGetLastErrorType() == CE_Failure ? -1 : 0;

    /*-----------------------------------------------------------------
     * OK, call the base class to write the block to disk.
     *----------------------------------------------------------------*/
    if (nStatus == 0)
    {
#ifdef DEBUG_VERBOSE
        CPLDebug("MITAB", "Committing GARBAGE block to offset %d",
                 m_nFileOffset);
#endif
        nStatus = TABRawBinBlock::CommitToFile();
        m_nSizeUsed = 0;
    }

    return nStatus;
}

/**********************************************************************
 *                   TABRawBinBlock::InitBlockFromData()
 *
 * Set the binary data buffer and initialize the block.
 *
 * Calling ReadFromFile() will automatically call InitBlockFromData() to
 * complete the initialization of the block after the data is read from the
 * file.  Derived classes should implement their own version of
 * InitBlockFromData() if they need specific initialization... in this
 * case the derived InitBlockFromData() should call
 * TABRawBinBlock::InitBlockFromData() before doing anything else.
 *
 * By default, the buffer will be copied, but if bMakeCopy = FALSE then
 * it won't be copied, and the object will keep a reference to the
 * user's buffer... and this object will eventually free the user's buffer.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::InitBlockFromData(GByte *pabyBuf, int nBlockSize,
                                      int nSizeUsed,
                                      GBool bMakeCopy /* = TRUE */,
                                      VSILFILE *fpSrc /* = NULL */,
                                      int nOffset /* = 0 */)
{
    m_fp = fpSrc;
    m_nFileOffset = nOffset;
    m_nCurPos = 0;
    m_bModified = FALSE;

    /*----------------------------------------------------------------
     * Alloc or realloc the buffer to contain the data if necessary
     *---------------------------------------------------------------*/
    if (!bMakeCopy)
    {
        if (m_pabyBuf != nullptr)
            CPLFree(m_pabyBuf);
        m_pabyBuf = pabyBuf;
        m_nBlockSize = nBlockSize;
        m_nSizeUsed = nSizeUsed;
    }
    else if (m_pabyBuf == nullptr || nBlockSize != m_nBlockSize)
    {
        m_pabyBuf = static_cast<GByte *>(
            CPLRealloc(m_pabyBuf, nBlockSize * sizeof(GByte)));
        m_nBlockSize = nBlockSize;
        m_nSizeUsed = nSizeUsed;
        memcpy(m_pabyBuf, pabyBuf, m_nSizeUsed);
    }

    /*----------------------------------------------------------------
     * Extract block type... header block (first block in a file) has
     * no block type, so we assign one by default.
     *---------------------------------------------------------------*/
    if (m_nFileOffset == 0)
        m_nBlockType = TABMAP_HEADER_BLOCK;
    else
    {
        // Block type will be validated only if GetBlockType() is called
        m_nBlockType = static_cast<int>(m_pabyBuf[0]);
    }

    return 0;
}

/**********************************************************************
 *                   TABRawBinBlock::InitNewBlock()
 *
 * Initialize the block so that it knows to which file is attached,
 * its block size, etc.
 *
 * This is an alternative to calling ReadFromFile() or InitBlockFromData()
 * that puts the block in a stable state without loading any initial
 * data in it.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::InitNewBlock(VSILFILE *fpSrc, int nBlockSize,
                                 int nFileOffset /* = 0*/)
{
    m_fp = fpSrc;
    m_nBlockSize = nBlockSize;
    m_nSizeUsed = 0;
    m_nCurPos = 0;
    m_bModified = FALSE;

    if (nFileOffset > 0)
        m_nFileOffset = nFileOffset;
    else
        m_nFileOffset = 0;

    if (m_fp != nullptr && m_nFileSize < 0 && m_eAccess == TABReadWrite)
    {
        int nCurPos = static_cast<int>(VSIFTellL(m_fp));
        VSIFSeekL(fpSrc, 0, SEEK_END);
        m_nFileSize = static_cast<int>(VSIFTellL(m_fp));
        VSIFSeekL(fpSrc, nCurPos, SEEK_SET);
    }

    m_nBlockType = -1;

    m_pabyBuf = static_cast<GByte *>(
        CPLRealloc(m_pabyBuf, m_nBlockSize * sizeof(GByte)));
    if (m_nBlockSize)
        memset(m_pabyBuf, 0, m_nBlockSize);

    return 0;
}

/**********************************************************************
 *                   TABRawBinBlock::GetBlockType()
 *
 * Return the block type for the current object.
 *
 * Returns a block type >= 0 if successful or -1 if an error happened, in
 * which case  CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::GetBlockType()
{
    if (m_pabyBuf == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GetBlockType(): Block has not been initialized.");
        return -1;
    }

    if (m_nBlockType > TABMAP_LAST_VALID_BLOCK_TYPE)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "GetBlockType(): Unsupported block type %d.", m_nBlockType);
        return -1;
    }

    return m_nBlockType;
}

/**********************************************************************
 *                   TABRawBinBlock::GotoByteInBlock()
 *
 * Move the block pointer to the specified position relative to the
 * beginning of the block.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::GotoByteInBlock(int nOffset)
{
    if ((m_eAccess == TABRead && nOffset > m_nSizeUsed) ||
        (m_eAccess != TABRead && nOffset > m_nBlockSize))
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GotoByteInBlock(): Attempt to go past end of data block.");
        return -1;
    }

    if (nOffset < 0)
    {
        CPLError(
            CE_Failure, CPLE_AppDefined,
            "GotoByteInBlock(): Attempt to go before start of data block.");
        return -1;
    }

    m_nCurPos = nOffset;

    m_nSizeUsed = std::max(m_nSizeUsed, m_nCurPos);

    return 0;
}

/**********************************************************************
 *                   TABRawBinBlock::GotoByteRel()
 *
 * Move the block pointer by the specified number of bytes relative
 * to its current position.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::GotoByteRel(int nOffset)
{
    return GotoByteInBlock(m_nCurPos + nOffset);
}

/**********************************************************************
 *                   TABRawBinBlock::GotoByteInFile()
 *
 * Move the block pointer to the specified position relative to the
 * beginning of the file.
 *
 * In read access, the current block may be reloaded to contain a right
 * block of binary data if necessary.
 *
 * In write mode, the current block may automagically be committed to
 * disk and a new block initialized if necessary.
 *
 * bForceReadFromFile is used in write mode to read the new block data from
 * file instead of creating an empty block. (Useful for TABCollection
 * or other cases that need to do random access in the file in write mode.)
 *
 * bOffsetIsEndOfData is set to TRUE to indicate that the nOffset
 * to which we are attempting to go is the end of the used data in this
 * block (we are positioning ourselves to append data), so if the nOffset
 * corresponds to the beginning of a block then we should really
 * be positioning ourselves at the end of the block that ends at this
 * address instead of at the beginning of the blocks that starts at this
 * address. This case can happen when going back and forth to write collection
 * objects to a Coordblock and is documented in bug 1657.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::GotoByteInFile(int nOffset,
                                   GBool bForceReadFromFile /*=FALSE*/,
                                   GBool bOffsetIsEndOfData /*=FALSE*/)
{
    if (nOffset < 0)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GotoByteInFile(): Attempt to go before start of file.");
        return -1;
    }

    int nNewBlockPtr =
        ((nOffset - m_nFirstBlockPtr) / m_nBlockSize) * m_nBlockSize +
        m_nFirstBlockPtr;

    if (m_eAccess == TABRead)
    {
        if ((nOffset < m_nFileOffset ||
             nOffset >= m_nFileOffset + m_nSizeUsed) &&
            ReadFromFile(m_fp, nNewBlockPtr, m_nBlockSize) != 0)
        {
            // Failed reading new block... error has already been reported.
            return -1;
        }
    }
    else if (m_eAccess == TABWrite)
    {
        if ((nOffset < m_nFileOffset ||
             nOffset >= m_nFileOffset + m_nBlockSize) &&
            (CommitToFile() != 0 ||
             InitNewBlock(m_fp, m_nBlockSize, nNewBlockPtr) != 0))
        {
            // Failed reading new block... error has already been reported.
            return -1;
        }
    }
    else if (m_eAccess == TABReadWrite)
    {
        // TODO: THIS IS NOT REAL read/write access (it is more extended write)
        // Currently we try to read from file only if explicitly requested.
        // If we ever want true read/write mode we should implement
        // more smarts to detect whether the caller wants an existing block to
        // be read, or a new one to be created from scratch.
        // CommitToFile() should only be called only if something changed.
        //
        if (bOffsetIsEndOfData && nOffset % m_nBlockSize == 0)
        {
            /* We're trying to go byte m_nBlockSize of a block that's full of
             * data. In this case it is okay to place the m_nCurPos at byte
             * m_nBlockSize which is past the end of the block.
             */

            /* Make sure we request the block that ends with requested
             * address and not the following block that doesn't exist
             * yet on disk */
            nNewBlockPtr -= m_nBlockSize;

            if ((nOffset < m_nFileOffset ||
                 nOffset > m_nFileOffset + m_nBlockSize) &&
                (CommitToFile() != 0 ||
                 (!bForceReadFromFile &&
                  InitNewBlock(m_fp, m_nBlockSize, nNewBlockPtr) != 0) ||
                 (bForceReadFromFile &&
                  ReadFromFile(m_fp, nNewBlockPtr, m_nBlockSize) != 0)))
            {
                // Failed reading new block... error has already been reported.
                return -1;
            }
        }
        else
        {
            if (!bForceReadFromFile && m_nFileSize > 0 && nOffset < m_nFileSize)
            {
                bForceReadFromFile = TRUE;
                if (!(nOffset < m_nFileOffset ||
                      nOffset >= m_nFileOffset + m_nBlockSize))
                {
                    if ((nOffset >= m_nFileOffset + m_nSizeUsed) &&
                        (CommitToFile() != 0 ||
                         ReadFromFile(m_fp, nNewBlockPtr, m_nBlockSize) != 0))
                    {
                        // Failed reading new block... error has already been
                        // reported.
                        return -1;
                    }
                }
            }

            if ((nOffset < m_nFileOffset ||
                 nOffset >= m_nFileOffset + m_nBlockSize) &&
                (CommitToFile() != 0 ||
                 (!bForceReadFromFile &&
                  InitNewBlock(m_fp, m_nBlockSize, nNewBlockPtr) != 0) ||
                 (bForceReadFromFile &&
                  ReadFromFile(m_fp, nNewBlockPtr, m_nBlockSize) != 0)))
            {
                // Failed reading new block... error has already been reported.
                return -1;
            }
        }
    }
    else
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Access mode not supported yet!");
        return -1;
    }

    m_nCurPos = nOffset - m_nFileOffset;

    m_nSizeUsed = std::max(m_nSizeUsed, m_nCurPos);

    return 0;
}

/**********************************************************************
 *                   TABRawBinBlock::SetFirstBlockPtr()
 *
 * Set the position in the file at which the first block starts.
 * This value will usually be the header size and needs to be specified
 * only if the header size is different from the other blocks size.
 *
 * This value will be used by GotoByteInFile() to properly align the data
 * blocks that it loads automatically when a requested position is outside
 * of the block currently in memory.
 **********************************************************************/
void TABRawBinBlock::SetFirstBlockPtr(int nOffset)
{
    m_nFirstBlockPtr = nOffset;
}

/**********************************************************************
 *                   TABRawBinBlock::GetNumUnusedBytes()
 *
 * Return the number of unused bytes in this block.
 **********************************************************************/
int TABRawBinBlock::GetNumUnusedBytes()
{
    return m_nBlockSize - m_nSizeUsed;
}

/**********************************************************************
 *                   TABRawBinBlock::GetFirstUnusedByteOffset()
 *
 * Return the position of the first unused byte in this block relative
 * to the beginning of the file, or -1 if the block is full.
 **********************************************************************/
int TABRawBinBlock::GetFirstUnusedByteOffset()
{
    if (m_nSizeUsed < m_nBlockSize)
        return m_nFileOffset + m_nSizeUsed;
    else
        return -1;
}

/**********************************************************************
 *                   TABRawBinBlock::GetCurAddress()
 *
 * Return the current pointer position, relative to beginning of file.
 **********************************************************************/
int TABRawBinBlock::GetCurAddress()
{
    return m_nFileOffset + m_nCurPos;
}

/**********************************************************************
 *                   TABRawBinBlock::ReadBytes()
 *
 * Copy the number of bytes from the data block's internal buffer to
 * the user's buffer pointed by pabyDstBuf.
 *
 * Passing pabyDstBuf = NULL will only move the read pointer by the
 * specified number of bytes as if the copy had happened... but it
 * won't crash.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::ReadBytes(int numBytes, GByte *pabyDstBuf)
{
    /*----------------------------------------------------------------
     * Make sure block is initialized with Read access and that the
     * operation won't go beyond the buffer's size.
     *---------------------------------------------------------------*/
    if (m_pabyBuf == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "ReadBytes(): Block has not been initialized.");
        return -1;
    }

    if (m_nCurPos + numBytes > m_nSizeUsed)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "ReadBytes(): Attempt to read past end of data block.");
        return -1;
    }

    if (pabyDstBuf)
    {
        memcpy(pabyDstBuf, m_pabyBuf + m_nCurPos, numBytes);
    }

    m_nCurPos += numBytes;

    return 0;
}

/**********************************************************************
 *                   TABRawBinBlock::Read<datatype>()
 *
 * MapInfo files are binary files with LSB first (Intel) byte
 * ordering.  The following functions will read from the input file
 * and return a value with the bytes ordered properly for the current
 * platform.
 **********************************************************************/
GByte TABRawBinBlock::ReadByte()
{
    GByte byValue = 0;

    ReadBytes(1, &byValue);

    return byValue;
}

GInt16 TABRawBinBlock::ReadInt16()
{
    GInt16 n16Value = 0;

    ReadBytes(2, reinterpret_cast<GByte *>(&n16Value));

#ifdef CPL_MSB
    return static_cast<GInt16>(CPL_SWAP16(n16Value));
#else
    return n16Value;
#endif
}

GInt32 TABRawBinBlock::ReadInt32()
{
    GInt32 n32Value = 0;

    ReadBytes(4, reinterpret_cast<GByte *>(&n32Value));

#ifdef CPL_MSB
    return static_cast<GInt32>(CPL_SWAP32(n32Value));
#else
    return n32Value;
#endif
}

GInt64 TABRawBinBlock::ReadInt64()
{
    GInt64 n64Value = 0;

    ReadBytes(8, reinterpret_cast<GByte *>(&n64Value));

#ifdef CPL_MSB
    CPL_LSBPTR64(&n64Value);
#endif
    return n64Value;
}

float TABRawBinBlock::ReadFloat()
{
    float fValue = 0.0f;

    ReadBytes(4, reinterpret_cast<GByte *>(&fValue));

#ifdef CPL_MSB
    CPL_LSBPTR32(&fValue);
#endif
    return fValue;
}

double TABRawBinBlock::ReadDouble()
{
    double dValue = 0.0;

    ReadBytes(8, reinterpret_cast<GByte *>(&dValue));

#ifdef CPL_MSB
    CPL_SWAPDOUBLE(&dValue);
#endif

    return dValue;
}

/**********************************************************************
 *                   TABRawBinBlock::WriteBytes()
 *
 * Copy the number of bytes from the user's buffer pointed by pabySrcBuf
 * to the data block's internal buffer.
 * Note that this call only writes to the memory buffer... nothing is
 * written to the file until WriteToFile() is called.
 *
 * Passing pabySrcBuf = NULL will only move the write pointer by the
 * specified number of bytes as if the copy had happened... but it
 * won't crash.
 *
 * Returns 0 if successful or -1 if an error happened, in which case
 * CPLError() will have been called.
 **********************************************************************/
int TABRawBinBlock::WriteBytes(int nBytesToWrite, const GByte *pabySrcBuf)
{
    /*----------------------------------------------------------------
     * Make sure block is initialized with Write access and that the
     * operation won't go beyond the buffer's size.
     *---------------------------------------------------------------*/
    if (m_pabyBuf == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "WriteBytes(): Block has not been initialized.");
        return -1;
    }

    if (m_eAccess == TABRead)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "WriteBytes(): Block does not support write operations.");
        return -1;
    }

    if (m_nCurPos + nBytesToWrite > m_nBlockSize)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "WriteBytes(): Attempt to write past end of data block.");
        return -1;
    }

    /*----------------------------------------------------------------
     * Everything is OK... copy the data
     *---------------------------------------------------------------*/
    if (pabySrcBuf)
    {
        memcpy(m_pabyBuf + m_nCurPos, pabySrcBuf, nBytesToWrite);
    }

    m_nCurPos += nBytesToWrite;

    m_nSizeUsed = std::max(m_nSizeUsed, m_nCurPos);

    m_bModified = TRUE;

    return 0;
}

/**********************************************************************
 *                    TABRawBinBlock::Write<datatype>()
 *
 * Arc/Info files are binary files with MSB first (Motorola) byte
 * ordering.  The following functions will reorder the byte for the
 * value properly and write that to the output file.
 *
 * If a problem happens, then CPLError() will be called and
 * CPLGetLastErrNo() can be used to test if a write operation was
 * successful.
 **********************************************************************/
int TABRawBinBlock::WriteByte(GByte byValue)
{
    return WriteBytes(1, &byValue);
}

int TABRawBinBlock::WriteInt16(GInt16 n16Value)
{
#ifdef CPL_MSB
    n16Value = static_cast<GInt16>(CPL_SWAP16(n16Value));
#endif

    return WriteBytes(2, reinterpret_cast<GByte *>(&n16Value));
}

int TABRawBinBlock::WriteInt32(GInt32 n32Value)
{
#ifdef CPL_MSB
    n32Value = static_cast<GInt32>(CPL_SWAP32(n32Value));
#endif

    return WriteBytes(4, reinterpret_cast<GByte *>(&n32Value));
}

int TABRawBinBlock::WriteInt64(GInt64 n64Value)
{
#ifdef CPL_MSB
    CPL_SWAP64PTR(&n64Value);
#endif

    return WriteBytes(8, reinterpret_cast<GByte *>(&n64Value));
}

int TABRawBinBlock::WriteFloat(float fValue)
{
#ifdef CPL_MSB
    CPL_LSBPTR32(&fValue);
#endif

    return WriteBytes(4, reinterpret_cast<GByte *>(&fValue));
}

int TABRawBinBlock::WriteDouble(double dValue)
{
#ifdef CPL_MSB
    CPL_SWAPDOUBLE(&dValue);
#endif

    return WriteBytes(8, reinterpret_cast<GByte *>(&dValue));
}

/**********************************************************************
 *                    TABRawBinBlock::WriteZeros()
 *
 * Write a number of zeros (specified in bytes) at the current position
 * in the file.
 *
 * If a problem happens, then CPLError() will be called and
 * CPLGetLastErrNo() can be used to test if a write operation was
 * successful.
 **********************************************************************/
int TABRawBinBlock::WriteZeros(int nBytesToWrite)
{
    const GByte acZeros[8] = {0, 0, 0, 0, 0, 0, 0, 0};
    int nStatus = 0;

    // Write by 8 bytes chunks.  The last chunk may be less than 8 bytes.
    for (int i = 0; nStatus == 0 && i < nBytesToWrite; i += 8)
    {
        nStatus = WriteBytes(std::min(8, nBytesToWrite - i), acZeros);
    }

    return nStatus;
}

/**********************************************************************
 *                   TABRawBinBlock::WritePaddedString()
 *
 * Write a string and pad the end of the field (up to nFieldSize) with
 * spaces number of spaces at the current position in the file.
 *
 * If a problem happens, then CPLError() will be called and
 * CPLGetLastErrNo() can be used to test if a write operation was
 * successful.
 **********************************************************************/
int TABRawBinBlock::WritePaddedString(int nFieldSize, const char *pszString)
{
    char acSpaces[8] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
    int i, nLen, numSpaces;
    int nStatus = 0;

    nLen = static_cast<int>(strlen(pszString));
    nLen = std::min(nLen, nFieldSize);
    numSpaces = nFieldSize - nLen;

    if (nLen > 0)
        nStatus = WriteBytes(nLen, reinterpret_cast<const GByte *>(pszString));

    /* Write spaces by 8 bytes chunks.  The last chunk may be less than 8 bytes
     */
    for (i = 0; nStatus == 0 && i < numSpaces; i += 8)
    {
        nStatus = WriteBytes(std::min(8, numSpaces - i),
                             reinterpret_cast<GByte *>(acSpaces));
    }

    return nStatus;
}

/**********************************************************************
 *                   TABRawBinBlock::Dump()
 *
 * Dump block contents... available only in DEBUG mode.
 **********************************************************************/
#ifdef DEBUG

void TABRawBinBlock::Dump(FILE *fpOut /*=NULL*/)
{
    if (fpOut == nullptr)
        fpOut = stdout;

    fprintf(fpOut, "----- TABRawBinBlock::Dump() -----\n");
    if (m_pabyBuf == nullptr)
    {
        fprintf(fpOut, "Block has not been initialized yet.");
    }
    else
    {
        if (m_nBlockType == TABMAP_GARB_BLOCK)
        {
            fprintf(fpOut, "Garbage Block (type %d) at offset %d.\n",
                    m_nBlockType, m_nFileOffset);
            int nNextGarbageBlock = 0;
            memcpy(&nNextGarbageBlock, m_pabyBuf + 2, 4);
            CPL_LSBPTR32(&nNextGarbageBlock);
            fprintf(fpOut, "  m_nNextGarbageBlock     = %d\n",
                    nNextGarbageBlock);
        }
        else
        {
            fprintf(fpOut,
                    "Block (type %d) size=%d bytes at offset %d in file.\n",
                    m_nBlockType, m_nBlockSize, m_nFileOffset);
            fprintf(fpOut, "Current pointer at byte %d\n", m_nCurPos);
        }
    }

    fflush(fpOut);
}

#endif  // DEBUG

/**********************************************************************
 *                          DumpBytes()
 *
 * Read and dump the contents of an Binary file.
 **********************************************************************/
void TABRawBinBlock::DumpBytes(GInt32 nValue, int nOffset /*=0*/,
                               FILE *fpOut /*=NULL*/)
{
    float fValue = 0.0f;
    memcpy(&fValue, &nValue, 4);

    char achValue[4];
    memcpy(achValue, &nValue, 4);

    GInt16 n16Val1 = 0;
    memcpy(&n16Val1, achValue + 2, sizeof(GInt16));
    GInt16 n16Val2 = 0;
    memcpy(&n16Val2, achValue, sizeof(GInt16));

    /* For double precision values, we only use the first half
     * of the height bytes... and leave the other 4 bytes as zeros!
     * It's a bit of a hack, but it seems to be enough for the
     * precision of the values we print!
     */
#ifdef CPL_MSB
    const GInt32 anVal[2] = {nValue, 0};
#else
    const GInt32 anVal[2] = {0, nValue};
#endif
    double dValue = 0.0;
    memcpy(&dValue, anVal, 8);

    if (fpOut == nullptr)
        fpOut = stdout;

    fprintf(fpOut, "%d\t0x%8.8x  %-5d\t%-6d %-6d %5.3e  d=%5.3e", nOffset,
            nValue, nValue, n16Val1, n16Val2, fValue, dValue);

    fprintf(fpOut, "\t[%c%c%c%c]\n", isprint(achValue[0]) ? achValue[0] : '.',
            isprint(achValue[1]) ? achValue[1] : '.',
            isprint(achValue[2]) ? achValue[2] : '.',
            isprint(achValue[3]) ? achValue[3] : '.');
}

/**********************************************************************
 *                   TABCreateMAPBlockFromFile()
 *
 * Load data from the specified file location and create and initialize
 * a TABMAP*Block of the right type to handle it.
 *
 * Returns the new object if successful or NULL if an error happened, in
 * which case CPLError() will have been called.
 **********************************************************************/
TABRawBinBlock *TABCreateMAPBlockFromFile(VSILFILE *fpSrc, int nOffset,
                                          int nSize,
                                          GBool bHardBlockSize /*= TRUE */,
                                          TABAccess eAccessMode /*= TABRead*/)
{
    if (fpSrc == nullptr || nSize == 0)
    {
        CPLError(CE_Failure, CPLE_AssertionFailed,
                 "TABCreateMAPBlockFromFile(): Assertion Failed!");
        return nullptr;
    }

    /*----------------------------------------------------------------
     * Alloc a buffer to contain the data
     *---------------------------------------------------------------*/
    GByte *pabyBuf = static_cast<GByte *>(CPLMalloc(nSize * sizeof(GByte)));

    /*----------------------------------------------------------------
     * Read from the file
     *---------------------------------------------------------------*/
    if (VSIFSeekL(fpSrc, nOffset, SEEK_SET) != 0 ||
        VSIFReadL(pabyBuf, sizeof(GByte), nSize, fpSrc) !=
            static_cast<unsigned int>(nSize))
    {
        CPLError(
            CE_Failure, CPLE_FileIO,
            "TABCreateMAPBlockFromFile() failed reading %d bytes at offset %d.",
            nSize, nOffset);
        CPLFree(pabyBuf);
        return nullptr;
    }

    /*----------------------------------------------------------------
     * Create an object of the right type
     * Header block is different: it does not start with the object
     * type byte but it is always the first block in a file
     *---------------------------------------------------------------*/
    TABRawBinBlock *poBlock = nullptr;

    if (nOffset == 0)
    {
        poBlock = new TABMAPHeaderBlock(eAccessMode);
    }
    else
    {
        switch (pabyBuf[0])
        {
            case TABMAP_INDEX_BLOCK:
                poBlock = new TABMAPIndexBlock(eAccessMode);
                break;
            case TABMAP_OBJECT_BLOCK:
                poBlock = new TABMAPObjectBlock(eAccessMode);
                break;
            case TABMAP_COORD_BLOCK:
                poBlock = new TABMAPCoordBlock(eAccessMode);
                break;
            case TABMAP_TOOL_BLOCK:
                poBlock = new TABMAPToolBlock(eAccessMode);
                break;
            case TABMAP_GARB_BLOCK:
            default:
                poBlock = new TABRawBinBlock(eAccessMode, bHardBlockSize);
                break;
        }
    }

    /*----------------------------------------------------------------
     * Init new object with the data we just read
     *---------------------------------------------------------------*/
    if (poBlock->InitBlockFromData(pabyBuf, nSize, nSize, FALSE, fpSrc,
                                   nOffset) != 0)
    {
        // Some error happened... and CPLError() has been called
        delete poBlock;
        poBlock = nullptr;
    }

    return poBlock;
}

/*=====================================================================
 *                      class TABBinBlockManager
 *====================================================================*/

/**********************************************************************
 *                   TABBinBlockManager::TABBinBlockManager()
 *
 * Constructor.
 **********************************************************************/
TABBinBlockManager::TABBinBlockManager()
    : m_nBlockSize(0), m_nLastAllocatedBlock(-1),
      m_psGarbageBlocksFirst(nullptr), m_psGarbageBlocksLast(nullptr)
{
    m_szName[0] = '\0';
}

/**********************************************************************
 *                   TABBinBlockManager::~TABBinBlockManager()
 *
 * Destructor.
 **********************************************************************/
TABBinBlockManager::~TABBinBlockManager()
{
    Reset();
}

/**********************************************************************
 *                   TABBinBlockManager::SetBlockSize()
 **********************************************************************/
void TABBinBlockManager::SetBlockSize(int nBlockSize)
{
    m_nBlockSize = nBlockSize;
}

/**********************************************************************
 *                   TABBinBlockManager::SetName()
 **********************************************************************/
void TABBinBlockManager::SetName(const char *pszName)
{
    strncpy(m_szName, pszName, sizeof(m_szName));
    m_szName[sizeof(m_szName) - 1] = '\0';
}

/**********************************************************************
 *                   TABBinBlockManager::AllocNewBlock()
 *
 * Returns and reserves the address of the next available block, either a
 * brand new block at end of file, or recycle a garbage block if one is
 * available.
 **********************************************************************/
GInt32 TABBinBlockManager::AllocNewBlock(CPL_UNUSED const char *pszReason)
{
    // Try to reuse garbage blocks first
    if (GetFirstGarbageBlock() > 0)
    {
        int nRetValue = PopGarbageBlock();
#ifdef DEBUG_VERBOSE
        CPLDebug("MITAB",
                 "AllocNewBlock(%s, %s) = %d (recycling garbage block)",
                 m_szName, pszReason, nRetValue);
#endif
        return nRetValue;
    }

    // ... or alloc a new block at EOF
    if (m_nLastAllocatedBlock == -1)
        m_nLastAllocatedBlock = 0;
    else
    {
        CPLAssert(m_nBlockSize);
        m_nLastAllocatedBlock += m_nBlockSize;
    }

#ifdef DEBUG_VERBOSE
    CPLDebug("MITAB", "AllocNewBlock(%s, %s) = %d", m_szName, pszReason,
             m_nLastAllocatedBlock);
#endif
    return m_nLastAllocatedBlock;
}

/**********************************************************************
 *                   TABBinBlockManager::Reset()
 *
 **********************************************************************/
void TABBinBlockManager::Reset()
{
    m_nLastAllocatedBlock = -1;

    // Flush list of garbage blocks
    while (m_psGarbageBlocksFirst != nullptr)
    {
        TABBlockRef *psNext = m_psGarbageBlocksFirst->psNext;
        CPLFree(m_psGarbageBlocksFirst);
        m_psGarbageBlocksFirst = psNext;
    }
    m_psGarbageBlocksLast = nullptr;
}

/**********************************************************************
 *                   TABBinBlockManager::PushGarbageBlockAsFirst()
 *
 * Insert a garbage block at the head of the list of garbage blocks.
 **********************************************************************/
void TABBinBlockManager::PushGarbageBlockAsFirst(GInt32 nBlockPtr)
{
    TABBlockRef *psNewBlockRef =
        static_cast<TABBlockRef *>(CPLMalloc(sizeof(TABBlockRef)));

    psNewBlockRef->nBlockPtr = nBlockPtr;
    psNewBlockRef->psPrev = nullptr;
    psNewBlockRef->psNext = m_psGarbageBlocksFirst;

    if (m_psGarbageBlocksFirst != nullptr)
        m_psGarbageBlocksFirst->psPrev = psNewBlockRef;
    m_psGarbageBlocksFirst = psNewBlockRef;
    if (m_psGarbageBlocksLast == nullptr)
        m_psGarbageBlocksLast = m_psGarbageBlocksFirst;
}

/**********************************************************************
 *                   TABBinBlockManager::PushGarbageBlockAsLast()
 *
 * Insert a garbage block at the tail of the list of garbage blocks.
 **********************************************************************/
void TABBinBlockManager::PushGarbageBlockAsLast(GInt32 nBlockPtr)
{
    TABBlockRef *psNewBlockRef =
        static_cast<TABBlockRef *>(CPLMalloc(sizeof(TABBlockRef)));

    psNewBlockRef->nBlockPtr = nBlockPtr;
    psNewBlockRef->psPrev = m_psGarbageBlocksLast;
    psNewBlockRef->psNext = nullptr;

    if (m_psGarbageBlocksLast != nullptr)
        m_psGarbageBlocksLast->psNext = psNewBlockRef;
    m_psGarbageBlocksLast = psNewBlockRef;
    if (m_psGarbageBlocksFirst == nullptr)
        m_psGarbageBlocksFirst = m_psGarbageBlocksLast;
}

/**********************************************************************
 *                   TABBinBlockManager::GetFirstGarbageBlock()
 *
 * Return address of the block at the head of the list of garbage blocks
 * or 0 if the list is empty.
 **********************************************************************/
GInt32 TABBinBlockManager::GetFirstGarbageBlock()
{
    if (m_psGarbageBlocksFirst)
        return m_psGarbageBlocksFirst->nBlockPtr;

    return 0;
}

/**********************************************************************
 *                   TABBinBlockManager::PopGarbageBlock()
 *
 * Return address of the block at the head of the list of garbage blocks
 * and remove that block from the list.
 * Returns 0 if the list is empty.
 **********************************************************************/
GInt32 TABBinBlockManager::PopGarbageBlock()
{
    GInt32 nBlockPtr = 0;

    if (m_psGarbageBlocksFirst)
    {
        nBlockPtr = m_psGarbageBlocksFirst->nBlockPtr;
        TABBlockRef *psNext = m_psGarbageBlocksFirst->psNext;
        CPLFree(m_psGarbageBlocksFirst);
        if (psNext != nullptr)
            psNext->psPrev = nullptr;
        else
            m_psGarbageBlocksLast = nullptr;
        m_psGarbageBlocksFirst = psNext;
    }

    return nBlockPtr;
}
