"""This ``utils`` module from the ``duplicate`` contains the ``duplicate`` function."""

from django.db.models.loading import get_models

from .duplicators import InstanceDuplicator


def is_through_model(model):
    """Tells if the given model is the ``through`` model in a M2M relation.

    Notes
    -----
    A cache is constructed on the first call. It's a set containing all "through" tables, so next
    calls will be really fast.

    """

    if is_through_model.cache is None:
        cache = set()

        for model in get_models():

            if not getattr(model._meta, 'many_to_many', None):
                continue

            for m2m in model._meta.many_to_many:
                cache.add(m2m.rel.through)

        is_through_model.cache = cache

    return model in is_through_model.cache

is_through_model.cache = None


def duplicate(instance, non_duplicable_relations=None, **kwargs):
    """Duplicate the given instance and all its duplicable relations.

    All relations from and to this instance will be duplicated if they inherit from
    ``Duplicable``.
    To avoid such a relation to be duplicated, add the name of this relation (the name
    of the field if it's a M2M, FK, O2O to another model, or the related name of the
    relation if it's a M2M, FK, O2O to *this* model) to the ``non_duplicable_relation``
    attribute of the model (see details in ``.models.Duplicable``).

    Parameters
    ----------
    instance : model instance
        The instance of a model we want to duplicate with all its relations
    non_duplicable_relations : iterable, optional
        Some entries to *add* to the default ``non_duplicable_relations`` set defined on the
        model.
    **kwargs : dict, optional
        To force some field (normal fields, foreign keys or OneToOne fields) on the new
        instance instead of taking the value from ``instance`` or, if it's a relation,
        to duplicate it.

    Returns
    -------
    tuple(new_instance, duplicated_objects)
        A tuple with two entries:
            0: the new instance, a duplicata of ``instance``
            1: a list of all the objects that were duplicated during the process.
               It's a list of tuple, having the old instance as first entry, and its
               duplicate as second entry.

    """

    duplicator = InstanceDuplicator(instance, non_duplicable_relations)

    return (
        duplicator.duplicate(**kwargs),
        duplicator.duplicated_objects
    )


def compare(instance, other_instance):
    """Compare two instances of the same model and check differences.

    Only fields and ``Duplicable`` relations are compared (and all ``through`` tables, too)

    Parameters
    ----------
    instance : model instance
        An instance to compare with ``other_instance``.
    other_instance : model instance
        An instance of the same model as ``instance`` to be compared with it.

    Returns
    -------
    tuple(is_different, compared_objects)
        A tuple with two entries:
            0: boolean, if the two entries are different (``True``) or not (``False``)
            1: dict, containing all compared objects. See the description of ``compare_refs`` in
               the docstring of ``InstanceDuplicator``.


    """

    duplicator = InstanceDuplicator(instance)

    return (
        duplicator.compare(other_instance),
        duplicator.compare_refs
    )
