/****************************************************************************
 *
 * MODULE:       v.proj
 * AUTHOR(S):    Irina Kosinovsky, US ARMY CERL,
 *               M.L. Holko, USDA, SCS, NHQ-CGIS,
 *               R.L. Glenn, USDA, SCS, NHQ-CGIS (original contributors)
 *               Update to GRASS 6: Radim Blazek <radim.blazek gmail.com>
 *               Huidae Cho <grass4u gmail.com>,
 *               Hamish Bowman <hamish_b yahoo.com>,
 *               Jachym Cepicky <jachym les-ejk.cz>,
 *               Markus Neteler <neteler itc.it>,
 *               Paul Kelly <paul-grass stjohnspoint.co.uk>
 *               Markus Metz
 * PURPOSE:
 * COPYRIGHT:    (C) 1999-2008, 2018 by the GRASS Development Team
 *
 *               This program is free software under the GNU General Public
 *               License (>=v2). Read the file COPYING that comes with GRASS
 *               for details.
 *
 *****************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <ctype.h>
#include <math.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <grass/gis.h>
#include <grass/vector.h>
#include <grass/gprojects.h>
#include <grass/glocale.h>
#include "local_proto.h"

int main(int argc, char *argv[])
{
    int i, type, stat;
    int day, yr, Out_proj;
    int overwrite; /* overwrite output map */
    const char *mapset;
    const char *omap_name, *map_name, *iset_name, *iloc_name;
    struct pj_info info_in;
    struct pj_info info_out;
    struct pj_info info_trans;
    const char *gbase;
    char date[40], mon[4];
    struct GModule *module;
    struct Option *omapopt, *mapopt, *isetopt, *ilocopt, *ibaseopt, *smax;

#ifdef HAVE_PROJ_H
    struct Option *pipeline; /* name of custom PROJ pipeline */
#endif
    struct Key_Value *in_proj_keys = NULL, *in_unit_keys = NULL;
    struct Key_Value *out_proj_keys, *out_unit_keys;
    struct line_pnts *Points, *Points2;
    struct line_cats *Cats;
    struct Map_info Map;
    struct Map_info Out_Map;
    struct bound_box src_box, tgt_box;
    int nowrap = 0, recommend_nowrap = 0;
    double lmax;
    struct {
        struct Flag *list;       /* list files in source location */
        struct Flag *transformz; /* treat z as ellipsoidal height */
        struct Flag *wrap;       /* latlon output: wrap to 0,360 */
        struct Flag *no_topol;   /* do not build topology */
    } flag;

    G_gisinit(argv[0]);

    module = G_define_module();
    G_add_keyword(_("vector"));
    G_add_keyword(_("projection"));
    G_add_keyword(_("transformation"));
    G_add_keyword(_("import"));
    module->description =
        _("Re-projects a vector map from one project to the current project.");

    /* set up the options and flags for the command line parser */

    ilocopt = G_define_standard_option(G_OPT_M_LOCATION);
    ilocopt->required = YES;
    ilocopt->label = _("Project (location) containing input vector map");
    ilocopt->guisection = _("Source");

    isetopt = G_define_standard_option(G_OPT_M_MAPSET);
    isetopt->label = _("Mapset containing input vector map");
    isetopt->description = _("Default: name of current mapset");
    isetopt->guisection = _("Source");

    mapopt = G_define_standard_option(G_OPT_V_INPUT);
    mapopt->required = NO;
    mapopt->label = _("Name of input vector map to re-project");
    mapopt->description = NULL;
    mapopt->guisection = _("Source");

    ibaseopt = G_define_standard_option(G_OPT_M_DBASE);
    ibaseopt->label = _("Path to GRASS database of input project");

    smax = G_define_option();
    smax->key = "smax";
    smax->type = TYPE_DOUBLE;
    smax->required = NO;
    smax->answer = "10000";
    smax->label = _("Maximum segment length in meters in output vector map");
    smax->description =
        _("Increases accuracy of reprojected shapes, disable with smax=0");
    smax->guisection = _("Target");

    omapopt = G_define_standard_option(G_OPT_V_OUTPUT);
    omapopt->required = NO;
    omapopt->description = _("Name for output vector map (default: input)");
    omapopt->guisection = _("Target");

#ifdef HAVE_PROJ_H
    pipeline = G_define_option();
    pipeline->key = "pipeline";
    pipeline->type = TYPE_STRING;
    pipeline->required = NO;
    pipeline->description = _("PROJ pipeline for coordinate transformation");
#endif

    flag.list = G_define_flag();
    flag.list->key = 'l';
    flag.list->description = _("List vector maps in input mapset and exit");

    flag.transformz = G_define_flag();
    flag.transformz->key = 'z';
    flag.transformz->description = _("3D vector maps only");
    flag.transformz->label = _("Assume z coordinate is ellipsoidal height and "
                               "transform if possible");
    flag.transformz->guisection = _("Target");

    flag.wrap = G_define_flag();
    flag.wrap->key = 'w';
    flag.wrap->description = _("Latlon output only, default is -180,180");
    flag.wrap->label = _("Disable wrapping to -180,180 for latlon output");
    flag.wrap->guisection = _("Target");

    flag.no_topol = G_define_flag();
    flag.no_topol->key = 'b';
    flag.no_topol->label = _("Do not build vector topology");
    flag.no_topol->description = _("Recommended for massive point projection");

    /* The parser checks if the map already exists in current mapset,
       we switch out the check and do it
       in the module after the parser */
    overwrite = G_check_overwrite(argc, argv);

    if (G_parser(argc, argv))
        exit(EXIT_FAILURE);

    /* start checking options and flags */
    /* set input vector map name and mapset */
    map_name = mapopt->answer;
    if (omapopt->answer)
        omap_name = omapopt->answer;
    else
        omap_name = map_name;
    if (omap_name && !flag.list->answer && !overwrite &&
        G_find_vector2(omap_name, G_mapset()))
        G_fatal_error(_("option <%s>: <%s> exists. To overwrite, use the "
                        "--overwrite flag"),
                      omapopt->key, omap_name);
    if (isetopt->answer)
        iset_name = isetopt->answer;
    else
        iset_name = G_store(G_mapset());

    iloc_name = ilocopt->answer;

    if (ibaseopt->answer)
        gbase = ibaseopt->answer;
    else
        gbase = G_store(G_gisdbase());

    if (!ibaseopt->answer && strcmp(iloc_name, G_location()) == 0)
        G_fatal_error(_("Input and output projects can not be the same"));

    lmax = atof(smax->answer);
    if (lmax < 0)
        lmax = 0;

    Out_proj = G_projection();
    if (Out_proj == PROJECTION_LL && flag.wrap->answer)
        nowrap = 1;

    G_begin_distance_calculations();

    /****** get the output projection parameters ******/
    out_proj_keys = G_get_projinfo();
    if (out_proj_keys == NULL)
        exit(EXIT_FAILURE);

    out_unit_keys = G_get_projunits();
    if (out_unit_keys == NULL)
        exit(EXIT_FAILURE);

    if (pj_get_kv(&info_out, out_proj_keys, out_unit_keys) < 0)
        exit(EXIT_FAILURE);

    info_out.srid = G_get_projsrid();
    info_out.wkt = G_get_projwkt();

    info_trans.def = NULL;
#ifdef HAVE_PROJ_H
    if (pipeline->answer) {
        info_trans.def = G_store(pipeline->answer);
    }
#endif

    /* Initialize the Point / Cat structure */
    Points = Vect_new_line_struct();
    Points2 = Vect_new_line_struct();
    Cats = Vect_new_cats_struct();

    /* Change the location here and then come back */

    select_target_env();
    G_setenv_nogisrc("GISDBASE", gbase);
    G_setenv_nogisrc("LOCATION_NAME", iloc_name);
    G_setenv_nogisrc("MAPSET", iset_name);
    stat = G_mapset_permissions(iset_name);

    if (stat >= 0) { /* yes, we can access the mapset */
        /* if requested, list the vector maps in source location - MN 5/2001 */
        if (flag.list->answer) {
            char **list;

            G_verbose_message(_("Checking project <%s> mapset <%s>"), iloc_name,
                              iset_name);
            list = G_list(G_ELEMENT_VECTOR, G_getenv_nofatal("GISDBASE"),
                          G_getenv_nofatal("LOCATION_NAME"), iset_name);
            if (list[0]) {
                for (i = 0; list[i]; i++) {
                    fprintf(stdout, "%s\n", list[i]);
                }
                fflush(stdout);
            }
            else {
                G_important_message(_("No vector maps found"));
            }
            exit(EXIT_SUCCESS); /* leave v.proj after listing */
        }

        if (mapopt->answer == NULL) {
            G_fatal_error(_("Required parameter <%s> not set"), mapopt->key);
        }

        G_setenv_nogisrc("MAPSET", iset_name);
        /* Make sure map is available */
        mapset = G_find_vector2(map_name, iset_name);
        if (mapset == NULL)
            G_fatal_error(
                _("Vector map <%s> in project <%s> mapset <%s> not found"),
                map_name, iloc_name, iset_name);

        /*** Get projection info for input mapset ***/
        in_proj_keys = G_get_projinfo();
        if (in_proj_keys == NULL)
            exit(EXIT_FAILURE);

        /* apparently the +over switch must be set in the input projection,
         * not the output latlon projection
         * TODO: for PROJ 6+, the +over switch must be added to the
         * transformation pipeline if authority:name or WKT are used as
         * crs definition */
        if (Out_proj == PROJECTION_LL && nowrap == 1)
            G_set_key_value("over", "defined", in_proj_keys);

        in_unit_keys = G_get_projunits();
        if (in_unit_keys == NULL)
            exit(EXIT_FAILURE);

        if (pj_get_kv(&info_in, in_proj_keys, in_unit_keys) < 0)
            exit(EXIT_FAILURE);

        info_in.srid = G_get_projsrid();
        info_in.wkt = G_get_projwkt();

        if (G_verbose() == G_verbose_max()) {
            pj_print_proj_params(&info_in, &info_out);
        }

        Vect_set_open_level(1);
        G_debug(1, "Open old: location: %s mapset : %s", G_location_path(),
                G_mapset());
        if (Vect_open_old(&Map, map_name, mapset) < 0)
            G_fatal_error(_("Unable to open vector map <%s>"), map_name);

#if PROJ_VERSION_MAJOR >= 6
        /* need to set the region to the input vector
         * for PROJ to select the appropriate pipeline */
        {
            int first = 1, counter = 0;
            struct Cell_head inwindow;

            G_unset_window();
            G_get_window(&inwindow);

            /* Cycle through all lines */
            Vect_rewind(&Map);
            while (1) {
                type = Vect_read_next_line(&Map, Points, Cats); /* read line */
                if (type == 0)
                    continue; /* Dead */

                if (type == -1)
                    G_fatal_error(_("Reading input vector map"));
                if (type == -2)
                    break;

                if (first && Points->n_points > 0) {
                    first = 0;
                    inwindow.east = inwindow.west = Points->x[0];
                    inwindow.north = inwindow.south = Points->y[0];
                    inwindow.top = inwindow.bottom = Points->z[0];
                }
                for (i = 0; i < Points->n_points; i++) {
                    if (inwindow.east < Points->x[i])
                        inwindow.east = Points->x[i];
                    if (inwindow.west > Points->x[i])
                        inwindow.west = Points->x[i];
                    if (inwindow.north < Points->y[i])
                        inwindow.north = Points->y[i];
                    if (inwindow.south > Points->y[i])
                        inwindow.south = Points->y[i];
                }
                counter++;
            }
            if (counter == 0) {
                G_warning(_("Input vector map <%s> is empty"), omap_name);
                exit(EXIT_SUCCESS);
            }
            inwindow.ns_res = inwindow.ew_res = 1;
            /* align to resolution */
            inwindow.east = ceil(inwindow.east);
            inwindow.west = floor(inwindow.west);
            inwindow.north = ceil(inwindow.north);
            inwindow.south = floor(inwindow.south);
            if (inwindow.east == inwindow.west) {
                inwindow.east += 0.5;
                inwindow.west -= 0.5;
            }
            if (inwindow.north == inwindow.south) {
                inwindow.north += 0.5;
                inwindow.south -= 0.5;
            }
            G_debug(1, "input map north: %.8f", inwindow.north);
            G_debug(1, "input map south: %.8f", inwindow.south);
            G_debug(1, "input map east: %.8f", inwindow.east);
            G_debug(1, "input map west: %.8f", inwindow.west);

            G_set_window(&inwindow);
        }
        /* GPJ_init_transform() must be called only after the region has been
         * set */
#endif
        if (GPJ_init_transform(&info_in, &info_out, &info_trans) < 0)
            G_fatal_error(_("Unable to initialize coordinate transformation"));
    }
    else if (stat < 0) {
        /* allow 0 (i.e. denied permission) */
        /* need to be able to read from others */
        if (stat == 0)
            G_fatal_error(
                _("Mapset <%s> in input project <%s> - permission denied"),
                iset_name, iloc_name);
        else
            G_fatal_error(_("Mapset <%s> in input project <%s> not found"),
                          iset_name, iloc_name);
    }

    select_current_env();

    G_free_key_value(in_proj_keys);
    G_free_key_value(in_unit_keys);
    G_free_key_value(out_proj_keys);
    G_free_key_value(out_unit_keys);

    /* test if latlon wrapping to -180,180 should be disabled */
    if (Out_proj == PROJECTION_LL && nowrap == 0) {
        int first = 1, counter = 0;
        double x, y;

        /* Cycle through all lines */
        Vect_rewind(&Map);
        while (1) {
            type = Vect_read_next_line(&Map, Points, Cats); /* read line */
            if (type == 0)
                continue; /* Dead */

            if (type == -1)
                G_fatal_error(_("Reading input vector map"));
            if (type == -2)
                break;

            if (first && Points->n_points > 0) {
                first = 0;
                src_box.E = src_box.W = Points->x[0];
                src_box.N = src_box.S = Points->y[0];
                src_box.T = src_box.B = Points->z[0];
            }
            for (i = 0; i < Points->n_points; i++) {
                if (src_box.E < Points->x[i])
                    src_box.E = Points->x[i];
                if (src_box.W > Points->x[i])
                    src_box.W = Points->x[i];
                if (src_box.N < Points->y[i])
                    src_box.N = Points->y[i];
                if (src_box.S > Points->y[i])
                    src_box.S = Points->y[i];
            }
            counter++;
        }
        if (counter == 0) {
            G_warning(_("Input vector map <%s> is empty"), omap_name);
            exit(EXIT_SUCCESS);
        }
        /* NW corner */
        x = src_box.W;
        y = src_box.N;
        if (GPJ_transform(&info_in, &info_out, &info_trans, PJ_FWD, &x, &y,
                          NULL) < 0)
            G_fatal_error(
                _("Error in %s (projection of input coordinate pair)"),
                "GPJ_transform()");

        tgt_box.E = x;
        tgt_box.W = x;
        tgt_box.N = y;
        tgt_box.S = y;
        /* SW corner */
        x = src_box.W;
        y = src_box.S;
        if (GPJ_transform(&info_in, &info_out, &info_trans, PJ_FWD, &x, &y,
                          NULL) < 0)
            G_fatal_error(
                _("Error in %s (projection of input coordinate pair)"),
                "GPJ_transform()");
        if (tgt_box.W > x)
            tgt_box.W = x;
        if (tgt_box.E < x)
            tgt_box.E = x;
        if (tgt_box.N < y)
            tgt_box.N = y;
        if (tgt_box.S > y)
            tgt_box.S = y;
        /* NE corner */
        x = src_box.E;
        y = src_box.N;
        if (GPJ_transform(&info_in, &info_out, &info_trans, PJ_FWD, &x, &y,
                          NULL) < 0)
            G_fatal_error(
                _("Error in %s (projection of input coordinate pair)"),
                "GPJ_transform()");
        if (tgt_box.W > x) {
            tgt_box.E = x + 360;
            recommend_nowrap = 1;
        }
        if (tgt_box.N < y)
            tgt_box.N = y;
        if (tgt_box.S > y)
            tgt_box.S = y;
        /* SE corner */
        x = src_box.E;
        y = src_box.S;
        if (GPJ_transform(&info_in, &info_out, &info_trans, PJ_FWD, &x, &y,
                          NULL) < 0)
            G_fatal_error(
                _("Error in %s (projection of input coordinate pair)"),
                "GPJ_transform()");
        if (tgt_box.W > x) {
            if (tgt_box.E < x + 360)
                tgt_box.E = x + 360;
            recommend_nowrap = 1;
        }
        if (tgt_box.N < y)
            tgt_box.N = y;
        if (tgt_box.S > y)
            tgt_box.S = y;
    }

    G_debug(1, "Open new: location: %s mapset : %s", G_location_path(),
            G_mapset());

    if (Vect_open_new(&Out_Map, omap_name, Vect_is_3d(&Map)) < 0)
        G_fatal_error(_("Unable to create vector map <%s>"), omap_name);

    Vect_set_error_handler_io(
        NULL, &Out_Map); /* register standard i/o error handler */

    Vect_copy_head_data(&Map, &Out_Map);
    Vect_hist_copy(&Map, &Out_Map);
    Vect_hist_command(&Out_Map);

    Vect_set_zone(&Out_Map, G_zone());

    /* Read and write header info */
    sprintf(date, "%s", G_date());
    sscanf(date, "%*s%s%d%*s%d", mon, &day, &yr);
    if (yr < 2000)
        yr = yr - 1900;
    else
        yr = yr - 2000;
    sprintf(date, "%s %d %d", mon, day, yr);
    Vect_set_date(&Out_Map, date);

    /* line densification works only with vector topology */
    if (Map.format != GV_FORMAT_NATIVE)
        lmax = 0;

    /* Cycle through all lines */
    Vect_rewind(&Map);
    i = 0;
    G_message(_("Reprojecting primitives ..."));
    while (TRUE) {
        ++i;
        G_progress(i, 1e3);
        type = Vect_read_next_line(&Map, Points, Cats); /* read line */
        if (type == 0)
            continue; /* Dead */

        if (type == -1)
            G_fatal_error(_("Reading input vector map"));
        if (type == -2)
            break;

        Vect_line_prune(Points);
        if (lmax > 0 && (type & GV_LINES) && Points->n_points > 1) {
            double x1, y1, z1, x2, y2, z2;
            double dx, dy, dz;
            double l;
            int n;

            Vect_reset_line(Points2);
            for (i = 0; i < Points->n_points - 1; i++) {
                x1 = Points->x[i];
                y1 = Points->y[i];
                z1 = Points->z[i];
                n = i + 1;
                x2 = Points->x[n];
                y2 = Points->y[n];
                z2 = Points->z[n];

                dx = x2 - x1;
                dy = y2 - y1;
                dz = z2 - z1;

                if (GPJ_transform(&info_in, &info_out, &info_trans, PJ_FWD, &x1,
                                  &y1,
                                  flag.transformz->answer ? &z1 : NULL) < 0)
                    G_fatal_error(
                        _("Error in %s (projection of input coordinate pair)"),
                        "GPJ_transform()");

                if (GPJ_transform(&info_in, &info_out, &info_trans, PJ_FWD, &x2,
                                  &y2,
                                  flag.transformz->answer ? &z2 : NULL) < 0)
                    G_fatal_error(
                        _("Error in %s (projection of input coordinate pair)"),
                        "GPJ_transform()");

                Vect_append_point(Points2, x1, y1, z1);

                l = G_distance(x1, y1, x2, y2);

                if (l > lmax) {
                    int j;
                    double x, y, z;

                    x1 = Points->x[i];
                    y1 = Points->y[i];
                    z1 = Points->z[i];

                    n = ceil(l / lmax);

                    for (j = 1; j < n; j++) {
                        x = x1 + dx * j / n;
                        y = y1 + dy * j / n;
                        z = z1 + dz * j / n;

                        if (GPJ_transform(
                                &info_in, &info_out, &info_trans, PJ_FWD, &x,
                                &y, flag.transformz->answer ? &z : NULL) < 0)
                            G_fatal_error(_("Unable to re-project vector map "
                                            "<%s> from <%s>"),
                                          Vect_get_full_name(&Map),
                                          ilocopt->answer);
                        Vect_append_point(Points2, x, y, z);
                    }
                }
            }
            Vect_append_point(Points2, x2, y2, z2);
            Vect_write_line(&Out_Map, type, Points2, Cats); /* write line */
        }
        else {
            if (GPJ_transform_array(&info_in, &info_out, &info_trans, PJ_FWD,
                                    Points->x, Points->y,
                                    flag.transformz->answer ? Points->z : NULL,
                                    Points->n_points) < 0) {
                G_fatal_error(
                    _("Unable to re-project vector map <%s> from <%s>"),
                    Vect_get_full_name(&Map), ilocopt->answer);
            }

            Vect_write_line(&Out_Map, type, Points, Cats); /* write line */
        }
    } /* end lines section */
    G_progress(1, 1);

    /* Copy tables */
    if (Vect_copy_tables(&Map, &Out_Map, 0))
        G_warning(_("Failed to copy attribute table to output map"));

    Vect_close(&Map);

    if (!flag.no_topol->answer)
        Vect_build(&Out_Map);
    Vect_close(&Out_Map);

    if (recommend_nowrap)
        G_important_message(_("Try to disable wrapping to -180,180 "
                              "if topological errors occurred"));

    exit(EXIT_SUCCESS);
}
