"""
Collection of postgres-specific extensions, currently including:

* Support for hstore, a key/value type storage
"""
import uuid

from peewee import *
from peewee import Expression
from peewee import logger
from peewee import Node
from peewee import OP
from peewee import Param
from peewee import Passthrough
from peewee import returns_clone
from peewee import QueryCompiler
from peewee import SelectQuery
from peewee import UUIDField  # For backwards-compatibility.

try:
    from psycopg2cffi import compat
    compat.register()
except ImportError:
    pass

from psycopg2.extensions import adapt
from psycopg2.extensions import AsIs
from psycopg2.extensions import register_adapter
from psycopg2.extras import register_hstore
try:
    from psycopg2.extras import Json
except:
    Json = None

@Node.extend(clone=False)
def cast(self, as_type):
    return Expression(self, OP.CAST, SQL(as_type))

class _LookupNode(Node):
    def __init__(self, node, parts):
        self.node = node
        self.parts = parts
        super(_LookupNode, self).__init__()

    def clone_base(self):
        return type(self)(self.node, list(self.parts))

    def cast(self, as_type):
        return Expression(Clause(self, parens=True), OP.CAST, SQL(as_type))

class _JsonLookupBase(_LookupNode):
    def __init__(self, node, parts, as_json=False):
        super(_JsonLookupBase, self).__init__(node, parts)
        self._as_json = as_json

    def clone_base(self):
        return type(self)(self.node, list(self.parts), self._as_json)

    @returns_clone
    def as_json(self, as_json=True):
        self._as_json = as_json

    def contains(self, other):
        clone = self.as_json(True)
        if isinstance(other, (list, dict)):
            return Expression(clone, OP.JSONB_CONTAINS, Json(other))
        return Expression(clone, OP.JSONB_EXISTS, other)

    def contains_any(self, *keys):
        return Expression(
            self.as_json(True),
            OP.JSONB_CONTAINS_ANY_KEY,
            Passthrough(list(keys)))

    def contains_all(self, *keys):
        return Expression(
            self.as_json(True),
            OP.JSONB_CONTAINS_ALL_KEYS,
            Passthrough(list(keys)))

class JsonLookup(_JsonLookupBase):
    _node_type = 'json_lookup'

    def __getitem__(self, value):
        return JsonLookup(self.node, self.parts + [value], self._as_json)

class JsonPath(_JsonLookupBase):
    _node_type = 'json_path'

class ObjectSlice(_LookupNode):
    _node_type = 'object_slice'

    @classmethod
    def create(cls, node, value):
        if isinstance(value, slice):
            parts = [value.start or 0, value.stop or 0]
        elif isinstance(value, int):
            parts = [value]
        else:
            parts = map(int, value.split(':'))
        return cls(node, parts)

    def __getitem__(self, value):
        return ObjectSlice.create(self, value)

class _Array(object):
    def __init__(self, field, items):
        self.field = field
        self.items = items

def adapt_array(arr):
    conn = arr.field.model_class._meta.database.get_conn()
    items = adapt(arr.items)
    items.prepare(conn)
    return AsIs('%s::%s%s' % (
        items,
        arr.field.get_column_type(),
        '[]' * arr.field.dimensions))
register_adapter(_Array, adapt_array)


class IndexedFieldMixin(object):
    default_index_type = 'GiST'

    def __init__(self, index_type=None, *args, **kwargs):
        kwargs.setdefault('index', True)  # By default, use an index.
        super(IndexedFieldMixin, self).__init__(*args, **kwargs)
        if self.index:
            self.index_type = index_type or self.default_index_type
        else:
            self.index_type = None


class ArrayField(IndexedFieldMixin, Field):
    default_index_type = 'GIN'

    def __init__(self, field_class=IntegerField, dimensions=1, *args,
                 **kwargs):
        self.__field = field_class(*args, **kwargs)
        self.dimensions = dimensions
        self.db_field = self.__field.get_db_field()
        super(ArrayField, self).__init__(*args, **kwargs)

    def __ddl_column__(self, column_type):
        sql = self.__field.__ddl_column__(column_type)
        sql.value += '[]' * self.dimensions
        return sql

    def db_value(self, value):
        if value is None:
            return
        if not isinstance(value, (list, _Array)):
            value = list(value)
        return _Array(self, value)

    def __getitem__(self, value):
        return ObjectSlice.create(self, value)

    def contains(self, *items):
        return Expression(self, OP.ACONTAINS, Param(items))

    def contains_any(self, *items):
        return Expression(self, OP.ACONTAINS_ANY, Param(items))


class DateTimeTZField(DateTimeField):
    db_field = 'timestamptz'


class HStoreField(IndexedFieldMixin, Field):
    db_field = 'hash'

    def __getitem__(self, key):
        return Expression(self, OP.HKEY, Param(key))

    def keys(self):
        return fn.akeys(self)

    def values(self):
        return fn.avals(self)

    def items(self):
        return fn.hstore_to_matrix(self)

    def slice(self, *args):
        return fn.slice(self, Passthrough(list(args)))

    def exists(self, key):
        return fn.exist(self, key)

    def defined(self, key):
        return fn.defined(self, key)

    def update(self, **data):
        return Expression(self, OP.HUPDATE, data)

    def delete(self, *keys):
        return fn.delete(self, Passthrough(list(keys)))

    def contains(self, value):
        if isinstance(value, dict):
            return Expression(self, OP.HCONTAINS_DICT, Passthrough(value))
        elif isinstance(value, (list, tuple)):
            return Expression(self, OP.HCONTAINS_KEYS, Passthrough(value))
        return Expression(self, OP.HCONTAINS_KEY, value)

    def contains_any(self, *keys):
        return Expression(self, OP.HCONTAINS_ANY_KEY, Passthrough(list(keys)))


class JSONField(Field):
    db_field = 'json'

    def __init__(self, dumps=None, *args, **kwargs):
        if Json is None:
            raise Exception('Your version of psycopg2 does not support JSON.')

        self.dumps = dumps
        super(JSONField, self).__init__(*args, **kwargs)

    def db_value(self, value):
        if value is None:
            return value
        if not isinstance(value, Json):
            return Json(value, dumps=self.dumps)
        return value

    def __getitem__(self, value):
        return JsonLookup(self, [value])

    def path(self, *keys):
        return JsonPath(self, keys)


class BinaryJSONField(IndexedFieldMixin, JSONField):
    db_field = 'jsonb'
    default_index_type = 'GIN'

    def contains(self, other):
        if isinstance(other, (list, dict)):
            return Expression(self, OP.JSONB_CONTAINS, Json(other))
        return Expression(self, OP.JSONB_EXISTS, Passthrough(other))

    def contained_by(self, other):
        return Expression(self, OP.JSONB_CONTAINED_BY, Json(other))

    def contains_any(self, *items):
        return Expression(
            self,
            OP.JSONB_CONTAINS_ANY_KEY,
            Passthrough(list(items)),)

    def contains_all(self, *items):
        return Expression(
            self,
            OP.JSONB_CONTAINS_ALL_KEYS,
            Passthrough(list(items)))


class TSVectorField(IndexedFieldMixin, TextField):
    db_field = 'tsvector'
    default_index_type = 'GIN'

    def match(self, query, language=None):
        params = (language, query) if language is not None else (query,)
        return Expression(self, OP.TS_MATCH, fn.to_tsquery(*params))


def Match(field, query, language=None):
    params = (language, query) if language is not None else (query,)
    return Expression(
        fn.to_tsvector(field),
        OP.TS_MATCH,
        fn.to_tsquery(*params))


OP.update(
    HKEY='key',
    HUPDATE='H@>',
    HCONTAINS_DICT='H?&',
    HCONTAINS_KEYS='H?',
    HCONTAINS_KEY='H?|',
    HCONTAINS_ANY_KEY='H||',
    ACONTAINS='A@>',
    ACONTAINS_ANY='A||',
    TS_MATCH='T@@',
    JSONB_CONTAINS='JB@>',
    JSONB_CONTAINED_BY='JB<@',
    JSONB_CONTAINS_ANY_KEY='JB?|',
    JSONB_CONTAINS_ALL_KEYS='JB?&',
    JSONB_EXISTS='JB?',
    CAST='::',
)


class PostgresqlExtCompiler(QueryCompiler):
    def _create_index(self, model_class, fields, unique=False):
        clause = super(PostgresqlExtCompiler, self)._create_index(
            model_class, fields, unique)
        # Allow fields to specify a type of index.  HStore and Array fields
        # may want to use GiST indexes, for example.
        index_type = None
        for field in fields:
            if isinstance(field, IndexedFieldMixin):
                index_type = field.index_type
        if index_type:
            clause.nodes.insert(-1, SQL('USING %s' % index_type))
        return clause

    def _parse_object_slice(self, node, alias_map, conv):
        sql, params = self.parse_node(node.node, alias_map, conv)
        # Postgresql uses 1-based indexes.
        parts = [str(part + 1) for part in node.parts]
        sql = '%s[%s]' % (sql, ':'.join(parts))
        return sql, params

    def _parse_json_lookup(self, node, alias_map, conv):
        sql, params = self.parse_node(node.node, alias_map, conv)
        lookups = [sql]
        for part in node.parts:
            part_sql, part_params = self.parse_node(
                part, alias_map, conv)
            lookups.append(part_sql)
            params.extend(part_params)

        if node._as_json:
            sql = '->'.join(lookups)
        else:
            # The last lookup should be converted to text.
            head, tail = lookups[:-1], lookups[-1]
            sql = '->>'.join(('->'.join(head), tail))

        return sql, params

    def _parse_json_path(self, node, alias_map, conv):
        sql, params = self.parse_node(node.node, alias_map, conv)
        if node._as_json:
            operand = '#>'
        else:
            operand = '#>>'
        params.append('{%s}' % ','.join(map(str, node.parts)))
        return operand.join((sql, self.interpolation)), params

    def get_parse_map(self):
        parse_map = super(PostgresqlExtCompiler, self).get_parse_map()
        parse_map.update(
            object_slice=self._parse_object_slice,
            json_lookup=self._parse_json_lookup,
            json_path=self._parse_json_path)
        return parse_map


class PostgresqlExtDatabase(PostgresqlDatabase):
    compiler_class = PostgresqlExtCompiler

    def __init__(self, *args, **kwargs):
        self.server_side_cursors = kwargs.pop('server_side_cursors', False)
        self.register_hstore = kwargs.pop('register_hstore', True)
        super(PostgresqlExtDatabase, self).__init__(*args, **kwargs)

    def get_cursor(self, name=None):
        if name:
            return self.get_conn().cursor(name=name)
        return self.get_conn().cursor()

    def execute_sql(self, sql, params=None, require_commit=True,
                    named_cursor=False):
        logger.debug((sql, params))
        use_named_cursor = (named_cursor or (
                            self.server_side_cursors and
                            sql.lower().startswith('select')))
        with self.exception_wrapper:
            if use_named_cursor:
                cursor = self.get_cursor(name=str(uuid.uuid1()))
                require_commit = False
            else:
                cursor = self.get_cursor()
            try:
                cursor.execute(sql, params or ())
            except Exception as exc:
                if self.get_autocommit() and self.autorollback:
                    self.rollback()
                raise
            else:
                if require_commit and self.get_autocommit():
                    self.commit()
        return cursor

    def _connect(self, database, **kwargs):
        conn = super(PostgresqlExtDatabase, self)._connect(database, **kwargs)
        if self.register_hstore:
            register_hstore(conn, globally=True)
        return conn


class ServerSideSelectQuery(SelectQuery):
    @classmethod
    def clone_from_query(cls, query):
        clone = ServerSideSelectQuery(query.model_class)
        return query._clone_attributes(clone)

    def _execute(self):
        sql, params = self.sql()
        return self.database.execute_sql(
            sql, params, require_commit=False, named_cursor=True)


PostgresqlExtDatabase.register_fields({
    'datetime_tz': 'timestamp with time zone',
    'hash': 'hstore',
    'json': 'json',
    'jsonb': 'jsonb',
    'tsvector': 'tsvector',
})
PostgresqlExtDatabase.register_ops({
    OP.HCONTAINS_DICT: '@>',
    OP.HCONTAINS_KEYS: '?&',
    OP.HCONTAINS_KEY: '?',
    OP.HCONTAINS_ANY_KEY: '?|',
    OP.HKEY: '->',
    OP.HUPDATE: '||',
    OP.ACONTAINS: '@>',
    OP.ACONTAINS_ANY: '&&',
    OP.TS_MATCH: '@@',
    OP.JSONB_CONTAINS: '@>',
    OP.JSONB_CONTAINED_BY: '<@',
    OP.JSONB_CONTAINS_ANY_KEY: '?|',
    OP.JSONB_CONTAINS_ALL_KEYS: '?&',
    OP.JSONB_EXISTS: '?',
    OP.CAST: '::',
})

def ServerSide(select_query):
    # Flag query for execution using server-side cursors.
    clone = ServerSideSelectQuery.clone_from_query(select_query)
    with clone.database.transaction():
        # Execute the query.
        query_result = clone.execute()

        # Patch QueryResultWrapper onto original query.
        select_query._qr = query_result

        # Expose generator for iterating over query.
        for obj in query_result.iterator():
            yield obj


def LateralJoin(lhs, rhs, join_type='LEFT', condition=True):
    return Clause(
        lhs,
        SQL('%s JOIN LATERAL' % join_type),
        rhs,
        SQL('ON %s', condition))
