/******************************************************************************
 *
 * Project:  UK NTF Reader
 * Purpose:  Implements OGRNTFDataSource class
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 1999, Frank Warmerdam
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "ntf.h"
#include "cpl_conv.h"
#include "cpl_string.h"

/************************************************************************/
/*                          OGRNTFDataSource()                          */
/************************************************************************/

OGRNTFDataSource::OGRNTFDataSource()
    : nLayers(0), papoLayers(nullptr), poFCLayer(nullptr), iCurrentFC(0),
      iCurrentReader(-1), nCurrentPos(0), nCurrentFID(0), nNTFFileCount(0),
      papoNTFFileReader(nullptr), nFCCount(0), papszFCNum(nullptr),
      papszFCName(nullptr),
      poSpatialRef(new OGRSpatialReference(
          "PROJCS[\"OSGB 1936 / British National Grid\",GEOGCS[\"OSGB 1936\","
          "DATUM[\"OSGB_1936\",SPHEROID[\"Airy 1830\",6377563.396,299.3249646,"
          "AUTHORITY[\"EPSG\",\"7001\"]],AUTHORITY[\"EPSG\",\"6277\"]],"
          "PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],"
          "UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4277\"]],"
          "PROJECTION[\"Transverse_Mercator\"],"
          "PARAMETER[\"latitude_of_origin\",49],"
          "PARAMETER[\"central_meridian\",-2],"
          "PARAMETER[\"scale_factor\",0.999601272],"
          "PARAMETER[\"false_easting\",400000],"
          "PARAMETER[\"false_northing\",-100000],"
          "UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],"
          "AUTHORITY[\"EPSG\",\"27700\"]]")),
      papszOptions(nullptr)
{
    poSpatialRef->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);

    /* -------------------------------------------------------------------- */
    /*      Allow initialization of options from the environment.           */
    /* -------------------------------------------------------------------- */
    if (getenv("OGR_NTF_OPTIONS") != nullptr)
    {
        papszOptions = CSLTokenizeStringComplex(getenv("OGR_NTF_OPTIONS"), ",",
                                                FALSE, FALSE);
    }
}

/************************************************************************/
/*                         ~OGRNTFDataSource()                          */
/************************************************************************/

OGRNTFDataSource::~OGRNTFDataSource()

{
    for (int i = 0; i < nNTFFileCount; i++)
        delete papoNTFFileReader[i];

    CPLFree(papoNTFFileReader);

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

    if (poFCLayer != nullptr)
        delete poFCLayer;

    CPLFree(papoLayers);

    CSLDestroy(papszOptions);

    CSLDestroy(papszFCNum);
    CSLDestroy(papszFCName);

    if (poSpatialRef)
        poSpatialRef->Release();
}

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

int OGRNTFDataSource::TestCapability(const char *pszCap) const

{
    if (EQUAL(pszCap, ODsCZGeometries))
        return true;

    return false;
}

/************************************************************************/
/*                           GetNamedLayer()                            */
/************************************************************************/

OGRNTFLayer *OGRNTFDataSource::GetNamedLayer(const char *pszNameIn)

{
    for (int i = 0; i < nLayers; i++)
    {
        if (EQUAL(papoLayers[i]->GetLayerDefn()->GetName(), pszNameIn))
            return static_cast<OGRNTFLayer *>(papoLayers[i]);
    }

    return nullptr;
}

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

void OGRNTFDataSource::AddLayer(OGRLayer *poNewLayer)

{
    papoLayers = static_cast<OGRLayer **>(
        CPLRealloc(papoLayers, sizeof(void *) * ++nLayers));

    papoLayers[nLayers - 1] = poNewLayer;
}

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

OGRLayer *OGRNTFDataSource::GetLayer(int iLayer) const

{
    if (iLayer < 0 || iLayer > nLayers)
        return nullptr;
    else if (iLayer == nLayers)
        return poFCLayer;
    else
        return papoLayers[iLayer];
}

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

int OGRNTFDataSource::GetLayerCount() const

{
    if (poFCLayer == nullptr)
        return nLayers;
    else
        return nLayers + 1;
}

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

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

{
    VSIStatBufL stat;
    char **papszFileList = nullptr;

    /* -------------------------------------------------------------------- */
    /*      Is the given path a directory or a regular file?                */
    /* -------------------------------------------------------------------- */
    if (VSIStatL(pszFilename, &stat) != 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, NTF access failed.\n",
                     pszFilename);

        return FALSE;
    }

    /* -------------------------------------------------------------------- */
    /*      Build a list of filenames we figure are NTF files.              */
    /* -------------------------------------------------------------------- */
    if (VSI_ISREG(stat.st_mode))
    {
        papszFileList = CSLAddString(nullptr, pszFilename);
    }
    else
    {
        char **candidateFileList = VSIReadDir(pszFilename);

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

            if (strlen(candidateFileList[i]) > 4 &&
                STARTS_WITH_CI(candidateFileList[i] +
                                   strlen(candidateFileList[i]) - 4,
                               ".ntf"))
            {
                papszFileList =
                    CSLAddString(papszFileList,
                                 CPLFormFilenameSafe(
                                     pszFilename, candidateFileList[i], nullptr)
                                     .c_str());
            }
        }

        CSLDestroy(candidateFileList);

        if (CSLCount(papszFileList) == 0)
        {
            if (!bTestOpen)
                CPLError(CE_Failure, CPLE_OpenFailed,
                         "No candidate NTF files (.ntf) 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 NTF 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.                                     */
    /* -------------------------------------------------------------------- */
    papoNTFFileReader = static_cast<NTFFileReader **>(
        CPLCalloc(sizeof(void *), CSLCount(papszFileList)));

    for (int i = 0; papszFileList != nullptr && papszFileList[i] != nullptr;
         i++)
    {
        if (bTestOpen)
        {
            VSILFILE *fp = VSIFOpenL(papszFileList[i], "rb");
            if (fp == nullptr)
                continue;

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

            VSIFCloseL(fp);

            if (!STARTS_WITH_CI(szHeader, "01"))
                continue;

            int j = 0;  // Used after for.
            for (; j < 80; j++)
            {
                if (szHeader[j] == 10 || szHeader[j] == 13)
                    break;
            }

            if (j == 80 || (j > 0 && szHeader[j - 1] != '%'))
                continue;
        }

        NTFFileReader *poFR = new NTFFileReader(this);

        if (!poFR->Open(papszFileList[i]))
        {
            delete poFR;
            CSLDestroy(papszFileList);

            return FALSE;
        }

        poFR->SetBaseFID(nNTFFileCount * 1000000 + 1);
        poFR->Close();

        EnsureTileNameUnique(poFR);

        papoNTFFileReader[nNTFFileCount++] = poFR;
    }

    CSLDestroy(papszFileList);

    if (nNTFFileCount == 0)
        return FALSE;

    /* -------------------------------------------------------------------- */
    /*      Establish generic layers.                                       */
    /* -------------------------------------------------------------------- */
    EstablishGenericLayers();

    /* -------------------------------------------------------------------- */
    /*      Loop over all the files, collecting a unique feature class      */
    /*      listing.                                                        */
    /* -------------------------------------------------------------------- */
    for (int iSrcFile = 0; iSrcFile < nNTFFileCount; iSrcFile++)
    {
        NTFFileReader *poSrcReader = papoNTFFileReader[iSrcFile];

        for (int iSrcFC = 0; iSrcFC < poSrcReader->GetFCCount(); iSrcFC++)
        {
            char *pszSrcFCName = nullptr;
            char *pszSrcFCNum = nullptr;

            poSrcReader->GetFeatureClass(iSrcFC, &pszSrcFCNum, &pszSrcFCName);

            int iDstFC = 0;
            for (; iDstFC < nFCCount; iDstFC++)
            {
                if (EQUAL(pszSrcFCNum, papszFCNum[iDstFC]))
                    break;
            }

            if (iDstFC >= nFCCount)
            {
                nFCCount++;
                papszFCNum = CSLAddString(papszFCNum, pszSrcFCNum);
                papszFCName = CSLAddString(papszFCName, pszSrcFCName);
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Create a new layer specifically for feature classes.            */
    /* -------------------------------------------------------------------- */
    if (nFCCount > 0)
        poFCLayer = new OGRNTFFeatureClassLayer(this);
    else
        poFCLayer = nullptr;

    return TRUE;
}

/************************************************************************/
/*                            ResetReading()                            */
/*                                                                      */
/*      Cleanup, and start over.                                        */
/************************************************************************/

void OGRNTFDataSource::ResetReading()

{
    for (int i = 0; i < nNTFFileCount; i++)
        papoNTFFileReader[i]->Close();

    iCurrentReader = -1;
    nCurrentPos = (vsi_l_offset)-1;
    nCurrentFID = 1;
    iCurrentFC = 0;
}

/************************************************************************/
/*                           GetNextFeature()                           */
/************************************************************************/

OGRFeature *OGRNTFDataSource::GetNextFeature(OGRLayer **ppoBelongingLayer,
                                             double *pdfProgressPct,
                                             GDALProgressFunc /* pfnProgress */,
                                             void * /* pProgressData */)

{
    if (pdfProgressPct != nullptr)
        *pdfProgressPct = 0.0;
    if (ppoBelongingLayer != nullptr)
        *ppoBelongingLayer = nullptr;

    OGRFeature *poFeature = nullptr;

    /* -------------------------------------------------------------------- */
    /*      If we have already read all the conventional features, we       */
    /*      should try and return feature class features.                   */
    /* -------------------------------------------------------------------- */
    if (iCurrentReader == nNTFFileCount)
    {
        if (iCurrentFC < nFCCount)
            return poFCLayer->GetFeature(iCurrentFC++);
        else
            return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Do we need to open a file?                                      */
    /* -------------------------------------------------------------------- */
    if (iCurrentReader == -1)
    {
        iCurrentReader++;
        nCurrentPos = (vsi_l_offset)-1;
    }

    if (papoNTFFileReader[iCurrentReader]->GetFP() == nullptr)
    {
        papoNTFFileReader[iCurrentReader]->Open();
    }

    /* -------------------------------------------------------------------- */
    /*      Ensure we are reading on from the same point we were reading    */
    /*      from for the last feature, even if some other access            */
    /*      mechanism has moved the file pointer.                           */
    /* -------------------------------------------------------------------- */
    if (nCurrentPos != (vsi_l_offset)-1)
        papoNTFFileReader[iCurrentReader]->SetFPPos(nCurrentPos, nCurrentFID);

    /* -------------------------------------------------------------------- */
    /*      Read a feature.  If we get NULL the file must be all            */
    /*      consumed, advance to the next file.                             */
    /* -------------------------------------------------------------------- */
    poFeature = papoNTFFileReader[iCurrentReader]->ReadOGRFeature();
    if (poFeature == nullptr)
    {
        papoNTFFileReader[iCurrentReader]->Close();
        if (GetOption("CACHING") != nullptr &&
            EQUAL(GetOption("CACHING"), "OFF"))
            papoNTFFileReader[iCurrentReader]->DestroyIndex();

        iCurrentReader++;
        nCurrentPos = (vsi_l_offset)-1;
        nCurrentFID = 1;

        poFeature = GetNextFeature(nullptr, nullptr, nullptr, nullptr);
    }
    else
    {
        papoNTFFileReader[iCurrentReader]->GetFPPos(&nCurrentPos, &nCurrentFID);
    }

    return poFeature;
}

/************************************************************************/
/*                          GetFeatureClass()                           */
/************************************************************************/

int OGRNTFDataSource::GetFeatureClass(int iFCIndex, char **ppszFCId,
                                      char **ppszFCName)

{
    if (iFCIndex < 0 || iFCIndex >= nFCCount)
    {
        *ppszFCId = nullptr;
        *ppszFCName = nullptr;
        return FALSE;
    }
    else
    {
        *ppszFCId = papszFCNum[iFCIndex];
        *ppszFCName = papszFCName[iFCIndex];
        return TRUE;
    }
}

/************************************************************************/
/*                             SetOptions()                             */
/************************************************************************/

void OGRNTFDataSource::SetOptionList(char **papszNewOptions)

{
    CSLDestroy(papszOptions);
    papszOptions = CSLDuplicate(papszNewOptions);
}

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

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

{
    return CSLFetchNameValue(papszOptions, pszOption);
}

/************************************************************************/
/*                        EnsureTileNameUnique()                        */
/*                                                                      */
/*      This method is called with an NTFFileReader to ensure that      */
/*      its tilename is unique relative to all the readers already      */
/*      assigned to this data source.  If not, a unique name is         */
/*      selected for it and assigned.  This method should not be        */
/*      called with readers that are already attached to the data      */
/*      source.                                                         */
/************************************************************************/

void OGRNTFDataSource::EnsureTileNameUnique(NTFFileReader *poNewReader)

{
    int iSequenceNumber = -1;
    bool bIsUnique = false;
    char szCandidateName[12] = {};

    do
    {
        bIsUnique = TRUE;
        if (iSequenceNumber++ == -1)
            strncpy(szCandidateName, poNewReader->GetTileName(),
                    sizeof(szCandidateName) - 1);
        else
            snprintf(szCandidateName, sizeof(szCandidateName), "%010d",
                     iSequenceNumber);

        for (int iReader = 0; iReader < nNTFFileCount && bIsUnique; iReader++)
        {
            const char *pszTileName = GetFileReader(iReader)->GetTileName();
            if (pszTileName != nullptr &&
                strcmp(szCandidateName, pszTileName) == 0)
            {
                bIsUnique = FALSE;
            }
        }
    } while (!bIsUnique);

    if (iSequenceNumber > 0)
    {
        poNewReader->OverrideTileName(szCandidateName);
        CPLError(CE_Warning, CPLE_AppDefined,
                 "Forcing TILE_REF to `%s' on file %s\n"
                 "to avoid conflict with other tiles in this data source.",
                 szCandidateName, poNewReader->GetFilename());
    }
}
