/******************************************************************************
 *
 * Project:  TIGER/Line Translator
 * Purpose:  Implements OGRTigerDataSource class
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 1999, Frank Warmerdam <warmerdam@pobox.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_conv.h"
#include "cpl_string.h"
#include "ogr_tiger.h"

#include <cctype>
#include <algorithm>

#define DIGIT_ZERO '0'

/************************************************************************/
/*                        TigerClassifyVersion()                        */
/************************************************************************/

TigerVersion TigerClassifyVersion(int nVersionCode)

{
    TigerVersion nVersion;
    int nYear, nMonth;

    /*
    ** TIGER Versions
    **
    ** 0000           TIGER/Line Precensus Files, 1990
    ** 0002           TIGER/Line Initial Voting District Codes Files, 1990
    ** 0003           TIGER/Line Files, 1990
    ** 0005           TIGER/Line Files, 1992
    ** 0021           TIGER/Line Files, 1994
    ** 0024           TIGER/Line Files, 1995
    ** 9706 to 9810   TIGER/Line Files, 1997
    ** 9812 to 9904   TIGER/Line Files, 1998
    ** 0006 to 0008   TIGER/Line Files, 1999
    ** 0010 to 0011   TIGER/Line Files, Redistricting Census 2000
    ** 0103 to 0108   TIGER/Line Files, Census 2000
    **
    ** 0203 to 0205   TIGER/Line Files, UA 2000
    ** ????    ????
    **
    ** 0206 to 0299   TIGER/Line Files, 2002
    ** 0300 to 0399   TIGER/Line Files, 2003
    ** 0400+          TIGER/Line Files, 2004 - one sample is 0405
    ** ????
    */

    nVersion = TIGER_Unknown;
    if (nVersionCode == 0)
        nVersion = TIGER_1990_Precensus;
    else if (nVersionCode == 2)
        nVersion = TIGER_1990;
    else if (nVersionCode == 3)
        nVersion = TIGER_1992;
    else if (nVersionCode == 5)
        nVersion = TIGER_1994;
    else if (nVersionCode == 21)
        nVersion = TIGER_1994;
    else if (nVersionCode == 24)
        nVersion = TIGER_1995;

    else if (nVersionCode == 9999) /* special hack, fme bug 7625 */
        nVersion = TIGER_UA2000;

    nYear = nVersionCode % 100;
    nMonth = nVersionCode / 100;

    nVersionCode = nYear * 100 + nMonth;

    if (nVersion != TIGER_Unknown)
        /* do nothing */;
    else if (nVersionCode >= 9706 && nVersionCode <= 9810)
        nVersion = TIGER_1997;
    else if (nVersionCode >= 9812 && nVersionCode <= 9904)
        nVersion = TIGER_1998;
    else if (nVersionCode >= 6 /*0006*/ && nVersionCode <= 8 /*0008*/)
        nVersion = TIGER_1999;
    else if (nVersionCode >= 10 /*0010*/ && nVersionCode <= 11 /*0011*/)
        nVersion = TIGER_2000_Redistricting;
    else if (nVersionCode >= 103 /*0103*/ && nVersionCode <= 108 /*0108*/)
        nVersion = TIGER_2000_Census;
    else if (nVersionCode >= 203 /*0302*/ && nVersionCode <= 205 /*0502*/)
        nVersion = TIGER_UA2000;
    else if (nVersionCode >= 210 /*1002*/ && nVersionCode <= 306 /*0603*/)
        nVersion = TIGER_2002;
    else if (nVersionCode >= 312 /*1203*/ && nVersionCode <= 403 /*0304*/)
        nVersion = TIGER_2003;
    else if (nVersionCode >= 404)
        nVersion = TIGER_2004;

    return nVersion;
}

/************************************************************************/
/*                         TigerVersionString()                         */
/************************************************************************/

const char *TigerVersionString(TigerVersion nVersion)
{

    if (nVersion == TIGER_1990_Precensus)
    {
        return "TIGER_1990_Precensus";
    }
    if (nVersion == TIGER_1990)
    {
        return "TIGER_1990";
    }
    if (nVersion == TIGER_1992)
    {
        return "TIGER_1992";
    }
    if (nVersion == TIGER_1994)
    {
        return "TIGER_1994";
    }
    if (nVersion == TIGER_1995)
    {
        return "TIGER_1995";
    }
    if (nVersion == TIGER_1997)
    {
        return "TIGER_1997";
    }
    if (nVersion == TIGER_1998)
    {
        return "TIGER_1998";
    }
    if (nVersion == TIGER_1999)
    {
        return "TIGER_1999";
    }
    if (nVersion == TIGER_2000_Redistricting)
    {
        return "TIGER_2000_Redistricting";
    }
    if (nVersion == TIGER_UA2000)
    {
        return "TIGER_UA2000";
    }
    if (nVersion == TIGER_2002)
    {
        return "TIGER_2002";
    }
    if (nVersion == TIGER_2003)
    {
        return "TIGER_2003";
    }
    if (nVersion == TIGER_2004)
    {
        return "TIGER_2004";
    }
    if (nVersion == TIGER_Unknown)
    {
        return "TIGER_Unknown";
    }
    return "???";
}

/************************************************************************/
/*                         TigerCheckVersion()                          */
/*                                                                      */
/*      Some tiger products seem to be generated with version info      */
/*      that doesn't match the tiger specs.  We can sometimes           */
/*      recognise the wrongness by checking the record length of        */
/*      some well known changing files and adjusting the version        */
/*      based on this.                                                  */
/************************************************************************/

TigerVersion OGRTigerDataSource::TigerCheckVersion(TigerVersion nOldVersion,
                                                   const char *pszFilename)

{
    if (nOldVersion != TIGER_2002)
        return nOldVersion;

    char *pszRTCFilename = BuildFilename(pszFilename, "C");
    VSILFILE *fp = VSIFOpenL(pszRTCFilename, "rb");
    CPLFree(pszRTCFilename);

    if (fp == nullptr)
        return nOldVersion;

    char szHeader[115];

    if (VSIFReadL(szHeader, sizeof(szHeader) - 1, 1, fp) < 1)
    {
        VSIFCloseL(fp);
        return nOldVersion;
    }

    VSIFCloseL(fp);

    /* -------------------------------------------------------------------- */
    /*      Is the record length 112?  If so, it is an older version        */
    /*      than 2002.                                                      */
    /* -------------------------------------------------------------------- */
    if (szHeader[112] == 10 || szHeader[112] == 13)
    {
        CPLDebug("TIGER",
                 "Forcing version back to UA2000 since RTC records are short.");
        return TIGER_UA2000;
    }

    return nOldVersion;
}

/************************************************************************/
/*                         OGRTigerDataSource()                         */
/************************************************************************/

OGRTigerDataSource::OGRTigerDataSource()
    : nLayers(0), papoLayers(nullptr), poSpatialRef(new OGRSpatialReference()),
      papszOptions(nullptr), pszPath(nullptr), nModules(0),
      papszModules(nullptr), nVersionCode(0), nVersion(TIGER_Unknown)
{
    poSpatialRef->SetWellKnownGeogCS("NAD83");
    poSpatialRef->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
}

/************************************************************************/
/*                        ~OGRTigerDataSource()                         */
/************************************************************************/

OGRTigerDataSource::~OGRTigerDataSource()

{
    for (int i = 0; i < nLayers; i++)
        delete papoLayers[i];

    CPLFree(papoLayers);

    CPLFree(pszPath);

    CSLDestroy(papszOptions);

    CSLDestroy(papszModules);

    delete poSpatialRef;
}

/************************************************************************/
/*                              AddLayer()                              */
/************************************************************************/

void OGRTigerDataSource::AddLayer(OGRTigerLayer *poNewLayer)

{
    poNewLayer->SetDescription(poNewLayer->GetName());
    papoLayers = static_cast<OGRTigerLayer **>(
        CPLRealloc(papoLayers, sizeof(void *) * ++nLayers));

    papoLayers[nLayers - 1] = poNewLayer;
}

/************************************************************************/
/*                              GetLayer()                              */
/************************************************************************/

OGRLayer *OGRTigerDataSource::GetLayer(int iLayer)

{
    if (iLayer < 0 || iLayer >= nLayers)
        return nullptr;

    return papoLayers[iLayer];
}

/************************************************************************/
/*                              GetLayer()                              */
/************************************************************************/

OGRLayer *OGRTigerDataSource::GetLayer(const char *pszLayerName)

{
    for (int iLayer = 0; iLayer < nLayers; iLayer++)
    {
        if (EQUAL(papoLayers[iLayer]->GetLayerDefn()->GetName(), pszLayerName))
            return papoLayers[iLayer];
    }

    return nullptr;
}

/************************************************************************/
/*                           GetLayerCount()                            */
/************************************************************************/

int OGRTigerDataSource::GetLayerCount()

{
    return nLayers;
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

int OGRTigerDataSource::Open(const char *pszFilename, int bTestOpen,
                             char **papszLimitedFileList)

{
    /* -------------------------------------------------------------------- */
    /*      Is the given path a directory or a regular file?                */
    /* -------------------------------------------------------------------- */
    VSIStatBufL stat;

    if (VSIStatExL(pszFilename, &stat,
                   VSI_STAT_EXISTS_FLAG | VSI_STAT_NATURE_FLAG) != 0 ||
        (!VSI_ISDIR(stat.st_mode) && !VSI_ISREG(stat.st_mode)))
    {
        if (!bTestOpen)
            CPLError(
                CE_Failure, CPLE_AppDefined,
                "%s is neither a file or directory, Tiger access failed.\n",
                pszFilename);

        return FALSE;
    }

    /* -------------------------------------------------------------------- */
    /*      Build a list of filenames we figure are Tiger files.            */
    /* -------------------------------------------------------------------- */
    char **papszFileList = nullptr;
    if (VSI_ISREG(stat.st_mode))
    {
        char szModule[128];

        if (strlen(CPLGetFilename(pszFilename)) == 0)
        {
            return FALSE;
        }

        pszPath = CPLStrdup(CPLGetPath(pszFilename));

        strncpy(szModule, CPLGetFilename(pszFilename), sizeof(szModule) - 1);
        /* Make sure the buffer is 0 terminated */
        szModule[sizeof(szModule) - 1] = '\0';

        /* And now remove last character of filename */
        szModule[strlen(szModule) - 1] = '\0';

        papszFileList = CSLAddString(papszFileList, szModule);
    }
    else
    {
        char **candidateFileList = VSIReadDir(pszFilename);

        pszPath = CPLStrdup(pszFilename);

        for (int i = 0;
             candidateFileList != nullptr && candidateFileList[i] != nullptr;
             i++)
        {
            size_t nCandidateLen = strlen(candidateFileList[i]);

            if (papszLimitedFileList != nullptr &&
                CSLFindString(papszLimitedFileList,
                              CPLGetBasename(candidateFileList[i])) == -1)
            {
                continue;
            }

            if (nCandidateLen > 4 &&
                candidateFileList[i][nCandidateLen - 4] == '.' &&
                candidateFileList[i][nCandidateLen - 1] == '1')
            {
                char szModule[128];

                snprintf(szModule, sizeof(szModule), "%s",
                         candidateFileList[i]);
                const size_t nLen = strlen(szModule);
                if (nLen)
                    szModule[nLen - 1] = '\0';

                papszFileList = CSLAddString(papszFileList, szModule);
            }
        }

        CSLDestroy(candidateFileList);

        if (CSLCount(papszFileList) == 0)
        {
            if (!bTestOpen)
                CPLError(CE_Failure, CPLE_OpenFailed,
                         "No candidate Tiger files (TGR*.RT1) found in\n"
                         "directory: %s",
                         pszFilename);
            CSLDestroy(papszFileList);
            return FALSE;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Loop over all these files trying to open them.  In testopen     */
    /*      mode we first read the first 80 characters, to verify that      */
    /*      it looks like an Tiger file.  Note that we don't keep the file  */
    /*      open ... we don't want to occupy a lot of file handles when      */
    /*      handling a whole directory.                                     */
    /* -------------------------------------------------------------------- */
    papszModules = nullptr;

    for (int i = 0; papszFileList && papszFileList[i] != nullptr; i++)
    {
        if (bTestOpen || i == 0)
        {
            char *l_pszFilename = BuildFilename(papszFileList[i], "1");

            VSILFILE *fp = VSIFOpenL(l_pszFilename, "rb");
            CPLFree(l_pszFilename);

            if (fp == nullptr)
                continue;

            char szHeader[500] = {};
            if (VSIFReadL(szHeader, sizeof(szHeader) - 1, 1, fp) < 1)
            {
                VSIFCloseL(fp);
                continue;
            }

            VSIFCloseL(fp);

            char *pszRecStart = szHeader;
            szHeader[sizeof(szHeader) - 1] = '\0';

            bool bIsGDT = false;

            if (STARTS_WITH_CI(pszRecStart, "Copyright (C)") &&
                strstr(pszRecStart, "Geographic Data Tech") != nullptr)
            {
                bIsGDT = true;

                while (*pszRecStart != '\0' && *pszRecStart != 10 &&
                       *pszRecStart != 13)
                    pszRecStart++;

                while (*pszRecStart == 10 || *pszRecStart == 13)
                    pszRecStart++;
            }

            if (pszRecStart[0] != '1')
                continue;

            if (!isdigit(static_cast<unsigned char>(pszRecStart[1])) ||
                !isdigit(static_cast<unsigned char>(pszRecStart[2])) ||
                !isdigit(static_cast<unsigned char>(pszRecStart[3])) ||
                !isdigit(static_cast<unsigned char>(pszRecStart[4])))
                continue;

            nVersionCode = atoi(TigerFileBase::GetField(pszRecStart, 2, 5));
            nVersion = TigerClassifyVersion(nVersionCode);
            nVersion = TigerCheckVersion(nVersion, papszFileList[i]);

            CPLDebug("OGR", "Tiger Version Code=%d, Classified as %s ",
                     nVersionCode, TigerVersionString(nVersion));

            if (nVersionCode != 0 && nVersionCode != 2 && nVersionCode != 3 &&
                nVersionCode != 5 && nVersionCode != 21 && nVersionCode != 24 &&
                pszRecStart[3] != '9' && pszRecStart[3] != DIGIT_ZERO &&
                !bIsGDT)
                continue;

            // we could (and should) add a bunch more validation here.
        }

        papszModules = CSLAddString(papszModules, papszFileList[i]);
    }

    CSLDestroy(papszFileList);

    nModules = CSLCount(papszModules);

    if (nModules == 0 || papszModules == nullptr)
    {
        if (!bTestOpen)
        {
            if (VSI_ISREG(stat.st_mode))
                CPLError(CE_Failure, CPLE_OpenFailed,
                         "No TIGER/Line files (TGR*.RT1) found in\n"
                         "directory: %s",
                         pszFilename);
            else
                CPLError(
                    CE_Failure, CPLE_OpenFailed,
                    "File %s does not appear to be a TIGER/Line .RT1 file.",
                    pszFilename);
        }

        return FALSE;
    }

    /* -------------------------------------------------------------------- */
    /*      Do we have a user provided version override?                    */
    /* -------------------------------------------------------------------- */
    const char *pszRequestedVersion =
        CPLGetConfigOption("TIGER_VERSION", nullptr);
    if (pszRequestedVersion != nullptr)
    {

        if (STARTS_WITH_CI(pszRequestedVersion, "TIGER_"))
        {
            int iCode = 1;  // Used after for.

            for (; iCode < TIGER_Unknown; iCode++)
            {
                if (EQUAL(TigerVersionString((TigerVersion)iCode),
                          pszRequestedVersion))
                {
                    nVersion = (TigerVersion)iCode;
                    break;
                }
            }

            if (iCode == TIGER_Unknown)
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "Failed to recognise TIGER_VERSION setting: %s",
                         pszRequestedVersion);
                return FALSE;
            }

            CPLDebug("OGR", "OVERRIDE Tiger Version %s ",
                     TigerVersionString(nVersion));
        }
        else
        {
            nVersionCode = atoi(pszRequestedVersion);
            nVersion = TigerClassifyVersion(nVersionCode);

            CPLDebug("OGR", "OVERRIDE Tiger Version Code=%d, Classified as %s ",
                     nVersionCode, TigerVersionString(nVersion));
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Create the layers which appear to exist.                        */
    /* -------------------------------------------------------------------- */
    // RT1, RT2, RT3
    AddLayer(
        new OGRTigerLayer(this, new TigerCompleteChain(this, papszModules[0])));

    /* should we have kept track of whether we encountered an RT4 file? */
    // RT4
    AddLayer(new OGRTigerLayer(this, new TigerAltName(this, papszModules[0])));

    // RT5
    AddLayer(
        new OGRTigerLayer(this, new TigerFeatureIds(this, papszModules[0])));

    // RT6
    AddLayer(new OGRTigerLayer(this, new TigerZipCodes(this, papszModules[0])));
    // RT7
    AddLayer(
        new OGRTigerLayer(this, new TigerLandmarks(this, papszModules[0])));

    // RT8
    AddLayer(
        new OGRTigerLayer(this, new TigerAreaLandmarks(this, papszModules[0])));

    // RT9
    if (nVersion < TIGER_2002)
    {
        AddLayer(new OGRTigerLayer(
            this, new TigerKeyFeatures(this, papszModules[0])));
    }

    // RTA, RTS
    AddLayer(new OGRTigerLayer(this, new TigerPolygon(this, papszModules[0])));

    // RTB
    if (nVersion >= TIGER_2002)
    {
        AddLayer(new OGRTigerLayer(
            this, new TigerPolygonCorrections(this, papszModules[0])));
    }

    // RTC
    AddLayer(
        new OGRTigerLayer(this, new TigerEntityNames(this, papszModules[0])));

    // RTE
    if (nVersion >= TIGER_2002)
    {
        AddLayer(new OGRTigerLayer(
            this, new TigerPolygonEconomic(this, papszModules[0])));
    }

    // RTH
    AddLayer(
        new OGRTigerLayer(this, new TigerIDHistory(this, papszModules[0])));

    // RTI
    AddLayer(
        new OGRTigerLayer(this, new TigerPolyChainLink(this, papszModules[0])));

    // RTM
    AddLayer(new OGRTigerLayer(
        this, new TigerSpatialMetadata(this, papszModules[0])));

    // RTP
    AddLayer(new OGRTigerLayer(this, new TigerPIP(this, papszModules[0])));

    // RTR
    AddLayer(
        new OGRTigerLayer(this, new TigerTLIDRange(this, papszModules[0])));

    // RTT
    if (nVersion >= TIGER_2002)
    {
        AddLayer(new OGRTigerLayer(this,
                                   new TigerZeroCellID(this, papszModules[0])));
    }

    // RTU
    if (nVersion >= TIGER_2002)
    {
        AddLayer(
            new OGRTigerLayer(this, new TigerOverUnder(this, papszModules[0])));
    }

    // RTZ
    AddLayer(new OGRTigerLayer(this, new TigerZipPlus4(this, papszModules[0])));

    return TRUE;
}

/************************************************************************/
/*                             GetOption()                              */
/************************************************************************/

const char *OGRTigerDataSource::GetOption(const char *pszOption)

{
    return CSLFetchNameValue(papszOptions, pszOption);
}

/************************************************************************/
/*                             GetModule()                              */
/************************************************************************/

const char *OGRTigerDataSource::GetModule(int iModule)

{
    if (iModule < 0 || iModule >= nModules)
        return nullptr;
    else
        return papszModules[iModule];
}

/************************************************************************/
/*                            CheckModule()                             */
/*                                                                      */
/*      This is used by the writer to check if this module has been     */
/*      written to before.                                              */
/************************************************************************/

bool OGRTigerDataSource::CheckModule(const char *pszModule)

{
    for (int i = 0; i < nModules; i++)
    {
        if (EQUAL(pszModule, papszModules[i]))
            return true;
    }
    return false;
}

/************************************************************************/
/*                             AddModule()                              */
/************************************************************************/

void OGRTigerDataSource::AddModule(const char *pszModule)

{
    if (CheckModule(pszModule))
        return;

    papszModules = CSLAddString(papszModules, pszModule);
    nModules++;
}

/************************************************************************/
/*                           BuildFilename()                            */
/************************************************************************/

char *OGRTigerDataSource::BuildFilename(const char *pszModuleName,
                                        const char *pszExtension)

{
    /* -------------------------------------------------------------------- */
    /*      Force the record type to lower case if the filename appears     */
    /*      to be in lower case.                                            */
    /* -------------------------------------------------------------------- */
    char szLCExtension[3] = {};
    if (*pszExtension >= 'A' && *pszExtension <= 'Z' && *pszModuleName == 't')
    {
        szLCExtension[0] = (*pszExtension) + 'a' - 'A';
        szLCExtension[1] = '\0';
        pszExtension = szLCExtension;
    }

    /* -------------------------------------------------------------------- */
    /*      Build the filename.                                             */
    /* -------------------------------------------------------------------- */
    const size_t nFilenameLen = strlen(GetDirPath()) + strlen(pszModuleName) +
                                strlen(pszExtension) + 10;
    char *pszFilename = (char *)CPLMalloc(nFilenameLen);

    if (strlen(GetDirPath()) == 0)
        snprintf(pszFilename, nFilenameLen, "%s%s", pszModuleName,
                 pszExtension);
    else
        snprintf(pszFilename, nFilenameLen, "%s/%s%s", GetDirPath(),
                 pszModuleName, pszExtension);

    return pszFilename;
}
