/**********************************************************************
 *
 * Name:     mitab_miffile.cpp
 * Project:  MapInfo TAB Read/Write library
 * Language: C++
 * Purpose:  Implementation of the MIDFile class.
 *           To be used by external programs to handle reading/writing of
 *           features from/to MID/MIF datasets.
 * Author:   Stephane Villeneuve, stephane.v@videotron.ca
 *
 **********************************************************************
 * Copyright (c) 1999-2003, Stephane Villeneuve
 * Copyright (c) 2011-2013, Even Rouault <even dot rouault at spatialys.com>
 * Copyright (c) 2014, Even Rouault <even.rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 **********************************************************************/

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

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

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_string.h"
#include "mitab_priv.h"
#include "mitab_utils.h"
#include "ogr_core.h"
#include "ogr_feature.h"
#include "ogr_spatialref.h"
#include "ogrsf_frmts.h"

/*=====================================================================
 *                      class MIFFile
 *====================================================================*/

/**********************************************************************
 *                   MIFFile::MIFFile()
 *
 * Constructor.
 **********************************************************************/
MIFFile::MIFFile(GDALDataset *poDS)
    : IMapInfoFile(poDS), m_pszFname(nullptr), m_eAccessMode(TABRead),
      m_nVersion(300),
      // Tab is default delimiter in MIF spec if not explicitly specified.  Use
      // that by default for read mode. In write mode, we will use "," as
      // delimiter since it is more common than tab (we do this in Open())
      m_pszDelimiter(CPLStrdup("\t")), m_pszUnique(nullptr),
      m_pszIndex(nullptr), m_pszCoordSys(nullptr), m_paeFieldType(nullptr),
      m_pabFieldIndexed(nullptr), m_pabFieldUnique(nullptr),
      m_dfXMultiplier(1.0), m_dfYMultiplier(1.0), m_dfXDisplacement(0.0),
      m_dfYDisplacement(0.0), m_dXMin(0), m_dYMin(0), m_dXMax(0), m_dYMax(0),
      m_bExtentsSet(FALSE), m_nPoints(0), m_nLines(0), m_nRegions(0),
      m_nTexts(0), m_nPreloadedId(0), m_poMIDFile(nullptr),
      m_poMIFFile(nullptr), m_poDefn(nullptr), m_poSpatialRef(nullptr),
      m_nFeatureCount(0), m_nWriteFeatureId(-1), m_nAttribute(0),
      m_bPreParsed(FALSE), m_bHeaderWrote(FALSE)
{
    m_nCurFeatureId = 0;
    m_poCurFeature = nullptr;
}

/**********************************************************************
 *                   MIFFile::~MIFFile()
 *
 * Destructor.
 **********************************************************************/
MIFFile::~MIFFile()
{
    MIFFile::Close();
}

/**********************************************************************
 *                   MIFFile::Open()
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MIFFile::Open(const char *pszFname, TABAccess eAccess,
                  GBool bTestOpenNoError /*=FALSE*/,
                  const char *pszCharset /* = NULL */)
{
    char *pszTmpFname = nullptr;
    int nFnameLen = 0;

    CPLErrorReset();

    if (m_poMIFFile)
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "Open() failed: object already contains an open file");

        return -1;
    }

    /*-----------------------------------------------------------------
     * Validate access mode
     *----------------------------------------------------------------*/
    const char *pszAccess = nullptr;
    if (eAccess == TABRead)
    {
        m_eAccessMode = TABRead;
        pszAccess = "rt";
    }
    else if (eAccess == TABWrite)
    {
        m_eAccessMode = TABWrite;
        pszAccess = "wt";

        // In write mode, use "," as delimiter since it is more common than tab
        CPLFree(m_pszDelimiter);
        m_pszDelimiter = CPLStrdup(",");
    }
    else
    {
        if (!bTestOpenNoError)
            CPLError(CE_Failure, CPLE_FileIO,
                     "Open() failed: access mode \"%d\" not supported",
                     eAccess);
        else
            CPLErrorReset();

        return -1;
    }

    /*-----------------------------------------------------------------
     * Make sure filename has a .MIF or .MID extension...
     *----------------------------------------------------------------*/
    m_pszFname = CPLStrdup(pszFname);
    nFnameLen = static_cast<int>(strlen(m_pszFname));
    if (nFnameLen > 4 && (strcmp(m_pszFname + nFnameLen - 4, ".MID") == 0 ||
                          strcmp(m_pszFname + nFnameLen - 4, ".MIF") == 0))
        strcpy(m_pszFname + nFnameLen - 4, ".MIF");
    else if (nFnameLen > 4 && (EQUAL(m_pszFname + nFnameLen - 4, ".mid") ||
                               EQUAL(m_pszFname + nFnameLen - 4, ".mif")))
        strcpy(m_pszFname + nFnameLen - 4, ".mif");
    else
    {
        if (!bTestOpenNoError)
            CPLError(CE_Failure, CPLE_FileIO,
                     "Open() failed for %s: invalid filename extension",
                     m_pszFname);
        else
            CPLErrorReset();

        return -1;
    }

    pszTmpFname = CPLStrdup(m_pszFname);

    /*-----------------------------------------------------------------
     * Open .MIF file
     *----------------------------------------------------------------*/

#ifndef _WIN32
    /*-----------------------------------------------------------------
     * On Unix, make sure extension uses the right cases
     * We do it even for write access because if a file with the same
     * extension already exists we want to overwrite it.
     *----------------------------------------------------------------*/
    TABAdjustFilenameExtension(pszTmpFname);
#endif

    m_poMIFFile = new MIDDATAFile(CharsetToEncoding(pszCharset));

    if (m_poMIFFile->Open(pszTmpFname, pszAccess) != 0)
    {
        if (!bTestOpenNoError)
            CPLError(CE_Failure, CPLE_NotSupported, "Unable to open %s.",
                     pszTmpFname);
        else
            CPLErrorReset();

        CPLFree(pszTmpFname);
        Close();

        return -1;
    }

    /*-----------------------------------------------------------------
     * Read MIF File Header
     *----------------------------------------------------------------*/
    int bIsEmpty = FALSE;
    if (m_eAccessMode == TABRead && ParseMIFHeader(&bIsEmpty) != 0)
    {
        Close();

        if (!bTestOpenNoError)
            CPLError(CE_Failure, CPLE_NotSupported,
                     "Failed parsing header in %s.", m_pszFname);
        else
            CPLErrorReset();

        CPLFree(pszTmpFname);

        return -1;
    }

    if (m_nAttribute > 0 || m_eAccessMode == TABWrite)
    {
        /*-----------------------------------------------------------------
         * Open .MID file
         *----------------------------------------------------------------*/
        if (nFnameLen > 4 && strcmp(pszTmpFname + nFnameLen - 4, ".MIF") == 0)
            strcpy(pszTmpFname + nFnameLen - 4, ".MID");
        else
            strcpy(pszTmpFname + nFnameLen - 4, ".mid");

#ifndef _WIN32
        TABAdjustFilenameExtension(pszTmpFname);
#endif

        m_poMIDFile = new MIDDATAFile("");
        if (eAccess == TABRead || eAccess == TABReadWrite)
        {
            m_poMIDFile->SetEncoding(CharsetToEncoding(GetCharset()));
        }
        else if (eAccess == TABWrite)
        {
            m_poMIDFile->SetEncoding(CharsetToEncoding(pszCharset));
        }

        if (m_poMIDFile->Open(pszTmpFname, pszAccess) != 0)
        {
            if (m_eAccessMode == TABWrite)
            {
                if (!bTestOpenNoError)
                    CPLError(CE_Failure, CPLE_NotSupported,
                             "Unable to open %s.", pszTmpFname);
                else
                    CPLErrorReset();

                CPLFree(pszTmpFname);
                Close();

                return -1;
            }
            else
            {
                CPLDebug("MITAB",
                         "%s is not found, although %d attributes are declared",
                         pszTmpFname, m_nAttribute);
                delete m_poMIDFile;
                m_poMIDFile = nullptr;
            }
        }
    }

    CPLFree(pszTmpFname);
    pszTmpFname = nullptr;

    /*-----------------------------------------------------------------
     * In write access, set some defaults
     *----------------------------------------------------------------*/
    if (m_eAccessMode == TABWrite)
    {
        m_nVersion = 300;
        if (pszCharset != nullptr)
            SetCharset(pszCharset);
        else
            SetCharset("Neutral");
    }

    m_poMIFFile->SetTranslation(m_dfXMultiplier, m_dfYMultiplier,
                                m_dfXDisplacement, m_dfYDisplacement);
    if (m_poMIDFile != nullptr)
        m_poMIDFile->SetTranslation(m_dfXMultiplier, m_dfYMultiplier,
                                    m_dfXDisplacement, m_dfYDisplacement);
    m_poMIFFile->SetDelimiter(m_pszDelimiter);
    if (m_poMIDFile != nullptr)
        m_poMIDFile->SetDelimiter(m_pszDelimiter);

    /*-------------------------------------------------------------
     * Set geometry type if the geometry objects are uniform.
     *------------------------------------------------------------*/
    int numPoints = 0, numRegions = 0, numTexts = 0, numLines = 0;

    if (GetFeatureCountByType(numPoints, numLines, numRegions, numTexts,
                              FALSE) == 0)
    {
        numPoints += numTexts;
        if (numPoints > 0 && numLines == 0 && numRegions == 0)
            m_poDefn->SetGeomType(wkbPoint);
        else if (numPoints == 0 && numLines > 0 && numRegions == 0)
            m_poDefn->SetGeomType(wkbLineString);
        else
        {
            /* we leave it unknown indicating a mixture */
        }
    }

    /* A newly created layer should have OGRFeatureDefn */
    if (m_poDefn == nullptr)
    {
        char *pszFeatureClassName = TABGetBasename(m_pszFname);
        m_poDefn = new OGRFeatureDefn(pszFeatureClassName);
        CPLFree(pszFeatureClassName);
        // Ref count defaults to 0... set it to 1
        m_poDefn->Reference();
        m_poDefn->Seal(/* bSealFields = */ true);
    }

    return 0;
}

/**********************************************************************
 *                   MIFFile::ParseMIFHeader()
 *
 * Scan the header of a MIF file, and store any useful information into
 * class members.  The main piece of information being the fields
 * definition that we use to build the OGRFeatureDefn for this file.
 *
 * This private method should be used only during the Open() call.
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MIFFile::ParseMIFHeader(int *pbIsEmpty)
{
    *pbIsEmpty = FALSE;

    char *pszFeatureClassName = TABGetBasename(m_pszFname);
    m_poDefn = new OGRFeatureDefn(pszFeatureClassName);
    CPLFree(pszFeatureClassName);
    // Ref count defaults to 0... set it to 1
    m_poDefn->Reference();
    m_poDefn->Seal(/* bSealFields = */ true);

    if (m_eAccessMode != TABRead)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "ParseMIDFile() can be used only with Read access.");
        return -1;
    }

    /*-----------------------------------------------------------------
     * Parse header until we find the "Data" line
     *----------------------------------------------------------------*/
    char **papszToken = nullptr;
    GBool bColumns = FALSE;
    GBool bAllColumnsRead = FALSE;
    int nColumns = 0;
    GBool bCoordSys = FALSE;
    CPLString osCoordSys;
    int nLineCount = 0;

    const char *pszLine = nullptr;
    while (((pszLine = m_poMIFFile->GetLine()) != nullptr) &&
           ((bAllColumnsRead == FALSE) || !STARTS_WITH_CI(pszLine, "Data")))
    {
        nLineCount++;
        if (nLineCount == 100000)
        {
            // Arbitrary threshold. The number of lines must be at least as big
            // as the number of fields we want to support.
            CPLError(CE_Failure, CPLE_NotSupported,
                     "Too many lines in MIF header");
            return -1;
        }

        if (bColumns == TRUE && nColumns > 0)
        {
            if (AddFields(pszLine) == 0)
            {
                nColumns--;
                if (nColumns == 0)
                {
                    bAllColumnsRead = TRUE;
                    bColumns = FALSE;
                }
            }
            else
            {
                bColumns = FALSE;
            }
        }
        else if (STARTS_WITH_CI(pszLine, "VERSION"))
        {
            papszToken =
                CSLTokenizeStringComplex(pszLine, " ()\t", TRUE, FALSE);
            bColumns = FALSE;
            bCoordSys = FALSE;
            if (CSLCount(papszToken) == 2)
                m_nVersion = atoi(papszToken[1]);

            CSLDestroy(papszToken);
        }
        else if (STARTS_WITH_CI(pszLine, "CHARSET"))
        {
            papszToken =
                CSLTokenizeStringComplex(pszLine, " ()\t", TRUE, FALSE);
            bColumns = FALSE;
            bCoordSys = FALSE;

            if (CSLCount(papszToken) == 2)
            {
                SetCharset(papszToken[1]);
            }
            CSLDestroy(papszToken);
        }
        else if (STARTS_WITH_CI(pszLine, "DELIMITER"))
        {
            papszToken =
                CSLTokenizeStringComplex(pszLine, " ()\t", TRUE, FALSE);
            bColumns = FALSE;
            bCoordSys = FALSE;

            if (CSLCount(papszToken) == 2)
            {
                CPLFree(m_pszDelimiter);
                m_pszDelimiter = CPLStrdup(papszToken[1]);
            }
            CSLDestroy(papszToken);
        }
        else if (m_pszUnique == nullptr && STARTS_WITH_CI(pszLine, "UNIQUE"))
        {
            bColumns = FALSE;
            bCoordSys = FALSE;

            m_pszUnique = CPLStrdup(pszLine + 6);
        }
        else if (m_pszIndex == nullptr && STARTS_WITH_CI(pszLine, "INDEX"))
        {
            bColumns = FALSE;
            bCoordSys = FALSE;

            m_pszIndex = CPLStrdup(pszLine + 5);
        }
        else if (osCoordSys.empty() && STARTS_WITH_CI(pszLine, "COORDSYS") &&
                 CPLStrnlen(pszLine, 9) >= 9)
        {
            bCoordSys = TRUE;
            osCoordSys = pszLine + 9;
        }
        else if (STARTS_WITH_CI(pszLine, "TRANSFORM"))
        {
            papszToken = CSLTokenizeStringComplex(pszLine, " ,\t", TRUE, FALSE);
            bColumns = FALSE;
            bCoordSys = FALSE;

            if (CSLCount(papszToken) == 5)
            {
                m_dfXMultiplier = CPLAtof(papszToken[1]);
                m_dfYMultiplier = CPLAtof(papszToken[2]);
                m_dfXDisplacement = CPLAtof(papszToken[3]);
                m_dfYDisplacement = CPLAtof(papszToken[4]);

                if (m_dfXMultiplier == 0.0)
                    m_dfXMultiplier = 1.0;
                if (m_dfYMultiplier == 0.0)
                    m_dfYMultiplier = 1.0;
            }
            CSLDestroy(papszToken);
        }
        else if (STARTS_WITH_CI(pszLine, "COLUMNS"))
        {
            papszToken =
                CSLTokenizeStringComplex(pszLine, " ()\t", TRUE, FALSE);
            bCoordSys = FALSE;
            bColumns = TRUE;
            if (CSLCount(papszToken) == 2)
            {
                nColumns = atoi(papszToken[1]);
                m_nAttribute = nColumns;
                if (nColumns == 0)
                {
                    // Permit to 0 columns
                    bAllColumnsRead = TRUE;
                    bColumns = FALSE;
                }
            }
            else
            {
                bColumns = FALSE;
                m_nAttribute = 0;
            }
            CSLDestroy(papszToken);
        }
        else if (bCoordSys == TRUE)
        {
            if (osCoordSys.size() > 10000)  // Arbitrary threshold
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "COORDSYS value too long");
                return -1;
            }
            osCoordSys += ' ';
            osCoordSys += pszLine;
        }
    }

    if (!osCoordSys.empty())
    {
        m_pszCoordSys = CPLStrdup(osCoordSys);

        // Extract bounds if present
        char **papszFields =
            CSLTokenizeStringComplex(osCoordSys, " ,()\t", TRUE, FALSE);
        int iBounds = CSLFindString(papszFields, "Bounds");
        if (iBounds >= 0 && iBounds + 4 < CSLCount(papszFields))
        {
            m_dXMin = CPLAtof(papszFields[++iBounds]);
            m_dYMin = CPLAtof(papszFields[++iBounds]);
            m_dXMax = CPLAtof(papszFields[++iBounds]);
            m_dYMax = CPLAtof(papszFields[++iBounds]);
            m_bBoundsSet = TRUE;
        }
        CSLDestroy(papszFields);
    }

    if (!bAllColumnsRead)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "COLUMNS keyword not found or invalid number of columns read "
                 "in %s.  File may be corrupt.",
                 m_pszFname);
        return -1;
    }

    if (m_poMIFFile->GetLastLine() == nullptr ||
        STARTS_WITH_CI(m_poMIFFile->GetLastLine(), "DATA") == FALSE)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "DATA keyword not found in %s.  File may be corrupt.",
                 m_pszFname);
        return -1;
    }

    /*-----------------------------------------------------------------
     * Move pointer to first line of first object
     *----------------------------------------------------------------*/
    while (((pszLine = m_poMIFFile->GetLine()) != nullptr) &&
           m_poMIFFile->IsValidFeature(pszLine) == FALSE)
        ;

    *pbIsEmpty = (pszLine == nullptr);

    /*-----------------------------------------------------------------
     * Check for Unique and Indexed flags
     *----------------------------------------------------------------*/
    if (m_pszIndex)
    {
        papszToken = CSLTokenizeStringComplex(m_pszIndex, " ,\t", TRUE, FALSE);
        for (int i = 0; papszToken && papszToken[i]; i++)
        {
            int nVal = atoi(papszToken[i]);
            if (nVal > 0 && nVal <= m_poDefn->GetFieldCount())
                m_pabFieldIndexed[nVal - 1] = TRUE;
        }
        CSLDestroy(papszToken);
    }

    if (m_pszUnique)
    {
        papszToken = CSLTokenizeStringComplex(m_pszUnique, " ,\t", TRUE, FALSE);
        for (int i = 0; papszToken && papszToken[i]; i++)
        {
            int nVal = atoi(papszToken[i]);
            if (nVal > 0 && nVal <= m_poDefn->GetFieldCount())
                m_pabFieldUnique[nVal - 1] = TRUE;
        }
        CSLDestroy(papszToken);
    }

    return 0;
}

/************************************************************************/
/*                             AddFields()                              */
/************************************************************************/

int MIFFile::AddFields(const char *pszLine)
{
    int nStatus = 0;

    CPLAssert(m_bHeaderWrote == FALSE);
    char **papszToken =
        CSLTokenizeStringComplex(pszLine, " (,)\t", TRUE, FALSE);
    int numTok = CSLCount(papszToken);

    CPLString osFieldName;
    if (numTok > 0)
    {
        osFieldName = papszToken[0];
        if (strlen(GetEncoding()) > 0)
        {
            osFieldName.Recode(GetEncoding(), CPL_ENC_UTF8);
        }
    }

    if (numTok >= 3 && EQUAL(papszToken[1], "char"))
    {
        /*-------------------------------------------------
         * CHAR type
         *------------------------------------------------*/
        nStatus = AddFieldNative(osFieldName, TABFChar, atoi(papszToken[2]));
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "integer"))
    {
        if (numTok == 2)
        {
            /*-------------------------------------------------
             * INTEGER type without a specified width
             *------------------------------------------------*/
            nStatus = AddFieldNative(osFieldName, TABFInteger);
        }
        else /* if (numTok > 2) */
        {
            /*-------------------------------------------------
             * INTEGER type with a specified width
             *------------------------------------------------*/
            nStatus =
                AddFieldNative(osFieldName, TABFInteger, atoi(papszToken[2]));
        }
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "smallint"))
    {
        if (numTok == 2)
        {
            /*-------------------------------------------------
             * SMALLINT type without a specified width
             *------------------------------------------------*/
            nStatus = AddFieldNative(osFieldName, TABFSmallInt);
        }
        else /* if (numTok > 2) */
        {
            /*-------------------------------------------------
             * SMALLINT type with a specified width
             *------------------------------------------------*/
            nStatus =
                AddFieldNative(osFieldName, TABFSmallInt, atoi(papszToken[2]));
        }
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "largeint"))
    {
        if (numTok == 2)
        {
            /*-------------------------------------------------
             * LargeInt type without a specified width
             *------------------------------------------------*/
            nStatus = AddFieldNative(osFieldName, TABFLargeInt);
        }
        else /* if (numTok > 2) */
        {
            /*-------------------------------------------------
             * LargeInt type with a specified width
             *------------------------------------------------*/
            nStatus =
                AddFieldNative(osFieldName, TABFLargeInt, atoi(papszToken[2]));
        }
    }
    else if (numTok >= 4 && EQUAL(papszToken[1], "decimal"))
    {
        /*-------------------------------------------------
         * DECIMAL type
         *------------------------------------------------*/
        nStatus = AddFieldNative(osFieldName, TABFDecimal, atoi(papszToken[2]),
                                 atoi(papszToken[3]));
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "float"))
    {
        /*-------------------------------------------------
         * FLOAT type
         *------------------------------------------------*/
        nStatus = AddFieldNative(osFieldName, TABFFloat);
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "date"))
    {
        /*-------------------------------------------------
         * DATE type (returned as a string: "DD/MM/YYYY" or "YYYYMMDD")
         *------------------------------------------------*/
        nStatus = AddFieldNative(osFieldName, TABFDate);
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "time"))
    {
        /*-------------------------------------------------
         *  TIME type (v900, returned as a string: "HH:MM:SS" or "HHMMSSmmm")
         *------------------------------------------------*/
        nStatus = AddFieldNative(osFieldName, TABFTime);
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "datetime"))
    {
        /*-------------------------------------------------
         * DATETIME type (v900, returned as a string: "DD/MM/YYYY HH:MM:SS",
         * "YYYY/MM/DD HH:MM:SS" or "YYYYMMDDHHMMSSmmm")
         *------------------------------------------------*/
        nStatus = AddFieldNative(osFieldName, TABFDateTime);
    }
    else if (numTok >= 2 && EQUAL(papszToken[1], "logical"))
    {
        /*-------------------------------------------------
         * LOGICAL type (value "T" or "F")
         *------------------------------------------------*/
        nStatus = AddFieldNative(osFieldName, TABFLogical);
    }
    else
        nStatus = -1;  // Unrecognized field type or line corrupt

    CSLDestroy(papszToken);
    papszToken = nullptr;

    if (nStatus != 0)
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "Failed to parse field definition in file %s", m_pszFname);
        return -1;
    }

    return 0;
}

/************************************************************************/
/*                          GetFeatureCount()                           */
/************************************************************************/

GIntBig MIFFile::GetFeatureCount(int bForce)
{

    if (m_poFilterGeom != nullptr || m_poAttrQuery != nullptr)
        return OGRLayer::GetFeatureCount(bForce);
    else
    {
        if (bForce == TRUE)
            PreParseFile();

        if (m_bPreParsed)
            return m_nFeatureCount;
        else
            return -1;
    }
}

/************************************************************************/
/*                            ResetReading()                            */
/************************************************************************/

void MIFFile::ResetReading()

{
    m_poMIFFile->Rewind();

    const char *pszLine = nullptr;
    while ((pszLine = m_poMIFFile->GetLine()) != nullptr)
        if (STARTS_WITH_CI(pszLine, "DATA"))
            break;

    while ((pszLine = m_poMIFFile->GetLine()) != nullptr)
    {
        if (m_poMIFFile->IsValidFeature(pszLine))
            break;
    }

    if (m_poMIDFile != nullptr)
    {
        m_poMIDFile->Rewind();
    }

    // We're positioned on first feature.  Feature Ids start at 1.
    if (m_poCurFeature)
    {
        delete m_poCurFeature;
        m_poCurFeature = nullptr;
    }

    m_nCurFeatureId = 0;
    m_nPreloadedId = 1;
}

/************************************************************************/
/*                            PreParseFile()                            */
/************************************************************************/

void MIFFile::PreParseFile()
{
    char **papszToken = nullptr;

    GBool bPLine = FALSE;
    GBool bText = FALSE;

    if (m_bPreParsed == TRUE)
        return;

    m_poMIFFile->Rewind();

    const char *pszLine = nullptr;
    while ((pszLine = m_poMIFFile->GetLine()) != nullptr)
        if (STARTS_WITH_CI(pszLine, "DATA"))
            break;

    m_nPoints = m_nLines = m_nRegions = m_nTexts = 0;

    while ((pszLine = m_poMIFFile->GetLine()) != nullptr)
    {
        if (m_poMIFFile->IsValidFeature(pszLine))
        {
            bPLine = FALSE;
            bText = FALSE;
            m_nFeatureCount++;
        }

        CSLDestroy(papszToken);
        papszToken = CSLTokenizeString2(pszLine, " \t", CSLT_HONOURSTRINGS);

        if (STARTS_WITH_CI(pszLine, "POINT"))
        {
            m_nPoints++;
            if (CSLCount(papszToken) == 3)
            {
                UpdateExtents(m_poMIFFile->GetXTrans(CPLAtof(papszToken[1])),
                              m_poMIFFile->GetYTrans(CPLAtof(papszToken[2])));
            }
        }
        else if (STARTS_WITH_CI(pszLine, "LINE") ||
                 STARTS_WITH_CI(pszLine, "RECT") ||
                 STARTS_WITH_CI(pszLine, "ROUNDRECT") ||
                 STARTS_WITH_CI(pszLine, "ARC") ||
                 STARTS_WITH_CI(pszLine, "ELLIPSE"))
        {
            if (CSLCount(papszToken) == 5)
            {
                m_nLines++;
                UpdateExtents(m_poMIFFile->GetXTrans(CPLAtof(papszToken[1])),
                              m_poMIFFile->GetYTrans(CPLAtof(papszToken[2])));
                UpdateExtents(m_poMIFFile->GetXTrans(CPLAtof(papszToken[3])),
                              m_poMIFFile->GetYTrans(CPLAtof(papszToken[4])));
            }
        }
        else if (STARTS_WITH_CI(pszLine, "REGION"))
        {
            m_nRegions++;
            bPLine = TRUE;
        }
        else if (STARTS_WITH_CI(pszLine, "PLINE"))
        {
            m_nLines++;
            bPLine = TRUE;
        }
        else if (STARTS_WITH_CI(pszLine, "TEXT"))
        {
            m_nTexts++;
            bText = TRUE;
        }
        else if (bPLine == TRUE)
        {
            if (CSLCount(papszToken) == 2 &&
                strchr("-.0123456789", papszToken[0][0]) != nullptr)
            {
                UpdateExtents(m_poMIFFile->GetXTrans(CPLAtof(papszToken[0])),
                              m_poMIFFile->GetYTrans(CPLAtof(papszToken[1])));
            }
        }
        else if (bText == TRUE)
        {
            if (CSLCount(papszToken) == 4 &&
                strchr("-.0123456789", papszToken[0][0]) != nullptr)
            {
                UpdateExtents(m_poMIFFile->GetXTrans(CPLAtof(papszToken[0])),
                              m_poMIFFile->GetYTrans(CPLAtof(papszToken[1])));
                UpdateExtents(m_poMIFFile->GetXTrans(CPLAtof(papszToken[2])),
                              m_poMIFFile->GetYTrans(CPLAtof(papszToken[3])));
            }
        }
    }

    CSLDestroy(papszToken);

    m_poMIFFile->Rewind();

    while ((pszLine = m_poMIFFile->GetLine()) != nullptr)
        if (STARTS_WITH_CI(pszLine, "DATA"))
            break;

    while ((pszLine = m_poMIFFile->GetLine()) != nullptr)
    {
        if (m_poMIFFile->IsValidFeature(pszLine))
            break;
    }

    if (m_poMIDFile != nullptr)
    {
        m_poMIDFile->Rewind();
    }

    m_bPreParsed = TRUE;
}

/**********************************************************************
 *                   MIFFile::WriteMIFHeader()
 *
 * Generate the .MIF header.
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MIFFile::WriteMIFHeader()
{
    GBool bFound;

    if (m_eAccessMode != TABWrite)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "WriteMIFHeader() can be used only with Write access.");
        return -1;
    }

    if (m_poDefn == nullptr || m_poDefn->GetFieldCount() == 0)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "File %s must contain at least 1 attribute field.",
                 m_pszFname);
        return -1;
    }

    /*-----------------------------------------------------------------
     * Start writing header.
     *----------------------------------------------------------------*/
    m_bHeaderWrote = TRUE;
    m_poMIFFile->WriteLine("Version %d\n", m_nVersion);
    m_poMIFFile->WriteLine("Charset \"%s\"\n", m_pszCharset);

    // Delimiter is not required if you use \t as delimiter
    if (!EQUAL(m_pszDelimiter, "\t"))
        m_poMIFFile->WriteLine("Delimiter \"%s\"\n", m_pszDelimiter);

    bFound = FALSE;

    for (int iField = 0; iField < m_poDefn->GetFieldCount(); iField++)
    {
        if (m_pabFieldUnique[iField])
        {
            if (!bFound)
                m_poMIFFile->WriteLine("Unique %d", iField + 1);
            else
                m_poMIFFile->WriteLine(",%d", iField + 1);
            bFound = TRUE;
        }
    }
    if (bFound)
        m_poMIFFile->WriteLine("\n");

    bFound = FALSE;
    for (int iField = 0; iField < m_poDefn->GetFieldCount(); iField++)
    {
        if (m_pabFieldIndexed[iField])
        {
            if (!bFound)
                m_poMIFFile->WriteLine("Index  %d", iField + 1);
            else
                m_poMIFFile->WriteLine(",%d", iField + 1);
            bFound = TRUE;
        }
    }
    if (bFound)
        m_poMIFFile->WriteLine("\n");

    if (m_pszCoordSys && m_bBoundsSet)
    {
        m_poMIFFile->WriteLine("CoordSys %s "
                               "Bounds (%.15g, %.15g) (%.15g, %.15g)\n",
                               m_pszCoordSys, m_dXMin, m_dYMin, m_dXMax,
                               m_dYMax);
    }
    else if (m_pszCoordSys)
    {
        m_poMIFFile->WriteLine("CoordSys %s\n", m_pszCoordSys);
    }

    /*-----------------------------------------------------------------
     * Column definitions
     *----------------------------------------------------------------*/
    CPLAssert(m_paeFieldType);

    m_poMIFFile->WriteLine("Columns %d\n", m_poDefn->GetFieldCount());

    for (int iField = 0; iField < m_poDefn->GetFieldCount(); iField++)
    {
        OGRFieldDefn *poFieldDefn = m_poDefn->GetFieldDefn(iField);
        CPLString osFieldName(poFieldDefn->GetNameRef());

        if (strlen(GetEncoding()) > 0)
            osFieldName.Recode(CPL_ENC_UTF8, GetEncoding());

        char *pszCleanName =
            TABCleanFieldName(osFieldName, GetEncoding(), m_bStrictLaundering);
        osFieldName = pszCleanName;
        CPLFree(pszCleanName);

        switch (m_paeFieldType[iField])
        {
            case TABFInteger:
                m_poMIFFile->WriteLine("  %s Integer\n", osFieldName.c_str());
                break;
            case TABFSmallInt:
                m_poMIFFile->WriteLine("  %s SmallInt\n", osFieldName.c_str());
                break;
            case TABFLargeInt:
                m_poMIFFile->WriteLine("  %s LargeInt\n", osFieldName.c_str());
                break;
            case TABFFloat:
                m_poMIFFile->WriteLine("  %s Float\n", osFieldName.c_str());
                break;
            case TABFDecimal:
                m_poMIFFile->WriteLine(
                    "  %s Decimal(%d,%d)\n", osFieldName.c_str(),
                    poFieldDefn->GetWidth(), poFieldDefn->GetPrecision());
                break;
            case TABFLogical:
                m_poMIFFile->WriteLine("  %s Logical\n", osFieldName.c_str());
                break;
            case TABFDate:
                m_poMIFFile->WriteLine("  %s Date\n", osFieldName.c_str());
                break;
            case TABFTime:
                m_poMIFFile->WriteLine("  %s Time\n", osFieldName.c_str());
                break;
            case TABFDateTime:
                m_poMIFFile->WriteLine("  %s DateTime\n", osFieldName.c_str());
                break;
            case TABFChar:
            default:
                m_poMIFFile->WriteLine("  %s Char(%d)\n", osFieldName.c_str(),
                                       poFieldDefn->GetWidth());
        }
    }

    /*-----------------------------------------------------------------
     * Ready to write objects
     *----------------------------------------------------------------*/
    m_poMIFFile->WriteLine("Data\n\n");

    return 0;
}

/**********************************************************************
 *                   MIFFile::Close()
 *
 * Close current file, and release all memory used.
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MIFFile::Close()
{
    /* flush .mif header if not already written */
    if (m_poDefn != nullptr && m_bHeaderWrote == FALSE &&
        m_eAccessMode != TABRead)
    {
        WriteMIFHeader();
    }

    if (m_poMIDFile)
    {
        m_poMIDFile->Close();
        delete m_poMIDFile;
        m_poMIDFile = nullptr;
    }

    if (m_poMIFFile)
    {
        m_poMIFFile->Close();
        delete m_poMIFFile;
        m_poMIFFile = nullptr;
    }

    if (m_poCurFeature)
    {
        delete m_poCurFeature;
        m_poCurFeature = nullptr;
    }

    /*-----------------------------------------------------------------
     * Note: we have to check the reference count before deleting
     * m_poSpatialRef and m_poDefn
     *----------------------------------------------------------------*/
    if (m_poDefn && m_poDefn->Dereference() == 0)
        delete m_poDefn;
    m_poDefn = nullptr;

    if (m_poSpatialRef && m_poSpatialRef->Dereference() == 0)
        delete m_poSpatialRef;
    m_poSpatialRef = nullptr;

    CPLFree(m_pszCoordSys);
    m_pszCoordSys = nullptr;

    CPLFree(m_pszDelimiter);
    m_pszDelimiter = nullptr;

    CPLFree(m_pszUnique);
    m_pszUnique = nullptr;

    CPLFree(m_pszFname);
    m_pszFname = nullptr;

    m_nVersion = 0;

    CPLFree(m_pszCharset);
    m_pszCharset = nullptr;

    CPLFree(m_pabFieldIndexed);
    m_pabFieldIndexed = nullptr;
    CPLFree(m_pabFieldUnique);
    m_pabFieldUnique = nullptr;

    CPLFree(m_pszIndex);
    m_pszIndex = nullptr;

    CPLFree(m_paeFieldType);
    m_paeFieldType = nullptr;

    m_nCurFeatureId = 0;
    m_nPreloadedId = 0;
    m_nFeatureCount = 0;

    m_bBoundsSet = FALSE;

    return 0;
}

/**********************************************************************
 *                   MIFFile::GetNextFeatureId()
 *
 * Returns feature id that follows nPrevId, or -1 if it is the
 * last feature id.  Pass nPrevId=-1 to fetch the first valid feature id.
 **********************************************************************/
GIntBig MIFFile::GetNextFeatureId(GIntBig nPrevId)
{
    if (m_eAccessMode != TABRead)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "GetNextFeatureId() can be used only with Read access.");
        return -1;
    }

    if (nPrevId <= 0 && m_poMIFFile->GetLastLine() != nullptr)
        return 1;  // Feature Ids start at 1
    else if (nPrevId > 0 && m_poMIFFile->GetLastLine() != nullptr)
        return nPrevId + 1;
    else
        return -1;
}

/**********************************************************************
 *                   MIFFile::GotoFeature()
 *
 * Private method to move MIF and MID pointers ready to read specified
 * feature.  Note that Feature Ids start at 1.
 *
 * Returns 0 on success, -1 on error (likely request for invalid feature id)
 **********************************************************************/
int MIFFile::GotoFeature(int nFeatureId)
{

    if (nFeatureId < 1)
        return -1;

    if (nFeatureId == m_nPreloadedId)  // CorrectPosition
    {
        return 0;
    }
    else
    {
        if (nFeatureId < m_nPreloadedId || m_nCurFeatureId == 0)
            ResetReading();

        while (m_nPreloadedId < nFeatureId)
        {
            const char *pszLine;
            while ((pszLine = m_poMIFFile->GetLine()) != nullptr)
            {
                if (m_poMIFFile->IsValidFeature(pszLine))
                {
                    m_nPreloadedId++;
                    if (m_poMIDFile != nullptr)
                        CSLDestroy(m_poMIDFile->GetTokenizedNextLine());
                    break;
                }
            }
            if (pszLine == nullptr)
                return -1;
        }

        CPLAssert(m_nPreloadedId == nFeatureId);

        return 0;
    }
}

/**********************************************************************
 *                   MIFFile::GetFeatureRef()
 *
 * Fill and return a TABFeature object for the specified feature id.
 *
 * The returned pointer is a reference to an object owned and maintained
 * by this MIFFile object.  It should not be altered or freed by the
 * caller and its contents is guaranteed to be valid only until the next
 * call to GetFeatureRef() or Close().
 *
 * Returns NULL if the specified feature id does not exist of if an
 * error happened.  In any case, CPLError() will have been called to
 * report the reason of the failure.
 **********************************************************************/
TABFeature *MIFFile::GetFeatureRef(GIntBig nFeatureId)
{
    if (m_eAccessMode != TABRead)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "GetFeatureRef() can be used only with Read access.");
        return nullptr;
    }

    /*-----------------------------------------------------------------
     * Make sure file is opened and Validate feature id by positioning
     * the read pointers for the .MAP and .DAT files to this feature id.
     *----------------------------------------------------------------*/
    if (m_poMIFFile == nullptr)
    {
        CPLError(CE_Failure, CPLE_IllegalArg,
                 "GetFeatureRef() failed: file is not opened!");
        return nullptr;
    }

    if (!CPL_INT64_FITS_ON_INT32(nFeatureId) ||
        GotoFeature(static_cast<int>(nFeatureId)) != 0)
    {
        CPLError(CE_Failure, CPLE_IllegalArg,
                 "GetFeatureRef() failed: invalid feature id " CPL_FRMT_GIB,
                 nFeatureId);
        return nullptr;
    }

    /*-----------------------------------------------------------------
     * Create new feature object of the right type
     *----------------------------------------------------------------*/
    const char *pszLine = nullptr;
    if ((pszLine = m_poMIFFile->GetLastLine()) != nullptr)
    {
        // Delete previous feature... we'll start we a clean one.
        if (m_poCurFeature)
            delete m_poCurFeature;
        m_poCurFeature = nullptr;

        m_nCurFeatureId = m_nPreloadedId;

        if (STARTS_WITH_CI(pszLine, "NONE"))
        {
            m_poCurFeature = new TABFeature(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "POINT"))
        {
            // Special case, we need to know two lines to decide the type
            char **papszToken =
                CSLTokenizeString2(pszLine, " \t", CSLT_HONOURSTRINGS);

            if (CSLCount(papszToken) != 3)
            {
                CSLDestroy(papszToken);
                CPLError(CE_Failure, CPLE_NotSupported,
                         "GetFeatureRef() failed: invalid point line: '%s'",
                         pszLine);
                return nullptr;
            }

            m_poMIFFile->SaveLine(pszLine);

            if ((pszLine = m_poMIFFile->GetLine()) != nullptr)
            {
                CSLDestroy(papszToken);
                papszToken =
                    CSLTokenizeStringComplex(pszLine, " ,()\t", TRUE, FALSE);
                if (CSLCount(papszToken) > 0 &&
                    STARTS_WITH_CI(papszToken[0], "SYMBOL"))
                {
                    switch (CSLCount(papszToken))
                    {
                        case 4:
                            m_poCurFeature = new TABPoint(m_poDefn);
                            break;
                        case 7:
                            m_poCurFeature = new TABFontPoint(m_poDefn);
                            break;
                        case 5:
                            m_poCurFeature = new TABCustomPoint(m_poDefn);
                            break;
                        default:
                            CSLDestroy(papszToken);
                            CPLError(CE_Failure, CPLE_NotSupported,
                                     "GetFeatureRef() failed: invalid symbol "
                                     "line: '%s'",
                                     pszLine);
                            return nullptr;
                            break;
                    }
                }
            }
            CSLDestroy(papszToken);

            if (m_poCurFeature == nullptr)
            {
                // No symbol clause... default to TABPoint
                m_poCurFeature = new TABPoint(m_poDefn);
            }
        }
        else if (STARTS_WITH_CI(pszLine, "LINE") ||
                 STARTS_WITH_CI(pszLine, "PLINE"))
        {
            m_poCurFeature = new TABPolyline(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "REGION"))
        {
            m_poCurFeature = new TABRegion(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "ARC"))
        {
            m_poCurFeature = new TABArc(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "TEXT"))
        {
            m_poCurFeature = new TABText(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "RECT") ||
                 STARTS_WITH_CI(pszLine, "ROUNDRECT"))
        {
            m_poCurFeature = new TABRectangle(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "ELLIPSE"))
        {
            m_poCurFeature = new TABEllipse(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "MULTIPOINT"))
        {
            m_poCurFeature = new TABMultiPoint(m_poDefn);
        }
        else if (STARTS_WITH_CI(pszLine, "COLLECTION"))
        {
            m_poCurFeature = new TABCollection(m_poDefn);
        }
        else
        {
            if (!EQUAL(pszLine, ""))
                CPLError(CE_Failure, CPLE_NotSupported,
                         "Error during reading, unknown type %s.", pszLine);

            // m_poCurFeature = new TABDebugFeature(m_poDefn);
            return nullptr;
        }
    }

    CPLAssert(m_poCurFeature);
    if (m_poCurFeature == nullptr)
        return nullptr;

    /*-----------------------------------------------------------------
     * Read fields from the .DAT file
     * GetRecordBlock() has already been called above...
     *----------------------------------------------------------------*/
    if (m_poMIDFile != nullptr &&
        m_poCurFeature->ReadRecordFromMIDFile(m_poMIDFile) != 0)
    {
        CPLError(CE_Failure, CPLE_NotSupported, "Error during reading Record.");

        delete m_poCurFeature;
        m_poCurFeature = nullptr;
        return nullptr;
    }

    /*-----------------------------------------------------------------
     * Read geometry from the .MAP file
     * MoveToObjId() has already been called above...
     *----------------------------------------------------------------*/
    if (m_poCurFeature->ReadGeometryFromMIFFile(m_poMIFFile) != 0)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Error during reading Geometry.");

        delete m_poCurFeature;
        m_poCurFeature = nullptr;
        return nullptr;
    }

    /* If the feature geometry is Text, and the value is empty(""), transform
       it to a geometry none */
    if (m_poCurFeature->GetFeatureClass() == TABFCText)
    {
        TABText *poTextFeature = cpl::down_cast<TABText *>(m_poCurFeature);
        if (strlen(poTextFeature->GetTextString()) == 0)
        {
            TABFeature *poTmpFeature = new TABFeature(m_poDefn);
            for (int i = 0; i < m_poDefn->GetFieldCount(); i++)
            {
                poTmpFeature->SetField(i, m_poCurFeature->GetRawFieldRef(i));
            }
            delete m_poCurFeature;
            m_poCurFeature = poTmpFeature;
        }
    }

    /*---------------------------------------------------------------------
     * The act of reading the geometry causes the first line of the
     * next object to be preloaded.  Set the preloaded id appropriately.
     *--------------------------------------------------------------------- */
    if (m_poMIFFile->GetLastLine() != nullptr)
        m_nPreloadedId++;
    else
        m_nPreloadedId = 0;

    /* Update the Current Feature ID */
    m_poCurFeature->SetFID(m_nCurFeatureId);

    return m_poCurFeature;
}

/**********************************************************************
 *                   MIFFile::CreateFeature()
 *
 * Write a new feature to this dataset. The passed in feature is updated
 * with the new feature id.
 *
 * Returns OGRERR_NONE on success, or an appropriate OGRERR_ code if an
 * error happened in which case, CPLError() will have been called to
 * report the reason of the failure.
 **********************************************************************/
OGRErr MIFFile::CreateFeature(TABFeature *poFeature)
{
    int nFeatureId = -1;

    if (m_eAccessMode != TABWrite)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "CreateFeature() can be used only with Write access.");
        return OGRERR_UNSUPPORTED_OPERATION;
    }

    /*-----------------------------------------------------------------
     * Make sure file is opened and establish new feature id.
     *----------------------------------------------------------------*/
    if (m_poMIDFile == nullptr)
    {
        CPLError(CE_Failure, CPLE_IllegalArg,
                 "CreateFeature() failed: file is not opened!");
        return OGRERR_FAILURE;
    }

    if (m_bHeaderWrote == FALSE)
    {
        /*-------------------------------------------------------------
         * OK, this is the first feature in the dataset... make sure the
         * .MID schema has been initialized.
         *------------------------------------------------------------*/
        if (m_poDefn == nullptr)
            SetFeatureDefn(poFeature->GetDefnRef(), nullptr);

        WriteMIFHeader();
        nFeatureId = 1;
    }
    else
    {
        nFeatureId = ++m_nWriteFeatureId;
    }

    /*-----------------------------------------------------------------
     * Write geometry to the .Mif file
     *----------------------------------------------------------------*/
    if (m_poMIFFile == nullptr ||
        poFeature->WriteGeometryToMIFFile(m_poMIFFile) != 0)
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "Failed writing geometry for feature id %d in %s", nFeatureId,
                 m_pszFname);
        return OGRERR_FAILURE;
    }

    if (m_poMIDFile == nullptr ||
        poFeature->WriteRecordToMIDFile(m_poMIDFile) != 0)
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "Failed writing attributes for feature id %d in %s",
                 nFeatureId, m_pszFname);
        return OGRERR_FAILURE;
    }

    poFeature->SetFID(nFeatureId);

    return OGRERR_NONE;
}

/**********************************************************************
 *                   MIFFile::GetLayerDefn()
 *
 * Returns a reference to the OGRFeatureDefn that will be used to create
 * features in this dataset.
 *
 * Returns a reference to an object that is maintained by this MIFFile
 * object (and thus should not be modified or freed by the caller) or
 * NULL if the OGRFeatureDefn has not been initialized yet (i.e. no file
 * opened yet)
 **********************************************************************/
OGRFeatureDefn *MIFFile::GetLayerDefn()
{
    return m_poDefn;
}

/**********************************************************************
 *                   MIFFile::SetFeatureDefn()
 *
 * Pass a reference to the OGRFeatureDefn that will be used to create
 * features in this dataset.  This function should be called after
 * creating a new dataset, but before writing the first feature.
 * All features that will be written to this dataset must share this same
 * OGRFeatureDefn.
 *
 * This function will use poFeatureDefn to create a local copy that
 * will be used to build the .MID file, etc.
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MIFFile::SetFeatureDefn(
    OGRFeatureDefn *poFeatureDefn,
    TABFieldType *paeMapInfoNativeFieldTypes /* =NULL */)
{
    /*-----------------------------------------------------------------
     * Check that call happens at the right time in dataset's life.
     *----------------------------------------------------------------*/
    if (m_eAccessMode == TABWrite && m_bHeaderWrote)
    {
        CPLError(CE_Failure, CPLE_AssertionFailed,
                 "SetFeatureDefn() must be called after opening a new "
                 "dataset, but before writing the first feature to it.");
        return -1;
    }

    /*-----------------------------------------------------------------
     * Delete current feature defn if there is already one.
     * AddFieldNative() will take care of creating a new one for us.
     *----------------------------------------------------------------*/
    if (m_poDefn && m_poDefn->Dereference() == 0)
        delete m_poDefn;
    m_poDefn = nullptr;

    /*-----------------------------------------------------------------
     * Copy field information
     *----------------------------------------------------------------*/
    const int numFields = poFeatureDefn->GetFieldCount();
    int nStatus = 0;

    for (int iField = 0; iField < numFields; iField++)
    {
        TABFieldType eMapInfoType;
        OGRFieldDefn *poFieldDefn = poFeatureDefn->GetFieldDefn(iField);

        if (paeMapInfoNativeFieldTypes)
        {
            eMapInfoType = paeMapInfoNativeFieldTypes[iField];
        }
        else
        {
            /*---------------------------------------------------------
             * Map OGRFieldTypes to MapInfo native types
             *--------------------------------------------------------*/
            switch (poFieldDefn->GetType())
            {
                case OFTInteger:
                    eMapInfoType = poFieldDefn->GetSubType() == OFSTBoolean
                                       ? TABFLogical
                                       : TABFInteger;
                    break;
                case OFTReal:
                    eMapInfoType = TABFFloat;
                    break;
                case OFTDateTime:
                    eMapInfoType = TABFDateTime;
                    break;
                case OFTDate:
                    eMapInfoType = TABFDate;
                    break;
                case OFTTime:
                    eMapInfoType = TABFTime;
                    break;
                case OFTString:
                default:
                    eMapInfoType = TABFChar;
            }
        }

        nStatus = AddFieldNative(poFieldDefn->GetNameRef(), eMapInfoType,
                                 poFieldDefn->GetWidth(),
                                 poFieldDefn->GetPrecision(), FALSE, FALSE);
    }

    return nStatus;
}

/**********************************************************************
 *                   MIFFile::AddFieldNative()
 *
 * Create a new field using a native mapinfo data type... this is an
 * alternative to defining fields through the OGR interface.
 * This function should be called after creating a new dataset, but before
 * writing the first feature.
 *
 * This function will build/update the OGRFeatureDefn that will have to be
 * used when writing features to this dataset.
 *
 * A reference to the OGRFeatureDefn can be obtained using GetLayerDefn().
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MIFFile::AddFieldNative(const char *pszName, TABFieldType eMapInfoType,
                            int nWidth /*=0*/, int nPrecision /*=0*/,
                            GBool bIndexed /*=FALSE*/, GBool bUnique /*=FALSE*/,
                            int /*bApproxOK*/)
{
    /*-----------------------------------------------------------------
     * Check that call happens at the right time in dataset's life.
     *----------------------------------------------------------------*/
    if (m_eAccessMode == TABWrite && m_bHeaderWrote)
    {
        CPLError(CE_Failure, CPLE_AssertionFailed,
                 "AddFieldNative() must be called after opening a new "
                 "dataset, but before writing the first feature to it.");
        return -1;
    }

    /*-----------------------------------------------------------------
     * Validate field width... must be <= 254
     *----------------------------------------------------------------*/
    if (nWidth > 254)
    {
        CPLError(CE_Warning, CPLE_IllegalArg,
                 "Invalid size (%d) for field '%s'.  "
                 "Size must be 254 or less.",
                 nWidth, pszName);
        nWidth = 254;
    }

    /*-----------------------------------------------------------------
     * Map fields with width=0 (variable length in OGR) to a valid default
     *----------------------------------------------------------------*/
    if (eMapInfoType == TABFDecimal && nWidth == 0)
        nWidth = 20;
    else if (eMapInfoType == TABFChar && nWidth == 0)
        nWidth = 254; /* char fields */

    /*-----------------------------------------------------------------
     * Create new OGRFeatureDefn if not done yet...
     *----------------------------------------------------------------*/
    if (m_poDefn == nullptr)
    {
        char *pszFeatureClassName = TABGetBasename(m_pszFname);
        m_poDefn = new OGRFeatureDefn(pszFeatureClassName);
        CPLFree(pszFeatureClassName);
        // Ref count defaults to 0... set it to 1
        m_poDefn->Reference();
        m_poDefn->Seal(/* bSealFields = */ true);
    }

    CPLString osName(NormalizeFieldName(pszName));

    /*-----------------------------------------------------------------
     * Map MapInfo native types to OGR types
     *----------------------------------------------------------------*/
    OGRFieldDefn *poFieldDefn = nullptr;

    switch (eMapInfoType)
    {
        case TABFChar:
            /*-------------------------------------------------
             * CHAR type
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(), OFTString);
            poFieldDefn->SetWidth(nWidth);
            break;
        case TABFInteger:
            /*-------------------------------------------------
             * INTEGER type
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(), OFTInteger);
            poFieldDefn->SetWidth(nWidth);
            break;
        case TABFSmallInt:
            /*-------------------------------------------------
             * SMALLINT type
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(), OFTInteger);
            poFieldDefn->SetWidth(nWidth);
            break;
        case TABFLargeInt:
            /*-------------------------------------------------
             * LargeInt type
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(), OFTInteger64);
            poFieldDefn->SetWidth(nWidth);
            break;
        case TABFDecimal:
            /*-------------------------------------------------
             * DECIMAL type
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(), OFTReal);
            poFieldDefn->SetWidth(nWidth);
            poFieldDefn->SetPrecision(nPrecision);
            break;
        case TABFFloat:
            /*-------------------------------------------------
             * FLOAT type
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(), OFTReal);
            break;
        case TABFDate:
            /*-------------------------------------------------
             * DATE type (V450, returned as a string: "DD/MM/YYYY" or
             *"YYYYMMDD")
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(),
#ifdef MITAB_USE_OFTDATETIME
                                           OFTDate);
#else
                                           OFTString);
#endif
            poFieldDefn->SetWidth(10);
            m_nVersion = std::max(m_nVersion, 450);
            break;
        case TABFTime:
            /*-------------------------------------------------
             * TIME type (v900, returned as a string: "HH:MM:SS" or "HHMMSSmmm")
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(),
#ifdef MITAB_USE_OFTDATETIME
                                           OFTTime);
#else
                                           OFTString);
#endif
            poFieldDefn->SetWidth(9);
            m_nVersion = std::max(m_nVersion, 900);
            break;
        case TABFDateTime:
            /*-------------------------------------------------
             * DATETIME type (v900, returned as a string: "DD/MM/YYYY HH:MM:SS",
             * "YYYY/MM/DD HH:MM:SS" or "YYYYMMDDHHMMSSmmm")
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(),
#ifdef MITAB_USE_OFTDATETIME
                                           OFTDateTime);
#else
                                           OFTString);
#endif
            poFieldDefn->SetWidth(19);
            m_nVersion = std::max(m_nVersion, 900);
            break;
        case TABFLogical:
            /*-------------------------------------------------
             * LOGICAL type (value "T" or "F")
             *------------------------------------------------*/
            poFieldDefn = new OGRFieldDefn(osName.c_str(), OFTInteger);
            poFieldDefn->SetSubType(OFSTBoolean);
            poFieldDefn->SetWidth(1);
            break;
        default:
            CPLError(CE_Failure, CPLE_NotSupported,
                     "Unsupported type for field %s", pszName);
            return -1;
    }

    /*-----------------------------------------------------
     * Add the FieldDefn to the FeatureDefn
     *----------------------------------------------------*/
    whileUnsealing(m_poDefn)->AddFieldDefn(poFieldDefn);
    m_oSetFields.insert(CPLString(poFieldDefn->GetNameRef()).toupper());
    delete poFieldDefn;

    /*-----------------------------------------------------------------
     * Keep track of native field type
     *----------------------------------------------------------------*/
    m_paeFieldType = static_cast<TABFieldType *>(CPLRealloc(
        m_paeFieldType, m_poDefn->GetFieldCount() * sizeof(TABFieldType)));
    m_paeFieldType[m_poDefn->GetFieldCount() - 1] = eMapInfoType;

    /*-----------------------------------------------------------------
     * Extend array of Indexed/Unique flags
     *----------------------------------------------------------------*/
    m_pabFieldIndexed = static_cast<GBool *>(CPLRealloc(
        m_pabFieldIndexed, m_poDefn->GetFieldCount() * sizeof(GBool)));
    m_pabFieldUnique = static_cast<GBool *>(CPLRealloc(
        m_pabFieldUnique, m_poDefn->GetFieldCount() * sizeof(GBool)));
    m_pabFieldIndexed[m_poDefn->GetFieldCount() - 1] = bIndexed;
    m_pabFieldUnique[m_poDefn->GetFieldCount() - 1] = bUnique;

    return 0;
}

/**********************************************************************
 *                   MIFFile::GetNativeFieldType()
 *
 * Returns the native MapInfo field type for the specified field.
 *
 * Returns TABFUnknown if file is not opened, or if specified field index is
 * invalid.
 **********************************************************************/
TABFieldType MIFFile::GetNativeFieldType(int nFieldId)
{
    if (m_poDefn == nullptr || m_paeFieldType == nullptr || nFieldId < 0 ||
        nFieldId >= m_poDefn->GetFieldCount())
        return TABFUnknown;

    return m_paeFieldType[nFieldId];
}

/************************************************************************
 *                       MIFFile::SetFieldIndexed()
 ************************************************************************/

int MIFFile::SetFieldIndexed(int nFieldId)

{
    if (m_poDefn == nullptr || m_pabFieldIndexed == nullptr || nFieldId < 0 ||
        nFieldId >= m_poDefn->GetFieldCount())
        return -1;

    m_pabFieldIndexed[nFieldId] = TRUE;

    return 0;
}

/************************************************************************
 *                       MIFFile::IsFieldIndexed()
 ************************************************************************/

GBool MIFFile::IsFieldIndexed(int nFieldId)

{
    if (m_poDefn == nullptr || m_pabFieldIndexed == nullptr || nFieldId < 0 ||
        nFieldId >= m_poDefn->GetFieldCount())
        return FALSE;

    return m_pabFieldIndexed[nFieldId];
}

/************************************************************************
 *                       MIFFile::IsFieldUnique()
 ************************************************************************/

GBool MIFFile::IsFieldUnique(int nFieldId)

{
    if (m_poDefn == nullptr || m_pabFieldUnique == nullptr || nFieldId < 0 ||
        nFieldId >= m_poDefn->GetFieldCount())
        return FALSE;

    return m_pabFieldUnique[nFieldId];
}

/************************************************************************/
/*                       MIFFile::SetSpatialRef()                       */
/************************************************************************/

int MIFFile::SetSpatialRef(OGRSpatialReference *poSpatialRef)

{
    CPLFree(m_pszCoordSys);
    m_pszCoordSys = nullptr;

    char *pszCoordSys = MITABSpatialRef2CoordSys(poSpatialRef);
    if (pszCoordSys)
    {
        SetMIFCoordSys(pszCoordSys);
        CPLFree(pszCoordSys);
    }

    return m_pszCoordSys != nullptr;
}

/************************************************************************/
/*                      MIFFile::SetMIFCoordSys()                       */
/************************************************************************/

int MIFFile::SetMIFCoordSys(const char *pszMIFCoordSys)

{
    char *pszCoordSys = nullptr;

    // Extract the word 'COORDSYS' if present
    if (STARTS_WITH_CI(pszMIFCoordSys, "COORDSYS"))
    {
        pszCoordSys = CPLStrdup(pszMIFCoordSys + 9);
    }
    else
    {
        pszCoordSys = CPLStrdup(pszMIFCoordSys);
    }

    // Extract bounds if present
    char **papszFields =
        CSLTokenizeStringComplex(pszCoordSys, " ,()\t", TRUE, FALSE);
    int iBounds = CSLFindString(papszFields, "Bounds");
    if (iBounds >= 0 && iBounds + 4 < CSLCount(papszFields))
    {
        m_dXMin = CPLAtof(papszFields[++iBounds]);
        m_dYMin = CPLAtof(papszFields[++iBounds]);
        m_dXMax = CPLAtof(papszFields[++iBounds]);
        m_dYMax = CPLAtof(papszFields[++iBounds]);
        m_bBoundsSet = TRUE;

        char *pszBounds = strstr(pszCoordSys, " Bounds");
        if (pszBounds == nullptr)
            pszBounds = strstr(pszCoordSys, "Bounds");
        pszCoordSys[pszBounds - pszCoordSys] = '\0';
    }
    CSLDestroy(papszFields);

    // Assign the CoordSys
    CPLFree(m_pszCoordSys);

    m_pszCoordSys = CPLStrdup(pszCoordSys);
    CPLFree(pszCoordSys);

    return m_pszCoordSys != nullptr;
}

int MIFFile::SetCharset(const char *pszCharset)
{
    if (0 != IMapInfoFile::SetCharset(pszCharset))
    {
        return -1;
    }

    if (m_poMIDFile != nullptr)
    {
        m_poMIDFile->SetEncoding(CharsetToEncoding(pszCharset));
    }
    if (m_poMIFFile != nullptr)
    {
        m_poMIFFile->SetEncoding(CharsetToEncoding(pszCharset));
    }
    if (EQUAL(pszCharset, "UTF-8"))
    {
        m_nVersion = std::max(m_nVersion, 1520);
    }
    return 0;
}

void MIFFile::SetStrictLaundering(bool bStrictLaundering)
{
    IMapInfoFile::SetStrictLaundering(bStrictLaundering);
    if (!bStrictLaundering)
    {
        m_nVersion = std::max(m_nVersion, 1520);
    }
}

/************************************************************************/
/*                       MIFFile::GetSpatialRef()                       */
/************************************************************************/

OGRSpatialReference *MIFFile::GetSpatialRef()

{
    if (m_poSpatialRef == nullptr)
        m_poSpatialRef = MITABCoordSys2SpatialRef(m_pszCoordSys);

    return m_poSpatialRef;
}

/**********************************************************************
 *                   MIFFile::UpdateExtents()
 *
 * Private method used to update the dataset extents.
 **********************************************************************/
void MIFFile::UpdateExtents(double dfX, double dfY)
{
    if (m_bExtentsSet == FALSE)
    {
        m_bExtentsSet = TRUE;
        m_sExtents.MinX = m_sExtents.MaxX = dfX;
        m_sExtents.MinY = m_sExtents.MaxY = dfY;
    }
    else
    {
        if (dfX < m_sExtents.MinX)
            m_sExtents.MinX = dfX;
        if (dfX > m_sExtents.MaxX)
            m_sExtents.MaxX = dfX;
        if (dfY < m_sExtents.MinY)
            m_sExtents.MinY = dfY;
        if (dfY > m_sExtents.MaxY)
            m_sExtents.MaxY = dfY;
    }
}

/**********************************************************************
 *                   MIFFile::SetBounds()
 *
 * Set projection coordinates bounds of the newly created dataset.
 *
 * This function must be called after creating a new dataset and before any
 * feature can be written to it.
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MIFFile::SetBounds(double dXMin, double dYMin, double dXMax, double dYMax)
{
    if (m_eAccessMode != TABWrite)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "SetBounds() can be used only with Write access.");
        return -1;
    }

    m_dXMin = dXMin;
    m_dXMax = dXMax;
    m_dYMin = dYMin;
    m_dYMax = dYMax;
    m_bBoundsSet = TRUE;

    return 0;
}

/**********************************************************************
 *                   MIFFile::GetFeatureCountByType()
 *
 * Return number of features of each type.
 *
 * NOTE: The current implementation always returns -1 for MIF files
 *       since this would require scanning the whole file.
 *
 * When properly implemented, the bForce flag will force scanning the
 * whole file by default.
 *
 * Returns 0 on success, or silently returns -1 (with no error) if this
 * information is not available.
 **********************************************************************/
int MIFFile::GetFeatureCountByType(int &numPoints, int &numLines,
                                   int &numRegions, int &numTexts, GBool bForce)
{
    if (m_bPreParsed || bForce)
    {
        PreParseFile();

        numPoints = m_nPoints;
        numLines = m_nLines;
        numRegions = m_nRegions;
        numTexts = m_nTexts;
        return 0;
    }
    else
    {
        numPoints = numLines = numRegions = numTexts = 0;
        return -1;
    }
}

/**********************************************************************
 *                   MIFFile::GetBounds()
 *
 * Fetch projection coordinates bounds of a dataset.
 *
 * Pass bForce=FALSE to avoid a scan of the whole file if the bounds
 * are not already available.
 *
 * Returns 0 on success, -1 on error or if bounds are not available and
 * bForce=FALSE.
 **********************************************************************/
int MIFFile::GetBounds(double &dXMin, double &dYMin, double &dXMax,
                       double &dYMax, GBool bForce /*= TRUE*/)
{
    if (m_bBoundsSet == FALSE && bForce == FALSE)
    {
        return -1;
    }
    else if (m_bBoundsSet == FALSE)
    {
        PreParseFile();
    }

    if (m_bBoundsSet == FALSE)
    {
        return -1;
    }

    dXMin = m_dXMin;
    dXMax = m_dXMax;
    dYMin = m_dYMin;
    dYMax = m_dYMax;

    return 0;
}

/**********************************************************************
 *                   MIFFile::GetExtent()
 *
 * Fetch extent of the data currently stored in the dataset.  We collect
 * this information while preparsing the file ... often already done for
 * other reasons, and if not it is still faster than fully reading all
 * the features just to count them.
 *
 * Returns OGRERR_NONE/OGRRERR_FAILURE.
 **********************************************************************/
OGRErr MIFFile::GetExtent(OGREnvelope *psExtent, int bForce)
{
    if (bForce == TRUE)
        PreParseFile();

    if (m_bPreParsed && m_bExtentsSet)
    {
        *psExtent = m_sExtents;
        return OGRERR_NONE;
    }
    else
        return OGRERR_FAILURE;
}

/************************************************************************/
/*                           TestCapability()                           */
/************************************************************************/

int MIFFile::TestCapability(const char *pszCap)

{
    if (EQUAL(pszCap, OLCRandomRead))
        return TRUE;

    else if (EQUAL(pszCap, OLCSequentialWrite))
        return TRUE;

    else if (EQUAL(pszCap, OLCFastFeatureCount))
        return m_bPreParsed;

    else if (EQUAL(pszCap, OLCFastSpatialFilter))
        return FALSE;

    else if (EQUAL(pszCap, OLCFastGetExtent))
        return m_bPreParsed;

    else if (EQUAL(pszCap, OLCCreateField))
        return TRUE;

    else if (EQUAL(pszCap, OLCStringsAsUTF8))
        return TestUtf8Capability();

    else
        return FALSE;
}
