import logging
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from fiction_outlines.models import Series, Character, Location, Outline
from fiction_outlines.models import CharacterInstance, LocationInstance, Arc
from fiction_outlines.models import ArcElementNode, StoryElementNode, MACE_TYPES


logger = logging.getLogger('fiction-outlines-api')

MOVE_POSITION_CHOICES = (
    ('first-child', 'first-child'),
    ('last-child', 'last-child'),
    ('first-sibling', 'first-sibling'),
    ('last-sibling', 'last-sibling'),
    ('left', 'left'),
    ('right', 'right'),
)


def convert_annotated_list(annotated_list, serializer_class):
    '''
    Takes an annotated list and a serializer class and returns a version that is suitable for
    serialization.
    '''
    converted_list = []  # pragma: no cover
    for item, info in annotated_list:
        converted_list.append((serializer_class(item).data, info))
    return converted_list


class SeriesSerializer(TaggitSerializer, serializers.ModelSerializer):
    '''
    Serializer for Series model.
    '''

    tags = TagListSerializerField()

    class Meta:  # pragma: no cover
        model = Series
        fields = ('id', 'title', 'description', 'tags')
        read_only_fields = ('id',)


class CharacterInstanceSerializer(serializers.ModelSerializer):
    '''
    Serializer for character instance.
    '''
    name = serializers.CharField(read_only=True, source='character.name')

    class Meta:  # pragma: no cover
        model = CharacterInstance
        fields = ('id', 'outline', 'character', 'name', 'main_character', 'pov_character', 'protagonist',
                  'antagonist', 'obstacle', 'villain')
        read_only_fields = ('id', 'outline', 'character', 'name')
        extra_kwargs = {}
        for field in read_only_fields:
            extra_kwargs['field'] = {'required': False}


class LocationInstanceSerializer(serializers.ModelSerializer):
    '''
    Serializer for location instance.
    '''
    name = serializers.CharField(read_only=True, source='location.name')

    class Meta:  # pragma: no cover
        model = LocationInstance
        fields = ('id', 'location', 'name', 'outline')
        read_only_fields = ('id', 'location', 'name', 'outline')
        extra_kwargs = {}
        for field in read_only_fields:
            extra_kwargs['field'] = {'required': False}


class CharacterSerializer(TaggitSerializer, serializers.ModelSerializer):
    '''
    Serializer for Character model.
    '''

    tags = TagListSerializerField()
    series = serializers.PrimaryKeyRelatedField(many=True, allow_null=True, queryset=Series.objects.all())
    character_instances = CharacterInstanceSerializer(many=True, read_only=True,
                                                      required=False, source='characterinstance_set')

    class Meta:  # pragma: no cover
        model = Character
        fields = ('id', 'name', 'description', 'series', 'tags', 'character_instances')
        read_only_fields = ('id', 'character_instances')


class LocationSerializer(TaggitSerializer, serializers.ModelSerializer):
    '''
    Serializer for Location model.
    '''

    tags = TagListSerializerField()
    series = serializers.PrimaryKeyRelatedField(many=True, allow_null=True, queryset=Series.objects.all())
    location_instances = LocationInstanceSerializer(many=True, read_only=True,
                                                    required=False, source='locationinstance_set')

    class Meta:  # pragma: no cover
        model = Location
        fields = ('id', 'name', 'description', 'series', 'tags', 'location_instances')
        read_only_fields = ('id', 'location_instances')


class OutlineSerializer(TaggitSerializer, serializers.ModelSerializer):
    '''
    Serializer for Outline model.
    Also provides ``outline_structure``, which is a treebeard annotated list of story element nodes.

    Special read-only fields:

    :attribute length_estimate:
        Auto calculated length (in words) based on the number of characters, locations, and arcs.
    :attribute arc_set:
        The :class:`fiction_outlines.models.Arc` objects associated with this outline.
    :attribute characterinstance_set:
        Character instances associated with this outline.
    :attribute locationinstance_set:
        Location instances associated with this outline.
    :attribute outline_structure:
        An annotated list of :class:`fiction_outlines.models.StoryElementNode` objects for visualizing the
        tree structure.
    '''

    length_estimate = serializers.IntegerField(read_only=True,
                                               required=False,
                                               help_text=_("Autogenerated length estimate for the story in number of words."))  # noqa: E501
    tags = TagListSerializerField()
    series = serializers.PrimaryKeyRelatedField(allow_null=True, queryset=Series.objects.all())
    arc_set = serializers.PrimaryKeyRelatedField(many=True, required=False, read_only=True)
    characterinstance_set = serializers.PrimaryKeyRelatedField(many=True, required=False, read_only=True)
    locationinstance_set = serializers.PrimaryKeyRelatedField(many=True, required=False, read_only=True)
    outline_structure = serializers.SerializerMethodField(read_only=True,
                                                          help_text=_("Annotated list of outline items."))

    def get_outline_structure(self, obj):
        '''
        Retrieves all :class:`fiction_outlines.StoryElementNode` objects associated with this outline,
        and returns it as an annotated list that can be serialized.

        :param obj:
            The outline object this serializer represents.

        :returns: An annotated list for serialization or an empty list.
        '''
        logger.debug("Checking outline structure.")
        if obj.story_tree_root:
            logger.debug('Found a root node')
            annotated_list = StoryElementNode.get_annotated_list(obj.story_tree_root)
            logger.debug('Annotated list retrieved.')
            if len(annotated_list) > 1:
                # There is more than just a single root node: serialize and return.
                annotated_list = convert_annotated_list(annotated_list, StoryElementNodeSerializer)
                logger.debug("Returning results to field.")  # pragma: no cover
                return annotated_list[1:]
        return []

    class Meta:  # pragma: no cover
        model = Outline
        fields = ('id', 'title', 'description', 'series', 'tags', 'length_estimate', 'arc_set',
                  'characterinstance_set', 'locationinstance_set', 'outline_structure')
        read_only_fields = ('id', 'length_estimate', 'arc_set', 'characterinstance_set',
                            'locationinstance_set', 'outline_structure')


class ArcSerializer(serializers.ModelSerializer):
    '''
    Serializer for Arc model.

    To create an Arc, make sure to instead use the :class:`ArcCreateSerializer` and
    post it to :class:`fiction_outlines_api.views.ArcCreateView`
    '''

    outline = serializers.PrimaryKeyRelatedField(read_only=True, required=False)
    current_errors = serializers.ReadOnlyField(read_only=True, required=False,
                                               help_text=_('dict current validation errors'))
    arc_structure = serializers.SerializerMethodField(read_only=True, help_text=_('Annotated list of arc elements.'))

    def get_arc_structure(self, obj):
        '''
        Fetches all the :class:`fiction_outlines.models.ArcElementNode` objects assocaited with this arc as an
        annotated list.

        :param obj:
            The arc this serializer represents.

        :returns: An annotated list ready to be serialized or an empty list.
        '''
        logger.debug("Attempting to retrieve arc structure for arc %s" % obj.pk)
        if obj.arc_root_node:
            annotated_list = ArcElementNode.get_annotated_list(obj.arc_root_node)
            logger.debug("Retrieved annotated list")
            if len(annotated_list) > 1:
                # There is more than just a root node which we don't want to display to users.
                logger.debug("Arc structure found, builidng serialized structure.")
                annotated_list = convert_annotated_list(annotated_list, ArcElementNodeSerializer)
                logger.debug("Success serializing arc!")
                logger.debug("Arc looks like this:\n\n%s" % annotated_list[1:])
                logger.debug("Returning result to field.")
                return annotated_list[1:]
            else:
                logger.debug("Only a root node... returning empty list.")
        return []  # pragma: no cover This is really unlikely to occur.

    class Meta:  # pragma: no cover
        model = Arc
        fields = ('id', 'name', 'mace_type', 'outline', 'name', 'current_errors', 'arc_structure')
        read_only_fields = ('id', 'outline', 'current_errors', 'arc_structure')


class ArcCreateSerializer(serializers.Serializer):
    '''
    Serializer used for arc creation. You can't create an Arc using it's primary serializer.
    '''
    mace_type = serializers.ChoiceField(choices=MACE_TYPES)
    name = serializers.CharField(max_length=255)


class ArcElementNodeSerializer(serializers.ModelSerializer):
    '''
    Serializer for ArcElementNode.

    Special read-only attributes:

    :attribute is_milestone:
        Does this element represent a milestone in the :class:`Arc`
    :attribute milestone_seq:
        If a milestone, what is the proscribed sequence where it should appear
        in the context of the other milestones.
    :attribute parent_outline:
        Convenience reference to the parent outline object.
    :attribute headline:
        Automatically generated from the first line of the description.
    '''

    arc = serializers.PrimaryKeyRelatedField(read_only=True, required=False)
    headline = serializers.ReadOnlyField(required=False, help_text=_('Auto generated from description'))
    story_element_node = serializers.PrimaryKeyRelatedField(required=False, allow_null=True,
                                                            queryset=StoryElementNode.objects.all())
    parent_outline = serializers.PrimaryKeyRelatedField(source='arc.outline', read_only=True, required=False)
    milestone_seq = serializers.IntegerField(allow_null=True, read_only=True, required=False,
                                             help_text=_('If a milestone, what order *should* it appear in?'))

    class Meta:  # pragma: no cover
        model = ArcElementNode
        fields = ('id', 'arc', 'arc_element_type', 'headline', 'description', 'story_element_node',
                  'assoc_characters', 'assoc_locations', 'milestone_seq', 'is_milestone', 'parent_outline')
        read_only_fields = ('id', 'arc', 'headline', 'milestone_seq', 'is_milestone', 'parent_outline')


class StoryElementNodeSerializer(serializers.ModelSerializer):
    '''
    Serializer for StoryElementNode

    Two special fields here:

    :attribute all_characters:
        All characters associated with this node or its descendants.
    :attribute all_locations:
        All locations associated with this node of its descendants.
    '''

    all_characters = CharacterInstanceSerializer(many=True, read_only=True, required=False,
                                                 help_text=_('All characters associated with this node its descendants.'))  # noqa: E501
    all_locations = LocationInstanceSerializer(many=True, read_only=True, required=False,
                                               help_text=_('All locations associated with this node or its descendants.'))  # noqa: E501
    outline = serializers.PrimaryKeyRelatedField(read_only=True, required=False)

    class Meta:  # pragma: no cover
        model = StoryElementNode
        fields = ('id', 'story_element_type', 'name', 'description', 'outline', 'assoc_characters',
                  'assoc_locations', 'all_characters', 'all_locations', 'impact_rating')
        read_only_fields = ('all_characters', 'all_locations', 'impact_rating', 'outline', 'id')
