"""Fiona data model"""

from binascii import hexlify
from collections.abc import MutableMapping
from collections import OrderedDict
from enum import Enum
import itertools
from json import JSONEncoder
from warnings import warn

from fiona.errors import FionaDeprecationWarning


class OGRGeometryType(Enum):
    Unknown = 0
    Point = 1
    LineString = 2
    Polygon = 3
    MultiPoint = 4
    MultiLineString = 5
    MultiPolygon = 6
    GeometryCollection = 7
    CircularString = 8
    CompoundCurve = 9
    CurvePolygon = 10
    MultiCurve = 11
    MultiSurface = 12
    Curve = 13
    Surface = 14
    PolyhedralSurface = 15
    TIN = 16
    Triangle = 17
    NONE = 100
    LinearRing = 101
    CircularStringZ = 1008
    CompoundCurveZ = 1009
    CurvePolygonZ = 1010
    MultiCurveZ = 1011
    MultiSurfaceZ = 1012
    CurveZ = 1013
    SurfaceZ = 1014
    PolyhedralSurfaceZ = 1015
    TINZ = 1016
    TriangleZ = 1017
    PointM = 2001
    LineStringM = 2002
    PolygonM = 2003
    MultiPointM = 2004
    MultiLineStringM = 2005
    MultiPolygonM = 2006
    GeometryCollectionM = 2007
    CircularStringM = 2008
    CompoundCurveM = 2009
    CurvePolygonM = 2010
    MultiCurveM = 2011
    MultiSurfaceM = 2012
    CurveM = 2013
    SurfaceM = 2014
    PolyhedralSurfaceM = 2015
    TINM = 2016
    TriangleM = 2017
    PointZM = 3001
    LineStringZM = 3002
    PolygonZM = 3003
    MultiPointZM = 3004
    MultiLineStringZM = 3005
    MultiPolygonZM = 3006
    GeometryCollectionZM = 3007
    CircularStringZM = 3008
    CompoundCurveZM = 3009
    CurvePolygonZM = 3010
    MultiCurveZM = 3011
    MultiSurfaceZM = 3012
    CurveZM = 3013
    SurfaceZM = 3014
    PolyhedralSurfaceZM = 3015
    TINZM = 3016
    TriangleZM = 3017
    Point25D = 0x80000001
    LineString25D = 0x80000002
    Polygon25D = 0x80000003
    MultiPoint25D = 0x80000004
    MultiLineString25D = 0x80000005
    MultiPolygon25D = 0x80000006
    GeometryCollection25D = 0x80000007


# Mapping of OGR integer geometry types to GeoJSON type names.
_GEO_TYPES = {
    OGRGeometryType.Unknown.value: "Unknown",
    OGRGeometryType.Point.value: "Point",
    OGRGeometryType.LineString.value: "LineString",
    OGRGeometryType.Polygon.value: "Polygon",
    OGRGeometryType.MultiPoint.value: "MultiPoint",
    OGRGeometryType.MultiLineString.value: "MultiLineString",
    OGRGeometryType.MultiPolygon.value: "MultiPolygon",
    OGRGeometryType.GeometryCollection.value: "GeometryCollection"
}

GEOMETRY_TYPES = {
    **_GEO_TYPES,
    OGRGeometryType.NONE.value: "None",
    OGRGeometryType.LinearRing.value: "LinearRing",
    OGRGeometryType.Point25D.value: "3D Point",
    OGRGeometryType.LineString25D.value: "3D LineString",
    OGRGeometryType.Polygon25D.value: "3D Polygon",
    OGRGeometryType.MultiPoint25D.value: "3D MultiPoint",
    OGRGeometryType.MultiLineString25D.value: "3D MultiLineString",
    OGRGeometryType.MultiPolygon25D.value: "3D MultiPolygon",
    OGRGeometryType.GeometryCollection25D.value: "3D GeometryCollection",
}


class Object(MutableMapping):
    """Base class for CRS, geometry, and feature objects

    In Fiona 2.0, the implementation of those objects will change.  They
    will no longer be dicts or derive from dict, and will lose some
    features like mutability and default JSON serialization.

    Object will be used for these objects in Fiona 1.9. This class warns
    about future deprecation of features.
    """

    _delegated_properties = []

    def __init__(self, **kwds):
        self._data = OrderedDict(**kwds)

    def _props(self):
        return {
            k: getattr(self._delegate, k)
            for k in self._delegated_properties
            if k is not None  # getattr(self._delegate, k) is not None
        }

    def __getitem__(self, item):
        props = self._props()
        props.update(**self._data)
        return props[item]

    def __iter__(self):
        props = self._props()
        return itertools.chain(iter(props), iter(self._data))

    def __len__(self):
        props = self._props()
        return len(props) + len(self._data)

    def __setitem__(self, key, value):
        warn(
            "instances of this class -- CRS, geometry, and feature objects -- will become immutable in fiona version 2.0",
            FionaDeprecationWarning,
            stacklevel=2,
        )
        if key in self._delegated_properties:
            setattr(self._delegate, key, value)
        else:
            self._data[key] = value

    def __delitem__(self, key):
        warn(
            "instances of this class -- CRS, geometry, and feature objects -- will become immutable in fiona version 2.0",
            FionaDeprecationWarning,
            stacklevel=2,
        )
        if key in self._delegated_properties:
            setattr(self._delegate, key, None)
        else:
            del self._data[key]

    def __eq__(self, other):
        return dict(**self) == dict(**other)


class _Geometry:
    def __init__(self, coordinates=None, type=None, geometries=None):
        self.coordinates = coordinates
        self.type = type
        self.geometries = geometries


class Geometry(Object):
    """A GeoJSON-like geometry

    Notes
    -----
    Delegates coordinates and type properties to an instance of
    _Geometry, which will become an extension class in Fiona 2.0.

    """

    _delegated_properties = ["coordinates", "type", "geometries"]

    def __init__(self, coordinates=None, type=None, geometries=None, **data):
        self._delegate = _Geometry(
            coordinates=coordinates, type=type, geometries=geometries
        )
        super().__init__(**data)

    @classmethod
    def from_dict(cls, ob=None, **kwargs):
        if ob is not None:
            data = dict(getattr(ob, "__geo_interface__", ob))
            data.update(kwargs)
        else:
            data = kwargs

        if "geometries" in data and data["type"] == "GeometryCollection":
            _ = data.pop("coordinates", None)
            _ = data.pop("type", None)
            return Geometry(
                type="GeometryCollection",
                geometries=[
                    Geometry.from_dict(part) for part in data.pop("geometries")
                ],
                **data
            )
        else:
            _ = data.pop("geometries", None)
            return Geometry(
                type=data.pop("type", None),
                coordinates=data.pop("coordinates", []),
                **data
            )

    @property
    def coordinates(self):
        """The geometry's coordinates

        Returns
        -------
        Sequence

        """
        return self._delegate.coordinates

    @property
    def type(self):
        """The geometry's type

        Returns
        -------
        str

        """
        return self._delegate.type

    @property
    def geometries(self):
        """A collection's geometries.

        Returns
        -------
        list

        """
        return self._delegate.geometries

    @property
    def __geo_interface__(self):
        return ObjectEncoder().default(self)


class _Feature:
    def __init__(self, geometry=None, id=None, properties=None):
        self.geometry = geometry
        self.id = id
        self.properties = properties


class Feature(Object):
    """A GeoJSON-like feature

    Notes
    -----
    Delegates geometry and properties to an instance of _Feature, which
    will become an extension class in Fiona 2.0.

    """

    _delegated_properties = ["geometry", "id", "properties"]

    def __init__(self, geometry=None, id=None, properties=None, **data):
        if properties is None:
            properties = Properties()
        self._delegate = _Feature(geometry=geometry, id=id, properties=properties)
        super().__init__(**data)

    @classmethod
    def from_dict(cls, ob=None, **kwargs):
        if ob is not None:
            data = dict(getattr(ob, "__geo_interface__", ob))
            data.update(kwargs)
        else:
            data = kwargs
        geom_data = data.pop("geometry", None)

        if isinstance(geom_data, Geometry):
            geom = geom_data
        else:
            geom = Geometry.from_dict(geom_data) if geom_data is not None else None

        props_data = data.pop("properties", None)

        if isinstance(props_data, Properties):
            props = props_data
        else:
            props = Properties(**props_data) if props_data is not None else None

        fid = data.pop("id", None)
        return Feature(geometry=geom, id=fid, properties=props, **data)

    def __eq__(self, other):
        return (
            self.geometry == other.geometry
            and self.id == other.id
            and self.properties == other.properties
        )

    @property
    def geometry(self):
        """The feature's geometry object

        Returns
        -------
        Geometry

        """
        return self._delegate.geometry

    @property
    def id(self):
        """The feature's id

        Returns
        ------
        object

        """
        return self._delegate.id

    @property
    def properties(self):
        """The feature's properties

        Returns
        -------
        object

        """
        return self._delegate.properties

    @property
    def type(self):
        """The Feature's type

        Returns
        -------
        str

        """
        return "Feature"

    @property
    def __geo_interface__(self):
        return ObjectEncoder().default(self)


class Properties(Object):
    """A GeoJSON-like feature's properties"""

    def __init__(self, **kwds):
        super().__init__(**kwds)

    @classmethod
    def from_dict(cls, mapping=None, **kwargs):
        if mapping:
            return Properties(**mapping, **kwargs)
        return Properties(**kwargs)


class ObjectEncoder(JSONEncoder):
    """Encodes Geometry, Feature, and Properties."""

    def default(self, o):
        if isinstance(o, (Geometry, Properties)):
            return {k: self.default(v) for k, v in o.items() if v is not None}
        elif isinstance(o, Feature):
            o_dict = dict(o)
            o_dict["type"] = "Feature"
            if o.geometry is not None:
                o_dict["geometry"] = self.default(o.geometry)
            if o.properties is not None:
                o_dict["properties"] = self.default(o.properties)
            return o_dict
        elif isinstance(o, bytes):
            return hexlify(o)
        else:
            return o


def decode_object(obj):
    """A json.loads object_hook

    Parameters
    ----------
    obj : dict
        A decoded dict.

    Returns
    -------
    Feature, Geometry, or dict

    """
    if isinstance(obj, Object):
        return obj
    else:
        obj = obj.get("__geo_interface__", obj)

        _type = obj.get("type", None)
        if (_type == "Feature") or "geometry" in obj:
            return Feature.from_dict(obj)
        elif _type in _GEO_TYPES.values():
            return Geometry.from_dict(obj)
        else:
            return obj


def to_dict(val):
    """Converts an object to a dict"""
    try:
        obj = ObjectEncoder().default(val)
    except TypeError:
        return val
    else:
        return obj
