/******************************************************************************
 *
 * Project:  Interlis 1 Reader
 * Purpose:  Implementation of ILI1Reader class.
 * Author:   Pirmin Kalberer, Sourcepole AG
 *
 ******************************************************************************
 * Copyright (c) 2004, Pirmin Kalberer, Sourcepole AG
 * Copyright (c) 2008-2013, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_conv.h"
#include "cpl_string.h"
#include "ogr_api.h"
#include "ogr_ili1.h"
#include "ogr_geos.h"

#include "ili1reader.h"
#include "ili1readerp.h"

#include <vector>

#ifdef HAVE_GEOS
#define POLYGONIZE_AREAS
#endif

#ifndef POLYGONIZE_AREAS
#if defined(__GNUC_PREREQ)
// #    warning Interlis 1 Area polygonizing disabled. Needs GEOS >= 3.1.0
#endif
#endif

//
// ILI1Reader
//
IILI1Reader::~IILI1Reader()
{
}

ILI1Reader::ILI1Reader()
    : fpItf(nullptr), nLayers(0), papoLayers(nullptr), curLayer(nullptr),
      codeBlank('_'), codeUndefined('@'), codeContinue('\\')
{
}

ILI1Reader::~ILI1Reader()
{
    if (fpItf)
        VSIFCloseL(fpItf);

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

/* -------------------------------------------------------------------- */
/*      Open the source file.                                           */
/* -------------------------------------------------------------------- */
int ILI1Reader::OpenFile(const char *pszFilename)
{
    fpItf = VSIFOpenL(pszFilename, "r");
    if (fpItf == nullptr)
    {
        CPLError(CE_Failure, CPLE_OpenFailed, "Failed to open ILI file `%s'.",
                 pszFilename);

        return FALSE;
    }
    return TRUE;
}

const char *ILI1Reader::GetLayerNameString(const char *topicname,
                                           const char *tablename)
{

    return CPLSPrintf("%s__%s", topicname, tablename);
}

int ILI1Reader::ReadModel(ImdReader *poImdReader, const char *pszModelFilename,
                          OGRILI1DataSource *poDS)
{

    poImdReader->ReadModel(pszModelFilename);
    for (FeatureDefnInfos::const_iterator it =
             poImdReader->featureDefnInfos.begin();
         it != poImdReader->featureDefnInfos.end(); ++it)
    {
#if DEBUG_VERBOSE
        CPLDebug("OGR_ILI", "Adding OGRILI1Layer with table '%s'",
                 it->GetTableDefnRef()->GetName());
#endif
        OGRILI1Layer *layer =
            new OGRILI1Layer(it->GetTableDefnRef(), it->poGeomFieldInfos, poDS);
        AddLayer(layer);
        // Create additional layers for surface and area geometries.
        for (GeomFieldInfos::const_iterator it2 = it->poGeomFieldInfos.begin();
             it2 != it->poGeomFieldInfos.end(); ++it2)
        {
            if (it2->second.GetGeomTableDefnRef())
            {
                OGRFeatureDefn *poGeomTableDefn =
                    it2->second.GetGeomTableDefnRef();
                OGRGeomFieldDefn *poOGRGeomFieldDefn =
                    poGeomTableDefn->GetGeomFieldDefn(0);
                GeomFieldInfos oGeomFieldInfos;
                // We add iliGeomType to recognize Ili1 geom tables
                oGeomFieldInfos[poOGRGeomFieldDefn->GetNameRef()].iliGeomType =
                    it2->second.iliGeomType;
#if DEBUG_VERBOSE
                CPLDebug("OGR_ILI",
                         "Adding OGRILI1Layer with geometry table '%s'",
                         poGeomTableDefn->GetName());
#endif
                OGRILI1Layer *geomlayer =
                    new OGRILI1Layer(poGeomTableDefn, oGeomFieldInfos, poDS);
                AddLayer(geomlayer);
            }
        }
    }

    codeBlank = poImdReader->codeBlank;
    CPLDebug("OGR_ILI", "Ili1Format blankCode '%c'", poImdReader->codeBlank);
    codeUndefined = poImdReader->codeUndefined;
    CPLDebug("OGR_ILI", "Ili1Format undefinedCode '%c'",
             poImdReader->codeUndefined);
    codeContinue = poImdReader->codeContinue;
    CPLDebug("OGR_ILI", "Ili1Format continueCode '%c'",
             poImdReader->codeContinue);
    return 0;
}

int ILI1Reader::ReadFeatures()
{
    char **tokens = nullptr;
    const char *pszLine = nullptr;
    char *topic = CPLStrdup("(null)");
    int ret = TRUE;

    while (ret && (tokens = ReadParseLine()) != nullptr)
    {
        const char *firsttok = tokens[0];
        if (EQUAL(firsttok, "SCNT"))
        {
            // read description
            do
            {
                pszLine = CPLReadLineL(fpItf);
            } while (pszLine && !STARTS_WITH_CI(pszLine, "////"));
            ret = (pszLine != nullptr);
        }
        else if (EQUAL(firsttok, "MOTR"))
        {
            // read model
            do
            {
                pszLine = CPLReadLineL(fpItf);
            } while (pszLine && !STARTS_WITH_CI(pszLine, "////"));
            ret = (pszLine != nullptr);
        }
        else if (EQUAL(firsttok, "MTID"))
        {
        }
        else if (EQUAL(firsttok, "MODL"))
        {
        }
        else if (EQUAL(firsttok, "TOPI") && CSLCount(tokens) >= 2)
        {
            CPLFree(topic);
            topic = CPLStrdup(CSLGetField(tokens, 1));
        }
        else if (EQUAL(firsttok, "TABL") && CSLCount(tokens) >= 2)
        {
            const char *layername =
                GetLayerNameString(topic, CSLGetField(tokens, 1));
            CPLDebug("OGR_ILI", "Reading table '%s'", layername);
            curLayer = GetLayerByName(layername);

            if (curLayer == nullptr)
            {  // create one
                CPLError(CE_Warning, CPLE_AppDefined,
                         "No model definition for table '%s' found, "
                         "using default field names.",
                         layername);
                OGRFeatureDefn *poFeatureDefn = new OGRFeatureDefn(
                    GetLayerNameString(topic, CSLGetField(tokens, 1)));
                poFeatureDefn->SetGeomType(wkbUnknown);
                GeomFieldInfos oGeomFieldInfos;
                curLayer =
                    new OGRILI1Layer(poFeatureDefn, oGeomFieldInfos, nullptr);
                AddLayer(curLayer);
            }
            if (curLayer != nullptr)
            {
                for (int i = 0; i < curLayer->GetLayerDefn()->GetFieldCount();
                     i++)
                {
                    CPLDebug("OGR_ILI", "Field %d: %s", i,
                             curLayer->GetLayerDefn()
                                 ->GetFieldDefn(i)
                                 ->GetNameRef());
                }
            }
            ret = ReadTable(layername);
        }
        else if (EQUAL(firsttok, "ETOP"))
        {
        }
        else if (EQUAL(firsttok, "EMOD"))
        {
        }
        else if (EQUAL(firsttok, "ENDE"))
        {
            CSLDestroy(tokens);
            CPLFree(topic);
            return TRUE;
        }
        else
        {
            CPLError(CE_Warning, CPLE_AppDefined, "Unexpected token: %s",
                     firsttok);
        }

        CSLDestroy(tokens);
        tokens = nullptr;
    }

    CSLDestroy(tokens);
    CPLFree(topic);

    return ret;
}

int ILI1Reader::ReadTable(CPL_UNUSED const char *layername)
{
    char **tokens = nullptr;
    int warned = FALSE;
    int geomIdx = -1;

    OGRFeatureDefn *featureDef = curLayer->GetLayerDefn();
    OGRFeature *feature = nullptr;
    bool bFeatureAdded = false;

    while ((tokens = ReadParseLine()) != nullptr)
    {
        const char *firsttok = CSLGetField(tokens, 0);
        if (EQUAL(firsttok, "OBJE"))
        {
            if (featureDef->GetFieldCount() == 0 &&
                curLayer->GetFeatureCount() == 0)
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "No field definition found for table: %s",
                         featureDef->GetName());
                // Model not read - use heuristics.
                for (int fIndex = 1; tokens[fIndex] != nullptr; fIndex++)
                {
                    char szFieldName[32];
                    snprintf(szFieldName, sizeof(szFieldName), "Field%02d",
                             fIndex);
                    OGRFieldDefn oFieldDefn(szFieldName, OFTString);
                    featureDef->AddFieldDefn(&oFieldDefn);
                }
            }
            // start new feature
            if (!bFeatureAdded)
                delete feature;
            feature = new OGRFeature(featureDef);

            for (int fIndex = 1, fieldno = 0;
                 tokens[fIndex] != nullptr &&
                 fieldno < featureDef->GetFieldCount();
                 fIndex++, fieldno++)
            {
                if (!(tokens[fIndex][0] == codeUndefined &&
                      tokens[fIndex][1] == '\0'))
                {
#ifdef DEBUG_VERBOSE
                    CPLDebug("READ TABLE OGR_ILI",
                             "Setting Field %d (Type %d): %s", fieldno,
                             featureDef->GetFieldDefn(fieldno)->GetType(),
                             tokens[fIndex]);
#endif
                    if (featureDef->GetFieldDefn(fieldno)->GetType() ==
                        OFTString)
                    {
                        // Interlis 1 encoding is ISO 8859-1 (Latin1) -> Recode
                        // to UTF-8
                        char *pszRecoded = CPLRecode(
                            tokens[fIndex], CPL_ENC_ISO8859_1, CPL_ENC_UTF8);
                        // Replace space marks
                        for (char *pszString = pszRecoded; *pszString != '\0';
                             pszString++)
                        {
                            if (*pszString == codeBlank)
                                *pszString = ' ';
                        }
                        feature->SetField(fieldno, pszRecoded);
                        CPLFree(pszRecoded);
                    }
                    else
                    {
                        feature->SetField(fieldno, tokens[fIndex]);
                    }
                    if (featureDef->GetFieldDefn(fieldno)->GetType() ==
                            OFTReal &&
                        fieldno > 0 &&
                        featureDef->GetFieldDefn(fieldno - 1)->GetType() ==
                            OFTReal)
                    {
                        // Check for Point geometry (Coord type).
                        // If there is no ili model read,
                        // we have no chance to detect the
                        // geometry column.
                        CPLString geomfldname =
                            featureDef->GetFieldDefn(fieldno)->GetNameRef();
                        // Check if name ends with _1.
                        if (geomfldname.size() >= 2 &&
                            geomfldname[geomfldname.size() - 2] == '_')
                        {
                            geomfldname =
                                geomfldname.substr(0, geomfldname.size() - 2);
                            geomIdx = featureDef->GetGeomFieldIndex(
                                geomfldname.c_str());
                            if (geomIdx == -1)
                            {
                                CPLError(
                                    CE_Warning, CPLE_AppDefined,
                                    "No matching definition for field '%s' of "
                                    "table %s found",
                                    geomfldname.c_str(), featureDef->GetName());
                            }
                        }
                        else
                        {
                            geomIdx = -1;
                        }
                        if (geomIdx >= 0)
                        {
                            if (featureDef->GetGeomFieldDefn(geomIdx)
                                    ->GetType() == wkbPoint)
                            {
                                // Add Point geometry.
                                OGRPoint *ogrPoint =
                                    new OGRPoint(CPLAtof(tokens[fIndex - 1]),
                                                 CPLAtof(tokens[fIndex]));
                                feature->SetGeomFieldDirectly(geomIdx,
                                                              ogrPoint);
                            }
                            else if (featureDef->GetGeomFieldDefn(geomIdx)
                                             ->GetType() == wkbPoint25D &&
                                     fieldno > 1 &&
                                     featureDef->GetFieldDefn(fieldno - 2)
                                             ->GetType() == OFTReal)
                            {
                                // Add 3D Point geometry.
                                OGRPoint *ogrPoint =
                                    new OGRPoint(CPLAtof(tokens[fIndex - 2]),
                                                 CPLAtof(tokens[fIndex - 1]),
                                                 CPLAtof(tokens[fIndex]));
                                feature->SetGeomFieldDirectly(geomIdx,
                                                              ogrPoint);
                            }
                        }
                    }
                }
            }
            if (!warned && featureDef->GetFieldCount() != CSLCount(tokens) - 1)
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "Field count of table %s doesn't match. %d declared, "
                         "%d found (e.g. ignored LINEATTR)",
                         featureDef->GetName(), featureDef->GetFieldCount(),
                         CSLCount(tokens) - 1);
                warned = TRUE;
            }
            if (feature->GetFieldCount() > 0)
            {
                // USE _TID as FID. TODO: respect IDENT field from model.
                feature->SetFID(feature->GetFieldAsInteger64(0));
            }
            curLayer->AddFeature(feature);
            bFeatureAdded = true;
            geomIdx = -1;  // Reset
        }
        else if (EQUAL(firsttok, "STPT") && feature != nullptr)
        {
            // Find next non-Point geometry
            if (geomIdx < 0)
                geomIdx = 0;
            while (geomIdx < featureDef->GetGeomFieldCount() &&
                   featureDef->GetGeomFieldDefn(geomIdx)->GetType() == wkbPoint)
            {
                geomIdx++;
            }
            OGRwkbGeometryType geomType =
                (geomIdx < featureDef->GetGeomFieldCount())
                    ? featureDef->GetGeomFieldDefn(geomIdx)->GetType()
                    : wkbNone;
            if (CSLCount(tokens) >= 3)
                ReadGeom(tokens, geomIdx, geomType, feature);
        }
        else if (EQUAL(firsttok, "ELIN"))
        {
            // Empty geom.
        }
        else if (EQUAL(firsttok, "EDGE") && feature != nullptr)
        {
            CSLDestroy(tokens);
            tokens = ReadParseLine();  // STPT
            // Find next non-Point geometry
            do
            {
                geomIdx++;
            } while (geomIdx < featureDef->GetGeomFieldCount() &&
                     featureDef->GetGeomFieldDefn(geomIdx)->GetType() ==
                         wkbPoint);
            if (CSLCount(tokens) >= 3)
                ReadGeom(tokens, geomIdx, wkbMultiLineString, feature);
        }
        else if (EQUAL(firsttok, "PERI"))
        {
        }
        else if (EQUAL(firsttok, "ETAB"))
        {
            CPLDebug("OGR_ILI", "Total features: " CPL_FRMT_GIB,
                     curLayer->GetFeatureCount());
            CSLDestroy(tokens);
            if (!bFeatureAdded)
                delete feature;
            return TRUE;
        }
        else
        {
            CPLError(CE_Warning, CPLE_AppDefined, "Unexpected token: %s",
                     firsttok);
        }

        CSLDestroy(tokens);
    }

    if (!bFeatureAdded)
        delete feature;

    return TRUE;
}

void ILI1Reader::ReadGeom(char **stgeom, int geomIdx, OGRwkbGeometryType eType,
                          OGRFeature *feature)
{
#ifdef DEBUG_VERBOSE
    CPLDebug("OGR_ILI", "ILI1Reader::ReadGeom geomIdx: %d OGRGeometryType: %s",
             geomIdx, OGRGeometryTypeToName(eType));
#endif
    if (eType == wkbNone)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "Calling ILI1Reader::ReadGeom with wkbNone");
    }

    // Initialize geometry.

    OGRCompoundCurve *ogrCurve = new OGRCompoundCurve();
    OGRCurvePolygon *ogrPoly = nullptr;     // current polygon
    OGRMultiCurve *ogrMultiLine = nullptr;  // current multi line

    if (eType == wkbMultiCurve || eType == wkbMultiLineString)
    {
        ogrMultiLine = new OGRMultiCurve();
    }
    else if (eType == wkbPolygon || eType == wkbCurvePolygon)
    {
        ogrPoly = new OGRCurvePolygon();
    }

    OGRPoint ogrPoint;  // Current point.
    ogrPoint.setX(CPLAtof(stgeom[1]));
    ogrPoint.setY(CPLAtof(stgeom[2]));

    OGRLineString *ogrLine = new OGRLineString();
    ogrLine->addPoint(&ogrPoint);

    // Parse geometry.

    char **tokens = nullptr;
    bool end = false;
    OGRCircularString *arc = nullptr;  // current arc

    while (!end && (tokens = ReadParseLine()) != nullptr)
    {
        const char *firsttok = CSLGetField(tokens, 0);
        if (firsttok == nullptr)
        {
            // do nothing
        }
        else if (EQUAL(firsttok, "LIPT") && CSLCount(tokens) >= 3)
        {
            ogrPoint.setX(CPLAtof(tokens[1]));
            ogrPoint.setY(CPLAtof(tokens[2]));
            if (arc)
            {
                arc->addPoint(&ogrPoint);
                OGRErr error = ogrCurve->addCurveDirectly(arc);
                if (error != OGRERR_NONE)
                {
                    char *pszJSon = arc->exportToJson();
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Could not add geometry: %s",
                             pszJSon ? pszJSon : "(null)");
                    CPLFree(pszJSon);
                    delete arc;
                }
                arc = nullptr;
            }
            ogrLine->addPoint(&ogrPoint);
        }
        else if (EQUAL(firsttok, "ARCP") && CSLCount(tokens) >= 3)
        {
            // Finish line and start arc
            if (ogrLine->getNumPoints() > 1)
            {
                OGRErr error = ogrCurve->addCurveDirectly(ogrLine);
                if (error != OGRERR_NONE)
                {
                    char *pszJSon = ogrLine->exportToJson();
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Could not add geometry: %s",
                             pszJSon ? pszJSon : "(null)");
                    CPLFree(pszJSon);
                    delete ogrLine;
                }
                ogrLine = new OGRLineString();
            }
            else
            {
                ogrLine->empty();
            }
            delete arc;
            arc = new OGRCircularString();
            arc->addPoint(&ogrPoint);
            ogrPoint.setX(CPLAtof(tokens[1]));
            ogrPoint.setY(CPLAtof(tokens[2]));
            arc->addPoint(&ogrPoint);
        }
        else if (EQUAL(firsttok, "ELIN"))
        {
            if (ogrLine->getNumPoints() > 1)
            {  // Ignore single LIPT after ARCP
                OGRErr error = ogrCurve->addCurveDirectly(ogrLine);
                if (error != OGRERR_NONE)
                {
                    char *pszJSon = ogrLine->exportToJson();
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Could not add geometry: %s",
                             pszJSon ? pszJSon : "(null)");
                    CPLFree(pszJSon);
                    delete ogrLine;
                }
                ogrLine = nullptr;
            }
            if (!ogrCurve->IsEmpty())
            {
                if (ogrMultiLine)
                {
                    OGRErr error = ogrMultiLine->addGeometryDirectly(ogrCurve);
                    if (error != OGRERR_NONE)
                    {
                        char *pszJSon = ogrCurve->exportToJson();
                        CPLError(CE_Warning, CPLE_AppDefined,
                                 "Could not add geometry: %s",
                                 pszJSon ? pszJSon : "(null)");
                        CPLFree(pszJSon);
                        delete ogrCurve;
                    }
                    ogrCurve = nullptr;
                }
                if (ogrPoly)
                {
                    OGRErr error = ogrPoly->addRingDirectly(ogrCurve);
                    if (error != OGRERR_NONE)
                    {
                        char *pszJSon = ogrCurve->exportToJson();
                        CPLError(CE_Warning, CPLE_AppDefined,
                                 "Could not add geometry: %s",
                                 pszJSon ? pszJSon : "(null)");
                        CPLFree(pszJSon);
                        delete ogrCurve;
                    }
                    ogrCurve = nullptr;
                }
            }
            end = true;
        }
        else if (EQUAL(firsttok, "EEDG"))
        {
            end = true;
        }
        else if (EQUAL(firsttok, "LATT"))
        {
            // Line Attributes (ignored)
        }
        else if (EQUAL(firsttok, "EFLA"))
        {
            end = true;
        }
        else if (EQUAL(firsttok, "ETAB"))
        {
            end = true;
        }
        else
        {
            CPLError(CE_Warning, CPLE_AppDefined, "Unexpected token: %s",
                     firsttok);
        }

        CSLDestroy(tokens);
    }
    delete arc;

    delete ogrLine;

    // Set feature geometry
    if (eType == wkbMultiCurve)
    {
        feature->SetGeomFieldDirectly(geomIdx, ogrMultiLine);
        delete ogrCurve;
    }
    else if (eType == wkbMultiLineString)
    {
        feature->SetGeomFieldDirectly(geomIdx,
                                      ogrMultiLine->getLinearGeometry());
        delete ogrMultiLine;
        delete ogrCurve;
    }
    else if (eType == wkbCurvePolygon)
    {
        feature->SetGeomFieldDirectly(geomIdx, ogrPoly);
        delete ogrCurve;
    }
    else if (eType == wkbPolygon)
    {
        feature->SetGeomFieldDirectly(geomIdx, ogrPoly->getLinearGeometry());
        delete ogrPoly;
        delete ogrCurve;
    }
    else
    {
        feature->SetGeomFieldDirectly(geomIdx, ogrCurve);
    }
}

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

void ILI1Reader::AddLayer(OGRILI1Layer *poNewLayer)

{
    nLayers++;

    papoLayers = static_cast<OGRILI1Layer **>(
        CPLRealloc(papoLayers, sizeof(void *) * nLayers));

    papoLayers[nLayers - 1] = poNewLayer;
}

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

OGRILI1Layer *ILI1Reader::GetLayer(int iLayer)

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

    return papoLayers[iLayer];
}

OGRILI1Layer *ILI1Reader::GetLayerByName(const char *pszLayerName)

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

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

int ILI1Reader::GetLayerCount()

{
    return nLayers;
}

/************************************************************************/
/*     Read one logical line, and return split into fields.  The return */
/*     result is a stringlist, in the sense of the CSL functions.       */
/************************************************************************/

char **ILI1Reader::ReadParseLine()
{
    CPLAssert(fpItf != nullptr);
    if (fpItf == nullptr)
        return nullptr;

    const char *pszLine = CPLReadLineL(fpItf);
    if (pszLine == nullptr)
        return nullptr;

    if (strlen(pszLine) == 0)
        return nullptr;

    char **tokens = CSLTokenizeString2(pszLine, " ", CSLT_PRESERVEESCAPES);
    int nCount = CSLCount(tokens);
    if (nCount == 0)
    {
        CSLDestroy(tokens);
        return nullptr;
    }
    const char *token = tokens[nCount - 1];

    // Append CONT lines
    while (strlen(pszLine) && token[0] == codeContinue && token[1] == '\0')
    {
        // remove last token
        CPLFree(tokens[CSLCount(tokens) - 1]);
        tokens[CSLCount(tokens) - 1] = nullptr;

        pszLine = CPLReadLineL(fpItf);
        if (pszLine == nullptr)
        {
            break;
        }
        char **conttok = CSLTokenizeString2(pszLine, " ", CSLT_PRESERVEESCAPES);
        if (!conttok || conttok[0] == nullptr || !EQUAL(conttok[0], "CONT") ||
            conttok[1] == nullptr)
        {
            CSLDestroy(conttok);
            break;
        }

        // append
        tokens = CSLInsertStrings(tokens, -1, &conttok[1]);
        token = tokens[CSLCount(tokens) - 1];

        CSLDestroy(conttok);
    }
    if (tokens[0] == nullptr)
    {
        CSLDestroy(tokens);
        tokens = nullptr;
    }
    return tokens;
}

IILI1Reader *CreateILI1Reader()
{
    return new ILI1Reader();
}

void DestroyILI1Reader(IILI1Reader *reader)
{
    if (reader)
        delete reader;
}
