#!/usr/bin/env python

############################################################################
#
# MODULE:    r.shaded.pca
# AUTHOR(S): Vaclav Petras
# PURPOSE:   Creates RGB composition from PCA of hill shades
# COPYRIGHT: (C) 2013-2014 by Vaclav Petras and 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.
#
#############################################################################

#%module
#% label: Creates relief shades from various directions and combines them into RGB composition.
#% description: The combined shades highlight terrain features which wouldn't be visible using standard shading technique.
#% keyword: raster
#% keyword: elevation
#% keyword: terrain
#% keyword: visualization
#%end
#%option
#% type: string
#% gisprompt: old,cell,raster
#% key: input
#% description: Name of the input elevation raster map
#% required: yes
#%end
#%option
#% type: string
#% gisprompt: new,cell,raster
#% key: output
#% description: Name for output PCA shaded relief map
#% required: yes
#%end
#%option
#% key: altitude
#% type: double
#% description: Altitude of the sun in degrees above the horizon
#% options: 0-90
#% answer: 30
#%end
#%option
#% key: nazimuths
#% type: integer
#% description: The number of azimuths (suggested values are 4, 8, 16, 32)
#% answer: 8
#%end
#%option
#% key: zscale
#% type: double
#% description: Factor for exaggerating relief
#% answer: 1
#%end
#%option
#% key: scale
#% type: double
#% description: Azimuth of the sun in degrees to the east of north
#% answer: 1
#%end
#%option
#% key: units
#% type: string
#% description: Elevation units (overrides scale factor)
#% options: intl,survey
#% descriptions: intl;international feet;survey;survey feet
#%end
#%option
#% key: shades_basename
#% type: string
#% label: Base name for output shades map
#% description: A base of the name of shades maps for all azimuths. An underscore ('_') and a azimuth will be added to the base name. When empty, no maps will be outputted (although they need to be generated).
#%end
#%option
#% key: pca_shades_basename
#% type: string
#% label: Base name for output PCA shades map
#% description: A base of the name of PCA shades maps. An underscore ('_') and a azimuth will be added to the base name. When empty, no maps will be outputted (although they need to be generated).
#%end
#%option
#% key: nprocs
#% type: integer
#% description: Number of r.shade.relief processes to run in parallel
#% options: 1-
#% answer: 1
#%end


import os
import atexit
from multiprocessing import Process

import grass.script as grass
import grass.script.core as core

REMOVE = []
MREMOVE = []


def cleanup():
    if REMOVE or MREMOVE:
        core.info(_("Cleaning temporary maps..."))
    for rast in REMOVE:
        grass.run_command('g.remove', flags = 'f', type = 'raster', name = rast, quiet=True)
    for pattern in MREMOVE:
        grass.run_command('g.remove', flags = 'f', type = 'raster', pattern ='%s*' % pattern,
                           quiet=True)


def is_grass_7():
    if core.version()['version'].split('.')[0] == '7':
        return True
    return False


def create_tmp_map_name(name):
    return '{mod}_{pid}_{map_}_tmp'.format(mod='r_shaded_pca',
                                           pid=os.getpid(),
                                           map_=name)


# add latitude map
def run_r_shaded_relief(elevation_input, shades_basename,
                        altitude, azimuth, z_exaggeration,
                        scale, units, suffix):
    params = {}
    if units:
        params.update({'units': units})
    grass.run_command('r.relief', input=elevation_input,
                      output=shades_basename + suffix,
                      azimuth=azimuth, zscale=z_exaggeration,
                      scale=scale, altitude=altitude,
                      overwrite=core.overwrite(), quiet=True,
                      **params)


def set_color_table(rasters, map_):
    if is_grass_7():
        grass.run_command('r.colors', map=rasters, raster=map_, quiet=True)
    else:
        for rast in rasters:
            grass.run_command('r.colors', map=rast, raster=map_, quiet=True)


def check_map_names(basename, mapset, suffixes):
    for suffix in suffixes:
        map_ = '%s%s%s' % (basename, '_', suffix)
        if grass.find_file(map_, element='cell', mapset=mapset)['file']:
            grass.fatal(_("Raster map <%s> already exists. "
                          "Change the base name or allow overwrite.") % map_)


def frange(x, y, step):
    # we want to be close to range behaviour, so no <=
    while x < y:
        yield x
        x += step


def main():
    options, flags = grass.parser()

    elevation_input = options['input']
    pca_shade_output = options['output']
    altitude = float(options['altitude'])
    number_of_azimuths = int(options['nazimuths'])
    z_exaggeration = float(options['zscale'])
    scale = float(options['scale'])
    units = options['units']
    shades_basename = options['shades_basename']
    pca_basename = pca_basename_user = options['pca_shades_basename']
    nprocs = int(options['nprocs'])

    full_circle = 360
    # let's use floats here and leave the consequences to the user
    smallest_angle = float(full_circle) / number_of_azimuths
    azimuths = list(frange(0, full_circle, smallest_angle))

    if not shades_basename:
        shades_basename = create_tmp_map_name('shade')
        MREMOVE.append(shades_basename)

    if not pca_basename:
        pca_basename = pca_shade_output + '_pca'
    pca_maps = [pca_basename + '.' + str(i)
                for i in range(1, number_of_azimuths + 1)]
    if not pca_basename_user:
        REMOVE.extend(pca_maps)

    # here we check all the posible
    if not grass.overwrite():
        check_map_names(shades_basename, grass.gisenv()['MAPSET'],
                        suffixes=azimuths)
        check_map_names(pca_basename, grass.gisenv()['MAPSET'],
                        suffixes=range(1, number_of_azimuths))

    grass.info(_("Running r.relief in a loop..."))
    count = 0
    # Parallel processing
    proc_list = []
    proc_count = 0
    suffixes = []
    all_suffixes = []
    core.percent(0, number_of_azimuths, 1)
    for azimuth in azimuths:
        count += 1
        core.percent(count, number_of_azimuths, 10)

        suffix = '_' + str(azimuth)
        proc_list.append(Process(target=run_r_shaded_relief,
                                 args=(elevation_input, shades_basename,
                                       altitude, azimuth, z_exaggeration,
                                       scale, units,
                                       suffix)))

        proc_list[proc_count].start()
        proc_count += 1
        suffixes.append(suffix)
        all_suffixes.append(suffix)

        if proc_count == nprocs or proc_count == number_of_azimuths \
                or count == number_of_azimuths:
            proc_count = 0
            exitcodes = 0
            for proc in proc_list:
                proc.join()
                exitcodes += proc.exitcode

            if exitcodes != 0:
                core.fatal(_("Error during r.relief computation"))

            # Empty process list
            proc_list = []
            suffixes = []
    # FIXME: how percent really works?
    # core.percent(1, 1, 1)

    shade_maps = [shades_basename + suf for suf in all_suffixes]

    grass.info(_("Running r.pca..."))

    # not quiet=True to get percents
    grass.run_command('i.pca', input=shade_maps, output=pca_basename,
                      overwrite=core.overwrite())

    grass.info(_("Creating RGB composite from "
                 "PC1 (red), PC2 (green), PC3 (blue) ..."))
    grass.run_command('r.composite',
                      red=pca_maps[0],
                      green=pca_maps[1],
                      blue=pca_maps[2],
                      output=pca_shade_output,
                      overwrite=core.overwrite(), quiet=True)
    grass.raster_history(pca_shade_output)

    if pca_basename_user:
        set_color_table(pca_maps, map_=shade_maps[0])


if __name__ == "__main__":
    atexit.register(cleanup)
    main()
