#!/usr/bin/env python
#-*- coding:utf-8 -*-
#
# This file is part of the PyNCulture project, which aims at providing tools to
# easily generate complex neuronal cultures.
# Copyright (C) 2017 SENeC Initiative
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# 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.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# These tools are inspired by Sean Gillies' `descartes` library, that you can
# find here: https://pypi.python.org/pypi/descartes. They are released under
# a BSD license.

""" Plotting functions for shapely objects """

import numpy as np

from matplotlib.patches import PathPatch
from matplotlib.path import Path


def plot_shape(shape, axis=None, m='', mc="#999999", fc="#8888ff",
               ec="#444444", alpha=0.5, brightness="height", show_contour=True,
               show=True, **kwargs):
    '''
    Plot a shape (you should set the `axis` aspect to 1 to respect the
    proportions).

    Parameters
    ----------
    shape : :class:`Shape`
        Shape to plot.
    axis : :class:`matplotlib.axes.Axes` instance, optional (default: None)
        Axis on which the shape should be plotted. By default, a new figure
        is created.
    m : str, optional (default: invisible)
        Marker to plot the shape's vertices, matplotlib syntax.
    mc : str, optional (default: "#999999")
        Color of the markers.
    fc : str, optional (default: "#8888ff")
        Color of the shape's interior.
    ec : str, optional (default: "#444444")
        Color of the shape's edges.
    alpha : float, optional (default: 0.5)
        Opacity of the shape's interior.
    brightness : str, optional (default: height)
        Show how different other areas are from the 'default_area' (lower
        values are darker, higher values are lighter).
        Difference can concern the 'height', or any of the `properties` of the
        :class:`Area` objects.
    show_contour : bool, optional (default: True)
        Whether the shapes should be drawn with a contour.
    show : bool, optional (default: True)
        Whether the plot should be displayed immediately.
    **kwargs: keywords arguments for :class:`matplotlib.patches.PathPatch`
    '''
    # import
    import matplotlib.pyplot as plt
    MultiPolygon = None
    try:
        from shapely.geometry import (MultiPolygon, Polygon, LineString,
                                      MultiLineString)
    except ImportError:
        pass

    if axis is None:
        fig, axis = plt.subplots()

    zorder = kwargs.get("zorder", 0)
    if "zorder" in kwargs:
        del kwargs["zorder"]

    # plot the main shape
    if isinstance(shape, MultiPolygon):
        for p in shape:
            plot_shape(p, axis=axis, m=m, mc=mc, fc=fc, ec=ec, alpha=alpha,
                       brightness=brightness, show=False,
                       show_contour=show_contour, **kwargs)
    elif isinstance(shape, Polygon) and shape.exterior.coords:
        if show_contour:
            _plot_coords(axis, shape.exterior, m, mc, ec)
            for path in shape.interiors:
                _plot_coords(axis, path.coords, m, mc, ec)
        patch = _make_patch(shape, color=fc, alpha=alpha, zorder=zorder,
                            **kwargs)
        axis.add_patch(patch)

        # take care of the areas
        if hasattr(shape, "areas"):
            def_area = shape.areas["default_area"]

            # get the highest and lowest properties
            mean = _get_prop(def_area, brightness)
            low, high = np.inf, -np.inf

            for name, area in shape.areas.items():
                if name != "default_area":
                    prop = _get_prop(area, brightness)
                    if prop < low:
                        low = prop
                    if prop > high:
                        high = prop

            # plot the areas
            for name, area in shape.areas.items():
                if name != "default_area":
                    prop         = _get_prop(area, brightness)
                    color        = fc
                    local_alpha  = 0
                    if prop < mean:
                        color       = "black"
                        local_alpha = alpha * (prop - mean) / (low - mean)
                    elif prop > mean:
                        color       = "white"
                        local_alpha = alpha * (prop - mean) / (high - mean)
                    # contour
                    _plot_coords(axis, area.exterior, m, mc, ec)
                    for path in shape.interiors:
                        _plot_coords(axis, path.coords, m, mc, ec)
                    # content
                    patch = _make_patch(
                        area, color=color, alpha=local_alpha, zorder=zorder,
                        **kwargs)
                    axis.add_patch(patch)
    elif isinstance(shape, (LineString, MultiLineString)):
        lines = [shape] if isinstance(shape, LineString) else shape.geoms
        for line in lines:
            _plot_coords(axis, line.coords, m, mc, ec)

    axis.set_aspect(1)

    if show:
        plt.show()


def _make_patch(shape, **kwargs):
    '''
    Construct a matplotlib patch from a geometric object

    Parameters
    ----------
    shape: :class:`NetGrowth.geometry.Shape`
        may be a Shapely or GeoJSON-like object with or without holes.
    kwargs: keywords arguments for :class:`matplotlib.patches.PathPatch`

    Returns
    -------
    an instance of :class:`matplotlib.patches.PathPatch`.

    Example
    -------
    (using Shapely Point and a matplotlib axes):

      >>> b = Point(0, 0).buffer(1.0)
      >>> patch = PolygonPatch(b, fc='blue', ec='blue', alpha=0.5)
      >>> axis.add_patch(patch)

    Modified from `descartes` by Sean Gillies (BSD license).
    '''
    vertices = np.concatenate(
        [np.asarray(shape.exterior.coords)[:, :2]] +
        [np.asarray(h.coords)[:, :2] for h in shape.interiors])
    instructions = np.concatenate(
        [_path_instructions(shape.exterior)] +
        [_path_instructions(h) for h in shape.interiors])

    path = Path(vertices, instructions)
    return PathPatch(path, **kwargs)


def _path_instructions(ob):
    '''
    Give instructions to build path from vertices.
    '''
    # The codes will be all "LINETO" commands, except for "MOVETO"s at the
    # beginning of each subpath
    n = len(ob.coords)
    vals = np.ones(n, dtype=Path.code_type) * Path.LINETO
    vals[0] = Path.MOVETO
    return vals


def _plot_coords(ax, ob, m, mc, ec):
    if hasattr(ob, 'coords') and isinstance(ob.coords, list):
        for coord in ob.coords:
            x, y = ob.xy
            ax.plot(x, y, m, ls='-', c=ec, markerfacecolor=mc, zorder=1)
    else:
        x, y = ob.xy
        ax.plot(x, y, m, ls='-', c=ec, markerfacecolor=mc, zorder=1)


def _get_prop(area, brightness):
    is_height = (brightness == "height")
    return area.height if is_height else area.properties[brightness]
