from datetime import datetime

import sqlalchemy
from sqlalchemy.orm.relationships import RelationshipProperty

from .extensions import db


# a bit of hackery to make type-hinting in PyCharm work correctly
class __relationship_type_hinter__(RelationshipProperty):
    # implement __call__ to silence the silly "not callable" warning
    def __call__(self, *args, **kwargs):
        pass


# alias common names
from sqlalchemy.ext.associationproxy import association_proxy
Table = db.Table                # type: sqlalchemy.schema.Table
String = db.String              # type: sqlalchemy.types.String
Text = db.Text                  # type: sqlalchemy.types.Text
Integer = db.Integer            # type: sqlalchemy.types.Integer
Boolean = db.Boolean            # type: sqlalchemy.types.Boolean
DateTime = db.DateTime          # type: sqlalchemy.types.DateTime
relationship = db.relationship  # type: __relationship_type_hinter__
backref = db.backref            # type: __relationship_type_hinter__
ForeignKey = db.ForeignKey      # type: sqlalchemy.schema.ForeignKey
UniqueConstraint = sqlalchemy.UniqueConstraint


class Column(db.Column):
    # overridden to make nullable False by default
    def __init__(self, *args, nullable=False, **kwargs):
        super(Column, self).__init__(*args, nullable=nullable, **kwargs)


class BaseModel(db.Model):
    """Base table class. It includes convenience methods for creating,
    querying, saving, updating and deleting models.
    """
    __abstract__ = True
    __table_args__ = {'extend_existing': True}

    __repr_props__ = ()
    """Set to customize automatic string representation.

    For example::

        class User(database.Model):
            __repr_props__ = ('id', 'email')

            email = Column(String)

        user = User(id=1, email='foo@bar.com')
        print(user)  # prints <User id=1 email="foo@bar.com">
    """

    @classmethod
    def get(cls, id):
        """Get one model by ID.

        :param id: The model ID to get.
        """
        return cls.query.get(int(id))

    @classmethod
    def get_by(cls, **kwargs):
        """Get one model by keyword arguments.

        :param kwargs: The model attribute values to filter by.
        """
        return cls.query.filter_by(**kwargs).first()

    @classmethod
    def filter_by(cls, **kwargs):
        """Find models by keyword arguments.

        :param kwargs: The model attribute values to filter by.
        """
        return cls.query.filter_by(**kwargs)

    @classmethod
    def create(cls, commit=True, **kwargs):
        """Create a new model and add it to the database session.

        :param bool commit: Whether or not to immediately commit the DB session.
        :param kwargs: The model attribute values to create the model with.
        """
        instance = cls(**kwargs)
        return instance.save(commit)

    def update(self, commit=False, **kwargs):
        """Update fields on the model.

        :param bool commit: Whether or not to immediately commit the DB session.
        """
        for attr, value in kwargs.items():
            setattr(self, attr, value)
        return self.save(commit)

    def save(self, commit=False):
        """Save the model.

        :param bool commit: Whether or not to immediately commit the DB session.
        """
        db.session.add(self)
        if commit:
            db.session.commit()
        return self

    def delete(self, commit=False):
        """Delete the model.

        :param bool commit: Whether or not to immediately commit the DB session.
        """
        db.session.delete(self)
        return commit and db.session.commit()

    def __repr__(self):
        properties = ['{!s}={!r}'.format(prop, getattr(self, prop))
                      for prop in self.__repr_props__ if hasattr(self, prop)]
        return '<{} {}>'.format(self.__class__.__name__, ' '.join(properties))


class Model(BaseModel):
    """Base table class that extends :class:`backend.database.BaseModel` and
    includes a primary key :attr:`id` field along with automatically
    date-stamped :attr:`created_at` and :attr:`updated_at` fields.
    """
    __abstract__ = True
    __table_args__ = {'extend_existing': True}

    id = Column(Integer, primary_key=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    __repr_props__ = ('id', 'created_at', 'updated_at')


# RELATIONSHIP DOCS
# http://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#relationship-patterns
# http://docs.sqlalchemy.org/en/rel_1_1/orm/backref.html#relationships-backref
# http://flask-sqlalchemy.pocoo.org/2.2/models/#one-to-many-relationships
# http://flask-sqlalchemy.pocoo.org/2.2/models/#many-to-many-relationships


def foreign_key(table_name, nullable=False, **kwargs):
    """Use to add a foreign key Column to a model.

    For example::

        class Post(database.Model):
            category_id = foreign_key('category')

    Is equivalent to::

        class Post(database.Model):
            category_id = Column(Integer, ForeignKey('category.id'))

    :param str table_name: the table name of the referenced model
    :param bool nullable: whether or not the foreign key can be null
    :param dict kwargs: any other kwargs to pass the Column constructor
    """
    return Column(Integer, db.ForeignKey(table_name + '.id'), nullable=nullable, **kwargs)


def join_table(model_name1, model_name2, *extra_columns):
    """Creates a join table.

    Usage::
        symbol_industry = join_table('Symbol', 'Industry')
        class Symbol(Model):
            industries = relationship('Industry', secondary=symbol_industry, back_populates='symbols')
        class Industry(Model)
            symbols = relationship('Symbol', secondary=symbol_industry, back_populates='industries')
    """
    table1 = model_name1.lower()
    table2 = model_name2.lower()
    return Table(
        '{}_{}'.format(table1, table2),
        Column('{}_id'.format(table1), Integer, db.ForeignKey('{}.id'.format(table1))),
        Column('{}_id'.format(table2), Integer, db.ForeignKey('{}.id'.format(table2))),
        *extra_columns
    )

@@ -5456,765 +5456,4 @@
 gs)%0A
-%0A%0Adef join_table(model_name1, model_name2, *extra_columns):%0A    %22%22%22Creates a join table.%0A%0A    Usage::%0A        symbol_industry = join_table('Symbol', 'Industry')%0A        class Symbol(Model):%0A            industries = relationship('Industry', secondary=symbol_industry, back_populates='symbols')%0A        class Industry(Model)%0A            symbols = relationship('Symbol', secondary=symbol_industry, back_populates='industries')%0A    %22%22%22%0A    table1 = model_name1.lower()%0A    table2 = model_name2.lower()%0A    return Table(%0A        '%7B%7D_%7B%7D'.format(table1, table2),%0A        Column('%7B%7D_id'.format(table1), Integer, db.ForeignKey('%7B%7D.id'.format(table1))),%0A        Column('%7B%7D_id'.format(table2), Integer, db.ForeignKey('%7B%7D.id'.format(table2))),%0A        *extra_columns%0A    )%0A

remove join_table helper fn in favor of creating explicit Model-based join tables