"""Provide the ``Instance`` class for the ``django_instances_graph`` package"""

from collections import Iterable
from inspect import isclass
from uuid import uuid4

from django import VERSION as DJANGO_VERSION
from django.apps import apps
from django.db import models
from django.db.models import NOT_PROVIDED

from django_fields_iterator import FieldsIterator, FieldTypes

from . import states
from .utils import get_through_info


class InstanceMetaclass(type):
    """A metaclass for the ``Instance`` class that allow to have only one entry for each instance"""

    def __call__(cls, source, serialize=False, graph=None, is_boundary=False, uuid=None):
        """Return the ``Instance`` in cache for the given instance, creating it if needed/possible.

        If the `source` is a model, or an instance with no pk, the cache won't be involved and a
        newly created ``Instance`` object will be returned.

        Parameters
        ----------
        source: models.Model
            The instance of the django model, or a django model, we want to represent
        serialize: bool
            When set to ``True``, if the ``source`` is an instance, it will be directly serialized
        graph: Graph
            The ``Graph`` used to hold together many instances related to the one we create.
            If not set, a new one will be created.
        is_boundary: bool
            If ``True``, mark the instance as a boundary of the graph.
        uuid: UUID
            The uuid (version 4) to use for this instance. If not set, a new one will be created.

        Raises
        ------
        ValueError
            - when `model_instance` is not an instance of a django model
            - when `model_instance` is not a django model class
            - when `model_instance` is an abstract model
            - when `serialize` is truly and `source` is a model
            - when creating an instance with a model, pk and an uuid that does not match the on
              in the already existing instance for this model+pk

        TypeError
            - when an attribute ``graph_instance_class`` is defined on the django model, that is
              not a subclass of ``Instance``

        """

        # Not so complex code, but too many branches for pylint (actually 14, the max being 12)
        # pylint: disable=too-many-branches

        from .graph import Graph  # avoid recursive import

        is_class = isclass(source)  # it may be a model
        model = source if is_class else source.__class__

        # Check we have a model or an instance of a model, and not abstract
        if (is_class and not issubclass(source, models.Model)) or \
                (not is_class and not isinstance(source, models.Model)) or \
                source._meta.abstract:
            raise ValueError('`source` must be a non abstract django model class or an instance of '
                             'such a model')

        if is_class and serialize:
            raise ValueError('`serialize` can only be passed when `source` is an instance')

        # Create a new graph if not passed
        if not graph:
            graph = Graph()

        # Check if the source defines a class to use instead of ``Instance``
        instance_class = getattr(source, 'graph_instance_class', None)
        # If not, maybe it's defined on the graph
        if instance_class is None and model in graph.instance_classes_for_models:
            instance_class = graph.instance_classes_for_models[model]
        # If not, it must be the default one
        if not instance_class and cls is not graph.default_instance_class:
            instance_class = graph.default_instance_class
        # Check if the class is valid
        if instance_class and cls is not instance_class:
            if not issubclass(instance_class, Instance):
                raise TypeError('`graph_instance_class` defined on %s must be a subclass of'
                                '`Instance`' % (source if is_class else source.__class__))

            # We have a class to use which is not the current one, retry with the current one
            return instance_class(source, serialize, graph, is_boundary, uuid)

        if not uuid and (is_class or not source.pk):
            # If model or instance without pk, we cannot manage uniqueness
            # So we create an ``Instance`` object in all cases
            instance = super().__call__(source, serialize, graph, is_boundary, uuid)
            graph.add_instance(instance)

        else:
            try:
                # Get the instance from the graph cache if present
                if (is_class or not source.pk) and uuid:
                    instance = graph.get_instance(uuid)
                else:
                    instance = graph.get_instance(source.__class__, source.pk)
            except KeyError:
                # Not in cache, so we create the instance and save it in the cache
                instance = super().__call__(source, serialize, graph, is_boundary, uuid)
                graph.add_instance(instance)
            else:
                # Update the ``is_boundary`` flag on the instance in one condition: it was
                # ``True`` and we ask to be ``False``. Indeed, an instance could be marked as
                # boundary, then, later, marked as not if encountered in another path of the
                # serialization process. So if it was marked at least once as not a boundary, we
                # keep it this way
                if instance.is_boundary and not is_boundary:
                    instance.is_boundary = False

            # Check that the uuid and the other parameters match what we asked for
            if uuid and uuid != instance.uuid or model is not instance.model:
                raise ValueError('There is a mismatch between the given uuid (%s) and the'
                                 'one we got' % uuid)

        # We must serialize AFTER having creating the object and setting in cache, because it's
        # a graph and the same object may be called in the serialize process
        if serialize and not instance.serialized:
            instance.serialize(source, simple_fields_only=is_boundary)

        return instance


class Instance(metaclass=InstanceMetaclass):
    """Represents an instance of a django model

    If it possible to directly pass a instance that can be serialized on the fly if `serialize`
    is truly, or a model, that can be filled manually (via ``self.fields``) or by calling
    `serialize` later with a model instance.

    Attributes
    ----------
    model: type of models.Model
        The django model from which this instance come from
    pk_field_name: str
        The name of the field used as primary key
    pk: scalar or str
        The value of the primary key for this instance
    fields: dict
        An dict to hold the fields values
    serialized: bool
        ``False`` by default, will be set to ``True`` when the instance will be serialized.
    graph: Graph
        The ``Graph`` used to hold together many instances related to the one we create.
        If not set, a new one will be created.
    is_boundary: bool
        If ``True``, mark the instance as a boundary of the graph.
    uuid: UUID
        The uuid (version 4) that uniquely identify this instance.

    """

    # Sorry about that, but we have to track a lot of things
    # pylint: disable=too-many-instance-attributes

    # pylint: disable=unused-argument
    def __init__(self, source, serialize=False, graph=None, is_boundary=False, uuid=None):
        """Initialize the object with the given instance or a django model

        Parameters
        ----------
        source: models.Model
            The instance of the django model, or a django model, we want to represent
        serialize: bool
            When set to ``True``, if the ``source`` is an instance, it will be directly serialized
        graph: Graph
            The ``Graph`` used to hold together many instances related to the one we create.
            If not set, a new one will be created.
        is_boundary: bool
            If ``True``, mark the instance as a boundary of the graph.
        uuid: UUID
            The uuid (version 4) to use for this instance. If not set, a new one will be created.

        Notes
        -----
        The ``Parameters`` section indicate that this method accepts a ``serialize`` argument, but
        it is managed by the metaclass, so it is ignored here.
        It's just mentioned if a developer needs to know the real expected signature.
        It is done this way because the serialization must be done after the instance is created
        and saved in the cache, because this process may be repeated for the same instance later,
        as you manage a graph of django instances

        """

        self.serialized = False
        self.deserialize_state = None
        self.deserialize_fields_state = {}
        self.deserialize_model_instance = None

        self.graph = graph  # never ``None`` because it is defined by the metaclass
        self.is_boundary = is_boundary

        is_model = isclass(source)

        if is_model:
            self.model = source
            self.__pk = None

        else:
            # We have an instance
            self.model = source.__class__
            self.__pk = source.pk

        self.pk_field_name = self.model._meta.pk.attname

        self.fields = {}

        self.uuid = uuid or uuid4()

    def __getstate__(self):
        """Prepare the state to pickle the instance

        Returns
        -------
        dict
            The state that will be used by ``pickle``

        Notes
        -----
        We reinitialize the attributes about deserialize state, and we convert the model to a tuple
        with the app label and the model name, because auto through models are not defined and so
        pickle cannot access them.

        We also update the ``__pk`` key to a key related to the class name as it is a private
        attribute, and the restore of the state (via ``__setstate__u`` simply update the
        ``__dict__`` of the object, so it's better to have it ready.

        """

        state = {
            'serialized': self.serialized,
            'deserialize_state': None,
            'deserialize_fields_state': {},
            'deserialize_model_instance': None,
            'graph': self.graph,
            'is_boundary': self.is_boundary,
            'model': (self.model._meta.app_label, self.model._meta.model_name),
            '_%s__pk' % self.__class__.__name__: self.__pk,
            'pk_field_name': self.pk_field_name,
            'fields': self.fields,
            'uuid': self.uuid,
        }

        return state

    def __setstate__(self, state):
        """Restore the state from pickle

        We simply update the ``__dict__`` of the object, and restore the real model instead of the
        tuple with the app label and the model name that was stored in the state.

        Parameters
        ----------
        state: dict
            The state that was previously returned by ``__getstate__``

        """

        self.__dict__.update(state)
        self.model = apps.get_model(*state['model'])

    @property
    def pk(self):
        """Getter for the `__pk` property"""

        return self.__pk

    @pk.setter
    def pk(self, value):
        """Setter for the `__pk` property

        Raises
        ------
        RuntimeError
            When the pk is set and will be changed. Once set, it can only be reset to ``None``

        """

        if self.__pk is not None and value is not None and value != self.__pk:
            raise RuntimeError('The `pk` of an `Instance` cannot be changed (was %s)' % self.__pk)

        self.__pk = value

        # will update the cache, useful if the pk changes
        self.graph.add_instance(self)

    def __hash__(self):
        """Compute the hash of the current instance

        We return the int version of the uuid

        Returns
        -------
        int
            The hash used by python for uniqueness (for sets, dicts...)

        """

        return self.uuid.int

    def get_model_instance(self):
        """Return the model instance from ``self.model`` and ``self.pk``

        Returns
        -------
        models.Model
            The django model instance from ``self.model`` with the primary key in ``self.pk``

        Raises
        ------
        ValueError
            When the current ``Instance`` has no ``pk`` defined

        """

        if not self.pk:
            raise ValueError(
                'Cannot load instance of `%s` without primary key' % self.model.__name__
            )

        return self.model.objects.get(**{self.pk_field_name: self.pk})

    def save_model_instance(self, model_instance):
        """Save the given model instance

        This method start by calling the method of the same name on the graph to let the graph
        decide if it will save the instance itself. It ``None`` is then returned, we assume it
        did nothing to the `model_instance` and we can save it. Otherwise, the `model_instance`
        will be saved here.

        Parameters
        ----------
        model_instance: models.Model
            The django model instance to save

        Returns
        -------
        models.Model
            The saved django model instance

        """

        # Start by asking the graph to save the instance
        saved_model_instance = self.graph.save_model_instance(self, model_instance)

        # It did not, so we save it ourselves
        if saved_model_instance is None:
            model_instance.save()
            saved_model_instance = model_instance

        # And we return the saved instance (the same as the one passed if we saved it ourselves)
        return saved_model_instance

    def __repr__(self):
        """Representation of an ``Instance``"""

        return '<%s %s(pk=%s)>' % (
            self.__class__.__name__,
            self.model.__name__,
            self.pk,
        )

    def is_relation_boundary(self, accessor_name, field, field_type):
        """Tell if the serialization should stop in the graph after encountered this field

        When this method returns ``True``, the target instance(s) will be created with
        ``is_boundary`` set to ``True``, and will not be serialized.

        Parameters
        ----------
        accessor_name: str
            The name of the attribute to get the relation on the current model
        field: models.fields.related.ForeignObject or models.fields.related.ForeignObjectRel
            The field for this relation. The related model can be accessed via `field.related_model`
        field_type: str
            The kind of field from which the value come from. One of ``FieldTypes.allowed_types``

        Returns
        -------
        bool
            ``False`` by default: the relation target will not be followed. If ``True``, it will.

        Notes
        -----
        This method calls the method of the same name on the current graph, but can be overridden
        to set boundaries to the graph.

        """

        return self.graph.is_relation_boundary(self, accessor_name, field, field_type)

    def serialize(self, model_instance, force=False, simple_fields_only=False):
        """Serialize all the django fields from the instance and save them in ``self.fields``

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we want to serialize the field from
        force: bool
            If set to ``True``, the instance will be serialized even if it was already done before
        simple_fields_only: bool
            If set to ``True``, only the simple fields will be serialized. It is for example used
            when serializing a "boundary" where we don't want to follow relations but still want
            simple fields for display.

        Raises
        ------
        RuntimeError
            If the instance is already serialized and `force` is not ``True``

        """

        if self.serialized and not force:
            raise RuntimeError('This ``Instance`` is already serialized')

        self.serialized = True
        self.fields.clear()

        if model_instance.pk:
            self.pk = model_instance.pk

        iterator = FieldsIterator(self.model)
        if simple_fields_only:
            iterator = iterator.iter_simple_fields()  # pylint: disable=redefined-variable-type

        for field, accessor_name, field_type in iterator:
            # Get the serialize method to call, based on the field type
            # We remove the final "s" because we manipulate only one field here
            method = 'serialize_%s' % field_type.rstrip('s')

            # Then we can call the serialize method
            getattr(self, method)(model_instance, field, accessor_name)

    # pylint: disable=unused-argument
    def override_value_to_serialize(self, model_instance, field, accessor_name, field_type, value):
        """Hook for subclasses to override the value got from the instance before serialization

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we extracted the value from
        field: models.Field or models.fields.related.ForeignObject
            The field object from the django model
        accessor_name: str
            The name of the attribute used to get the value from the instance
        field_type: str
            The kind of field from which the value come from. One of ``FieldTypes.allowed_types``
        value: None or scalar or str or models.Model or list of models.Model
            The value extracted from the given model instance for the given field.
             Note that for M2Ms and reverse M2Ms, it will always be a list/queryset of instances
             from the ``through`` model (even for M2Ms where the ``through`` model was not
             defined in the model)

        Returns
        -------
        None or scalar or str or models.Model or list of models.Model
            By default will return the value passed in arguments.

        """

        return value

    # pylint: disable=unused-argument
    def serialize_simple_field(self, model_instance, field, accessor_name):
        """Serialize a simple field from the instance.

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we want to serialize the field from
        field: models.fields.Field
            The field object from the django model
        accessor_name: str
            The name of the attribute to use to get the value from the instance

        """

        value = self.override_value_to_serialize(
            model_instance=model_instance,
            field=field,
            accessor_name=accessor_name,
            field_type=FieldTypes.SIMPLE,
            value=getattr(model_instance, accessor_name)
        )

        self.fields[accessor_name] = value

    # pylint: disable=unused-argument
    def serialize_foreign_key(self, model_instance, field, accessor_name):
        """Serialize a foreign key from the instance.

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we want to serialize the field from
        field: models.ForeignKey
            The field object from the django model
        accessor_name: str
            The name of the attribute to use to get the value from the instance

        """

        # Get the instance on the other side of the foreign key
        try:
            related_instance = getattr(model_instance, accessor_name)
        except field.related_model.DoesNotExist:
            related_instance = None

        related_instance = self.override_value_to_serialize(
            model_instance,
            field,
            accessor_name,
            FieldTypes.FK,
            value=related_instance
        )

        # And save the relation
        if related_instance:

            is_relation_boundary = self.is_relation_boundary(accessor_name, field, FieldTypes.FK)

            target = Instance(
                source=related_instance,
                serialize=True,
                graph=self.graph,
                is_boundary=is_relation_boundary,
            )

            self.add_relation(accessor_name, target)


    def serialize_manytomany_field(self, model_instance, field, accessor_name):
        """Serialize a reverse foreign key from the instance.

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we want to serialize the field from
        field: models.ManyToManyField
            The field object from the django model
        accessor_name: str
            The name of the attribute to use to get the values from the instance

        """

        # We'll save instances of the through model between the two parts of the M2M
        through_model, through_field = get_through_info(self.model, accessor_name)[:2]

        # Get the instances of the through table
        related_instances = through_model.objects.filter(**{through_field.name: model_instance})

        related_instances = self.override_value_to_serialize(
            model_instance,
            field,
            accessor_name,
            FieldTypes.M2M,
            value=related_instances
        )

        is_relation_boundary = self.is_relation_boundary(accessor_name, field, FieldTypes.M2M)

        if related_instances:
            # Create a relation from each of the through entries, to self

            for related_instance in related_instances:

                target = Instance(
                    source=related_instance,
                    serialize=True,
                    graph=self.graph,
                    is_boundary=is_relation_boundary,
                )

                target.add_relation(through_field.name, self)

    def serialize_reverse_foreign_key(self, model_instance, field, accessor_name):
        """Serialize a reverse foreign key from the instance.

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we want to serialize the field from
        field: models.ManyToOneRel
            The field object from the django model
        accessor_name: str
            The name of the attribute to use to get the values from the instance

        """

        # Get the instances on the other side of the reverse foreign keys
        related_instances = getattr(model_instance, accessor_name).all()

        related_instances = self.override_value_to_serialize(
            model_instance,
            field,
            accessor_name,
            FieldTypes.REVERSE_FK,
            value=related_instances
        )

        is_relation_boundary = self.is_relation_boundary(accessor_name, field,
                                                         FieldTypes.REVERSE_FK)

        if related_instances:
            # Create a relation from each of the related entries, to self

            for related_instance in related_instances:

                target = Instance(
                    source=related_instance,
                    serialize=True,
                    graph=self.graph,
                    is_boundary=is_relation_boundary,
                )

                self.add_relation(accessor_name, target)

    def serialize_reverse_manytomany_field(self, model_instance, field, accessor_name):
        """Serialize a reverse many to many from the instance.

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we want to serialize the field from
        field: models.ManyToManyRel
            The field object from the django model
        accessor_name: str
            The name of the attribute to use to get the values from the instance

        """

        # We'll save instances of the through model between the two parts of the M2M
        through_model, through_field = get_through_info(self.model, accessor_name)[:2]

        # Get the instances of the through table
        related_instances = through_model.objects.filter(**{through_field.name: model_instance})

        related_instances = self.override_value_to_serialize(
            model_instance,
            field,
            accessor_name,
            FieldTypes.REVERSE_M2M,
            related_instances
        )

        is_relation_boundary = self.is_relation_boundary(accessor_name, field,
                                                         FieldTypes.REVERSE_M2M)

        if related_instances:
            # Create a relation from each of the through entries, to self

            for related_instance in related_instances:

                target = Instance(
                    source=related_instance,
                    serialize=True,
                    graph=self.graph,
                    is_boundary=is_relation_boundary,
                )

                target.add_relation(through_field.name, self)

    def serialize_reverse_onetoone_field(self, model_instance, field, accessor_name):
        """Serialize a reverse one to one field from the instance.

        Parameters
        ----------
        model_instance: models.Model
            An instance of a django model we want to serialize the field from
        field: models.OneToOneRel
            The field object from the django model
        accessor_name: str
            The name of the attribute to use to get the value from the instance

        """

        # Get the instance on the other side of the foreign key
        try:
            related_instance = getattr(model_instance, accessor_name)
        except field.related_model.DoesNotExist:
            related_instance = None

        related_instance = self.override_value_to_serialize(
            model_instance,
            field,
            accessor_name,
            FieldTypes.REVERSE_O2O,
            related_instance
        )

        is_relation_boundary = self.is_relation_boundary(accessor_name, field,
                                                         FieldTypes.REVERSE_O2O)

        # And save the relation
        if related_instance:

            # Create a relation from the other instance (the one defining the o2o to self)
            target = Instance(
                source=related_instance,
                serialize=True,
                graph=self.graph,
                is_boundary=is_relation_boundary,
            )

            target.add_relation(field.field.name, self)

    def ensure_instance_is_from_model(self, accessor_name, instance, model):
        """Check that the given `instance` is an ``Instance`` of the given model

        Parameters
        ----------
        accessor_name: str
            The name of the field for which we have instances
        instance: Instance
            This must be an ``Instance`` having `model` as model reference
        model: type of models.Model
            The django model to compare to

        Raises
        ------
        ValueError
            If `instance` is not an ``Instance`` having `model` as model reference

        """

        if not isinstance(instance, Instance) or instance.model is not model:
            raise ValueError(
                'The value for `%s.%s` must be an `Instance` for the `%s` model' % (
                    self.model.__name__,
                    accessor_name,
                    model.__name__,
                )
            )

    def ensure_all_instances_are_from_model(self, accessor_name, instances, model):
        """Check that all instances are ``Instance`` of the given model

        Parameters
        ----------
        accessor_name: str
            The name of the field for which we have instances
        instances: list of Instance
            This must be an iterable of ``Instance`` having `model` as model reference
        model: type of models.Model
            The django model to compare to

        Raises
        ------
        ValueError
            If `instances` is not an iterable or if any of its entries is not an ``Instance``
            having `model` as model reference

        """

        all_good = True

        if not isinstance(instances, Iterable):
            all_good = False
        else:
            try:
                for instance in instances:
                    self.ensure_instance_is_from_model(accessor_name, instance, model)
            except ValueError:
                all_good = False

        if not all_good:
            raise ValueError(
                'The value for `%s.%s` must be an iterable of `Instance` for the `%s` model' % (
                    self.model.__name__,
                    accessor_name,
                    model.__name__,
                )
            )

    def deserialize(self, model_instance=None):
        """Create a django `model_instance` from the current `Instance`

        The deserialization consist of extracting data from ``self.fields`` and set these data
        in the model instance. This is done in many steps:

        begin
        1. saving simple fields
        2. saving foreign keys
        3. saving other relations
        end

        The "begin" step simply consist in telling the graph we start the deserialization process
        of this instance.

        The first step is simple and should not cause errors (except if the field is not nullable
        and not value was saved in ``self.fields``).

        The second step may need the related instance to be deserialized too, in this case the
        current deserialization is paused.
        If a related instance cannot be fully deserialized (lack of deserialized data), the
        current deserialization process will be aborted and ``self.deserialize_state`` will be
        set to "running" and the method will exit. It's up to the caller to act after that: the
        graph will do another deserialization pass once the current pass of the deserialization
        process is done for all instances.

        If this second step is done, ie all foreign keys are saved, the `model_instance` is saved
        and ``self.deserialize_state`` is set to "saved".

        Then the third step can start. Deserializing the many to many fields is in fact
        deserializing the through entries, which have at least two fields: the foreign keys to the
        current instance, and to the other side.

        If this third step is done, ie all the through entries saved, ``self.deserialize_state`` is
        set to "done": the deserialization process is done and `model_instance` is fully saved with
        all its relations.

        In all cases (saved or not, done or not), the "end" step occurs and simply consist in
        telling the graph we finished the current pass of deserialization for this instance.

        Parameters
        ----------
        model_instance: models.Model, optional
            A django model instance to be used for deserialization. If not provided, a new one
            will be instantiated from ``self.model``.

        Raises
        ------
        ValueError
            If the model used to create the passed `model_instance` is not the same as
            ``self.model``

        Returns
        -------
        models.Model
            The django model instance filled with data from ``self.fields``

        """

        if self.deserialize_state == states.RUNNING:
            return model_instance

        self.graph.begin_deserializing_instance(self)
        self.deserialize_state = states.RUNNING

        if not model_instance:
            model_instance = self.model()
        elif not isinstance(model_instance, self.model):
            raise ValueError(
                'The `model_instance` for deserialization must be a `%s`' % self.model.__name__
            )

        # Keep a reference if we need more than one pass
        self.deserialize_model_instance = model_instance

        if self.pk:
            model_instance.pk = self.pk

        fields_iterator = FieldsIterator(self.model)

        def deserialize_fields(iterator_method, deserialize_method):
            """Call the `deserialize_method` for all fields returned by `iterator_method`

            Parameters
            ----------
            iterator_method: function
                The bound method from a FieldsIterator that return fields to iterate from
            deserialize_method: function
                The method to call for each field, to deserialize them

            Returns
            -------
            bool
                ``True`` if all fields were successfully serialized, ``False`` if not (waiting a
                related object to be saved, field already being serialized...)

            """

            # all results should be ``True`` to return ``True``
            return all(
                deserialize_method(field, accessor_name, model_instance)
                for field, accessor_name, __
                in iterator_method()
                if accessor_name not in self.deserialize_fields_state
            )

        # Manage simple fields
        simple_fields_done = deserialize_fields(
            fields_iterator.iter_simple_fields,
            self.deserialize_simple_field
        )

        # Manage foreign keys
        fks_done = deserialize_fields(
            fields_iterator.iter_foreign_keys,
            self.deserialize_foreign_key
        )

        # We can save the instance now that all its direct fields are ready
        if simple_fields_done and fks_done:
            model_instance = self.save_model_instance(model_instance)

            # We save the pk for future use
            self.pk = model_instance.pk

            # And we mark the instance as saved (it will be ``done`` once the m2ms are done too)
            self.deserialize_state = states.SAVED

            # We can now manage the m2m and reverse fields
            m2ms_done = deserialize_fields(
                fields_iterator.iter_manytomany_fields,
                self.deserialize_manytomany_field
            )
            reverse_fks_done = deserialize_fields(
                fields_iterator.iter_reverse_foreign_keys,
                self.deserialize_reverse_foreign_key
            )
            reverse_m2ms_done = deserialize_fields(
                fields_iterator.iter_reverse_manytomany_fields,
                self.deserialize_reverse_manytomany_field
            )
            reverse_o2os_done = deserialize_fields(
                fields_iterator.iter_reverse_onetoone_fields,
                self.deserialize_reverse_onetoone_field
            )

            # We can mark the deserialization as done if all M2Ms are done
            if m2ms_done and reverse_fks_done and reverse_m2ms_done and reverse_o2os_done:
                self.deserialize_state = states.DONE
                # No need to keep the reference anymore
                self.deserialize_model_instance = None

        if self.deserialize_state == states.RUNNING:
            self.deserialize_state = states.STARTED

        self.graph.end_deserializing_instance(self)

        return model_instance

    # pylint: disable=unused-argument
    def override_value_to_deserialize(self, field, accessor_name, field_type, value,
                                      model_instance):
        """Hook for subclasses to override the value got from ``fields`` for deserialization

        In case of foreign key, `value` is an `Instance` of the related model, and for many to many
        fields, its an iterable of `Instance` of the through table (the same kind of value should
        be returned)

        Parameters
        ----------
        field: models.Field or models.fields.related.ForeignObject
            The field object from the django model
        accessor_name: str
            The name of the attribute used to get the value from ``self.fields``
        field_type: str
            The kind of field. One of ``FieldTypes.allowed_types``
        value: None or scalar or str or Instance or list of Instance
            The value got from ``self.fields`` that can be changed.
        model_instance: models.Model
            An instance of a django model that will receive the value for the given field

        Returns
        -------
        None or scalar or str or Instance or list of Instance
            By default will return the value passed in arguments

        """

        return value

    def deserialize_single_instance_related_field(self, field, field_type, accessor_name,
                                                  model_instance, related_model, default_value):
        """Deserialize a single-instance field from ``self.fields`` into `model_instance`

        Parameters
        ----------
        field: models.fields.Field
            The field object from the django model
        field_type: str
            The kind of field we want to deserialize
            A value of an attribute of ``django_fields_iterator.FieldTypes``
        accessor_name: str
            The name of the attribute to use to get the value from ``self.fields`` and set on
            `model_instance`
        model_instance: models.Model
            The instance being created by the deserializing process
        related_model: type of models.Model
            The django model used to created the related instances
        default_value: None or NOT_PROVIDED
            The value to use if the field is not defined in ``self.fields``

        Returns
        -------
        bool or None
            ``True`` if the related instance was saved (it may still have some related fields not
            yet serialized). Or if the result is ``None``.
            ``False`` if the related instance could not be saved: a new pass will be
            needed.
            ``None`` if the deserialization of this field is already running.

        Raises
        ------
        ValueError
            - If the value got from ``override_value_to_deserialize`` is not an ``Instance`` or not
            for the correct model
            - If no relation is found and the default value is ``NOT_PROVIDED``

        """

        # Not so complex code, but too many branches for pylint (actually 13, the max being 12)
        # pylint: disable=too-many-branches

        if accessor_name in self.deserialize_fields_state:
            return (self.deserialize_fields_state[accessor_name] == states.DONE) or None

        self.deserialize_fields_state[accessor_name] = states.RUNNING

        try:
            value = list(self.get_relation_targets(accessor_name))[0]
        except IndexError:
            if default_value is NOT_PROVIDED:
                raise ValueError
            value = default_value

        value = self.override_value_to_deserialize(
            field=field,
            accessor_name=accessor_name,
            field_type=field_type,
            value=value,
            model_instance=model_instance,
        )

        if value is not None:

            # Check the validity of the value
            self.ensure_instance_is_from_model(accessor_name, value, related_model)

            # deserialize the `Instance` if needed
            deserialize_needed = True

            if value.pk:
                # We have a PK: try to get it from the DB, or we need to deserialize it
                try:
                    value = value.get_model_instance()
                except self.model.DoesNotExist:
                    pass
                else:
                    deserialize_needed = False

            if deserialize_needed:
                related_model_instance = value.deserialize()

                if value.deserialize_state in (states.STARTED, states.RUNNING):
                    # The related instance was NOT saved, we stop here for now and let
                    # the next pass do the job
                    del self.deserialize_fields_state[accessor_name]
                    return False

                # The related instance was fully deserialized, we can save it
                value = related_model_instance

        # We have a fully saved related instance, we can set it and mark the field as done

        if value is None and field_type == FieldTypes.REVERSE_O2O and DJANGO_VERSION < (1, 9):
            # Special case, in django 1.8, we cannot set to null a reverse one 2 one field, so we
            # do it the reverse way if we have a o2o set on the original instance
            # https://code.djangoproject.com/ticket/14368
            try:
                original_instance = getattr(model_instance, accessor_name)
            except AttributeError:
                # No o2o set on the other side, no need to set it to ``None`` again
                pass
            else:
                # We set to ``None`` on the other side
                original_accessor_name = field.field.name
                setattr(original_instance, original_accessor_name, None)

        else:
            setattr(model_instance, accessor_name, value)

        self.deserialize_fields_state[accessor_name] = states.DONE

        return True

    def deserialize_multi_instances_related_field(self, field, field_type, accessor_name,
                                                  model_instance, related_model):
        """Deserialize a multi-instances field from ``self.fields`` into `model_instance`

        Parameters
        ----------
        field: models.fields.Field
            The field object from the django model
        field_type: str
            The kind of field we want to deserialize
            A value of an attribute of ``django_fields_iterator.FieldTypes``
        accessor_name: str
            The name of the attribute to use to get the values from ``self.fields`` and set on
            `model_instance`
        model_instance: models.Model
            The instance being created by the deserializing process
        related_model: type of models.Model
            The django model used to created the related instances

        Returns
        -------
        bool or None
            ``True`` if all the through instances are saved (they may still have some related
            fields not yet serialized)
            ``False`` if at least one through instances couldn't be saved
            ``None`` if the deserialization of this field is already running.

        Raises
        ------
        ValueError
            If at least one entry got from ``override_value_to_deserialize`` is not an
            ``Instance`` or not for the correct model

        """

        # Not so complex code, but too many branches for pylint (actually 13, the max being 12)
        # pylint: disable=too-many-branches

        through_accessor_name = None
        if field_type in (FieldTypes.M2M, FieldTypes.REVERSE_M2M):
            through_field = get_through_info(self.model, accessor_name)[1]
            through_accessor_name = through_field.remote_field.get_accessor_name()

        if accessor_name in self.deserialize_fields_state:
            return (self.deserialize_fields_state[accessor_name] == states.DONE) or None

        if through_accessor_name and through_accessor_name in self.deserialize_fields_state:
            return (self.deserialize_fields_state[through_accessor_name] == states.DONE) or None

        self.deserialize_fields_state[accessor_name] = states.RUNNING
        if through_accessor_name:
            self.deserialize_fields_state[through_accessor_name] = states.RUNNING

        try:
            value = list(self.get_relation_targets(through_accessor_name or accessor_name))
        except IndexError:
            value = []

        value = self.override_value_to_deserialize(
            field=field,
            accessor_name=accessor_name,
            field_type=field_type,
            value=value,
            model_instance=model_instance,
        )

        if not value:
            # No through entries to set, we're done
            self.deserialize_fields_state[accessor_name] = states.DONE
            if through_accessor_name:
                self.deserialize_fields_state[through_accessor_name] = states.DONE
            return True

        # All entries in ``value`` should be ``Instance`` of the ``related_model``
        self.ensure_all_instances_are_from_model(accessor_name, value, related_model)

        # deserialize the ``Instance`` objects if needed
        all_done = True

        for related_instance in value:
            if not related_instance.pk:
                related_instance.deserialize()
                if related_instance.deserialize_state in (states.STARTED, states.RUNNING):
                    all_done = False

        if all_done:
            # We have all through entries ready, we mark the field as done
            self.deserialize_fields_state[accessor_name] = states.DONE
            if through_accessor_name:
                self.deserialize_fields_state[through_accessor_name] = states.DONE
        else:
            # Not everything is finished, we let the next pass do the job
            del self.deserialize_fields_state[accessor_name]
            if through_accessor_name:
                del self.deserialize_fields_state[through_accessor_name]

        return all_done

    def deserialize_simple_field(self, field, accessor_name, model_instance):
        """Deserialize a simple field from ``self.fields`` into `model_instance`

        Parameters
        ----------
        field: models.fields.Field
            The field object from the django model
        accessor_name: str
            The name of the attribute to use to get the value from ``self.fields`` and set on
            `model_instance`
        model_instance: models.Model
            An instance of a django model on which the field will be set

        Returns
        -------
        bool or None
            ``True`` if the field correctly deserialized, ie the value extracted from
            ``self.fields`` and set in `model_instance`
            ``None`` if the deserialization of this field is already running.

        Raises
        ------
        AttributeError
            If the `field` is not defined on ``self.fields`` and is marked as non nullable on the
            django model (raised by ``self.fields.get``)

        Notes
        -----
        If the `field` is not defined on ``self.fields`` and marked as nullable on the django
        model, ``None`` will be used to set the attribute on `model_instance`.

        """

        if accessor_name in self.deserialize_fields_state:
            return (self.deserialize_fields_state[accessor_name] == states.DONE) or None

        self.deserialize_fields_state[accessor_name] = states.RUNNING

        if field.null:
            value = self.fields.get(accessor_name, None)
        else:
            value = self.fields[accessor_name]

        value = self.override_value_to_deserialize(
            field=field,
            accessor_name=accessor_name,
            field_type=FieldTypes.SIMPLE,
            value=value,
            model_instance=model_instance,
        )

        setattr(model_instance, accessor_name, value)
        self.deserialize_fields_state[accessor_name] = states.DONE

        return True

    def deserialize_foreign_key(self, field, accessor_name, model_instance):
        """Deserialize a foreign key from ``self.fields`` into `model_instance`

        For parameters, see the ``deserialize_single_instance_related_field`` method (idem for
        return value and other exceptions raised)

        Raises
        ------
        AttributeError
            If the `field` is not defined on ``self.fields`` and is marked as non nullable on the
            django model (raised by ``self.fields.get``)

        Notes
        -----
            If the `field` is not defined on ``self.fields`` and marked as nullable on the django
            model, ``None`` will be used to set the attribute on `model_instance`.

        """

        default_value = None if field.null else NOT_PROVIDED

        return self.deserialize_single_instance_related_field(
            field, FieldTypes.FK, accessor_name, model_instance,
            field.remote_field.model, default_value
        )

    def deserialize_manytomany_field(self, field, accessor_name, model_instance):
        """Deserialize a many to many field from ``self.fields`` into `model_instance`

        For parameters, see the ``deserialize_multi_instances_related_field`` method (idem for
        return value and exceptions raised)
        """

        through_model = get_through_info(self.model, accessor_name)[0]

        return self.deserialize_multi_instances_related_field(
            field, FieldTypes.M2M, accessor_name, model_instance, through_model
        )

    def deserialize_reverse_foreign_key(self, field, accessor_name, model_instance):
        """Deserialize a reverse foreign key field from ``self.fields`` into `model_instance`

        For parameters, see the ``deserialize_multi_instances_related_field`` method (idem for
        return value and exceptions raised)
        """

        return self.deserialize_multi_instances_related_field(
            field, FieldTypes.REVERSE_FK, accessor_name, model_instance, field.related_model
        )

    def deserialize_reverse_manytomany_field(self, field, accessor_name, model_instance):
        """Deserialize a reverse many to many field from ``self.fields`` into `model_instance`

        For parameters, see the ``deserialize_multi_instances_related_field`` method (idem for
        return value and exceptions raised)
        """

        through_model = get_through_info(self.model, accessor_name)[0]

        return self.deserialize_multi_instances_related_field(
            field, FieldTypes.REVERSE_M2M, accessor_name, model_instance, through_model
        )

    def deserialize_reverse_onetoone_field(self, field, accessor_name, model_instance):
        """Deserialize a reverse one to one from ``self.fields`` into `model_instance`

        For parameters, see the ``deserialize_single_instance_related_field`` method (idem for
        return value and other exceptions raised)
        """

        return self.deserialize_single_instance_related_field(
            field, FieldTypes.REVERSE_O2O, accessor_name, model_instance, field.related_model, None
        )

    def clear_deserialization_state(self):
        """Clear all the deserialization related attributes"""

        self.deserialize_state = None
        self.deserialize_fields_state = {}
        self.deserialize_model_instance = None

    def add_relation(self, accessor_name, target):
        """Create if needed, and return the relation stored in the graph cache

        Both the direct and reverse relations are created.

        Parameters
        ----------
        accessor_name: str
            The name of the accessor ("field") used to go from `self` to the `target`
        target: Instance
            The instance on the other side of the relation from `self` via its `accessor_name`

        """

        return self.graph.add_relation(self, accessor_name, target)

    def add_direct_m2m_relation(self, accessor_name, targets):
        """Helper that add the relations between the through model and the both sides of a m2m

        This only works for DIRECT m2m, without a "through" model defined on the source model (at
        least it work but none of the fields on the "through" model except the FKs to `source`
        and `targets` will be filled.)

        Parameters
        ----------
        accessor_name: str
            The name of the accessor ("field") used to go from `self` to the `target`
        targets: list of Instance
            The instances on the other side of the m2m

        Returns
        -------
        list of Instance
            Returns the list of created through instances (in the same order as the `targets`)

        """

        return self.graph.add_direct_m2m_relation(self, accessor_name, targets)

    def get_relation_targets(self, accessor_name):
        """Returns all available targets accessible from `self` via `accessor_name`

        Parameters
        ----------
        accessor_name: str
            The name of the "field" used to access the targets from `self`

        Returns
        -------
        set of Instance
            A set of unique targets got from `self`

        """

        return self.graph.get_relation_targets(self, accessor_name)

    def get_m2m_relation_targets(self, accessor_name):
        """Helper to get the instances on the other side of a m2m or reverse m2m relation.

        It get the target on the reverse fk from self to the through, then the fk from the
        through to the reverse side

        Parameters
        ----------
        accessor_name: str
            The name of the "field" used to access the targets from `self`

        Returns
        -------
        set of Instance
            A set of unique targets got in the relations cache

        """

        return self.graph.get_m2m_relation_targets(self, accessor_name)

    def remove_relation(self, accessor_name, target):
        """Remove a relation from the cache if it exists

        Both the direct and reverse relations are removed.

        Parameters
        ----------
        accessor_name: str
            The name of the accessor ("field") used to go from `self` to the `target`
        target: Instance
            The instance on the other side of the relation from `self` via its `accessor_name`

        """

        return self.graph.remove_relation(self, accessor_name, target)

    def remove_m2m_relation(self, accessor_name, targets, remove_through_instances=True):
        """Helper that remove a m2m relation from the cache if it exists

        All the relations from self to the through instances, and from the targets to the
        through instances are removed.

        Parameters
        ----------
        accessor_name: str
            The name of the accessor ("field") used to go from `self` to the `targets`
        targets: list of Instance
            The instances on the other side of the relation from `self` via its `accessor_name`
        remove_through_instances: bool
            If ``True`` (the default), the ``Instance`` objects for the "through" entries will be
            removed from the graph too, else they'll still be there but not tied to any relation.

        Returns
        -------
        list of Instance
            Returns the list of through instances that were at the center of the removed
            relations between `self` and the `targets` (in the same order as the `targets`)

        """

        return self.graph.remove_m2m_relation(self, accessor_name, targets,
                                              remove_through_instances)

    def clear_relation(self, accessor_name):
        """Clear all the relations for `self` and a specific `accessor_name`

        Both the direct and reverse relations are removed.

        Parameters
        ----------
        accessor_name: str
            The name of the accessor ("field") used to go from `self` to the targets

        """

        return self.graph.clear_relation(self, accessor_name)

    def clear_m2m_relation(self, accessor_name, remove_through_instances=True):
        """Helper that remove all m2m relations for `self` via `accessor_name`

        All the relations from self to the through instances, and from all the targets to the
        through instances are removed.

        Parameters
        ----------
        accessor_name: str
            The name of the accessor ("field") used to go from `self` to the `target`
        remove_through_instances: bool
            If ``True`` (the default), the ``Instance`` objects for the "through" entries will be
            removed from the graph too, else they'll still be there but not tied to any relation.

        Returns
        -------
        list of Instance
            Returns the list of through instances that were at the center of the removed
            relations between `self` and the targets

        """

        return self.graph.clear_m2m_relation(self, accessor_name, remove_through_instances)
