# -*- coding: utf-8 -*-
import functools
import inspect
import uuid

import marshmallow as ma
import sqlalchemy as sa
from marshmallow import fields, validate
from sqlalchemy.dialects import mssql, mysql, postgresql

from .exceptions import ModelConversionError
from .fields import Related


def _is_field(value):
    return (
        isinstance(value, type) and
        issubclass(value, fields.Field)
    )


def _has_default(column):
    return (
        column.default is not None or
        column.server_default is not None or
        _is_auto_increment(column)
    )


def _is_auto_increment(column):
    return (
        column.table is not None and
        column is column.table._autoincrement_column
    )


def _postgres_array_factory(converter, data_type):
    return functools.partial(
        fields.List,
        converter._get_field_class_for_data_type(data_type.item_type),
    )


def _should_exclude_field(column, fields=None, exclude=None):
    if fields and column.key not in fields:
        return True
    if exclude and column.key in exclude:
        return True
    return False


class ModelConverter(object):
    """Class that converts a SQLAlchemy model into a dictionary of corresponding
    marshmallow `Fields <marshmallow.fields.Field>`.
    """

    SQLA_TYPE_MAPPING = {
        sa.Enum: fields.Field,

        postgresql.BIT: fields.Integer,
        postgresql.UUID: fields.UUID,
        postgresql.MACADDR: fields.String,
        postgresql.INET: fields.String,
        postgresql.JSON: fields.Raw,
        postgresql.JSONB: fields.Raw,
        postgresql.HSTORE: fields.Raw,
        postgresql.ARRAY: _postgres_array_factory,

        mysql.BIT: fields.Integer,
        mysql.YEAR: fields.Integer,
        mysql.SET: fields.List,
        mysql.ENUM: fields.Field,

        mssql.BIT: fields.Integer,
    }
    if hasattr(sa, 'JSON'):
        SQLA_TYPE_MAPPING[sa.JSON] = fields.Raw

    DIRECTION_MAPPING = {
        'MANYTOONE': False,
        'MANYTOMANY': True,
        'ONETOMANY': True,
    }

    def __init__(self, schema_cls=None):
        self.schema_cls = schema_cls

    @property
    def type_mapping(self):
        if self.schema_cls:
            return self.schema_cls.TYPE_MAPPING
        else:
            return ma.Schema.TYPE_MAPPING

    def fields_for_model(self, model, include_fk=False, fields=None, exclude=None, base_fields=None,
                         dict_cls=dict):
        result = dict_cls()
        base_fields = base_fields or {}
        for prop in model.__mapper__.iterate_properties:
            if _should_exclude_field(prop, fields=fields, exclude=exclude):
                continue
            if hasattr(prop, 'columns'):
                if not include_fk:
                    # Only skip a column if there is no overridden column
                    # which does not have a Foreign Key.
                    for column in prop.columns:
                        if not column.foreign_keys:
                            break
                    else:
                        continue
            field = base_fields.get(prop.key) or self.property2field(prop)
            if field:
                result[prop.key] = field
        return result

    def fields_for_table(self, table, include_fk=False, fields=None, exclude=None, base_fields=None,
                         dict_cls=dict):
        result = dict_cls()
        base_fields = base_fields or {}
        for column in table.columns:
            if _should_exclude_field(column, fields=fields, exclude=exclude):
                continue
            if not include_fk and column.foreign_keys:
                continue
            field = base_fields.get(column.key) or self.column2field(column)
            if field:
                result[column.key] = field
        return result

    def property2field(self, prop, instance=True, field_class=None, **kwargs):
        field_class = field_class or self._get_field_class_for_property(prop)
        if not instance:
            return field_class
        field_kwargs = self._get_field_kwargs_for_property(prop)
        field_kwargs.update(kwargs)
        ret = field_class(**field_kwargs)
        if (
            hasattr(prop, 'direction') and
            self.DIRECTION_MAPPING[prop.direction.name] and
            prop.uselist is True
        ):
            ret = fields.List(ret, **kwargs)
        return ret

    def column2field(self, column, instance=True, **kwargs):
        field_class = self._get_field_class_for_column(column)
        if not instance:
            return field_class
        field_kwargs = self.get_base_kwargs()
        self._add_column_kwargs(field_kwargs, column)
        field_kwargs.update(kwargs)
        return field_class(**field_kwargs)

    def field_for(self, model, property_name, **kwargs):
        prop = model.__mapper__.get_property(property_name)
        return self.property2field(prop, **kwargs)

    def _get_field_class_for_column(self, column):