import argparse, itertools, sys, json, logging
import ModestMaps.Core, ModestMaps.OpenStreetMap, uritemplate, requests, google.protobuf.message
from google.protobuf.internal.decoder import _DecodeVarint32
from . import sharedstreets_pb2

logger = logging.getLogger(__name__)

# https://github.com/sharedstreets/sharedstreets-ref-system/issues/16
DATA_URL_TEMPLATE, DATA_ZOOM = 'https://tiles.sharedstreets.io/osm/planet-180312/{z}-{x}-{y}.{layer}.6.pbf', 12
data_classes = {
    'reference': sharedstreets_pb2.SharedStreetsReference,
    'intersection': sharedstreets_pb2.SharedStreetsIntersection,
    'geometry': sharedstreets_pb2.SharedStreetsGeometry,
    'metadata': sharedstreets_pb2.SharedStreetsMetadata,
    }

# Used for Mercator projection and tile space
OSM = ModestMaps.OpenStreetMap.Provider()

UPSTREAM_SHST_REQUEST_TIMEOUT_S = 20

class Tile:
    ''' Container for dicts of SharedStreets geometries, intersections, references, and metadata.
    '''
    def __init__(self, geometries, intersections, references, metadata):
        self.geometries = geometries
        self.intersections = intersections
        self.references = references
        self.metadata = metadata

def truncate_id(id):
    ''' Truncate SharedStreets hash to save space.
    '''
    return id[:12]

def round_coord(float):
    ''' Round a latitude or longitude to appropriate length.
    '''
    return round(float, 7)

def iter_objects(url, DataClass):
    ''' Generate a stream of objects from the protobuf URL.
    '''
    response = requests.get(url, timeout=UPSTREAM_SHST_REQUEST_TIMEOUT_S)
    position = 0
    logger.debug('Got {} bytes: {}'.format(len(response.content), repr(response.content[:32])))

    if response.status_code not in range(200, 299):
        logger.debug('Got HTTP {}'.format(response.status_code))
        return

    while position < len(response.content):
        message_length, new_position = _DecodeVarint32(response.content, position)
        position = new_position
        message = response.content[position:position+message_length]
        position += message_length

        try:
            object = DataClass()
            object.ParseFromString(message)
        except google.protobuf.message.DecodeError:
            # Empty tile? Shrug.
            continue
        else:
            yield object

def is_inside(southwest, northeast, geometry):
    ''' Return True if the geometry bbox is inside a location pair bbox.
    '''
    lons = [geometry.lonlats[i] for i in range(0, len(geometry.lonlats), 2)]
    lats = [geometry.lonlats[i] for i in range(1, len(geometry.lonlats), 2)]

    if max(lons) < southwest.lon or northeast.lon < min(lons):
        return False

    elif max(lats) < southwest.lat or northeast.lat < min(lats):
        return False

    return True

def get_tile(zoom, x, y, data_url_template=None):
    ''' Get a single Tile instance.

        zoom, x, y: Web mercator tile coordinates using OpenStreetMap convention.

            https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels

        data_url_template: RFC 6570 URI template for upstream protobuf tiles
            with z, x, y, and layer expressions. Default to DATA_URL_TEMPLATE.

            https://tools.ietf.org/html/rfc6570#section-2.2)
    '''
    if data_url_template is None:
        data_url_template = DATA_URL_TEMPLATE

    # Define lat/lon for filtered area
    tile_coord = ModestMaps.Core.Coordinate(y, x, zoom)
    data_coord = tile_coord.zoomTo(DATA_ZOOM).container()
    tile_sw = OSM.coordinateLocation(tile_coord.down())
    tile_ne = OSM.coordinateLocation(tile_coord.right())
    data_zxy = dict(z=int(data_coord.zoom), x=int(data_coord.column), y=int(data_coord.row))

    logger.debug((tile_coord, data_coord, tile_sw, tile_ne))

    # Filter geometries within the selected tile
    geom_data_url = uritemplate.expand(data_url_template, layer='geometry', **data_zxy)
    geometries = {geom.id: geom for geom in iter_objects(geom_data_url,
        data_classes['geometry']) if is_inside(tile_sw, tile_ne, geom)}

    logger.debug('{} geometries'.format(len(geometries)))

    # Get intersections attached to one of the filtered geometries
    inter_data_url = uritemplate.expand(data_url_template, layer='intersection', **data_zxy)

    intersection_ids = {id for id in itertools.chain(*[(geom.fromIntersectionId,
        geom.toIntersectionId) for geom in geometries.values()])}
    intersections = {inter.id: inter for inter in iter_objects(inter_data_url,
        data_classes['intersection']) if inter.id in intersection_ids}

    logger.debug('{} intersections'.format(len(intersections)))

    # Get references attached to one of the filtered geometries
    ref_data_url = uritemplate.expand(data_url_template, layer='reference', **data_zxy)
    references = {ref.id: ref for ref in iter_objects(ref_data_url,
        data_classes['reference']) if ref.geometryId in geometries}

    logger.debug('{} references'.format(len(references)))

    # Get metadata attached to one of the filtered geometries
    md_data_url = uritemplate.expand(data_url_template, layer='metadata', **data_zxy)
    metadata = {md.geometryId: md for md in iter_objects(md_data_url,
        data_classes['metadata']) if md.geometryId in geometries}

    logger.debug('{} metadata'.format(len(metadata)))

    return Tile(geometries, intersections, references, metadata)

def geometry_feature(geometry, metadata, id_length):
    '''
    '''
    return {
        'type': 'Feature',
        'role': 'SharedStreets:Geometry',
        'id': geometry.id[:id_length],
        'properties': {
            'id': geometry.id[:id_length],
            'forwardReferenceId': geometry.forwardReferenceId[:id_length],
            'startIntersectionId': geometry.fromIntersectionId[:id_length],
            'backReferenceId': geometry.backReferenceId[:id_length],
            'endIntersectionId': geometry.toIntersectionId[:id_length],
            'roadClass': geometry.roadClass,
            'osmName': str(metadata.osmMetadata.name),
            },
        'geometry': {
            'type': 'LineString',
            'coordinates': [[x, y] for (x, y) in zip(
                [round_coord(geometry.lonlats[i]) for i in range(0, len(geometry.lonlats), 2)],
                [round_coord(geometry.lonlats[i]) for i in range(1, len(geometry.lonlats), 2)]
                )
                ]
            }
        }

def intersection_feature(intersection, id_length):
    '''
    '''
    return {
        'type': 'Feature',
        'role': 'SharedStreets:Intersection',
        'id': intersection.id[:id_length],
        'properties': {
            'id': intersection.id[:id_length],

            'inboundReferenceIds': [id[:id_length] for id in intersection.inboundReferenceIds],
            'outboundReferenceIds': [id[:id_length] for id in intersection.outboundReferenceIds],
            },
        'geometry': {
            'type': 'Point',
            'coordinates': [round_coord(intersection.lon), round_coord(intersection.lat)]
            }
        }

def reference_feature(reference, id_length):
    '''
    '''
    LR0, LR1 = reference.locationReferences

    return {
        'role': 'SharedStreets:Reference',
        'id': reference.id[:id_length],
        'geometryId': reference.geometryId[:id_length],
        'formOfWay': reference.formOfWay,
        'locationReferences': [
            {
                'sequence': 0,
                'intersectionId': LR0.intersectionId[:id_length],
                'distanceToNextRef': LR0.distanceToNextRef,
                'point': [round_coord(LR0.lon), round_coord(LR0.lat)],

                'inboundBearing': LR0.inboundBearing,
                'outboundBearing': LR0.outboundBearing,
                },
            {
                'sequence': 1,
                'intersectionId': LR1.intersectionId[:id_length],
                'distanceToNextRef': None,
                'point': [round_coord(LR1.lon), round_coord(LR1.lat)],

                'inboundBearing': None,
                'outboundBearing': None,
                },
            ]
        }

def make_geojson(tile, id_length=32):
    ''' Get a GeoJSON dictionary for a geographic tile.

        tile: Tile instance with lists of SharedStreets entities.

        id_length: Desired length of SharedStreets ID strings. Normally 32-char
            MD5 hashes, these can be truncated to conserve storage. Default 12.
    '''
    geojson = dict(type='FeatureCollection', features=[], references=[])

    for geometry in tile.geometries.values():
        geojson['features'].append(geometry_feature(geometry, tile.metadata[geometry.id], id_length))
        #break

    for intersection in tile.intersections.values():
        geojson['features'].append(intersection_feature(intersection, id_length))
        #break

    for reference in tile.references.values():
        geojson['references'].append(reference_feature(reference, id_length))
        #break

    return geojson

parser = argparse.ArgumentParser(description='Download a tile of SharedStreets data')
parser.add_argument('zoom', type=int, help='Tile zoom')
parser.add_argument('x', type=int, help='Tile X coordinate')
parser.add_argument('y', type=int, help='Tile Y coordinate')

def main():
    args = parser.parse_args()
    geojson = make_geojson(get_tile(args.zoom, args.x, args.y), id_length=32)
    print(json.dumps(geojson, indent=2))
