import re
from decimal import Decimal

from django.conf import settings
from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.postgresql_psycopg2.base import DatabaseOperations
from django.db.utils import DatabaseError
from django.utils import six

#### Classes used in constructing PostGIS spatial SQL ####
class PostGISOperator(SpatialOperation):
    "For PostGIS operators (e.g. `&&`, `~`)."
    def __init__(self, operator):
        super(PostGISOperator, self).__init__(operator=operator)

class PostGISFunction(SpatialFunction):
    "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
    def __init__(self, prefix, function, **kwargs):
        super(PostGISFunction, self).__init__(prefix + function, **kwargs)

class PostGISFunctionParam(PostGISFunction):
    "For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
    sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'

class PostGISDistance(PostGISFunction):
    "For PostGIS distance operations."
    dist_func = 'Distance'
    sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'

    def __init__(self, prefix, operator):
        super(PostGISDistance, self).__init__(prefix, self.dist_func,
                                              operator=operator)

class PostGISSpheroidDistance(PostGISFunction):
    "For PostGIS spherical distance operations (using the spheroid)."
    dist_func = 'distance_spheroid'
    sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s'
    def __init__(self, prefix, operator):
        # An extra parameter in `end_subst` is needed for the spheroid string.
        super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func,
                                                      operator=operator)

class PostGISSphereDistance(PostGISDistance):
    "For PostGIS spherical distance operations."
    dist_func = 'distance_sphere'

class PostGISRelate(PostGISFunctionParam):
    "For PostGIS Relate(<geom>, <pattern>) calls."
    pattern_regex = re.compile(r'^[012TF\*]{9}$')
    def __init__(self, prefix, pattern):
        if not self.pattern_regex.match(pattern):
            raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
        super(PostGISRelate, self).__init__(prefix, 'Relate')


class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
    compiler_module = 'django.contrib.gis.db.models.sql.compiler'
    name = 'postgis'
    postgis = True
    version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
    valid_aggregates = dict([(k, None) for k in
                             ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])

    Adapter = PostGISAdapter
    Adaptor = Adapter # Backwards-compatibility alias.

    def __init__(self, connection):
        super(PostGISOperations, self).__init__(connection)

        # Trying to get the PostGIS version because the function
        # signatures will depend on the version used.  The cost
        # here is a database query to determine the version, which
        # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
        # comprising user-supplied values for the major, minor, and
        # subminor revision of PostGIS.
        try:
            if hasattr(settings, 'POSTGIS_VERSION'):
                vtup = settings.POSTGIS_VERSION
                if len(vtup) == 3:
                    # The user-supplied PostGIS version.
                    version = vtup
                else:
                    # This was the old documented way, but it's stupid to
                    # include the string.
                    version = vtup[1:4]
            else:
                vtup = self.postgis_version_tuple()

                version = vtup[1:]

            # Getting the prefix -- even though we don't officially support
            # PostGIS 1.2 anymore, keeping it anyway in case a prefix change
            
# for something else is necessary.
            if version >= (1, 2, 2):
                prefix = 'ST_'
            else:
                prefix = ''

            self.geom_func_prefix = prefix
            self.spatial_version = version
        except DatabaseError:
            raise ImproperlyConfigured(
                'Cannot determine PostGIS version for database "%s". '
                'GeoDjango requires at least PostGIS version 1.3. '
                'Was the database created from a spatial database '
                'template?' % self.connection.settings_dict['NAME']
                )
        # TODO: Raise helpful exceptions as they become known.

        # PostGIS-specific operators. The commented descriptions of these
        # operators come from Section 7.6 of the PostGIS 1.4 documentation.
        self.geometry_operators = {
            # The "&<" operator returns true if A's bounding box overlaps or
            # is to the left of B's bounding box.
            'overlaps_left' : PostGISOperator('&<'),
            # The "&>" operator returns true if A's bounding box overlaps or
            # is to the right of B's bounding box.
            'overlaps_right' : PostGISOperator('&>'),
            # The "<<" operator returns true if A's bounding box is strictly
            # to the left of B's bounding box.
            'left' : PostGISOperator('<<'),
            # The ">>" operator returns true if A's bounding box is strictly
            # to the right of B's bounding box.
            'right' : PostGISOperator('>>'),
            # The "&<|" operator returns true if A's bounding box overlaps or
            # is below B's bounding box.
            'overlaps_below' : PostGISOperator('&<|'),
            # The "|&>" operator returns true if A's bounding box overlaps or
            # is above B's bounding box.
            'overlaps_above' : PostGISOperator('|&>'),
            # The "<<|" operator returns true if A's bounding box is strictly
            # below B's bounding box.
            'strictly_below' : PostGISOperator('<<|'),
            # The "|>>" operator returns true if A's bounding box is strictly
            # above B's bounding box.
            'strictly_above' : PostGISOperator('|>>'),
            # The "~=" operator is the "same as" operator. It tests actual
            # geometric equality of two features. So if A and B are the same feature,
            # vertex-by-vertex, the operator returns true.
            'same_as' : PostGISOperator('~='),
            'exact' : PostGISOperator('~='),
            # The "@" operator returns true if A's bounding box is completely contained
            # by B's bounding box.
            'contained' : PostGISOperator('@'),
            # The "~" operator returns true if A's bounding box completely contains
            #  by B's bounding box.
            'bbcontains' : PostGISOperator('~'),
            # The "&&" operator returns true if A's bounding box overlaps
            # B's bounding box.
            'bboverlaps' : PostGISOperator('&&'),
            }

        self.geometry_functions = {
            'equals' : PostGISFunction(prefix, 'Equals'),
            'disjoint' : PostGISFunction(prefix, 'Disjoint'),
            'touches' : PostGISFunction(prefix, 'Touches'),
            'crosses' : PostGISFunction(prefix, 'Crosses'),
            'within' : PostGISFunction(prefix, 'Within'),
            'overlaps' : PostGISFunction(prefix, 'Overlaps'),
            'contains' : PostGISFunction(prefix, 'Contains'),
            'intersects' : PostGISFunction(prefix, 'Intersects'),
            'relate' : (PostGISRelate, six.string_types),
            'coveredby' : PostGISFunction(prefix, 'CoveredBy'),
            'covers' : PostGISFunction(prefix, 'Covers'),
        }

        # Valid distance types and substitutions
        dtypes = (Decimal, Distance, float) + si