from weakref import WeakKeyDictionary

from sqlalchemy import event, inspect

from taal import TranslatableString as TaalTranslatableString
from taal.sqlalchemy.types import (
    TranslatableString, pending_translatables, make_from_obj, NotNullValue)


translator_registry = WeakKeyDictionary()
flush_log = WeakKeyDictionary()


def register_translator(owner, translator):
    translator_registry[owner] = translator


def get_translator(owner):
    return translator_registry[owner]


def set_(target, value, oldvalue, initiator):
    """ Wrap any value in ``TranslatableString`` (except None) """
    if value is None:
        return None

    if isinstance(value, TaalTranslatableString):
        return TaalTranslatableString(
            value.context, value.message_id, value.pending_value)

    translatable = make_from_obj(target, initiator.key, value)
    return translatable


def load(target, context):
    """ Wrap columns when loading data from the db """
    mapper = inspect(target.__class__)
    for column in mapper.columns:
        if isinstance(column.type, TranslatableString):
            value = getattr(target, column.name)
            if value is None:
                continue
            elif value is NotNullValue:
                translatable = make_from_obj(target, column.name, value)
                setattr(target, column.name, translatable)
            elif isinstance(value, TaalTranslatableString):
                continue  # during session.merge
            else:
                raise TypeError("Unexpected column value '{}'".format(
                    value))


def refresh(target, args, attrs):
    mapper = inspect(target.__class__)
    if attrs is None:
        attrs = mapper.columns.keys()

    for column_name in attrs:
        if column_name not in mapper.columns:
            continue
        column = mapper.columns[column_name]
        if isinstance(column.type, TranslatableString):
            value = getattr(target, column.name)
            if value is not None:
                translatable = make_from_obj(target, column.name, value)
                setattr(target, column.name, translatable)
    return target


def add_to_flush_log(session, target, delete=False):
    mapper = inspect(target.__class__)
    for column in mapper.columns:
        if isinstance(column.type, TranslatableString):
            if delete:
                value = None  # will trigger deletion of translations
            else:
                value = getattr(target, column.name)
            if value is not None:
                pending_translatables.add(value)
                value = value.pending_value
            flush_log.setdefault(session, []).append(
                (session.transaction, target, column, value))


def before_flush(session, flush_context, instances):
    """ Queue up translations to be saved on commit. We don't want to
        save them until commit, but by then we no longer know what's
        changed. At this point we introspect the session to find added,
        changed and deleted objects and save them, along with information
        about the active session and transaction (to handle savepoint
        rollbacks) to the queue (``flush_log``)


        Notes:
            The ``instances`` argument is from a deprecated api
    """

    for target in session.dirty:
        if session.is_modified(target):
            add_to_flush_log(session, target)

    for target in session.new:
        add_to_flush_log(session, target)

    for target in session.deleted:
        add_to_flush_log(session, target, delete=True)


def after_bulk_update(session, query, query_context, result):
    # bulk updating to None would be ok, but leaves dangling Translations
    for bind in result.context.compiled.binds.values():
        field_type = bind.type
        if isinstance(field_type, TranslatableString):
            raise NotImplementedError("Bulk updates are not yet supported")


def after_commit(session):
    """ Save any pending translations for this session """
    for transaction, target, column, value in flush_log.pop(session, []):
        translator = get_translator(session)
        if value is None:
            # pending_value is ignored as we are deleting
            # just needs to be not-None
            translatable = make_from_obj(target, column.name, '')
            translator.delete_translations(translatable)
        else:
            translatable = make_from_obj(target, column.name, value)
            translator.save_translation(translatable, commit=True)

        old_value = getattr(target, column.name)
        if old_value is not None:
            # we may now have a primary key
            old_value.message_id = translatable.message_id
            # value is now saved. No need to keep around
            old_value.pending_value = None


def after_soft_rollback(session, previous_transaction):
    """ Drop any pending translations from this transaction """
    if session in flush_log:
        flush_log[session] = [
            pending for pending in flush_log[session]
            if pending[0] != previous_transaction]


def register_session(session):
    event.listen(session, 'before_flush', before_flush)
    event.listen(session, 'after_bulk_update', after_bulk_update)
    event.listen(session, 'after_commit', after_commit)
    event.listen(session, 'after_soft_rollback', after_soft_rollback)
