"""
 This module contains the 'base' GEOSGeometry object -- all GEOS Geometries
 inherit from this object.
"""
from __future__ import unicode_literals

from ctypes import addressof, byref, c_double

from django.contrib.gis import gdal
from django.contrib.gis.geometry.regex import hex_regex, json_regex, wkt_regex
from django.contrib.gis.geos import prototypes as capi
from django.contrib.gis.geos.base import GEOSBase
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError
from django.contrib.gis.geos.libgeos import GEOM_PTR
from django.contrib.gis.geos.mutable_list import ListMixin
from django.contrib.gis.geos.prepared import PreparedGeometry
from django.contrib.gis.geos.prototypes.io import (
    ewkb_w, wkb_r, wkb_w, wkt_r, wkt_w,
)
from django.utils import six
from django.utils.encoding import force_bytes, force_text


class GEOSGeometry(GEOSBase, ListMixin):
    "A class that, generally, encapsulates a GEOS geometry."

    # Raise GEOSIndexError instead of plain IndexError
    # (see ticket #4740 and GEOSIndexError docstring)
    _IndexError = GEOSIndexError
    _GEOS_CLASSES = None

    ptr_type = GEOM_PTR
    has_cs = False  # Only Point, LineString, LinearRing have coordinate sequences

    def __init__(self, geo_input, srid=None):
        """
        The base constructor for GEOS geometry objects, and may take the
        following inputs:

         * strings:
            - WKT
            - HEXEWKB (a PostGIS-specific canonical form)
            - GeoJSON (requires GDAL)
         * buffer:
            - WKB

        The `srid` keyword is used to specify the Source Reference Identifier
        (SRID) number for this Geometry.  If not set, the SRID will be None.
        """
        if isinstance(geo_input, bytes):
            geo_input = force_text(geo_input)
        if isinstance(geo_input, six.string_types):
            wkt_m = wkt_regex.match(geo_input)
            if wkt_m:
                # Handling WKT input.
                if wkt_m.group('srid'):
                    srid = int(wkt_m.group('srid'))
                g = wkt_r().read(force_bytes(wkt_m.group('wkt')))
            elif hex_regex.match(geo_input):
                # Handling HEXEWKB input.
                g = wkb_r().read(force_bytes(geo_input))
            elif json_regex.match(geo_input):
                # Handling GeoJSON input.
                if not gdal.HAS_GDAL:
                    raise ValueError('Initializing geometry from JSON input requires GDAL.')
                g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb)
            else:
                raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.')
        elif isinstance(geo_input, GEOM_PTR):
            # When the input is a pointer to a geometry (GEOM_PTR).
            g = geo_input
        elif isinstance(geo_input, six.memoryview):
            # When the input is a buffer (WKB).
            g = wkb_r().read(geo_input)
        elif isinstance(geo_input, GEOSGeometry):
            g = capi.geom_clone(geo_input.ptr)
        else:
            # Invalid geometry type.
            raise TypeError('Improper geometry input type: %s' % str(type(geo_input)))

        if g:
            # Setting the pointer object with a valid pointer.
            self.ptr = g
        else:
            raise GEOSException('Could not initialize GEOS Geometry with given input.')

        # Post-initialization setup.
        self._post_init(srid)

    def _post_init(self, srid):
        "Helper routine for performing post-initialization setup."
        # Setting the SRID, if given.
        if srid and isinstance(srid, int):
            self.srid = srid

        # Setting the class type (e.g., Point, Polygon, etc.)
        if GEOSGeometry._GEOS_CLASSES is None:
            # Lazy-loaded variable to avoid import conflicts with GEOSGeometry.
            from .linestring import LineString, LinearRing
            from .point import Point
            from .polygon import Polygon
            from .collections import (
                GeometryCollection, MultiPoint, MultiLineString, MultiPolygon)
            GEOSGeometry._GEOS_CLASSES = {
                0: Point,
                1: LineString,
                2: LinearRing,
                3: Polygon,
                4: MultiPoint,
                5: MultiLineString,
                6: MultiPolygon,
                7: GeometryCollection,
            }
        self.__class__ = GEOSGeometry._GEOS_CLASSES[self.geom_typeid]

        # Setting the coordinate sequence for the geometry (will be None on
        # geometries that do not have coordinate sequences)
        self._set_cs()

    def __del__(self):
        """
        Destroys this Geometry; in other words, frees the memory used by the
        GEOS C++ object.
        """
        if self._ptr and capi:
            capi.destroy_geom(self._ptr)

    def __copy__(self):
        """
        Returns a clone because the copy of a GEOSGeometry may contain an
        invalid pointer location if the original is garbage collected.
        """
        return self.clone()

    def __deepcopy__(self, memodict):
        """
        The `deepcopy` routine is used by the `Node` class of django.utils.tree;
        thus, the protocol routine needs to be implemented to return correct
        copies (clones) of these GEOS objects, which use C pointers.
        """
        return self.clone()

    def __str__(self):
        "EWKT is used for the string representation."
        return self.ewkt

    def __repr__(self):
        "Short-hand representation because WKT may be very large."
        return '<%s object at %s>' % (self.geom_type, hex(addressof(self.ptr)))

    # Pickling support
    def __getstate__(self):
        # The pickled state is simply a tuple of the WKB (in string form)
        # and the SRID.
        return bytes(self.wkb), self.srid

    def __setstate__(self, state):
        # Instantiating from the tuple state that was pickled.
        wkb, srid = state
        ptr = wkb_r().read(six.memoryview(wkb))
        if not ptr:
            raise GEOSException('Invalid Geometry loaded from pickled state.')
        self.ptr = ptr
        self._post_init(srid)

    # Comparison operators
    def __eq__(self, other):
        """
        Equivalence testing, a Geometry may be compared with another Geometry
        or a WKT representation.
        """
        if isinstance(other, six.string_types):
            return self.wkt == other
        elif isinstance(other, GEOSGeometry):
            return self.equals_exact(other)
        else:
            return False

    def __ne__(self, other):
        "The not equals operator."
        return not (self == other)

    # ### Geometry set-like operations ###
    # Thanks to Sean Gillies for inspiration:
    #  http://lists.gispython.org/pipermail/community/2007-July/001034.html
    # g = g1 | g2
    def __or__(self, other):
        "Returns the union of this Geometry and the other."
        return self.union(other)

    # g = g1 & g2
    def __and__(self, other):
        "Returns the intersection of this Geometry and the other."
        return self.intersection(other)

    # g = g1 - g2
    def __sub__(self, other):
        "Return the difference this Geometry and the other."
        return self.difference(other)

    # g = g1 ^ g2
    def __xor__(self, other):
        "Return the symmetric difference of this Geometry and the other."
        return self.sym_difference(other)

    # #### Coordinate Sequence Routines ####
    def _set_cs(self):
        "Sets the coordinate sequence for this Geometry."
        if self.has_cs:
            self._cs = GEOSCoordSeq(capi.get_cs(self.ptr), self.hasz)
        else:
            self._cs = None

    @property
    def coord_seq(self):
        "Returns a clone of the coordinate sequence for this Geometry."
        if self.has_cs:
            return self._cs.clone()