"""
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
  Ontology Engineering Group
        http://www.oeg-upm.net/
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
  Copyright (C) 2016 Ontology Engineering Group.
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

            http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
"""
import calendar
import json
import os
import ssl
import sys
import time
from datetime import datetime
from decimal import Decimal
from functools import wraps
from urllib2 import urlopen, HTTPError, Request
from wsgiref.handlers import format_date_time

import overpy
import shapely
from LatLon import LatLon
from concurrent.futures import ThreadPoolExecutor
from flask import Flask, request, jsonify
from flask_caching import Cache
from fuzzywuzzy import process
from overpy import exception
from overpy.exception import DataIncomplete
from redis_cache import cache_it_json, SimpleCache
from shapely.errors import TopologicalError
from shapely.geometry import Point, Polygon, LineString, MultiPoint, MultiPolygon
from shapely.ops import nearest_points
from shapely.wkt import dumps

from geo_pass import geocoding, debug, error, ZSimpleCache
from geo_pass.poly import ways2poly

__author__ = 'Fernando Serena'

MAX_AGE = int(os.environ.get('MAX_AGE', 86400))
CACHE_LIMIT = int(os.environ.get('CACHE_LIMIT', 100000))
CACHE_REDIS_HOST = os.environ.get('CACHE_REDIS_HOST', '127.0.0.1')
CACHE_REDIS_DB = int(os.environ.get('CACHE_REDIS_DB', 0))
CACHE_REDIS_PORT = int(os.environ.get('CACHE_REDIS_PORT', 6379))

app = Flask(__name__)
cache = Cache(app, config={
    'CACHE_TYPE': 'redis',
    'CACHE_KEY_PREFIX': 'geo',
    'CACHE_REDIS_HOST': CACHE_REDIS_HOST,
    'CACHE_REDIS_DB': CACHE_REDIS_DB,
    'CACHE_REDIS_PORT': CACHE_REDIS_PORT
})


class Overpass(overpy.Overpass):
    def __init__(self, url=None, cache=None, jwt=None):
        super(Overpass, self).__init__(url=url)
        self.cache = cache
        self.jwt = jwt
        self.pool = ThreadPoolExecutor(max_workers=4)

    def parse_json(self, data, encoding="utf-8"):
        """
        Parse raw response from Overpass service.

        :param data: Raw JSON Data
        :type data: String or Bytes
        :param encoding: Encoding to decode byte string
        :type encoding: String
        :return: Result object
        :rtype: overpy.Result
        """

        try:
            elements = data.get('elements', [{}])[0]
            if elements.get('type', '') == 'count':
                return {k: int(v) for k, v in elements['tags'].iteritems()}
            else:
                members = elements.get('members', [])

        except IndexError:
            members = None
        if members:
            members = filter(lambda m: m.get('geometry', True), members)
            data['elements'][0]['members'] = members
            for m in members:
                if 'geometry' in m:
                    geometry = filter(lambda p: p, m['geometry'])
                    m['geometry'] = geometry

        return overpy.Result.from_json(data, api=self)

    def __request(self, query):
        debug("querying:" + query)

        if self.url.startswith('https'):
            gcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
        else:
            gcontext = None

        if self.jwt:
            request = Request(self.url, headers={"Authorization": self.jwt})
        else:
            request = self.url

        future = self.pool.submit(urlopen, request, query, context=gcontext)

        try:
            f = future.result()
        except HTTPError as e:
            f = e

        response = f.read(self.read_chunk_size)
        while True:
            data = f.read(self.read_chunk_size)
            if len(data) == 0:
                break
            response = response + data
        f.close()

        if f.code == 200:
            if overpy.PY2:
                http_info = f.info()
                content_type = http_info.getheader("content-type")
            else:
                content_type = f.getheader("Content-Type")

            if content_type == "application/json":
                if isinstance(response, bytes):
                    response = response.decode("utf-8")
                response = json.loads(response)
                return response

            raise exception.OverpassUnknownContentType(content_type)

        if f.code == 400:
            msgs = []
            for msg in self._regex_extract_error_msg.finditer(response):
                tmp = self._regex_remove_tag.sub(b"", msg.group("msg"))
                try:
                    tmp = tmp.decode("utf-8")
                except UnicodeDecodeError:
                    tmp = repr(tmp)
                msgs.append(tmp)

            raise exception.OverpassBadRequest(
                query,
                msgs=msgs
            )

        if f.code == 429:
            raise exception.OverpassTooManyRequests

        if f.code == 504:
            raise exception.OverpassGatewayTimeout

        raise exception.OverpassUnknownHTTPStatusCode(f.code)

    def query(self, query, cache=True, expire=MAX_AGE, maxsize=536870912):
        if not isinstance(query, bytes):
            query = query.encode("utf-8")

        query = "[maxsize:{}][out:json];\n".format(maxsize) + query

        if cache:
            opt_query = ';'.join([st.strip('\n').strip(' ') for st in query.split(';')])
            try:
                response = cache_it_json(cache=self.cache, expire=expire)(self.__request)(opt_query)
            except Exception as e:
                raise ValueError(e.message)
        else:
            response = self.__request(query)

        response_str = json.dumps(response)
        try:
            return self.parse_json(json.loads(response_str, parse_float=Decimal))
        except AttributeError:
            return None


cache_proc = SimpleCache(hashkeys=True, host=CACHE_REDIS_HOST, port=CACHE_REDIS_PORT,
                         db=CACHE_REDIS_DB + 1, namespace='pr', limit=10000000, expire=MAX_AGE)

cache_q = ZSimpleCache(hashkeys=True, host=CACHE_REDIS_HOST, port=CACHE_REDIS_PORT,
                       db=CACHE_REDIS_DB + 2, namespace='q', limit=100000, expire=MAX_AGE)

api = Overpass(url=os.environ.get('OVERPASS_API_URL', 'http://127.0.0.1:5000/api/interpreter'),
               cache=cache_q, jwt=os.environ.get('JWT_TOKEN', None))


def make_cache_key(*args, **kwargs):
    path = request.path
    qargs = dict(request.args.items())
    args = 'u'.join([u'{}{}'.format(k, qargs[k]) for k in sorted(qargs.keys())])
    return (path + args).encode('utf-8')


def make_around_cache_key(*args, **kwargs):
    qargs = dict(request.args.items())
    args = 'around' + ''.join(['{}{}'.format(k, qargs[k]) for k in sorted(qargs.keys())])
    return args.encode('utf-8')


def make_tags_cache_key(*args, **kwargs):
    path = request.path
    return path.encode('utf-8')


def way_distance(result, way, point):
    min = sys.maxint
    for nid in way._node_ids:
        try:
            node = result.get_node(nid, resolve_missing=False)
            node_ll = LatLon(node.lat, node.lon)
            node_d = point.distance(node_ll)
            if node_d < min:
                min = node_d
        except DataIncomplete:
            pass
    return min


def node_distance(node, point):
    node_ll = LatLon(node.lat, node.lon)
    return point.distance(node_ll)


def is_road(way):
    tags = way.get('tag', {})
    return 'highway' in tags or 'footway' in tags


def is_building(way):
    tags = way.get('tag', {})
    return 'amenity' in tags or ('building' in tags and tags['building'] != 'no')


@cache_it_json(cache=cache_proc)
def query_way_buildings(id):
    debug('Way buildings of ' + str(id))
    query = """
     way({});
    way(around:20)[building]["building"!~"no"];        
    out center;""".format(id)
    result = api.query(query, cache=False)
    all_way_buildings = filter(lambda w: w.id != int(id), result.ways)
    return map(lambda x: {'id': x.id, 'center_lat': float(x.center_lat), 'center_lon': float(x.center_lon)},
               all_way_buildings)


@cache_it_json(cache=cache_proc)
def query_way_elms(id):
    debug('Way elements of ' + str(id))
    query = """
    way({});
    (
     node(around:20)[highway];
     node(around:20)[footway];
    );        
    out body;""".format(id)
    result = api.query(query, cache=False)
    return map(lambda x: {'id': x.id, 'lat': float(x.lat), 'lon': float(x.lon), 'tags': x.tags}, result.nodes)


@cache_it_json(cache=cache_proc)
def query_building_ways(id):
    debug('Building ways of ' + str(id))

    def filter_way(w):
        buildings = query_way_buildings(w.id)
        return any([b for b in buildings if b['id'] == int(id)])

    result = api.query("""
        way({});
        way(around:20) -> .aw;                
        (
          way.aw[highway];
          way.aw[footway];
        );
        out ids;
        """.format(id), cache=False)
    all_near_ways = result.ways
    building_ways = filter(lambda w: filter_way(w), all_near_ways)
    return map(lambda x: {'id': x.id}, filter(lambda w: w.id != int(id), building_ways))


@cache_it_json(cache=cache_proc)
def query_building_elms(way):
    debug('Building elements of ' + str(way['id']))
    result = api.query("""way({}); out geom; >; out body;""".format(way['id']))

    points = [(float(result.get_node(n).lon), float(result.get_node(n).lat)) for n in way['nd']]
    shape = Polygon(points).envelope

    result = api.query("""
        way({});
        node(around:5) -> .na;
        (
           node.na[shop];
           node.na[amenity];
           node.na[tourism];
        );                
        out;
        """.format(way['id']), cache=False)

    elms = []

    for n in result.nodes:
        n_point = Point(float(n.lon), float(n.lat))
        if n_point.within(shape):
            elms.append({'id': n.id, 'lat': float(n.lat), 'lon': float(n.lon), 'tags': n.tags})

    return elms


@cache_it_json(cache=cache_proc)
def query_surrounding_buildings(id):
    debug('Surrounding buildings of ' + str(id))
    result = api.query("""
        way({});
        out center;
        > -> .wn;
        node(around:10) -> .ar;
        node.ar.wn;
        <;        
        out center;""".format(id), cache=False)
    return map(lambda x: {'id': x.id, 'center_lat': float(x.center_lat), 'center_lon': float(x.center_lon)},
               filter(lambda w: w.id != int(id) and 'building' in w.tags, result.ways))


@cache_it_json(cache=cache_proc)
def query_intersect_ways(id):
    debug('Intersecting ways of ' + str(id))
    result = api.query("""
    way({});
    >;
    out body;
    < -> .wall;
    (
        way.wall[highway];
        way.wall[footway];
    );    
    out geom;""".format(id), cache=False)
    node_ids = set(map(lambda x: x.id, result.nodes))
    ways = filter(lambda w: w.id != int(id), result.ways)

    intersections = {}

    for w in ways:
        cuts = set(w._node_ids).intersection(node_ids)
        if cuts:
            intersections[w.id] = list(cuts)

    return intersections


@cache_it_json(cache=cache_proc)
def query_node_building(node):
    result = api.query("""
        node({});
        way(around:10)[building]["building"!~"no"];
        (._; >;);
        out geom;""".format(node['id']), cache=False)
    point = Point(node['lon'], node['lat'])
    for w in result.ways:
        geom = w.nodes
        points = map(lambda n: [float(n.lon), float(n.lat)], geom)
        shape = Polygon(points)
        if point.within(shape.envelope):
            return {'id': w.id}


@cache_it_json(cache=cache_proc)
def query_around(id, way=True, lat=None, lon=None, radius=None):
    type = 'way' if way else 'node'
    result = api.query("""
        {}({});
        (node(around:{},{},{}); <;) -> .all;
        (node.all[highway];
         node.all[shop];
         node.all[amenity];
         node.all[tourism];
         way.all[highway];
         way.all[footway];
         way.all[building];
        );
        out ids;        
    """.format(type, id, radius, lat, lon), cache=False)
    elements = list(result.ways) + list(result.nodes)
    return map(lambda x: x.id, elements)


@cache_it_json(cache=cache_proc)
def query_nodes(*nodes):
    q_nodes = map(lambda x: 'node({});'.format(x), nodes)
    result = api.query("""
        ({});
        out;""".format('\n'.join(q_nodes)), cache=False)

    return list(result.nodes)


def transform(f):
    def transform_value(v):
        if isinstance(v, datetime):
            return calendar.timegm(v.timetuple())
        return v

    @wraps(f)
    def wrapper(*args, **kwargs):
        data = {}
        for elm in f(*args, **kwargs):
            data['id'] = elm.id
            if elm.tags:
                data['tag'] = elm.tags
            if elm.attributes:
                data.update({k: transform_value(elm.attributes[k]) for k in elm.attributes})
            if isinstance(elm, overpy.Way):
                data['nd'] = elm._node_ids
                if is_building(data):
                    if elm.center_lat:
                        data['center'] = {'lat': float(elm.center_lat), 'lon': float(elm.center_lon)}
            else:
                data['lat'] = float(elm.lat)
                data['lon'] = float(elm.lon)
        return data

    return wrapper


@cache_it_json(cache=cache_proc)
@transform
def g_way(id):
    center = api.query("""
                way({});   
                out center;            
            """.format(id))
    return list(center.ways)


def g_coord_area(lat, lon):
    result = api.query("""
                is_in({},{});
                out tags;
    """.format(lat, lon))

    max_admin_level = max([int(a.tags['admin_level']) for a in result.areas if 'admin_level' in a.tags])
    return [a.id for a in result.areas if 'admin_level' in a.tags and
            a.tags.get('boundary', '') == 'administrative' and int(a.tags['admin_level']) == max_admin_level]


@cache_it_json(cache=cache_proc)
def g_way_geom(id):
    debug('Geometry of ' + str(id))
    geom = api.query("""
                way({});
                (._; >;);
                out geom;
            """.format(id), cache=False)
    return map(lambda n: (float(n.lon), float(n.lat)), list(geom.ways).pop().nodes)


def relation_multipolygon(rels):
    rel_members = [[m for m in r.members if m.geometry and m.role == 'outer'] for r in rels]
    all_members = reduce(lambda x, y: x + y, rel_members, [])
    poly, incomplete = ways2poly(all_members)

    geoms = []
    for p in poly:
        geoms.append(Polygon([(n.lon, n.lat) for n in p]))  # .simplify(0.000001))
    for l in incomplete:
        geoms.append(Polygon(LineString([(n.lon, n.lat) for n in l])))  # .simplify(0.000001))

    return MultiPolygon(geoms)


@cache_it_json(cache=cache_proc)
def g_area_geom(id):
    debug('Area geometry of ' + str(id))

    def transform_tag_value(k, v):
        if k == 'admin_level':
            return int(v)
        return u'"{}"'.format(v)

    result = api.query("""area({});out;""".format(id))

    area_tags = result.areas[0].tags
    area_tags = {key: transform_tag_value(key, v) for key, v in area_tags.items() if
                 key in ['type', 'name:en', 'name', 'boundary', 'wikidata', 'is_in']}

    if 'name' not in area_tags:
        return []

    geom = api.query(u"""    
            area({});        
            rel(pivot);            
            out geom;
    """.format(id), cache=False)

    if geom is None:
        return []

    tag_filters = u''.join(
        [u'["{}"={}]'.format(key, value) for key, value in area_tags.items()])

    if not geom.relations:
        geom = api.query(u"""    
                    rel{};            
                    out geom;
            """.format(tag_filters), cache=False)

    if geom.relations:
        area_poly = relation_multipolygon(geom.relations)
    else:
        geom = api.query(u"""
                    area({});
                    way(pivot);            
                    out geom;
                    >;
                    out skel qt;
            """.format(id), cache=False)
        all_points = list(geom.ways).pop().nodes
        area_poly = Polygon(map(lambda n: (float(n.lon), float(n.lat)), all_points))

    if isinstance(area_poly, Polygon):
        area_poly = MultiPolygon([area_poly])

    return [map(lambda x: tuple(x[0]), zip(p.exterior.coords)) for p in area_poly]


def get_area_multipolygon(id):
    area_geoms = g_area_geom(str(id))
    area_polys = [Polygon(points) for points in area_geoms]
    return MultiPolygon(area_polys)


@cache_it_json(cache=cache_proc)
@transform
def g_node(id):
    result = api.query("""
            node({});            
            out;
        """.format(id))
    return list(result.nodes)


@cache_it_json(cache=cache_proc)
def g_node_position(id):
    r = api.query("""
        node({});
        out;
    """.format(id))
    node = list(r.nodes).pop()
    return float(node.lat), float(node.lon)


def nodes_in_buffer(nodes, buffer):
    for n in nodes:
        n_lat, n_lon = g_node_position(n)
        p = Point(float(n_lon), float(n_lat))
        if p.within(buffer):
            return True
    return False


def filter_building(way, b, polygons):
    near_way_points = nearest_points(polygons[b['id']], way)
    b_near_way = near_way_points[0]
    buff = b_near_way.buffer(b_near_way.distance(near_way_points[1]) + 0.00001, mitre_limit=1.0)
    buff = shapely.affinity.scale(buff, 1.0, 0.75)

    n_intersect = buff.boundary.intersection(way)
    n_intersect = [n_intersect] if isinstance(n_intersect, Point) else list(n_intersect)
    n_intersect = MultiPoint(n_intersect)

    filtered = True
    if n_intersect:
        nearest = nearest_points(polygons[b['id']], n_intersect)
        mp = MultiPoint(list(n_intersect) + [nearest[0]])

        try:
            mp = Polygon(mp)
            filtered = any(
                filter(lambda (bid, p): bid != b['id'] and p.boundary.intersects(mp.boundary),
                       polygons.items()))
        except ValueError:
            pass

    return filtered


def get_way_attrs(id, tags, buffer):
    way = g_way(id)
    if is_road(way):
        way['type'] = 'way'
        if not tags:
            all_w_buildings = query_way_buildings(id)
            w_buildings = filter(lambda x: not buffer or Point(x['center_lon'], x['center_lat']).within(buffer),
                                 all_w_buildings)

            shape = LineString(g_way_geom(id))

            all_building_polygons = {b['id']: Polygon(g_way_geom(b['id'])) for b in w_buildings}
            w_buildings = filter(lambda b: not filter_building(shape, b, all_building_polygons), w_buildings)

            way['buildings'] = map(lambda x: 'way/{}'.format(x['id']),
                                   w_buildings)

            all_intersects = query_intersect_ways(id)
            w_intersects = filter(lambda (w, cuts): not buffer or nodes_in_buffer(cuts, buffer), all_intersects.items())
            way['intersect'] = map(lambda x: 'way/{}'.format(x[0]), w_intersects)

            for node in query_way_elms(id):
                p = Point(node['lon'], node['lat'])
                if not buffer or p.within(buffer):