import math
import collections
import geojson
from pyproj import Proj # map projection transformation shite

from poobrains import Markup, app, abort, g
import poobrains.storage
import poobrains.auth
import poobrains.commenting

from . import base


def normalize_coords(longitude, latitude):

    #projection = Proj(init='epsg:4326') # WGS84 according to epsg.io, but results in equidistant latitudes
    projection = Proj(init='epsg:3857') # WGS84, as used by OSM etc.
    x_rad, y_rad = projection(longitude, latitude)

    x = (x_rad / (math.pi * 2)) + 0.5
    y = 1 - ((y_rad / math.pi) + 0.5)

    return x, y


def normalize_bbox(bbox):

    return [
        normalize_coords(bbox[0], bbox[1]),
        normalize_coords(bbox[2], bbox[3])
    ]



def merge_bboxes(a, b):

    return [
        a[0] if a[0] < b[0] else b[0],
        a[1] if a[1] > b[1] else b[1],
        a[2] if a[2] > b[2] else b[2],
        a[3] if a[3] < b[3] else b[3]
    ]


def find_bbox(data):

    bbox = [180, -80, -180, 80]

    if isinstance(data, geojson.FeatureCollection):

        for feature in data.features:
            #left, top, right, bottom = find_bbox(feature)
            bbox = merge_bboxes(bbox, find_bbox(feature))

        return bbox

    elif isinstance(data, geojson.Feature):
        return find_bbox(data['geometry'])

    elif isinstance(data, geojson.GeometryCollection):

        for geometry in data.geometries:
            bbox = merge_bboxes(bbox, find_bbox(geometry))

        return bbox

    elif isinstance(data, geojson.Point):
        return merge_bboxes(bbox, [data.coordinates[0], data.coordinates[1], data.coordinates[0], data.coordinates[1]])

    elif isinstance(data, (geojson.LineString, geojson.MultiPoint)):

        for coord in data.coordinates:
            bbox = merge_bboxes(bbox, [coord[0], coord[1], coord[0], coord[1]])

        return bbox

    elif isinstance(data, (geojson.Polygon, geojson.MultiLineString)):

        for ring in data.coordinates:
            for coord in ring:
                bbox = merge_bboxes(bbox, [coord[0], coord[1], coord[0], coord[1]])

        return bbox

    elif isinstance(data, geojson.MultiPolygon):

        for polygon in data.coordinates:
            for ring in polygon:
                for coord in ring:
                    bbox = merge_bboxes(bbox, [coord[0], coord[1], coord[0], coord[1]])

        return bbox

    else:
        if app.config['DEBUG']:
            # this should never happen (unless geojson is invalid)
            app.debugger.set_trace()


def span(bbox):

    normalized_bbox = normalize_bbox(bbox) 

    span_horizontal = abs(normalized_bbox[1][0] - normalized_bbox[0][0])
    span_vertical = abs(normalized_bbox[1][1] - normalized_bbox[0][1])

    return (span_horizontal, span_vertical)


def aspect_ratio(bbox):

    # multiply the result of this with map width to get its height
    # FIXME: name is technically a misnomer

    span_horizontal, span_vertical = span(bbox)

    if 0 in (span_horizontal, span_vertical):
        return 1

    return (span_vertical / span_horizontal) / 2


def render_geojson(bbox, data):

    normalized_bbox = normalize_bbox(bbox)

    span_horizontal, span_vertical = span(bbox)

    width = app.config['SVG_MAP_WIDTH']
    #height = width * (span_vertical / span_horizontal) / 2
    height = width * aspect_ratio(bbox)

    r = ''

    if isinstance(data, geojson.feature.FeatureCollection):

        if hasattr(data, 'id'):
            r += f'<g id="{data.id}" class="featurecollection">\n'
        else:
            r += '<g class="featurecollection">\n'

        for feature in data.features:
            r += str(render_geojson(bbox, feature))

        r += '</g>'
    
    elif isinstance(data, geojson.feature.Feature):

        if hasattr(data, 'id'):
            r += f'<g id="{data.id}" class="feature">\n'
        else:
            r += '<g class="feature">\n'

        r += str(render_geojson(bbox, data.geometry))
        r += '</g>'

    elif isinstance(data, geojson.geometry.GeometryCollection):

        if hasattr(data, 'id'):
            r += f'<g id="{data.id}" class="geometrycollection">\n'
        else:
            r += '<g class="geometrycollection">\n'

        for geometry in data.geometries:
            r += str(render_geojson(bbox, geometry))

        r += '</g>'

    elif isinstance(data, geojson.geometry.Geometry):

        if isinstance(data, geojson.geometry.Point):
            if hasattr(data, 'id'):
                r += '<use id="%s" class="geojson-point" ' % data.id

            else:
                r += '<use class="geojson-point marker" '

            x, y = normalize_coords(data.coordinates[0], data.coordinates[1])
            x -= normalized_bbox[0][0]
            x /= span_horizontal

            y -= normalized_bbox[0][1]
            y /= span_vertical

            x *= width
            y *= height

            r += 'href="#marker" x="%f" y="%f" />' % (x, y)


        elif isinstance(data, geojson.geometry.Polygon):
            
            if hasattr(data, 'id'):
                r += '<path id="%s" class="geojson-polygon"' % data.id
            else:
                r += '<path class="geojson-polygon" '

            r += 'd="'

            for path in data.coordinates:

                x, y = normalize_coords(path[0][0], path[0][1])
                x -= normalized_bbox[0][0]
                x /= span_horizontal

                y -= normalized_bbox[0][1]
                y /= span_vertical

                x *= width
                y *= height

                r += 'M %f %f' % (x, y) # Move to first coordinate
                for point in path[1:-1]: # ignore last coordinate pair because as per RFC7946, first and last coordinate pair MUST be the same

                    x, y = normalize_coords(point[0], point[1])

                    x -= normalized_bbox[0][0]
                    x /= span_horizontal

                    y -= normalized_bbox[0][1]
                    y /= span_vertical

                    x *= width
                    y *= height

                    r += ' L ' + str(x) + ' ' + str(y)
                r+= ' z\n'
            r += '" />\n'

        elif isinstance(data, geojson.geometry.MultiPolygon):

            r += '<g class="multipolygon">'

            for coordinates in data.coordinates:
                fakepoly = geojson.geometry.Polygon(coordinates)
                r += str(render_geojson(bbox, fakepoly))

            r += '</g>'

        else:
            r += "<!-- unhandled geometry of type %s -->" % type(data).__name__

    return Markup(r)


class GeoDataForm(poobrains.auth.AutoForm):

    def process(self, submit, exceptions=False):
        
        if self.fields['data'].value.mimetype in ('application/json', 'application/geo+json'):
            fd = self.fields['data'].value.stream
            self.fields['data'].value = fd.read()
            fd.close()

        else:
            raise ValueError('data is not JSON!')

        return super(GeoDataForm, self).process(submit, exceptions=exceptions)



class GeoData(poobrains.auth.NamedOwned):

    form_add = GeoDataForm
    form_edit = GeoDataForm

    class Meta:

        related_use_form = True

        modes = collections.OrderedDict([
            ('add', 'create'),
            ('teaser', 'read'),
            ('raw', 'read'),
            ('inline', 'read'),
            ('full', 'read'),
            ('edit', 'update'),
            ('delete', 'delete')
        ])

    parent = poobrains.storage.fields.ForeignKeyField('self', null=True) # Build me some forests
    title = poobrains.storage.fields.CharField()
    description = poobrains.md.MarkdownField(null=True)
    bbox_left = poobrains.storage.fields.FloatField(null=True)
    bbox_top = poobrains.storage.fields.FloatField(null=True)
    bbox_right = poobrains.storage.fields.FloatField(null=True)
    bbox_bottom = poobrains.storage.fields.FloatField(null=True)
    data = poobrains.storage.fields.TextField(form_widget=poobrains.form.fields.File)

    def __init__(self, *args, **kwargs):

        super(GeoData, self).__init__(*args, **kwargs)
        #self.infobox_width = app.config['SVG_MAP_INFOBOX_WIDTH']
        #self.infobox_height = app.config['SVG_MAP_INFOBOX_HEIGHT']
        self._bbox = None

        self.map_width = app.config['SVG_MAP_WIDTH']
        self.map_height = None
        self.description_height = app.config['SVG_MAP_DESCRIPTION_HEIGHT']


    @property
    def ref_id(self):
        return self.name


    @property
    def children(self):
        return self.__class__.select().where(self.__class__.parent == self)


    @property
    def geojson(self):

        try:
            return geojson.loads(self.data)

        except Exception:
            return None


    @property
    def bbox(self):

        if not self._bbox is None:
            bbox = self._bbox

        elif not self.bbox_left is None and\
               not self.bbox_top is None and\
               not self.bbox_right is None and\
               not self.bbox_bottom is None: # i.e. all bbox coordinates set

            bbox = [self.bbox_left, self.bbox_top, self.bbox_right, self.bbox_bottom]

        else:

            if self.geojson:
                bbox = find_bbox(self.geojson)
            else:
                bbox = None

            for child in self.children:
                if bbox is None:
                    bbox = child.bbox
                else:
                    bbox = merge_bboxes(bbox, child.bbox)

        if bbox[0] == bbox[2]:
            bbox[2] += 1
        if bbox[1] == bbox[3]:
            bbox[3] += 1

        return bbox


    @property
    def map_url(self):
        m = Map(root_datapoints=[self])
        return m.url('raw')


    def override_bbox(self, bbox):
        self._bbox = bbox
        self.map_height = self.map_width * aspect_ratio(self._bbox)


    def render(self, mode='full', bbox=None):

        if not bbox is None:
            self.override_bbox(bbox)

        return super(GeoData, self).render(mode=mode)


    def render_geojson(self, bbox):
        return render_geojson(bbox, self.geojson)


@app.expose('/svg/map')
class Map(base.SVG):
    
    width = None
    height = None

    root_datapoints = None

    def __init__(self, handle=None, mode=None, terrain=None, root_datapoints=None, **kwargs):
        
        super(Map, self).__init__(handle=handle, mode=mode, **kwargs)
        
        if handle is None and root_datapoints is None:
            abort(404, "No root_datapoints selected")

        self.width = app.config['SVG_MAP_WIDTH']
        self.description_height = app.config['SVG_MAP_DESCRIPTION_HEIGHT']

        if root_datapoints:
            
            self.terrain = terrain
            self.root_datapoints = root_datapoints

        else:

            if '+' in handle:
                terrain_name, rest = handle.split('+', 1)

                if len(terrain_name):
                    self.terrain = GeoData.load(terrain_name)
                else:
                    self.terrain = None

            else:
                self.terrain = None
                rest = handle

            self.root_datapoints = []

            datapoint_names = rest.split(',')

            for name in datapoint_names:

                datapoint = GeoData.load(name)
                if datapoint.permissions['read'].check(g.user):
                    self.root_datapoints.append(datapoint)

        self.handle = ''
        if self.terrain:
            self.handle += self.terrain.name + '+'
        self.handle += ','.join([datapoint.name for datapoint in self.root_datapoints]) # needed for proper URL generation

        if len(self.root_datapoints) > 0:
            self.bbox = self.root_datapoints[0].bbox

        else:
            self.bbox = [-180, 80, 180, -80] # show roundabout the whole world if we have no data, should probably™ never happen

        if len(self.root_datapoints) > 1:
            for datapoint in self.root_datapoints[1:]:
                self.bbox = merge_bboxes(self.bbox, datapoint.bbox)

        #self.height = self.width * (self.span_horizontal / self.span_vertical)
        #self.height = self.width * (self.span_horizontal / self.span_vertical) * 0.83
        self.height = self.width * aspect_ratio(self.bbox)
        self.normalized_bbox = normalize_bbox(self.bbox)
        self.span_horizontal, self.span_vertical = span(self.bbox)


    def render_geojson(self, data):
        return render_geojson(self.bbox, data)


    def grid(self):

        r = '<g class="grid">'

        for lat in range(-70, 80, 10):

            _, y = normalize_coords(0, lat)
            y -= self.normalized_bbox[0][1]
            y /= self.span_vertical
            y *= self.height

            r += '<line class="grid-y lat-' + str(lat) + '" x1="0" y1="' + str(y) + '" x2="100%" y2="' + str(y) + '" />\n'

        r += '</g>'

        return Markup(r)


