"""
Wraps leaflet TileLayer, WmsTileLayer (TileLayer.WMS), ImageOverlay, and VideoOverlay

"""

from branca.element import Element, Figure
from jinja2 import Environment, PackageLoader, Template

from folium.map import Layer
from folium.utilities import image_to_url, mercator_transform, parse_options

ENV = Environment(loader=PackageLoader("folium", "templates"))


class TileLayer(Layer):
    """
    Create a tile layer to append on a Map.

    Parameters
    ----------
    tiles: str or :class:`xyzservices.TileProvider`, default 'OpenStreetMap'
        Map tileset to use. Can choose from this list of built-in tiles:
            - "OpenStreetMap"
            - "Stamen Terrain", "Stamen Toner", "Stamen Watercolor"
            - "CartoDB positron", "CartoDB dark_matter"

        You can pass a custom tileset to Folium by passing a
        :class:`xyzservices.TileProvider` or a Leaflet-style
        URL to the tiles parameter: ``http://{s}.yourtiles.com/{z}/{x}/{y}.png``.

        You can find a list of free tile providers here:
        ``http://leaflet-extras.github.io/leaflet-providers/preview/``.
        Be sure to check their terms and conditions and to provide attribution
        with the `attr` keyword.
    min_zoom: int, default 0
        Minimum allowed zoom level for this tile layer.
    max_zoom: int, default 18
        Maximum allowed zoom level for this tile layer.
    max_native_zoom: int, default None
        The highest zoom level at which the tile server can provide tiles.
        If provided you can zoom in past this level. Else tiles will turn grey.
    attr: string, default None
        Map tile attribution; only required if passing custom tile URL.
    detect_retina: bool, default False
        If true and user is on a retina display, it will request four
        tiles of half the specified size and a bigger zoom level in place
        of one to utilize the high resolution.
    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).
    subdomains: list of strings, default ['abc']
        Subdomains of the tile service.
    tms: bool, default False
        If true, inverses Y axis numbering for tiles (turn this on for TMS
        services).
    opacity: float, default 1
        Sets the opacity for the layer.
    **kwargs : additional keyword arguments
        Other keyword arguments are passed as options to the Leaflet tileLayer
        object.
    """

    _template = Template(
        """
        {% macro script(this, kwargs) %}
            var {{ this.get_name() }} = L.tileLayer(
                {{ this.tiles|tojson }},
                {{ this.options|tojson }}
            ).addTo({{ this._parent.get_name() }});
        {% endmacro %}
        """
    )

    def __init__(
        self,
        tiles="OpenStreetMap",
        min_zoom=0,
        max_zoom=18,
        max_native_zoom=None,
        attr=None,
        detect_retina=False,
        name=None,
        overlay=False,
        control=True,
        show=True,
        no_wrap=False,
        subdomains="abc",
        tms=False,
        opacity=1,
        **kwargs
    ):

        # check for xyzservices.TileProvider without importing it
        if isinstance(tiles, dict):
            attr = attr if attr else tiles.html_attribution
            min_zoom = tiles.get("min_zoom", min_zoom)
            max_zoom = tiles.get("max_zoom", max_zoom)
            subdomains = tiles.get("subdomains", subdomains)
            tiles = tiles.build_url(fill_subdomain=False, scale_factor="{r}")

        self.tile_name = (
            name if name is not None else "".join(tiles.lower().strip().split())
        )
        super().__init__(
            name=self.tile_name, overlay=overlay, control=control, show=show
        )
        self._name = "TileLayer"
        self._env = ENV

        tiles_flat = "".join(tiles.lower().strip().split())
        if tiles_flat in {"cloudmade", "mapbox", "mapboxbright", "mapboxcontrolroom"}:
            # added in May 2020 after v0.11.0, remove in a future release
            raise ValueError(
                "Built-in templates for Mapbox and Cloudmade have been removed. "
                "You can still use these providers by passing a URL to the `tiles` "
                "argument. See the documentation of the `TileLayer` class."
            )
        templates = list(
            self._env.list_templates(filter_func=lambda x: x.startswith("tiles/"))
        )
        tile_template = "tiles/" + tiles_flat + "/tiles.txt"
        attr_template = "tiles/" + tiles_flat + "/attr.txt"

        if tile_template in templates and attr_template in templates:
            self.tiles = self._env.get_template(tile_template).render()
            attr = self._env.get_template(attr_template).render()
        else:
            self.tiles = tiles
            if not attr:
                raise ValueError("Custom tiles must have an attribution.")

        self.options = parse_options(
            min_zoom=min_zoom,
            max_zoom=max_zoom,
            max_native_zoom=max_native_zoom or max_zoom,
            no_wrap=no_wrap,
            attribution=attr,
            subdomains=subdomains,
            detect_retina=detect_retina,
            tms=tms,
            opacity=opacity,
            **kwargs
        )


class WmsTileLayer(Layer):
    """
    Creates a Web Map Service (WMS) layer.

    Parameters
    ----------
    url : str
        The url of the WMS server.
    layers : str
        Comma-separated list of WMS layers to show.
    styles : str, optional
        Comma-separated list of WMS styles.
    fmt : str, default 'image/jpeg'
        The format of the service output. Ex: 'image/png'
    transparent: bool, default False
        Whether the layer shall allow transparency.
    version : str, default '1.1.1'
        Version of the WMS service to use.
    attr : str, default ''
        The attribution of the service.
        Will be displayed in the bottom right corner.
    name : string, optional
        The name of the Layer, as it will appear in LayerControls
    overlay : bool, default True
        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).
    **kwargs : additional keyword arguments
        Passed through to the underlying tileLayer.wms object and can be used
        for setting extra tileLayer.wms parameters or as extra parameters in
        the WMS request.

    See https://leafletjs.com/reference.html#tilelayer-wms
    """

    _template = Template(
        """
        {% macro script(this, kwargs) %}
            var {{ this.get_name() }} = L.tileLayer.wms(
                {{ this.url|tojson }},
                {{ this.options|tojson }}
            ).addTo({{ this._parent.get_name() }});
        {% endmacro %}
        """
    )  # noqa

    def __init__(
        self,
        url,
        layers,
        styles="",
        fmt="image/jpeg",
        transparent=False,
        version="1.1.1",
        attr="",
        name=None,
        overlay=True,
        control=True,
        show=True,
        **kwargs
    ):
        super().__init__(name=name, overlay=overlay, control=control, show=show)
        self.url = url
        kwargs["format"] = fmt
        cql_filter = kwargs.pop("cql_filter", None)
        self.options = parse_options(
            layers=layers,
            styles=styles,
            transparent=transparent,
            version=version,
            attribution=attr,
            **kwargs
        )
        if cql_filter:
            # special parameter that shouldn't be camelized
            self.options["cql_filter"] = cql_filter


class ImageOverlay(Layer):
    """
    Used to load and display a single image over specific bounds of
    the map, implements ILayer interface.

    Parameters
    ----------
    image: string, file or array-like object
        The data you want to draw on the map.
        * If string, it will be written directly in the output file.
        * If file, it's content will be converted as embedded in the output file.
        * If array-like, it will be converted to PNG base64 string and embedded in the output.
    bounds: list
        Image bounds on the map in the form
         [[lat_min, lon_min], [lat_max, lon_max]]
    opacity: float, default Leaflet's default (1.0)
    alt: string, default Leaflet's default ('')
    origin: ['upper' | 'lower'], optional, default 'upper'
        Place the [0,0] index of the array in the upper left or
        lower left corner of the axes.
    colormap: callable, used only for `mono` image.
        Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
        for transforming a mono image into RGB.
        It must output iterables of length 3 or 4,
        with values between 0 and 1.
        Hint: you can use colormaps from `matplotlib.cm`.
    mercator_project: bool, default False.
        Used only for array-like image.  Transforms the data to
        project (longitude, latitude) coordinates to the Mercator projection.
        Beware that this will only work if `image` is an array-like object.
        Note that if used the image will be clipped beyond latitude -85 and 85.
    pixelated: bool, default True
        Sharp sharp/crips (True) or aliased corners (False).
    name : string, default None
        The name of the Layer, as it will appear in LayerControls
    overlay : bool, default True
        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).

    See https://leafletjs.com/reference.html#imageoverlay for more
    options.

    """

    _template = Template(
        """
        {% macro script(this, kwargs) %}
            var {{ this.get_name() }} = L.imageOverlay(
                {{ this.url|tojson }},
                {{ this.bounds|tojson }},
                {{ this.options|tojson }}
            ).addTo({{ this._parent.get_name() }});
        {% endmacro %}
        """
    )

    def __init__(
        self,
        image,
        bounds,
        origin="upper",
        colormap=None,
        mercator_project=False,
        pixelated=True,
        name=None,
        overlay=True,
        control=True,
        show=True,
        **kwargs
    ):
        super().__init__(name=name, overlay=overlay, control=control, show=show)
        self._name = "ImageOverlay"
        self.bounds = bounds
        self.options = parse_options(**kwargs)
        self.pixelated = pixelated
        if mercator_project:
            image = mercator_transform(
                image, [bounds[0][0], bounds[1][0]], origin=origin
            )

        self.url = image_to_url(image, origin=origin, colormap=colormap)

    def render(self, **kwargs):
        super().render()

        figure = self.get_root()
        assert isinstance(
            figure, Figure
        ), "You cannot render this Element if it is not in a Figure."
        if self.pixelated:
            pixelated = """
                <style>
                    .leaflet-image-layer {
                        /* old android/safari*/
                        image-rendering: -webkit-optimize-contrast;
                        image-rendering: crisp-edges; /* safari */
                        image-rendering: pixelated; /* chrome */
                        image-rendering: -moz-crisp-edges; /* firefox */
                        image-rendering: -o-crisp-edges; /* opera */
                        -ms-interpolation-mode: nearest-neighbor; /* ie */
                    }
                </style>
            """
            figure.header.add_child(
                Element(pixelated), name="leaflet-image-layer"
            )  # noqa

    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 self.bounds


class VideoOverlay(Layer):
    """
    Used to load and display a video over the map.

    Parameters
    ----------
    video_url: str
        URL of the video
    bounds: list
        Video bounds on the map in the form
         [[lat_min, lon_min], [lat_max, lon_max]]
    autoplay: bool, default True
    loop: bool, default True
    name : string, default None
        The name of the Layer, as it will appear in LayerControls
    overlay : bool, default True
        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).
    **kwargs:
        Other valid (possibly inherited) options. See:
        https://leafletjs.com/reference.html#videooverlay

    """

    _template = Template(
        """
        {% macro script(this, kwargs) %}
            var {{ this.get_name() }} = L.videoOverlay(
                {{ this.video_url|tojson }},
                {{ this.bounds|tojson }},
                {{ this.options|tojson }}
            ).addTo({{ this._parent.get_name() }});
        {% endmacro %}
        """
    )

    def __init__(
        self,
        video_url,
        bounds,
        autoplay=True,
        loop=True,
        name=None,
        overlay=True,
        control=True,
        show=True,
        **kwargs
    ):
        super().__init__(name=name, overlay=overlay, control=control, show=show)
        self._name = "VideoOverlay"
        self.video_url = video_url

        self.bounds = bounds
        self.options = parse_options(autoplay=autoplay, loop=loop, **kwargs)

    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 self.bounds
