# coding=utf-8
"""
Geojson layer
=============

.. note::

    Currently experimental and a work in progress, not fully optimized.


Supports:

- html color in properties
- polygon geometry are cached and not redrawed when the parent mapview changes
- linestring are redrawed everymove, it's ugly and slow.
- marker are NOT supported

"""

__all__ = ["GeoJsonMapLayer"]

import json

from kivy.graphics import (
    Canvas,
    Color,
    Line,
    MatrixInstruction,
    Mesh,
    PopMatrix,
    PushMatrix,
    Scale,
    Translate,
)
from kivy.graphics.tesselator import TYPE_POLYGONS, WINDING_ODD, Tesselator
from kivy.metrics import dp
from kivy.properties import ObjectProperty, StringProperty
from kivy.utils import get_color_from_hex

from mapview.constants import CACHE_DIR
from mapview.downloader import Downloader
from mapview.view import MapLayer

COLORS = {
    'aliceblue': '#f0f8ff',
    'antiquewhite': '#faebd7',
    'aqua': '#00ffff',
    'aquamarine': '#7fffd4',
    'azure': '#f0ffff',
    'beige': '#f5f5dc',
    'bisque': '#ffe4c4',
    'black': '#000000',
    'blanchedalmond': '#ffebcd',
    'blue': '#0000ff',
    'blueviolet': '#8a2be2',
    'brown': '#a52a2a',
    'burlywood': '#deb887',
    'cadetblue': '#5f9ea0',
    'chartreuse': '#7fff00',
    'chocolate': '#d2691e',
    'coral': '#ff7f50',
    'cornflowerblue': '#6495ed',
    'cornsilk': '#fff8dc',
    'crimson': '#dc143c',
    'cyan': '#00ffff',
    'darkblue': '#00008b',
    'darkcyan': '#008b8b',
    'darkgoldenrod': '#b8860b',
    'darkgray': '#a9a9a9',
    'darkgrey': '#a9a9a9',
    'darkgreen': '#006400',
    'darkkhaki': '#bdb76b',
    'darkmagenta': '#8b008b',
    'darkolivegreen': '#556b2f',
    'darkorange': '#ff8c00',
    'darkorchid': '#9932cc',
    'darkred': '#8b0000',
    'darksalmon': '#e9967a',
    'darkseagreen': '#8fbc8f',
    'darkslateblue': '#483d8b',
    'darkslategray': '#2f4f4f',
    'darkslategrey': '#2f4f4f',
    'darkturquoise': '#00ced1',
    'darkviolet': '#9400d3',
    'deeppink': '#ff1493',
    'deepskyblue': '#00bfff',
    'dimgray': '#696969',
    'dimgrey': '#696969',
    'dodgerblue': '#1e90ff',
    'firebrick': '#b22222',
    'floralwhite': '#fffaf0',
    'forestgreen': '#228b22',
    'fuchsia': '#ff00ff',
    'gainsboro': '#dcdcdc',
    'ghostwhite': '#f8f8ff',
    'gold': '#ffd700',
    'goldenrod': '#daa520',
    'gray': '#808080',
    'grey': '#808080',
    'green': '#008000',
    'greenyellow': '#adff2f',
    'honeydew': '#f0fff0',
    'hotpink': '#ff69b4',
    'indianred': '#cd5c5c',
    'indigo': '#4b0082',
    'ivory': '#fffff0',
    'khaki': '#f0e68c',
    'lavender': '#e6e6fa',
    'lavenderblush': '#fff0f5',
    'lawngreen': '#7cfc00',
    'lemonchiffon': '#fffacd',
    'lightblue': '#add8e6',
    'lightcoral': '#f08080',
    'lightcyan': '#e0ffff',
    'lightgoldenrodyellow': '#fafad2',
    'lightgray': '#d3d3d3',
    'lightgrey': '#d3d3d3',
    'lightgreen': '#90ee90',
    'lightpink': '#ffb6c1',
    'lightsalmon': '#ffa07a',
    'lightseagreen': '#20b2aa',
    'lightskyblue': '#87cefa',
    'lightslategray': '#778899',
    'lightslategrey': '#778899',
    'lightsteelblue': '#b0c4de',
    'lightyellow': '#ffffe0',
    'lime': '#00ff00',
    'limegreen': '#32cd32',
    'linen': '#faf0e6',
    'magenta': '#ff00ff',
    'maroon': '#800000',
    'mediumaquamarine': '#66cdaa',
    'mediumblue': '#0000cd',
    'mediumorchid': '#ba55d3',
    'mediumpurple': '#9370d8',
    'mediumseagreen': '#3cb371',
    'mediumslateblue': '#7b68ee',
    'mediumspringgreen': '#00fa9a',
    'mediumturquoise': '#48d1cc',
    'mediumvioletred': '#c71585',
    'midnightblue': '#191970',
    'mintcream': '#f5fffa',
    'mistyrose': '#ffe4e1',
    'moccasin': '#ffe4b5',
    'navajowhite': '#ffdead',
    'navy': '#000080',
    'oldlace': '#fdf5e6',
    'olive': '#808000',
    'olivedrab': '#6b8e23',
    'orange': '#ffa500',
    'orangered': '#ff4500',
    'orchid': '#da70d6',
    'palegoldenrod': '#eee8aa',
    'palegreen': '#98fb98',
    'paleturquoise': '#afeeee',
    'palevioletred': '#d87093',
    'papayawhip': '#ffefd5',
    'peachpuff': '#ffdab9',
    'peru': '#cd853f',
    'pink': '#ffc0cb',
    'plum': '#dda0dd',
    'powderblue': '#b0e0e6',
    'purple': '#800080',
    'red': '#ff0000',
    'rosybrown': '#bc8f8f',
    'royalblue': '#4169e1',
    'saddlebrown': '#8b4513',
    'salmon': '#fa8072',
    'sandybrown': '#f4a460',
    'seagreen': '#2e8b57',
    'seashell': '#fff5ee',
    'sienna': '#a0522d',
    'silver': '#c0c0c0',
    'skyblue': '#87ceeb',
    'slateblue': '#6a5acd',
    'slategray': '#708090',
    'slategrey': '#708090',
    'snow': '#fffafa',
    'springgreen': '#00ff7f',
    'steelblue': '#4682b4',
    'tan': '#d2b48c',
    'teal': '#008080',
    'thistle': '#d8bfd8',
    'tomato': '#ff6347',
    'turquoise': '#40e0d0',
    'violet': '#ee82ee',
    'wheat': '#f5deb3',
    'white': '#ffffff',
    'whitesmoke': '#f5f5f5',
    'yellow': '#ffff00',
    'yellowgreen': '#9acd32',
}


def flatten(lst):
    return [item for sublist in lst for item in sublist]


class GeoJsonMapLayer(MapLayer):

    source = StringProperty()
    geojson = ObjectProperty()
    cache_dir = StringProperty(CACHE_DIR)

    def __init__(self, **kwargs):
        self.first_time = True
        self.initial_zoom = None
        super().__init__(**kwargs)
        with self.canvas:
            self.canvas_polygon = Canvas()
            self.canvas_line = Canvas()
        with self.canvas_polygon.before:
            PushMatrix()
            self.g_matrix = MatrixInstruction()
            self.g_scale = Scale()
            self.g_translate = Translate()
        with self.canvas_polygon:
            self.g_canvas_polygon = Canvas()
        with self.canvas_polygon.after:
            PopMatrix()

    def reposition(self):
        vx, vy = self.parent.delta_x, self.parent.delta_y
        pzoom = self.parent.zoom
        zoom = self.initial_zoom
        if zoom is None:
            self.initial_zoom = zoom = pzoom
        if zoom != pzoom:
            diff = 2 ** (pzoom - zoom)
            vx /= diff
            vy /= diff
            self.g_scale.x = self.g_scale.y = diff
        else:
            self.g_scale.x = self.g_scale.y = 1.0
        self.g_translate.xy = vx, vy
        self.g_matrix.matrix = self.parent._scatter.transform

        if self.geojson:
            update = not self.first_time
            self.on_geojson(self, self.geojson, update=update)
            self.first_time = False

    def traverse_feature(self, func, part=None):
        """Traverse the whole geojson and call the func with every element
        found.
        """
        if part is None:
            part = self.geojson
        if not part:
            return
        tp = part["type"]
        if tp == "FeatureCollection":
            for feature in part["features"]:
                func(feature)
        elif tp == "Feature":
            func(part)

    @property
    def bounds(self):
        # return the min lon, max lon, min lat, max lat
        bounds = [float("inf"), float("-inf"), float("inf"), float("-inf")]

        def _submit_coordinate(coord):
            lon, lat = coord
            bounds[0] = min(bounds[0], lon)
            bounds[1] = max(bounds[1], lon)
            bounds[2] = min(bounds[2], lat)
            bounds[3] = max(bounds[3], lat)

        def _get_bounds(feature):
            geometry = feature["geometry"]
            tp = geometry["type"]
            if tp == "Point":
                _submit_coordinate(geometry["coordinates"])
            elif tp == "Polygon":
                for coordinate in geometry["coordinates"][0]:
                    _submit_coordinate(coordinate)
            elif tp == "MultiPolygon":
                for polygon in geometry["coordinates"]:
                    for coordinate in polygon[0]:
                        _submit_coordinate(coordinate)

        self.traverse_feature(_get_bounds)
        return bounds

    @property
    def center(self):
        min_lon, max_lon, min_lat, max_lat = self.bounds
        cx = (max_lon - min_lon) / 2.0
        cy = (max_lat - min_lat) / 2.0
        return min_lon + cx, min_lat + cy

    def on_geojson(self, instance, geojson, update=False):
        if self.parent is None:
            return
        if not update:
            self.g_canvas_polygon.clear()
            self._geojson_part(geojson, geotype="Polygon")
        self.canvas_line.clear()
        self._geojson_part(geojson, geotype="LineString")

    def on_source(self, instance, value):
        if value.startswith(("http://", "https://")):
            Downloader.instance(cache_dir=self.cache_dir).download(
                value, self._load_geojson_url
            )
        else:
            with open(value, "rb") as fd:
                geojson = json.load(fd)
            self.geojson = geojson

    def _load_geojson_url(self, url, response):
        self.geojson = response.json()

    def _geojson_part(self, part, geotype=None):
        tp = part["type"]
        if tp == "FeatureCollection":
            for feature in part["features"]:
                if geotype and feature["geometry"]["type"] != geotype:
                    continue
                self._geojson_part_f(feature)
        elif tp == "Feature":
            if geotype and part["geometry"]["type"] == geotype:
                self._geojson_part_f(part)
        else:
            # unhandled geojson part
            pass

    def _geojson_part_f(self, feature):
        properties = feature["properties"]
        geometry = feature["geometry"]
        graphics = self._geojson_part_geometry(geometry, properties)
        for g in graphics:
            tp = geometry["type"]
            if tp == "Polygon":
                self.g_canvas_polygon.add(g)
            else:
                self.canvas_line.add(g)

    def _geojson_part_geometry(self, geometry, properties):
        tp = geometry["type"]
        self.tp = tp

        graphics = []
        if tp == "Polygon":
            tess = Tesselator()
            for c in geometry["coordinates"]:
                xy = list(self._lonlat_to_xy(c))
                xy = flatten(xy)
                tess.add_contour(xy)

            tess.tesselate(WINDING_ODD, TYPE_POLYGONS)

            color = self._get_color_from(properties.get("color", "FF000088"))
            graphics.append(Color(*color))
            for vertices, indices in tess.meshes:
                graphics.append(
                    Mesh(vertices=vertices, indices=indices, mode="triangle_fan")
                )

        elif tp == "LineString":
            stroke = get_color_from_hex(properties.get("stroke", "#ffffff"))
            stroke_width = dp(properties.get("stroke-width"))
            xy = list(self._lonlat_to_xy(geometry["coordinates"]))
            xy = flatten(xy)
            graphics.append(Color(*stroke))
            graphics.append(Line(points=xy, width=stroke_width))

        return graphics

    def _lonlat_to_xy(self, lonlats):
        view = self.parent
        zoom = view.zoom
        for lon, lat in lonlats:
            p = view.get_window_xy_from(lat, lon, zoom)

            # Make LineString and Polygon works at the same time
            if self.tp == "Polygon":
                p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y
                p = self.parent._scatter.to_local(*p)

            yield p

    def _get_color_from(self, value):
        color = COLORS.get(value.lower(), value)
        color = get_color_from_hex(color)
        return color