from __future__ import unicode_literals, division, absolute_import
from builtins import *  # noqa pylint: disable=unused-import, redefined-builtin
from past.builtins import basestring, long, unicode

import functools
from collections import Mapping
from datetime import datetime

from sqlalchemy import extract, func
from sqlalchemy.orm import synonym
from sqlalchemy.ext.hybrid import Comparator, hybrid_property

from flexget.manager import Session
from flexget.utils import qualities, json
from flexget.entry import Entry


def with_session(*args, **kwargs):
    """"
    A decorator which creates a new session if one was not passed via keyword argument to the function.

    Automatically commits and closes the session if one was created, caller is responsible for commit if passed in.

    If arguments are given when used as a decorator, they will automatically be passed to the created Session when
    one is not supplied.
    """

    def decorator(func):
        def wrapper(*args, **kwargs):
            if kwargs.get('session'):
                return func(*args, **kwargs)
            with _Session() as session:
                kwargs['session'] = session
                return func(*args, **kwargs)

        return wrapper

    if len(args) == 1 and not kwargs and callable(args[0]):
        # Used without arguments, e.g. @with_session
        # We default to expire_on_commit being false, in case the decorated function returns db instances
        _Session = functools.partial(Session, expire_on_commit=False)
        return decorator(args[0])
    else:
        # Arguments were specified, turn them into arguments for Session creation e.g. @with_session(autocommit=True)
        _Session = functools.partial(Session, *args, **kwargs)
        return decorator


def pipe_list_synonym(name):
    """Converts pipe separated text into a list"""

    def getter(self):
        attr = getattr(self, name)
        if attr:
            return attr.strip('|').split('|')

    def setter(self, value):
        if isinstance(value, str):
            setattr(self, name, value)
        else:
            setattr(self, name, '|'.join(value))

    return synonym(name, descriptor=property(getter, setter))


def text_date_synonym(name):
    """Converts Y-M-D date strings into datetime objects"""

    def getter(self):
        return getattr(self, name)

    def setter(self, value):
        if isinstance(value, basestring):
            try:
                setattr(self, name, datetime.strptime(value, '%Y-%m-%d'))
            except ValueError:
                # Invalid date string given, set to None
                setattr(self, name, None)
        else:
            setattr(self, name, value)

    return synonym(name, descriptor=property(getter, setter))


def entry_synonym(name):
    """Use json to serialize python objects for db storage."""

    def only_builtins(item):
        supported_types = (str, unicode, int, float, long, bool, datetime)
        # dict, list, tuple and set are also supported, but handled separately

        if isinstance(item, supported_types):
            return item
        elif isinstance(item, Mapping):
            result = {}
            for key, value in item.items():
                try:
                    result[key] = only_builtins(value)
                except TypeError:
                    continue
            return result
        elif isinstance(item, (list, tuple, set)):
            result = []
            for value in item:
                try:
                    result.append(only_builtins(value))
                except ValueError:
                    continue
            if isinstance(item, list):
                return result
            elif isinstance(item, tuple):
                return tuple(result)
            else:
                return set(result)
        elif isinstance(item, qualities.Quality):
            return item.name
        else:
            for s_type in supported_types:
                if isinstance(item, s_type):
                    return s_type(item)

        # If item isn't a subclass of a builtin python type, raise ValueError.
        raise TypeError('%r is not of type Entry.' % type(item))

    def getter(self):
        return Entry(json.loads(getattr(self, name), decode_datetime=True))

    def setter(self, entry):
        if isinstance(entry, Entry) or isinstance(entry, dict):
            setattr(
                self, name, unicode(json.dumps(only_builtins(dict(entry)), encode_datetime=True))
            )
        else:
            raise TypeError('%r is not of type Entry or dict.' % type(entry))

    return synonym(name, descriptor=property(getter, setter))


def json_synonym(name):
    """Use json to serialize python objects for db storage."""

    def getter(self):
        return json.loads(getattr(self, name), decode_datetime=True)

    def setter(self, entry):
        setattr(self, name, unicode(json.dumps(entry, encode_datetime=True)))

    return synonym(name, descriptor=property(getter, setter))


class CaseInsensitiveWord(Comparator):
    """Hybrid value representing a string that compares case insensitively."""

    def __init__(self, word):
        if isinstance(word, CaseInsensitiveWord):
            self.word = word.word
        else:
            self.word = word

    def lower(self):
        if isinstance(self.word, str):
            return self.word.lower()
        else:
            return func.lower(self.word)

    def operate(self, op, other):
        if not isinstance(other, CaseInsensitiveWord):
            other = CaseInsensitiveWord(other)
        return op(self.lower(), other.lower())

    def __clause_element__(self):
        return self.lower()

    def __str__(self):
        return self.word

    def __getattr__(self, item):
        """Expose string methods to be called directly on this object."""
        return getattr(self.word, item)


def quality_property(text_attr):
    def getter(self):
        return qualities.Quality(getattr(self, text_attr))

    def setter(self, value):
        if isinstance(value, str):
            setattr(self, text_attr, value)
        else:
            setattr(self, text_attr, value.name)

    class QualComparator(Comparator):
        def operate(self, op, other):
            if isinstance(other, qualities.Quality):
                other = other.name
            return op(self.__clause_element__(), other)

    def comparator(self):
        return QualComparator(getattr(self, text_attr))

    prop = hybrid_property(getter, setter)
    prop = prop.comparator(comparator)
    return prop


def quality_requirement_property(text_attr):
    def getter(self):
        return qualities.Requirements(getattr(self, text_attr))

    def setter(self, value):
        if isinstance(value, str):
            setattr(self, text_attr, value)
        else:
            setattr(self, text_attr, value.text)

    prop = hybrid_property(getter, setter)
    return prop


def ignore_case_property(text_attr):
    def getter(self):
        return CaseInsensitiveWord(getattr(self, text_attr))

    def setter(self, value):
        setattr(self, text_attr, value)

    return hybrid_property(getter, setter)


def year_property(date_attr):
    def getter(self):
        date = getattr(self, date_attr)
        return date and date.year

    def expr(cls):
        return extract('year', getattr(cls, date_attr))

    return hybrid_property(getter, expr=expr)
