/******************************************************************************
 *
 * Project:  Viewshed Generator
 * Purpose:  Viewshed Generator mainline.
 * Author:   Tamas Szekeres <szekerest@gmail.com>
 *
 ******************************************************************************
 *
 * 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_conv.h"
#include "cpl_string.h"
#include "gdal_version.h"
#include "gdal.h"
#include "gdal_alg.h"
#include "gdal_priv.h"
#include "ogr_api.h"
#include "ogr_srs_api.h"
#include "ogr_spatialref.h"
#include "commonutils.h"

/************************************************************************/
/*                               Usage()                                */
/************************************************************************/

static void Usage(bool bIsError, const char *pszErrorMsg = nullptr)

{
    fprintf(
        bIsError ? stderr : stdout,
        "Usage: gdal_viewshed [--help] [--help-general]\n"
        "                     [-b <band>]\n"
        "                     [-a_nodata <value>] [-f <formatname>]\n"
        "                     [-oz <observer_height>] [-tz <target_height>] "
        "[-md <max_distance>]\n"
        "                     -ox <observer_x> -oy <observer_y>\n"
        "                     [-vv <visibility>] [-iv <invisibility>]\n"
        "                     [-ov <out_of_range>] [-cc <curvature_coef>]\n"
        "                     [-co <NAME>=<VALUE>]...\n"
        "                     [-q] [-om <output mode>]\n"
        "                     <src_filename> <dst_filename>\n");

    if (pszErrorMsg != nullptr)
        fprintf(stderr, "\nFAILURE: %s\n", pszErrorMsg);

    exit(bIsError ? 1 : 0);
}

static double CPLAtofTaintedSuppressed(const char *pszVal)
{
    // coverity[tainted_data]
    return CPLAtof(pszVal);
}

/************************************************************************/
/*                                main()                                */
/************************************************************************/

#define CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(nExtraArg)                            \
    do                                                                         \
    {                                                                          \
        if (i + nExtraArg >= argc)                                             \
            Usage(true, CPLSPrintf("%s option requires %d argument(s)",        \
                                   argv[i], nExtraArg));                       \
    } while (false)

MAIN_START(argc, argv)

{
    int nBandIn = 1;
    double dfObserverHeight = 2.0;
    double dfTargetHeight = 0.0;
    double dfMaxDistance = 0.0;
    bool bObserverXSpecified = false;
    double dfObserverX = 0.0;
    bool bObserverYSpecified = false;
    double dfObserverY = 0.0;
    double dfVisibleVal = 255.0;
    double dfInvisibleVal = 0.0;
    double dfOutOfRangeVal = 0.0;
    double dfNoDataVal = -1.0;
    // Value for standard atmospheric refraction. See
    // doc/source/programs/gdal_viewshed.rst
    bool bCurvCoeffSpecified = false;
    double dfCurvCoeff = 0.85714;
    const char *pszDriverName = nullptr;
    const char *pszSrcFilename = nullptr;
    const char *pszDstFilename = nullptr;
    bool bQuiet = false;
    GDALProgressFunc pfnProgress = nullptr;
    char **papszCreateOptions = nullptr;
    const char *pszOutputMode = nullptr;

    GDALAllRegister();

    argc = GDALGeneralCmdLineProcessor(argc, &argv, 0);

    /* -------------------------------------------------------------------- */
    /*      Parse arguments.                                                */
    /* -------------------------------------------------------------------- */
    for (int i = 1; i < argc; i++)
    {
        if (EQUAL(argv[i], "--utility_version"))
        {
            printf("%s was compiled against GDAL %s and "
                   "is running against GDAL %s\n",
                   argv[0], GDAL_RELEASE_NAME, GDALVersionInfo("RELEASE_NAME"));
            CSLDestroy(argv);
            return 0;
        }
        else if (EQUAL(argv[i], "--help"))
            Usage(false);
        else if (EQUAL(argv[i], "-f") || EQUAL(argv[i], "-of"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            pszDriverName = argv[++i];
        }
        else if (EQUAL(argv[i], "-ox"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            bObserverXSpecified = true;
            dfObserverX = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-oy"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            bObserverYSpecified = true;
            dfObserverY = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-oz"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            dfObserverHeight = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-vv"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            dfVisibleVal = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-iv"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            dfInvisibleVal = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-ov"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            dfOutOfRangeVal = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-co"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            papszCreateOptions = CSLAddString(papszCreateOptions, argv[++i]);
        }
        else if (EQUAL(argv[i], "-a_nodata"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            dfNoDataVal = CPLAtofM(argv[++i]);
            ;
        }
        else if (EQUAL(argv[i], "-tz"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            dfTargetHeight = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-md"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            dfMaxDistance = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-cc"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            bCurvCoeffSpecified = true;
            dfCurvCoeff = CPLAtofTaintedSuppressed(argv[++i]);
        }
        else if (EQUAL(argv[i], "-b"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            nBandIn = atoi(argv[++i]);
        }
        else if (EQUAL(argv[i], "-om"))
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            pszOutputMode = argv[++i];
        }
        else if (EQUAL(argv[i], "-q") || EQUAL(argv[i], "-quiet"))
        {
            bQuiet = TRUE;
        }
        else if (pszSrcFilename == nullptr)
        {
            pszSrcFilename = argv[i];
        }
        else if (pszDstFilename == nullptr)
        {
            pszDstFilename = argv[i];
        }
        else
            Usage(true, "Too many command options.");
    }

    if (pszSrcFilename == nullptr)
    {
        Usage(true, "Missing source filename.");
    }

    if (pszDstFilename == nullptr)
    {
        Usage(true, "Missing destination filename.");
    }

    if (!bObserverXSpecified)
    {
        Usage(true, "Missing -ox.");
    }

    if (!bObserverYSpecified)
    {
        Usage(true, "Missing -oy.");
    }

    if (!bQuiet)
        pfnProgress = GDALTermProgress;

    CPLString osFormat;
    if (pszDriverName == nullptr)
    {
        osFormat = GetOutputDriverForRaster(pszDstFilename);
        if (osFormat.empty())
        {
            exit(2);
        }
    }

    GDALViewshedOutputType outputMode = GVOT_NORMAL;
    if (pszOutputMode != nullptr)
    {
        if (EQUAL(pszOutputMode, "NORMAL"))
        {
        }
        else if (EQUAL(pszOutputMode, "DEM"))
        {
            outputMode = GVOT_MIN_TARGET_HEIGHT_FROM_DEM;
        }
        else if (EQUAL(pszOutputMode, "GROUND"))
        {
            outputMode = GVOT_MIN_TARGET_HEIGHT_FROM_GROUND;
        }
        else
        {
            Usage(true, "-om must be either NORMAL, DEM or GROUND");
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Open source raster file.                                        */
    /* -------------------------------------------------------------------- */
    GDALDatasetH hSrcDS = GDALOpen(pszSrcFilename, GA_ReadOnly);
    if (hSrcDS == nullptr)
        exit(2);

    GDALRasterBandH hBand = GDALGetRasterBand(hSrcDS, nBandIn);
    if (hBand == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Band %d does not exist on dataset.", nBandIn);
        exit(2);
    }

    if (!bCurvCoeffSpecified)
    {
        const OGRSpatialReference *poSRS =
            GDALDataset::FromHandle(hSrcDS)->GetSpatialRef();
        if (poSRS)
        {
            OGRErr eSRSerr;
            const double dfSemiMajor = poSRS->GetSemiMajor(&eSRSerr);
            if (eSRSerr != OGRERR_FAILURE &&
                fabs(dfSemiMajor - SRS_WGS84_SEMIMAJOR) >
                    0.05 * SRS_WGS84_SEMIMAJOR)
            {
                dfCurvCoeff = 1.0;
                CPLDebug("gdal_viewshed",
                         "Using -cc=1.0 as a non-Earth CRS has been detected");
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Invoke.                                                         */
    /* -------------------------------------------------------------------- */
    GDALDatasetH hDstDS = GDALViewshedGenerate(
        hBand, pszDriverName ? pszDriverName : osFormat.c_str(), pszDstFilename,
        papszCreateOptions, dfObserverX, dfObserverY, dfObserverHeight,
        dfTargetHeight, dfVisibleVal, dfInvisibleVal, dfOutOfRangeVal,
        dfNoDataVal, dfCurvCoeff, GVM_Edge, dfMaxDistance, pfnProgress, nullptr,
        outputMode, nullptr);
    bool bSuccess = hDstDS != nullptr;
    GDALClose(hSrcDS);
    if (GDALClose(hDstDS) != CE_None)
        bSuccess = false;

    CSLDestroy(argv);
    CSLDestroy(papszCreateOptions);
    GDALDestroyDriverManager();
    OGRCleanupAll();

    return bSuccess ? 0 : 1;
}
MAIN_END
