#  Copyright 2013 Lars Butler & individual contributors
#
#  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 json
import geomet


def load(source_file):
    """
    Converts Esri Json File to GeoJSON.

    :param source_file:
        Path to a file that contains the Esri JSON data.

    :returns:
         A GeoJSON `dict` representing the geometry read from the file.

    """
    return json.load(source_file)


def loads(string):
    """
    Construct a GeoJSON `dict` from Esri JSON (string/dict).

    :param string:
        The Esri JSON geometry representation

    :returns:
         A GeoJSON `dict` representing the geometry read from the file.
    """
    data = json.loads(string)

    if 'rings' in data:
        return _esri_to_geojson_convert['rings'](data)
    elif 'paths' in data:
        return _esri_to_geojson_convert['paths'](data)
    elif 'x' in data or 'y' in data:
        return _esri_to_geojson_convert['x'](data)
    elif 'points' in data:
        return _esri_to_geojson_convert['points'](data)
    else:
        raise geomet.InvalidGeoJSONException('Invalid EsriJSON: %s' % string)


def dump(obj, dest_file, srid=None):
    """
    Converts GeoJSON to Esri JSON File.
    """
    return json.dump(dumps(obj, srid=srid), dest_file)


def dumps(obj, srid=None):
    """
    Dump a GeoJSON-like `dict` to a Esri JSON.

    :param string:
        The GeoJSON geometry representation
    :param int:
        The default SRID value if none is present.

    """
    if 'type' in obj and \
            obj['type'].lower() in _gj_to_esri.keys():
        convert = _gj_to_esri[obj['type'].lower()]
        return convert(obj, srid=srid)
    else:
        raise geomet.InvalidGeoJSONException("Invalid GeoJSON type %s" % obj)


def _extract_geojson_srid(obj):
    """
    Extracts the SRID code (WKID code) from geojson. If not found, SRID=4326

    :returns: Integer
    """
    meta_srid = obj.get('meta', {}).get('srid', None)
    # Also try to get it from `crs.properties.name`:
    crs_srid = obj.get('crs', {}).get('properties', {}).get('name', None)
    if crs_srid is not None:
        # Shave off the EPSG: prefix to give us the SRID:
        crs_srid = crs_srid.replace('EPSG:', '')

    if (meta_srid is not None and
            crs_srid is not None and
            str(meta_srid) != str(crs_srid)):
        raise ValueError(
            'Ambiguous CRS/SRID values: %s and %s' % (meta_srid, crs_srid)
        )
    srid = meta_srid or crs_srid

    return srid or 4326


def _dump_geojson_point(obj, srid=None):
    """
    Loads GeoJSON to Esri JSON for Geometry type Point.


    """
    coordkey = 'coordinates'
    coords = obj[coordkey]
    srid = _extract_geojson_srid(obj) or srid
    return {'x': coords[0], 'y': coords[1], "spatialReference": {'wkid': srid}}


def _dump_geojson_multipoint(obj, srid=None):
    """
    Loads GeoJSON to Esri JSON for Geometry type MultiPoint.

    """
    coordkey = 'coordinates'
    srid = _extract_geojson_srid(obj) or srid
    return {"points": obj[coordkey], "spatialReference": {"wkid": srid}}


def _dump_geojson_polyline(obj, srid=None):
    """
    Loads GeoJSON to Esri JSON for Geometry type LineString and
    MultiLineString.

    """
    coordkey = 'coordinates'
    if obj['type'].lower() == 'linestring':
        coordinates = [obj[coordkey]]
    else:
        coordinates = obj[coordkey]
    srid = _extract_geojson_srid(obj) or srid
    return {"paths": coordinates, "spatialReference": {"wkid": srid}}


def _dump_geojson_polygon(data, srid=None):
    """
    Loads GeoJSON to Esri JSON for Geometry type Polygon or MultiPolygon.

    """
    coordkey = 'coordinates'
    coordinates = data[coordkey]
    typekey = ([d for d in data if d.lower() == 'type']
               or ['type']).pop()
    if data[typekey].lower() == "polygon":
        coordinates = [coordinates]
    part_list = []
    for part in coordinates:
        part_item = []
        for idx, ring in enumerate(part):
            if idx:
                part_item.append(None)
            for coord in ring:
                part_item.append(coord)
        if part_item:
            part_list.append(part_item)
    srid = _extract_geojson_srid(data) or srid
    return {'rings': part_list, "spatialReference": {"wkid": srid}}


def _to_gj_point(obj):
    """
    Dump a Esri JSON Point to GeoJSON Point.

    :param dict obj:
        A EsriJSON-like `dict` representing a Point.


    :returns:
        GeoJSON representation of the Esri JSON Point
    """
    if obj.get("x", None) is None or \
            obj.get("y", None) is None:
        return {'type': 'Point', 'coordinates': ()}
    return {'type': 'Point', 'coordinates': (obj.get("x"),
                                             obj.get("y"))}


def _to_gj_polygon(obj):
    """
    Dump a EsriJSON-like Polygon object to GeoJSON.

    Input parameters and return value are the POLYGON equivalent to
    :func:`_to_gj_point`.
    """
    def split_part(a_part):
        part_list = []
        for item in a_part:
            if item is None:
                if part_list:
                    yield part_list
                part_list = []
            else:
                part_list.append((item[0], item[1]))
        if part_list:
            yield part_list
    part_json = [list(split_part(part))
                 for part in obj['rings']]
    return {'type': 'MultiPolygon', 'coordinates': part_json}


def _to_gj_multipoint(data):
    """
    Dump a EsriJSON-like MultiPoint object to GeoJSON-dict.

    Input parameters and return value are the MULTIPOINT equivalent to
    :func:`_to_gj_point`.

    :returns: `dict`
    """

    return {'type': 'Multipoint', 'coordinates': [pt for pt in data['points']]}


def _to_gj_polyline(data):
    """
    Dump a GeoJSON-like MultiLineString object to WKT.

    Input parameters and return value are the MULTILINESTRING equivalent to
    :func:`_dump_point`.
    """
    return {
        'type': 'MultiLineString',
        'coordinates': [
            [((pt[0], pt[1]) if pt else None) for pt in part]
            for part in data["paths"]
        ],
    }


_esri_to_geojson_convert = {
    "x": _to_gj_point,
    "y": _to_gj_point,
    "points": _to_gj_multipoint,
    "rings": _to_gj_polygon,
    "paths": _to_gj_polyline}

_gj_to_esri = {
    "point": _dump_geojson_point,
    "multipoint": _dump_geojson_multipoint,
    "linestring": _dump_geojson_polyline,
    "multilinestring": _dump_geojson_polyline,
    "polygon": _dump_geojson_polygon,
    "multipolygon": _dump_geojson_polygon}
