/**********************************************************************
 *
 * Name:     mitab_coordsys.cpp
 * Project:  MapInfo TAB Read/Write library
 * Language: C++
 * Purpose:  Implementation translation between MIF CoordSys format, and
 *           and OGRSpatialRef format.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 **********************************************************************
 * Copyright (c) 1999-2001, Frank Warmerdam
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **********************************************************************/

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

#include <cmath>
#include <cstdlib>
#include <cstring>
#include <string>

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_string.h"
#include "mitab_priv.h"
#include "ogr_spatialref.h"

CPL_CVSID("$Id: mitab_coordsys.cpp 327bfdc0f5dd563c3b1c4cbf26d34967c5c9c790 2020-02-28 13:51:40 +0100 Even Rouault $")

extern const MapInfoDatumInfo asDatumInfoList[];
extern const MapInfoSpheroidInfo asSpheroidInfoList[];

/************************************************************************/
/*                      MITABCoordSys2SpatialRef()                      */
/*                                                                      */
/*      Convert a MIF COORDSYS string into a new OGRSpatialReference    */
/*      object.                                                         */
/************************************************************************/

OGRSpatialReference *MITABCoordSys2SpatialRef( const char * pszCoordSys )

{
    TABProjInfo sTABProj;
    if(MITABCoordSys2TABProjInfo(pszCoordSys, &sTABProj) < 0 )
        return nullptr;
    OGRSpatialReference *poSR = TABFile::GetSpatialRefFromTABProj(sTABProj);

    // Report on translation.
    char *pszWKT = nullptr;

    poSR->exportToWkt(&pszWKT);
    if( pszWKT != nullptr )
    {
        CPLDebug("MITAB",
                 "This CoordSys value:\n%s\nwas translated to:\n%s",
                 pszCoordSys, pszWKT);
        CPLFree(pszWKT);
    }

    return poSR;
}

/************************************************************************/
/*                      MITABSpatialRef2CoordSys()                      */
/*                                                                      */
/*      Converts a OGRSpatialReference object into a MIF COORDSYS       */
/*      string.                                                         */
/*                                                                      */
/*      The function returns a newly allocated string that should be    */
/*      CPLFree()'d by the caller.                                      */
/************************************************************************/

char *MITABSpatialRef2CoordSys( const OGRSpatialReference * poSR )

{
    if( poSR == nullptr )
        return nullptr;

    TABProjInfo sTABProj;
    int nParmCount = 0;
    TABFile::GetTABProjFromSpatialRef(poSR, sTABProj, nParmCount);

    // Do coordsys lookup.
    double dXMin = 0.0;
    double dYMin = 0.0;
    double dXMax = 0.0;
    double dYMax = 0.0;
    bool bHasBounds = false;
    if( sTABProj.nProjId > 1 &&
        MITABLookupCoordSysBounds(&sTABProj,
                                  dXMin, dYMin,
                                  dXMax, dYMax, true) )
    {
        bHasBounds = true;
    }

    // Translate the units.
    const char *pszMIFUnits = TABUnitIdToString(sTABProj.nUnitsId);

    // Build coordinate system definition.
    CPLString osCoordSys;

    if( sTABProj.nProjId != 0 )
    {
        osCoordSys.Printf("Earth Projection %d", sTABProj.nProjId);
    }
    else
    {
        osCoordSys.Printf("NonEarth Units");
    }

    // Append Datum.
    if( sTABProj.nProjId != 0 )
    {
        osCoordSys += CPLSPrintf(", %d", sTABProj.nDatumId);

        if( sTABProj.nDatumId == 999 || sTABProj.nDatumId == 9999 )
        {
            osCoordSys +=
                CPLSPrintf(", %d, %.15g, %.15g, %.15g", sTABProj.nEllipsoidId,
                           sTABProj.dDatumShiftX, sTABProj.dDatumShiftY,
                           sTABProj.dDatumShiftZ);
        }

        if( sTABProj.nDatumId == 9999 )
        {
            osCoordSys +=
                CPLSPrintf(", %.15g, %.15g, %.15g, %.15g, %.15g",
                           sTABProj.adDatumParams[0], sTABProj.adDatumParams[1],
                           sTABProj.adDatumParams[2], sTABProj.adDatumParams[3],
                           sTABProj.adDatumParams[4]);
        }
    }

    // Append units.
    if( sTABProj.nProjId != 1 && pszMIFUnits != nullptr )
    {
        if( sTABProj.nProjId != 0 )
            osCoordSys += ",";

        osCoordSys += CPLSPrintf(" \"%s\"", pszMIFUnits);
    }

    // Append Projection Parms.
    for( int iParm = 0; iParm < nParmCount; iParm++ )
        osCoordSys += CPLSPrintf(", %.15g", sTABProj.adProjParams[iParm]);

    // Append user bounds.
    if( bHasBounds )
    {
        if( fabs(dXMin - floor(dXMin + 0.5)) < 1e-8 &&
            fabs(dYMin - floor(dYMin + 0.5)) < 1e-8 &&
            fabs(dXMax - floor(dXMax + 0.5)) < 1e-8 &&
            fabs(dYMax - floor(dYMax + 0.5)) < 1e-8 )
        {
            osCoordSys +=
                CPLSPrintf(" Bounds (%d, %d) (%d, %d)",
                           static_cast<int>(dXMin), static_cast<int>(dYMin),
                           static_cast<int>(dXMax), static_cast<int>(dYMax));
        }
        else
        {
            osCoordSys += CPLSPrintf(" Bounds (%f, %f) (%f, %f)",
                                     dXMin, dYMin, dXMax, dYMax);
        }
    }

    // Report on translation.
    char *pszWKT = nullptr;

    poSR->exportToWkt(&pszWKT);
    if( pszWKT != nullptr )
    {
        CPLDebug("MITAB",
                 "This WKT Projection:\n%s\n\ntranslates to:\n%s",
                 pszWKT, osCoordSys.c_str());
        CPLFree(pszWKT);
    }

    return CPLStrdup(osCoordSys.c_str());
}

/************************************************************************/
/*                      MITABExtractCoordSysBounds                      */
/*                                                                      */
/* Return true if MIF coordsys string contains a BOUNDS parameter and   */
/* Set x/y min/max values.                                              */
/************************************************************************/

bool MITABExtractCoordSysBounds( const char * pszCoordSys,
                                 double &dXMin, double &dYMin,
                                 double &dXMax, double &dYMax )

{
    if( pszCoordSys == nullptr )
        return false;

    char **papszFields =
        CSLTokenizeStringComplex(pszCoordSys, " ,()", TRUE, FALSE);

    int iBounds = CSLFindString(papszFields, "Bounds");

    if (iBounds >= 0 && iBounds + 4 < CSLCount(papszFields))
    {
        dXMin = CPLAtof(papszFields[++iBounds]);
        dYMin = CPLAtof(papszFields[++iBounds]);
        dXMax = CPLAtof(papszFields[++iBounds]);
        dYMax = CPLAtof(papszFields[++iBounds]);
        CSLDestroy(papszFields);
        return true;
    }

    CSLDestroy(papszFields);
    return false;
}

/**********************************************************************
 *                     MITABCoordSys2TABProjInfo()
 *
 * Convert a MIF COORDSYS string into a TABProjInfo structure.
 *
 * Returns 0 on success, -1 on error.
 **********************************************************************/
int MITABCoordSys2TABProjInfo(const char * pszCoordSys, TABProjInfo *psProj)

{
    // Set all fields to zero, equivalent of NonEarth Units "mi"
    memset(psProj, 0, sizeof(TABProjInfo));

    if( pszCoordSys == nullptr )
        return -1;

    // Parse the passed string into words.
    while(*pszCoordSys == ' ')
        pszCoordSys++;  // Eat leading spaces.
    if( STARTS_WITH_CI(pszCoordSys, "CoordSys") && pszCoordSys[8] != '\0' )
        pszCoordSys += 9;

    char **papszFields =
        CSLTokenizeStringComplex(pszCoordSys, " ,", TRUE, FALSE);

    // Clip off Bounds information.
    int iBounds = CSLFindString(papszFields, "Bounds");

    while( iBounds != -1 && papszFields[iBounds] != nullptr )
    {
        CPLFree( papszFields[iBounds] );
        papszFields[iBounds] = nullptr;
        iBounds++;
    }

    // Fetch the projection.
    char **papszNextField = nullptr;

    if( CSLCount(papszFields) >= 3 &&
        EQUAL(papszFields[0], "Earth") &&
        EQUAL(papszFields[1], "Projection") )
    {
        int nProjId = atoi(papszFields[2]);
        if (nProjId >= 3000) nProjId -= 3000;
        else if (nProjId >= 2000) nProjId -= 2000;
        else if (nProjId >= 1000) nProjId -= 1000;

        psProj->nProjId = static_cast<GByte>(nProjId);
        papszNextField = papszFields + 3;
    }
    else if (CSLCount(papszFields) >= 2 &&
             EQUAL(papszFields[0],"NonEarth") )
    {
        // NonEarth Units "..." Bounds (x, y) (x, y)
        psProj->nProjId = 0;
        papszNextField = papszFields + 2;

        if( papszNextField[0] != nullptr && EQUAL(papszNextField[0], "Units") )
            papszNextField++;
    }
    else
    {
        // Invalid projection string ???
        if (CSLCount(papszFields) > 0)
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "Failed parsing CoordSys: '%s'", pszCoordSys);
        CSLDestroy(papszFields);
        return -1;
    }

    // Fetch the datum information.
    int nDatum = 0;

    if( psProj->nProjId != 0 && CSLCount(papszNextField) > 0 )
    {
        nDatum = atoi(papszNextField[0]);
        papszNextField++;
    }

    if( (nDatum == 999 || nDatum == 9999) &&
        CSLCount(papszNextField) >= 4 )
    {
        psProj->nEllipsoidId = static_cast<GByte>(atoi(papszNextField[0]));
        psProj->dDatumShiftX = CPLAtof(papszNextField[1]);
        psProj->dDatumShiftY = CPLAtof(papszNextField[2]);
        psProj->dDatumShiftZ = CPLAtof(papszNextField[3]);
        papszNextField += 4;

        if( nDatum == 9999 &&
            CSLCount(papszNextField) >= 5 )
        {
            psProj->adDatumParams[0] = CPLAtof(papszNextField[0]);
            psProj->adDatumParams[1] = CPLAtof(papszNextField[1]);
            psProj->adDatumParams[2] = CPLAtof(papszNextField[2]);
            psProj->adDatumParams[3] = CPLAtof(papszNextField[3]);
            psProj->adDatumParams[4] = CPLAtof(papszNextField[4]);
            papszNextField += 5;
        }
    }
    else if (nDatum != 999 && nDatum != 9999)
    {
        // Find the datum, and collect its parameters if possible.
        const MapInfoDatumInfo *psDatumInfo = nullptr;

        int iDatum = 0;  // Used after for.
        for( ; asDatumInfoList[iDatum].nMapInfoDatumID != -1; iDatum++ )
        {
            if( asDatumInfoList[iDatum].nMapInfoDatumID == nDatum )
            {
                psDatumInfo = asDatumInfoList + iDatum;
                break;
            }
        }

        if( asDatumInfoList[iDatum].nMapInfoDatumID == -1 )
        {
            // Use WGS84.
            psDatumInfo = asDatumInfoList + 0;
        }

        if( psDatumInfo != nullptr )
        {
            psProj->nEllipsoidId = static_cast<GByte>(psDatumInfo->nEllipsoid);
            psProj->nDatumId =
                static_cast<GInt16>(psDatumInfo->nMapInfoDatumID);
            psProj->dDatumShiftX = psDatumInfo->dfShiftX;
            psProj->dDatumShiftY = psDatumInfo->dfShiftY;
            psProj->dDatumShiftZ = psDatumInfo->dfShiftZ;
            psProj->adDatumParams[0] = psDatumInfo->dfDatumParm0;
            psProj->adDatumParams[1] = psDatumInfo->dfDatumParm1;
            psProj->adDatumParams[2] = psDatumInfo->dfDatumParm2;
            psProj->adDatumParams[3] = psDatumInfo->dfDatumParm3;
            psProj->adDatumParams[4] = psDatumInfo->dfDatumParm4;
        }
    }

    // Fetch the units string.
    if( CSLCount(papszNextField) > 0 )
    {
        if( isdigit(papszNextField[0][0]) )
        {
            psProj->nUnitsId =
                static_cast<GByte>(atoi(papszNextField[0]));
        }
        else
        {
            psProj->nUnitsId =
                static_cast<GByte>(TABUnitIdFromString(papszNextField[0]));
        }
        papszNextField++;
    }

    // Finally the projection parameters.
    for(int iParam = 0; iParam < 6 && CSLCount(papszNextField) > 0; iParam++)
    {
        psProj->adProjParams[iParam] = CPLAtof(papszNextField[0]);
        papszNextField++;
    }

    CSLDestroy(papszFields);

    return 0;
}
