# ext/declarative/api.py
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""Public API functions and helpers for declarative."""


from sqlalchemy.schema import Table
from sqlalchemy.orm.properties import ColumnProperty
from sqlalchemy import util
from sqlalchemy.orm.util import _is_mapped_class
from sqlalchemy.sql import expression
from sqlalchemy.orm import class_mapper

def _declared_mapping_info(cls):
    # deferred mapping
    if cls in _MapperConfig.configs:
        return _MapperConfig.configs[cls]
    # regular mapping
    elif _is_mapped_class(cls):
        return class_mapper(cls, configure=False)
    else:
        return None

class _MapperConfig(object):
    configs = util.OrderedDict()
    mapped_table = None

    def __init__(self, mapper_cls,
                        cls,
                        table,
                        inherits,
                        declared_columns,
                        column_copies,
                        properties, mapper_args_fn):
        self.mapper_cls = mapper_cls
        self.cls = cls
        self.local_table = table
        self.inherits = inherits
        self.properties = properties
        self.mapper_args_fn = mapper_args_fn
        self.declared_columns = declared_columns
        self.column_copies = column_copies
        self.configs[cls] = self

    def _prepare_mapper_arguments(self):
        properties = self.properties
        if self.mapper_args_fn:
            mapper_args = self.mapper_args_fn()
        else:
            mapper_args = {}

        # make sure that column copies are used rather
        # than the original columns from any mixins
        for k in ('version_id_col', 'polymorphic_on',):
            if k in mapper_args:
                v = mapper_args[k]
                mapper_args[k] = self.column_copies.get(v, v)

        assert 'inherits' not in mapper_args, \
            "Can't specify 'inherits' explicitly with declarative mappings"

        if self.inherits:
            mapper_args['inherits'] = self.inherits

        if self.inherits and not mapper_args.get('concrete', False):
            # single or joined inheritance
            # exclude any cols on the inherited table which are
            # not mapped on the parent class, to avoid
            # mapping columns specific to sibling/nephew classes
            inherited_mapper = _declared_mapping_info(self.inherits)
            inherited_table = inherited_mapper.local_table

            if 'exclude_properties' not in mapper_args:
                mapper_args['exclude_properties'] = exclude_properties = \
                    set([c.key for c in inherited_table.c
                         if c not in inherited_mapper._columntoproperty])
                exclude_properties.difference_update(
                        [c.key for c in self.declared_columns])

            # look through columns in the current mapper that
            # are keyed to a propname different than the colname
            # (if names were the same, we'd have popped it out above,
            # in which case the mapper makes this combination).
            # See if the superclass has a similar column property.
            # If so, join them together.
            for k, col in properties.items():
                if not isinstance(col, expression.ColumnElement):
                    continue
                if k in inherited_mapper._props:
                    p = inherited_mapper._props[k]
                    if isinstance(p, ColumnProperty):
                        # note here we place the subclass column
                        # first.  See [ticket:1892] for background.
                        properties[k] = [col] + p.columns
        result_mapper_args = mapper_args.copy()
        result_mapper_args['properties'] = properties
        return result_mapper_args

    def map(self):
        self.configs.pop(self.cls, None)
        mapper_args = self._prepare_mapper_arguments()
        self.cls.__mapper__ = self.mapper_cls(
            self.cls,
            self.local_table,
            **mapper_args
        )

class _MappedAttribute(object):
    """Mixin for attributes which should be replaced by mapper-assigned
    attributes.

    """

class declared_attr(_MappedAttribute, property):
    """Mark a class-level method as representing the definition of
    a mapped property or special declarative member name.

    .. versionchanged:: 0.6.{2,3,4}
        ``@declared_attr`` is available as
        ``sqlalchemy.util.classproperty`` for SQLAlchemy versions
        0.6.2, 0.6.3, 0.6.4.

    @declared_attr turns the attribute into a scalar-like
    property that can be invoked from the uninstantiated class.
    Declarative treats attributes specifically marked with
    @declared_attr as returning a construct that is specific
    to mapping or declarative table configuration.  The name
    of the attribute is that of what the non-dynamic version
    of the attribute would be.

    @declared_attr is more often than not applicable to mixins,
    to define relationships that are to be applied to different
    implementors of the class::

        class ProvidesUser(object):
            "A mixin that adds a 'user' relationship to classes."

            @declared_attr
            def user(self):
                return relationship("User")

    It also can be applied to mapped classes, such as to provide
    a "polymorphic" scheme for inheritance::

        class Employee(Base):
            id = Column(Integer, primary_key=True)
            type = Column(String(50), nullable=False)

            @declared_attr
            def __tablename__(cls):
                return cls.__name__.lower()

            @declared_attr
            def __mapper_args__(cls):
                if cls.__name__ == 'Employee':
                    return {
                            "polymorphic_on":cls.type,
                            "polymorphic_identity":"Employee"
                    }
                else:
                    return {"polymorphic_identity":cls.__name__}

    """

    def __init__(self, fget, *arg, **kw):
        super(declared_attr, self).__init__(fget, *arg, **kw)
        self.__doc__ = fget.__doc__

    def __get__(desc, self, cls):
        return desc.fget(cls)

class DeferredReflection(object):
    """A helper class for construction of mappings based on
    a deferred reflection step.

    Normally, declarative can be used with reflection by
    setting a :class:`.Table` object using autoload=True
    as the ``__table__`` attribute on a declarative class.
    The caveat is that the :class:`.Table` must be fully
    reflected, or at the very least have a primary key column,
    at the point at which a normal declarative mapping is
    constructed, meaning the :class:`.Engine` must be available
    at class declaration time.

    The :class:`.DeferredReflection` mixin moves the construction
    of mappers to be at a later point, after a specific
    method is called which first reflects all :class:`.Table`
    objects created so far.   Classes can define it as such::

        from sqlalchemy.ext.declarative import declarative_base, DeferredReflection
        Base = declarative_base()

        class MyClass(DeferredReflection, Base):
            __tablename__ = 'mytable'

    Above, ``MyClass`` is not yet mapped.   After a series of
    classes have been defined in the above fashion, all tables
    can be reflected and mappings created using :meth:`.DeferredReflection.prepare`::

        engine = create_engine("someengine://...")
        DeferredReflection.prepare(engine)

    The :class:`.DeferredReflection` mixin can be applied to individual
    classes, used as the base for the declarative base itself,
    or used in a custom abstract class.   Using an abstract base
    allows that only a subset of classes to be prepared for a
    particular prepare step, which is necessary for applications
    that use more than one engine.  For example, if an application
    has two engines, you might use two bases, and prepare each
    separately, e.g.::

        class ReflectedOne(DeferredReflection, Base):
            __abstract__ = True

        class ReflectedTwo(DeferredReflection, Base):
            __abstract__ = True

        class MyClass(ReflectedOne):
            __tablename__ = 'mytable'

        class MyOtherClass(ReflectedOne):
            __tablename__ = 'myothertable'

        class YetAnotherClass(ReflectedTwo):
            __tablename__ = 'yetanothertable'

        # ... etc.

    Above, the class hierarchies for ``ReflectedOne`` and
    ``ReflectedTwo`` can be configured separately::

        ReflectedOne.prepare(engine_one)
        ReflectedTwo.prepare(engine_two)

    .. versionadded:: 0.8

    """
    @classmethod
    def prepare(cls, engine):
        """Reflect all :class:`.Table` objects for all current
        :class:`.DeferredReflection` subclasses"""
        to_map = [m for m in _MapperConfig.configs.values()
                    if issubclass(m.cls, cls)]
        for thingy in to_map:
            cls._sa_decl_prepare(thingy.local_table, engine)
            thingy.map()

    @classmethod
    def _sa_decl_prepare(cls, local_table, engine):
        # autoload Table, which is already
        # present in the metadata.  This
        # will fill in db-loaded columns
        # into the existing Table object.
        if local_table is not None:
            Table(local_table.name,
                local_table.metadata,
                extend_existing=True,
                 autoload_replace=False,
                autoload=True,
                autoload_with=engine,
                schema=local_table.schema)