#!/opt/local/bin/python3.12
############################################################################
#
# MODULE:       r3.in.xyz
# AUTHOR:       M. Hamish Bowman, Dunedin, New Zealand
# PURPOSE:      Run r.in.xyz in a loop for various z-levels and construct
# 		a 3D raster. Unlike r.in.xyz, reading from stdin and z-scaling
# 		won't work.
#
# COPYRIGHT:    (c) 2011-2012 Hamish Bowman, and the GRASS Development Team
# 		Port of r3.in.xyz(.sh) for GRASS 6.4.
#               This program is free software under the GNU General Public
#               License (>=v2). Read the file COPYING that comes with GRASS
#               for details.
#
# 		This program is distributed in the hope that it will be useful,
# 		but WITHOUT ANY WARRANTY; without even the implied warranty of
# 		MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# 		GNU General Public License for more details.
#
#############################################################################

# %Module
# % description: Create a 3D raster map from an assemblage of many coordinates using univariate statistics
# % keyword: raster3d
# % keyword: import
# % keyword: voxel
# % keyword: LIDAR
# % keyword: statistics
# % keyword: conversion
# % keyword: aggregation
# % keyword: binning
# %End
# %Flag
# % key: s
# % description: Scan data file for extent then exit
# %End
# %Flag
# % key: g
# % description: In scan mode, print using shell script style
# %End
# %Flag
# % key: i
# % description: Ignore broken lines
# %End
# %Option G_OPT_F_INPUT
# % required: yes
# % description: ASCII file containing input data
# %End
# %Option
# % key: output
# % type: string
# % required: yes
# % multiple: no
# % key_desc: name
# % description: Name for output raster map
# % gisprompt: new,grid3,3d-raster
# %End
# %Option
# % key: method
# % type: string
# % required: no
# % multiple: no
# % options: n,min,max,range,sum,mean,stddev,variance,coeff_var,median,percentile,skewness,trimmean
# % description: Statistic to use for raster values
# % answer: mean
# % guisection: Statistic
# %End
# %Option
# % key: type
# % type: string
# % required: no
# % multiple: no
# % options: float,double
# % description: Storage type for resultant raster map
# % answer: float
# %End
# %Option G_OPT_F_SEP
# % guisection: Input
# %End
# %Option
# % key: x
# % type: integer
# % required: no
# % multiple: no
# % description: Column number of x coordinates in input file (first column is 1)
# % answer: 1
# % guisection: Input
# %End
# %Option
# % key: y
# % type: integer
# % required: no
# % multiple: no
# % description: Column number of y coordinates in input file
# % answer: 2
# % guisection: Input
# %End
# %Option
# % key: z
# % type: integer
# % required: no
# % multiple: no
# % description: Column number of z coordinates in input file
# % answer: 3
# % guisection: Input
# %End
# %Option
# % key: value_column
# % type: integer
# % required: no
# % multiple: no
# % label: Column number of data values in input file
# % description: If not given or set to 0, the data points' z-values are used
# % answer: 0
# % guisection: Input
# %End
# %Option
# % key: vrange
# % type: double
# % required: no
# % key_desc: min,max
# % description: Filter range for value column data (min,max)
# %End
# %option
# % key: vscale
# % type: double
# % required: no
# % multiple: no
# % description: Scaling factor to apply to value column data
# % answer: 1.0
# %end
# %Option
# % key: percent
# % type: integer
# % required: no
# % multiple: no
# % options: 1-100
# % description: Percent of map to keep in memory
# % answer: 100
# %End
# %Option
# % key: pth
# % type: integer
# % required: no
# % multiple: no
# % options: 1-100
# % description: pth percentile of the values
# % guisection: Statistic
# %End
# %Option
# % key: trim
# % type: double
# % required: no
# % multiple: no
# % options: 0-50
# % description: Discard <trim> percent of the smallest and <trim> percent of the largest observations
# % guisection: Statistic
# %End
# %Option
# % key: workers
# % type: integer
# % required: no
# % multiple: no
# % options: 1-256
# % answer: 1
# % description: Number of parallel processes to launch
# %End


import sys
import os
import atexit
from grass.script import core as grass
from grass.exceptions import CalledModuleError


def cleanup():
    grass.run_command(
        "g.remove",
        flags="f",
        type="rast",
        pattern="tmp.r3xyz.%d.*" % os.getpid(),
        quiet=True,
    )


def main():
    infile = options["input"]
    output = options["output"]
    method = options["method"]
    dtype = options["type"]
    fs = options["separator"]
    x = options["x"]
    y = options["y"]
    z = options["z"]
    value_column = options["value_column"]
    vrange = options["vrange"]
    vscale = options["vscale"]
    percent = options["percent"]
    pth = options["pth"]
    trim = options["trim"]
    workers = int(options["workers"])
    scan_only = flags["s"]
    shell_style = flags["g"]
    ignore_broken = flags["i"]

    if workers == 1 and "WORKERS" in os.environ:
        workers = int(os.environ["WORKERS"])

    if not os.path.exists(infile):
        grass.fatal(_("Unable to read input file <%s>") % infile)

    addl_opts = {}
    if pth:
        addl_opts["pth"] = "%s" % pth
    if trim:
        addl_opts["trim"] = "%s" % trim
    if value_column:
        addl_opts["value_column"] = "%s" % value_column
    if vrange:
        addl_opts["vrange"] = "%s" % vrange
    if vscale:
        addl_opts["vscale"] = "%s" % vscale
    if ignore_broken:
        addl_opts["flags"] = "i"

    if scan_only or shell_style:
        if shell_style:
            doShell = "g"
        else:
            doShell = ""
        grass.run_command(
            "r.in.xyz",
            flags="s" + doShell,
            input=infile,
            output="dummy",
            sep=fs,
            x=x,
            y=y,
            z=z,
            **addl_opts,
        )
        sys.exit()

    if dtype == "float":
        data_type = "FCELL"
    else:
        data_type = "DCELL"

    region = grass.region(region3d=True)

    if region["nsres"] != region["nsres3"] or region["ewres"] != region["ewres3"]:
        grass.run_command("g.region", flags="3p")
        grass.fatal(_("The 2D and 3D region settings are different. Can not continue."))

    grass.verbose(
        _("Region bottom=%.15g  top=%.15g  vertical_cell_res=%.15g  (%d depths)")
        % (region["b"], region["t"], region["tbres"], region["depths"])
    )

    grass.verbose(_("Creating slices ..."))

    # to avoid a point which falls exactly on a top bound from being
    # considered twice, we shrink the
    # For the top slice we keep it though, as someone scanning the bounds
    # may have set the bounds exactly to the data extent (a bad idea, but
    # it happens..)
    eps = 1.0e-15

    # if there are thousands of depths hopefully this array doesn't get too
    # large and so we don't have to worry much about storing/looping through
    # all the finished process infos.
    proc = {}
    pout = {}

    depths = list(range(1, 1 + region["depths"]))

    for i in depths:
        tmp_layer_name = "tmp.r3xyz.%d.%s" % (os.getpid(), "%05d" % i)

        zrange_min = region["b"] + (region["tbres"] * (i - 1))

        if i < region["depths"]:
            zrange_max = region["b"] + (region["tbres"] * i) - eps
        else:
            zrange_max = region["b"] + (region["tbres"] * i)

        # spawn depth layer import job in the background
        # grass.debug("slice %d, <%s>  %% %d" % (band, image[band], band % workers))
        grass.message(
            _("Processing horizontal slice %d of %d [%.15g,%.15g) ...")
            % (i, region["depths"], zrange_min, zrange_max)
        )

        proc[i] = grass.start_command(
            "r.in.xyz",
            input=infile,
            output=tmp_layer_name,
            sep=fs,
            method=method,
            x=x,
            y=y,
            z=z,
            percent=percent,
            type=data_type,
            zrange="%.15g,%.15g" % (zrange_min, zrange_max),
            **addl_opts,
        )

        grass.debug("i=%d, %%=%d  (workers=%d)" % (i, i % workers, workers))
        # print sys.getsizeof(proc)  # sizeof(proc array)  [not so big]

        if i % workers == 0:
            # wait for the ones launched so far to finish
            for p_i in depths[:i]:
                pout[p_i] = proc[p_i].communicate()[0]
                if proc[p_i].wait() != 0:
                    grass.fatal(_("Trouble importing data. Aborting."))

    # wait for jSobs to finish, collect any stray output
    for i in depths:
        pout[i] = proc[i].communicate()[0]
        if proc[i].wait() != 0:
            grass.fatal(_("Trouble importing data. Aborting."))

    del proc

    grass.verbose(_("Assembling 3D cube ..."))

    # input order: lower most strata first
    slices = grass.read_command(
        "g.list", type="raster", sep=",", pattern="tmp.r3xyz.%d.*" % os.getpid()
    ).rstrip(os.linesep)
    grass.debug(slices)

    try:
        grass.run_command("r.to.rast3", input=slices, output=output)
    except CalledModuleError:
        grass.message(_("Done. 3D raster map <%s> created.") % output)


if __name__ == "__main__":
    options, flags = grass.parser()
    atexit.register(cleanup)
    main()
