"""
Vector related functions to be used in Python scripts.

Usage:

::

    from grass.script import vector as grass

    grass.vector_db(map)

(C) 2008-2010 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.

.. sectionauthor:: Glynn Clements
.. sectionauthor:: Martin Landa <landa.martin gmail.com>
"""

import os

from .utils import parse_key_val
from .core import (
    run_command,
    read_command,
    parse_command,
    error,
    fatal,
    debug,
)

from grass.exceptions import CalledModuleError, ScriptError


def vector_db(map, env=None, **kwargs):
    """Return the database connection details for a vector map
    (interface to `v.db.connect -g`).

    :Example:
      .. code-block:: pycon

        >>> vector_db("geology")  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
        {1: {'layer': 1, ... 'table': 'geology'}}

    :param str map: vector map
    :param kwargs: other v.db.connect's arguments
    :param env: environment

    :return: dictionary
    """
    s = read_command(
        "v.db.connect", quiet=True, flags="g", map=map, sep=";", env=env, **kwargs
    )
    result = {}

    for line in s.splitlines():
        f = line.split(";")
        if len(f) != 5:
            continue

        if "/" in f[0]:
            f1 = f[0].split("/")
            layer = f1[0]
            name = f1[1]
        else:
            layer = f[0]
            name = ""

        result[int(layer)] = {
            "layer": int(layer),
            "name": name,
            "table": f[1],
            "key": f[2],
            "database": f[3],
            "driver": f[4],
        }

    return result


def vector_layer_db(map, layer, env=None):
    """Return the database connection details for a vector map layer.
    If db connection for given layer is not defined, fatal() is called.

    :param str map: map name
    :param layer: layer number
    :param env: environment

    :return: parsed output
    """
    try:
        f = vector_db(map, env=env)[int(layer)]
    except KeyError:
        fatal(_("Database connection not defined for layer %s") % layer, env=env)

    return f


# run "v.info -c ..." and parse output


def vector_columns(map, layer=None, getDict=True, env=None, **kwargs):
    """Return a dictionary (or a list) of the columns for the
    database table connected to a vector map (interface to `v.info -c`).

    >>> vector_columns("geology", getDict=True)  # doctest: +NORMALIZE_WHITESPACE
    {'PERIMETER': {'index': 2, 'type': 'DOUBLE PRECISION'}, 'GEOL250_':
    {'index': 3, 'type': 'INTEGER'}, 'SHAPE_area': {'index': 6, 'type':
    'DOUBLE PRECISION'}, 'onemap_pro': {'index': 1, 'type': 'DOUBLE
    PRECISION'}, 'SHAPE_len': {'index': 7, 'type': 'DOUBLE PRECISION'},
    'cat': {'index': 0, 'type': 'INTEGER'}, 'GEOL250_ID': {'index': 4, 'type':
    'INTEGER'}, 'GEO_NAME': {'index': 5, 'type': 'CHARACTER'}}

    >>> vector_columns("geology", getDict=False)  # doctest: +NORMALIZE_WHITESPACE
    ['cat',
     'onemap_pro',
     'PERIMETER',
     'GEOL250_',
     'GEOL250_ID',
     'GEO_NAME',
     'SHAPE_area',
     'SHAPE_len']

    :param str map: map name
    :param layer: layer number or name (None for all layers)
    :param bool getDict: True to return dictionary of columns otherwise list
                         of column names is returned
    :param kwargs: (v.info's arguments)
    :param env: environment

    :return: dictionary/list of columns
    """
    s = read_command(
        "v.info", flags="c", map=map, layer=layer, quiet=True, env=env, **kwargs
    )
    result = {} if getDict else []
    i = 0
    for line in s.splitlines():
        ctype, cname = line.split("|")
        if getDict:
            result[cname] = {"type": ctype, "index": i}
        else:
            result.append(cname)
        i += 1

    return result


def vector_history(map, replace=False, env=None):
    """Set the command history for a vector map to the command used to
    invoke the script (interface to `v.support`).

    :param str map: mapname
    :param bool replace: Replace command line instead of appending it
    :param env: environment

    :return: v.support output
    """
    run_command(
        "v.support",
        map=map,
        cmdhist=os.environ["CMDLINE"],
        flags="h" if replace else None,
        env=env,
    )


def vector_info_topo(map, layer=1, env=None):
    """Return information about a vector map (interface to `v.info -t`).

    :Example:
      .. code-block:: pycon

        >>> vector_info_topo("geology")  # doctest: +NORMALIZE_WHITESPACE
        {'lines': 0, 'centroids': 1832, 'boundaries': 3649, 'points': 0,
        'primitives': 5481, 'islands': 907, 'nodes': 2724, 'map3d': False,
        'areas': 1832}

    :param str map: map name
    :param int layer: layer number
    :param env: environment

    :return: parsed output
    """
    s = read_command("v.info", flags="t", layer=layer, map=map, env=env)
    ret = parse_key_val(s, val_type=int)
    if "map3d" in ret:
        ret["map3d"] = bool(ret["map3d"])

    return ret


def vector_info(map, layer=1, env=None):
    """Return information about a vector map (interface to
    `v.info`).

    :Example:
      .. code-block:: pycon

        >>> vector_info("geology")  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
        {'comment': '', 'projection': 'Lambert Conformal Conic' ... 'south': 10875.8272320917}

    :param str map: map name
    :param int layer: layer number
    :param env: environment

    :return: parsed vector info
    """  # noqa: E501

    s = read_command("v.info", flags="get", layer=layer, map=map, env=env)

    kv = parse_key_val(s)
    for k in ["north", "south", "east", "west", "top", "bottom"]:
        kv[k] = float(kv[k])
    for k in ["level", "num_dblinks"]:
        kv[k] = int(kv[k])
    for k in [
        "nodes",
        "points",
        "lines",
        "boundaries",
        "centroids",
        "areas",
        "islands",
        "primitives",
    ]:
        kv[k] = int(kv[k])
    if "map3d" in kv:
        kv["map3d"] = bool(int(kv["map3d"]))
        if kv["map3d"]:
            for k in ["faces", "kernels", "volumes", "holes"]:
                kv[k] = int(kv[k])

    return kv


def vector_db_select(map, layer=1, env=None, **kwargs):
    """Get attribute data of selected vector map layer.

    Function returns list of columns and dictionary of values ordered by
    key column value.

    :Example:
      .. code-block:: pycon

        >>> print(vector_db_select("geology")["columns"])
        ['cat', 'onemap_pro', 'PERIMETER', 'GEOL250_', 'GEOL250_ID', 'GEO_NAME', 'SHAPE_area', 'SHAPE_len']
        >>> print(vector_db_select("geology")["values"][3])
        ['3', '579286.875', '3335.55835', '4', '3', 'Zml', '579286.829631', '3335.557182']
        >>> print(vector_db_select("geology", columns="GEO_NAME")["values"][3])
        ['Zml']

    :param str map: map name
    :param int layer: layer number
    :param kwargs: v.db.select options
    :param env: environment

    :return: dictionary ('columns' and 'values')
    """  # noqa: E501
    try:
        key = vector_db(map=map, env=env)[layer]["key"]
    except KeyError:
        error(
            _("Missing layer %(layer)d in vector map <%(map)s>")
            % {"layer": layer, "map": map},
            env=env,
        )
        return {"columns": [], "values": {}}

    include_key = True
    if "columns" in kwargs:
        if key not in kwargs["columns"].split(","):
            # add key column if missing
            include_key = False
            debug("Adding key column to the output", env=env)
            kwargs["columns"] += "," + key

    if "format" in kwargs:
        kwargs.pop("format")
    if "separator" in kwargs:
        kwargs.pop("separator")
    try:
        data = parse_command(
            "v.db.select", map=map, layer=layer, env=env, format="csv", **kwargs
        )
    except CalledModuleError:
        error(_("vector_db_select() failed"), env=env)
        return {"columns": [], "values": {}}

    if len(data) == 0:
        return {"columns": [], "values": {}}

    columns = list(data[0].keys())
    # user didn't provide key in columns, so we added it at the end
    # and need to remove it now
    if not include_key:
        columns = columns[:-1]

    values = {}
    if not include_key:
        for record in data:
            values[int(record[key])] = [v for k, v in record.items() if k != key]
    else:
        values = {int(record[key]): list(record.values()) for record in data}

    return {"columns": columns, "values": values}


json = None
orderedDict = None


def vector_what(
    map,
    coord,
    distance=0.0,
    ttype=None,
    encoding=None,
    skip_attributes=False,
    layer=None,
    multiple=False,
    env=None,
):
    """Query vector map at given locations

    To query one vector map at one location

    ::

        print grass.vector_what(map='archsites', coord=(595743, 4925281),
                                distance=250)

        [{'Category': 8, 'Map': 'archsites', 'Layer': 1, 'Key_column': 'cat',
          'Database': '/home/martin/grassdata/spearfish60/PERMANENT/dbf/',
          'Mapset': 'PERMANENT', 'Driver': 'dbf',
          'Attributes': {'str1': 'No_Name', 'cat': '8'},
          'Table': 'archsites', 'Type': 'Point', 'Id': 8}]

    To query one vector map with multiple layers (no additional parameters
    required)

    ::

        for q in grass.vector_what(map='some_map', distance=100.0,
                                   coord=(596532.357143,4920486.21429)):
            print q['Map'], q['Layer'], q['Attributes']

        new_bug_sites 1 {'str1': 'Beetle_site', 'GRASSRGB': '', 'cat': '80'}
        new_bug_sites 2 {'cat': '80'}

    To query more vector maps at one location

    ::

        for q in grass.vector_what(map=('archsites', 'roads'),
                                   coord=(595743, 4925281), distance=250):
            print q['Map'], q['Attributes']

        archsites {'str1': 'No_Name', 'cat': '8'}
        roads {'label': 'interstate', 'cat': '1'}

    To query one vector map at more locations

    ::

        for q in grass.vector_what(map='archsites', distance=250,
                                   coord=[(595743, 4925281), (597950, 4918898)]):
            print q['Map'], q['Attributes']

        archsites {'str1': 'No_Name', 'cat': '8'}
        archsites {'str1': 'Bob_Miller', 'cat': '22'}

    :param map: vector map(s) to query given as string or list/tuple
    :param coord: coordinates of query given as tuple (easting, northing) or
                  list of tuples
    :param distance: query threshold distance (in map units)
    :param ttype: list of topology types (default of v.what are point, line,
                  area, face)
    :param encoding: attributes encoding
    :param skip_attributes: True to skip querying attributes
    :param layer: layer number or list of layers (one for each vector),
                  if None, all layers (-1) are used
    :param multiple: find multiple features within threshold distance
    :param env: environment

        .. deprecated:: 8.5.0
            Parameter ``encoding`` is deprecated.

    :return: parsed list
    """
    if not env:
        env = os.environ.copy()
    if "LC_ALL" in env:
        env["LC_ALL"] = "C"

    map_list = [map] if isinstance(map, (bytes, str)) else map

    if layer:
        if isinstance(layer, (tuple, list)):
            layer_list = [str(item) for item in layer]
        else:
            layer_list = [str(layer)]
        if len(layer_list) != len(map_list):
            raise ScriptError(
                _(
                    "Number of given vector maps ({m}) "
                    "differs from number of layers ({l})"
                ).format(m=len(map_list), l=len(layer_list))
            )
    else:
        layer_list = ["-1"] * len(map_list)

    coord_list = []
    if isinstance(coord, tuple):
        coord_list.append("%f,%f" % (coord[0], coord[1]))
    else:
        for e, n in coord:
            coord_list.append("%f,%f" % (e, n))

    flags = "j"
    if not skip_attributes:
        flags += "a"
    if multiple:
        flags += "m"
    cmdParams = {
        "quiet": True,
        "flags": flags,
        "map": ",".join(map_list),
        "layer": ",".join(layer_list),
        "coordinates": ",".join(coord_list),
        "distance": float(distance),
    }
    if ttype:
        cmdParams["type"] = ",".join(ttype)

    try:
        ret = read_command("v.what", env=env, **cmdParams).strip()
    except CalledModuleError as e:
        raise ScriptError(e.msg)

    data = []
    if not ret:
        return data

    # lazy import
    global json, orderedDict
    if json is None:
        import json
    if orderedDict is None:
        try:
            from collections import OrderedDict

            orderedDict = OrderedDict
        except ImportError:
            orderedDict = dict

    kwargs = {}
    kwargs["object_pairs_hook"] = orderedDict

    try:
        result = json.loads(ret, **kwargs)
    except ValueError:
        raise ScriptError(
            _("v.what output is not valid JSON format:\n {ret}").format(ret=ret)
        )

    if multiple:
        for vmap in result["Maps"]:
            features = vmap.pop("Features", None)
            if features:
                for feature in features:
                    cats = feature.pop("Categories", None)
                    if cats:
                        for cat in cats:
                            tmp = feature.copy()
                            tmp.update(cat)
                            tmp2 = vmap.copy()
                            tmp2.update(tmp)
                            data.append(tmp2)
    else:
        for vmap in result["Maps"]:
            cats = vmap.pop("Categories", None)
            if cats:
                for cat in cats:
                    tmp = vmap.copy()
                    tmp.update(cat)
                    data.append(tmp)
            else:
                data.append(vmap)
    return data
