# -*- coding: utf-8 -*-

"""
Classes for drawing maps.

"""

from __future__ import (absolute_import, division, print_function)

import json

from collections import OrderedDict

from branca.element import CssLink, Element, Figure, Html, JavascriptLink, MacroElement  # noqa

from folium.utilities import _validate_coordinates, get_bounds, camelize

from jinja2 import Template

from six import binary_type, text_type


class Layer(MacroElement):
    """An abstract class for everything that is a Layer on the map.
    It will be used to define whether an object will be included in
    LayerControls.

    Parameters
    ----------
    name : string, default None
        The name of the Layer, as it will appear in LayerControls
    overlay : bool, default False
        Adds the layer as an optional overlay (True) or the base layer (False).
    control : bool, default True
        Whether the Layer will be included in LayerControls.
    show: bool, default True
        Whether the layer will be shown on opening (only for overlays).
    """
    def __init__(self, name=None, overlay=False, control=True, show=True):
        super(Layer, self).__init__()
        self.layer_name = name if name is not None else self.get_name()
        self.overlay = overlay
        self.control = control
        self.show = show


class FeatureGroup(Layer):
    """
    Create a FeatureGroup layer ; you can put things in it and handle them
    as a single layer.  For example, you can add a LayerControl to
    tick/untick the whole group.

    Parameters
    ----------
    name : str, default None
        The name of the featureGroup layer.
        It will be displayed in the LayerControl.
        If None get_name() will be called to get the technical (ugly) name.
    overlay : bool, default True
        Whether your layer will be an overlay (ticked with a check box in
        LayerControls) or a base layer (ticked with a radio button).
    control: bool, default True
        Whether the layer will be included in LayerControls.
    show: bool, default True
        Whether the layer will be shown on opening (only for overlays).
    """
    _template = Template(u"""
        {% macro script(this, kwargs) %}
            var {{this.get_name()}} = L.featureGroup(
                ).addTo({{this._parent.get_name()}});
        {% endmacro %}
        """)

    def __init__(self, name=None, overlay=True, control=True, show=True):
        super(FeatureGroup, self).__init__(name=name, overlay=overlay,
                                           control=control, show=show)
        self._name = 'FeatureGroup'

        self.tile_name = name if name is not None else self.get_name()


class LayerControl(MacroElement):
    """
    Creates a LayerControl object to be added on a folium map.

    Parameters
    ----------
    position : str
          The position of the control (one of the map corners), can be
          'topleft', 'topright', 'bottomleft' or 'bottomright'
          default: 'topright'
    collapsed : boolean
          If true the control will be collapsed into an icon and expanded on
          mouse hover or touch.
          default: True
    autoZIndex : boolean
          If true the control assigns zIndexes in increasing order to all of
          its layers so that the order is preserved when switching them on/off.
          default: True
    """
    _template = Template("""
        {% macro script(this,kwargs) %}
            var {{this.get_name()}} = {
                base_layers : { {% for key,val in this.base_layers.items() %}"{{key}}" : {{val}},{% endfor %} },
                overlays : { {% for key,val in this.overlays.items() %}"{{key}}" : {{val}},{% endfor %} }
                };
            L.control.layers(
                {{this.get_name()}}.base_layers,
                {{this.get_name()}}.overlays,
                {position: '{{this.position}}',
                 collapsed: {{this.collapsed}},
                 autoZIndex: {{this.autoZIndex}}
                }).addTo({{this._parent.get_name()}});
            {% for val in this.layers_untoggle %}
                {{ val }}.remove();{% endfor %}
        {% endmacro %}
        """)  # noqa

    def __init__(self, position='topright', collapsed=True, autoZIndex=True):
        super(LayerControl, self).__init__()
        self._name = 'LayerControl'
        self.position = position
        self.collapsed = str(collapsed).lower()
        self.autoZIndex = str(autoZIndex).lower()
        self.base_layers = OrderedDict()
        self.overlays = OrderedDict()
        self.layers_untoggle = []

    def render(self, **kwargs):
        """Renders the HTML representation of the element."""
        self.base_layers = OrderedDict(
            [(val.layer_name, val.get_name()) for key, val in
             self._parent._children.items() if isinstance(val, Layer)
             and not val.overlay and val.control])
        self.overlays = OrderedDict(
            [(val.layer_name, val.get_name()) for key, val in
             self._parent._children.items() if isinstance(val, Layer)
             and val.overlay and val.control])
        self.layers_untoggle = [
            val.get_name() for val in
            self._parent._children.values() if isinstance(val, Layer)
            and val.overlay and val.control and not val.show]
        for additional_base_layer in list(self.base_layers.values())[1:]:
            self.layers_untoggle.append(additional_base_layer)
        super(LayerControl, self).render()


class Icon(MacroElement):
    """
    Creates an Icon object that will be rendered
    using Leaflet.awesome-markers.

    Parameters
    ----------
    color : str, default 'blue'
        The color of the marker. You can use:

            ['red', 'blue', 'green', 'purple', 'orange', 'darkred',
             'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue',
             'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen',
             'gray', 'black', 'lightgray']

    icon_color : str, default 'white'
        The color of the drawing on the marker. You can use colors above,
        or an html color code.
    icon : str, default 'info-sign'
        The name of the marker sign.
        See Font-Awesome website to choose yours.
        Warning : depending on the icon you choose you may need to adapt
        the `prefix` as well.
    angle : int, default 0
        The icon will be rotated by this amount of degrees.
    prefix : str, default 'glyphicon'
        The prefix states the source of the icon. 'fa' for font-awesome or
        'glyphicon' for bootstrap 3.


    https://github.com/lvoogdt/Leaflet.awesome-markers

    """
    _template = Template(u"""
            {% macro script(this, kwargs) %}

                var {{this.get_name()}} = L.AwesomeMarkers.icon({
                    icon: '{{this.icon}}',
                    iconColor: '{{this.icon_color}}',
                    markerColor: '{{this.color}}',
                    prefix: '{{this.prefix}}',
                    extraClasses: 'fa-rotate-{{this.angle}}'
                    });
                {{this._parent.get_name()}}.setIcon({{this.get_name()}});
            {% endmacro %}
            """)

    def __init__(self, color='blue', icon_color='white', icon='info-sign',
                 angle=0, prefix='glyphicon'):
        super(Icon, self).__init__()
        self._name = 'Icon'
        self.color = color
        self.icon = icon
        self.icon_color = icon_color
        self.angle = angle
        self.prefix = prefix


class Marker(MacroElement):
    """
    Create a simple stock Leaflet marker on the map, with optional
    popup text or Vincent visualization.

    Parameters
    ----------
    location: tuple or list, default None
        Latitude and Longitude of Marker (Northing, Easting)
    popup: string or folium.Popup, default None
        Label for the Marker; either an escaped HTML string to initialize
        folium.Popup or a folium.Popup instance.
    tooltip: str or folium.Tooltip, default None
        Display a text when hovering over the object.
    icon: Icon plugin
        the Icon plugin to use to render the marker.

    Returns
    -------
    Marker names and HTML in obj.template_vars

    Examples
    --------
    >>> Marker(location=[45.5, -122.3], popup='Portland, OR')
    >>> Marker(location=[45.5, -122.3], popup=folium.Popup('Portland, OR'))
    # If the popup label has characters that need to be escaped in HTML
    >>> Marker(location=[45.5, -122.3],
               popup=folium.Popup('Mom & Pop Arrow Shop >>', parse_html=True))
    """
    _template = Template(u"""
        {% macro script(this, kwargs) %}
        var {{this.get_name()}} = L.marker(
            [{{this.location[0]}}, {{this.location[1]}}],
            {
                icon: new L.Icon.Default()
                }
            ).addTo({{this._parent.get_name()}});
        {% endmacro %}
        """)

    def __init__(self, location, popup=None, tooltip=None, icon=None):
        super(Marker, self).__init__()
        self._name = 'Marker'
        self.tooltip = tooltip
        self.location = _validate_coordinates(location)
        if icon is not None:
            self.add_child(icon)
        if isinstance(popup, text_type) or isinstance(popup, binary_type):
            self.add_child(Popup(popup))
        elif popup is not None:
            self.add_child(popup)
        if isinstance(tooltip, Tooltip):
            self.add_child(tooltip)
        elif tooltip is not None:
            self.add_child(Tooltip(tooltip.__str__()))

    def _get_self_bounds(self):
        """
        Computes the bounds of the object itself (not including it's children)
        in the form [[lat_min, lon_min], [lat_max, lon_max]].

        """
        return get_bounds(self.location)


class Popup(Element):
    """Create a Popup instance that can be linked to a Layer.

    Parameters
    ----------
    html: string or Element
        Content of the Popup.
    parse_html: bool, default False
        True if the popup is a template that needs to the rendered first.
    max_width: int, default 300
        The maximal width of the popup.
    show: bool, default False
        True renders the popup open on page load.
    sticky: bool, default False
        True prevents map and other popup clicks from closing.
    """
    _template = Template(u"""
            var {{this.get_name()}} = L.popup({maxWidth: '{{this.max_width}}'
            {% if this.show or this.sticky %}, autoClose: false{% endif %}
            {% if this.sticky %}, closeOnClick: false{% endif %}});

            {% for name, element in this.html._children.items() %}
                var {{name}} = $('{{element.render(**kwargs).replace('\\n',' ')}}')[0];
                {{this.get_name()}}.setContent({{name}});
            {% endfor %}

            {{this._parent.get_name()}}.bindPopup({{this.get_name()}})
            {% if this.show %}.openPopup(){% endif %};

            {% for name, element in this.script._children.items() %}
                {{element.render()}}
            {% endfor %}
        """)  # noqa

    def __init__(self, html=None, parse_html=False, max_width=300, show=False,
                 sticky=False):
        super(Popup, self).__init__()
        self._name = 'Popup'
        self.header = Element()
        self.html = Element()
        self.script = Element()

        self.header._parent = self
        self.html._parent = self
        self.script._parent = self

        script = not parse_html

        if isinstance(html, Element):
            self.html.add_child(html)
        elif isinstance(html, text_type) or isinstance(html, binary_type):
            self.html.add_child(Html(text_type(html), script=script))

        self.max_width = max_width
        self.show = show
        self.sticky = sticky

    def render(self, **kwargs):
        """Renders the HTML representation of the element."""
        for name, child in self._children.items():
            child.render(**kwargs)

        figure = self.get_root()
        assert isinstance(figure, Figure), ('You cannot render this Element '
                                            'if it is not in a Figure.')

        figure.script.add_child(Element(
            self._template.render(this=self, kwargs=kwargs)),
            name=self.get_name())


class Tooltip(MacroElement):
    """
    Create a tooltip that shows text when hovering over its parent object.

    Parameters
    ----------
    text: str
        String to display as a tooltip on the object. If the argument is of a
        different type it will be converted to str.
    style: str, default None.
        HTML inline style properties like font and colors. Will be applied to
        a div with the text in it.
    sticky: bool, default True
        Whether the tooltip should follow the mouse.
    **kwargs: Assorted.
        These values will map directly to the Leaflet Options. More info
        available here: https://leafletjs.com/reference-1.2.0#tooltip

    """
    _template = Template(u"""
        {% macro script(this, kwargs) %}
        {{ this._parent.get_name() }}.bindTooltip(
            '<div{% if this.style %} style="{{ this.style }}"{% endif %}>'
            + '{{ this.text }}' + '</div>',
            {{ this.options }}
        );
        {% endmacro %}
        """)
    valid_options = {
        "pane": (str, ),
        "offset": (tuple, ),
        "direction": (str, ),
        "permanent": (bool, ),
        "sticky": (bool, ),
        "interactive": (bool, ),
        "opacity": (float, int),
        "attribution": (str, ),
        "className": (str, ),
    }

    def __init__(self, text, style=None, sticky=True, **kwargs):
        super(Tooltip, self).__init__()
        self._name = "Tooltip"

        self.text = str(text)

        kwargs.update({'sticky': sticky})
        self.options = self.parse_options(kwargs)

        if style:
            assert isinstance(style, str), \
                "Pass a valid inline HTML style property string to style."
            # noqa outside of type checking.
            self.style = style

    def parse_options(self, kwargs):
        """Validate the provided kwargs and return options as json string."""
        kwargs = {camelize(key): value for key, value in kwargs.items()}
        for key in kwargs.keys():
            assert key in self.valid_options, (
                "The option {} is not in the available options: {}."
                .format(key, ', '.join(self.valid_options))
            )
            assert isinstance(kwargs[key], self.valid_options[key]), (
                "The option {} must be one of the following types: {}."
                .format(key, self.valid_options[key])
            )
        return json.dumps(kwargs)


class FitBounds(MacroElement):
    """Fit the map to contain a bounding box with the
    maximum zoom level possible.

    Parameters
    ----------
    bounds: list of (latitude, longitude) points
        Bounding box specified as two points [southwest, northeast]
    padding_top_left: (x, y) point, default None
        Padding in the top left corner. Useful if some elements in
        the corner, such as controls, might obscure objects you're zooming
        to.
    padding_bottom_right: (x, y) point, default None
        Padding in the bottom right corner.
    padding: (x, y) point, default None
        Equivalent to setting both top left and bottom right padding to
        the same value.
    max_zoom: int, default None
        Maximum zoom to be used.
    """
    _template = Template(u"""
            {% macro script(this, kwargs) %}
                {% if this.autobounds %}
                    var autobounds = L.featureGroup({{ this.features }}).getBounds()
                {% endif %}

                {{this._parent.get_name()}}.fitBounds(
                    {% if this.bounds %}{{ this.bounds }}{% else %}"autobounds"{% endif %},
                    {{ this.fit_bounds_options }}
                    );
            {% endmacro %}
            """)  # noqa

    def __init__(self, bounds, padding_top_left=None,
                 padding_bottom_right=None, padding=None, max_zoom=None):
        super(FitBounds, self).__init__()
        self._name = 'FitBounds'
        self.bounds = json.loads(json.dumps(bounds))
        options = {
            'maxZoom': max_zoom,
            'paddingTopLeft': padding_top_left,
            'paddingBottomRight': padding_bottom_right,
            'padding': padding,
        }
        self.fit_bounds_options = json.dumps({key: val for key, val in
                                              options.items() if val},
                                             sort_keys=True)
