"""
Grades Transformer
"""
import json
from base64 import b64encode
from functools import reduce as functools_reduce
from hashlib import sha1
from logging import getLogger

from lms.djangoapps.course_blocks.transformers.utils import collect_unioned_set_field, get_field_on_block
from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer

log = getLogger(__name__)


class GradesTransformer(BlockStructureTransformer):
    """
    The GradesTransformer collects grading information and stores it on
    the block structure.

    No runtime transformations are performed.

    The following values are stored as xblock_fields on their respective blocks
    in the block structure:

        due: (datetime) when the problem is due.
        format: (string) what type of problem it is
        graded: (boolean)
        has_score: (boolean)
        weight: (numeric)
        show_correctness: (string) when to show grades (one of 'always', 'past_due', 'never')

    Additionally, the following value is calculated and stored as a
    transformer_block_field for each block:

        max_score: (numeric)
    """
    WRITE_VERSION = 4
    READ_VERSION = 4
    FIELDS_TO_COLLECT = [
        u'due',
        u'format',
        u'graded',
        u'has_score',
        u'weight',
        u'course_version',
        u'subtree_edited_on',
        u'show_correctness',
    ]

    EXPLICIT_GRADED_FIELD_NAME = 'explicit_graded'

    @classmethod
    def name(cls):
        """
        Unique identifier for the transformer's class;
        same identifier used in setup.py.
        """
        return u'grades'

    @classmethod
    def collect(cls, block_structure):
        """
        Collects any information that's necessary to execute this
        transformer's transform method.
        """
        block_structure.request_xblock_fields(*cls.FIELDS_TO_COLLECT)
        cls._collect_max_scores(block_structure)
        collect_unioned_set_field(
            block_structure=block_structure,
            transformer=cls,
            merged_field_name='subsections',
            filter_by=lambda block_key: block_key.block_type == 'sequential',
        )
        cls._collect_explicit_graded(block_structure)
        cls._collect_grading_policy_hash(block_structure)

    def transform(self, block_structure, usage_context):
        """
        Perform no transformations.
        """
        pass

    @classmethod
    def grading_policy_hash(cls, course):
        """
        Returns the grading policy hash for the given course.
        """
        ordered_policy = json.dumps(
            course.grading_policy,
            separators=(',', ':'),  # Remove spaces from separators for more compact representation
            sort_keys=True,
        )
        return b64encode(sha1(ordered_policy).digest())

    @classmethod
    def _collect_explicit_graded(cls, block_structure):
        """
        Collect the 'explicit_graded' field for every block.
        """
        def _set_field(block_key, field_value):
            """
            Sets the explicit graded field to the given value for the
            given block.
            """
            block_structure.set_transformer_block_field(block_key, cls, cls.EXPLICIT_GRADED_FIELD_NAME, field_value)

        def _get_field(block_key):
            """
            Gets the explicit graded field to the given value for the
            given block.
            """
            return block_structure.get_transformer_block_field(block_key, cls, cls.EXPLICIT_GRADED_FIELD_NAME)

        block_types_to_ignore = {'course', 'chapter', 'sequential'}

        for block_key in block_structure.topological_traversal():
            if block_key.block_type in block_types_to_ignore:
                _set_field(block_key, None)
            else:
                explicit_field_on_block = get_field_on_block(block_structure.get_xblock(block_key), 'graded')
                if explicit_field_on_block is not None:
                    _set_field(block_key, explicit_field_on_block)
                else:
                    values_from_parents = [
                        _get_field(parent)
                        for parent in block_structure.get_parents(block_key)
                        if parent.block_type not in block_types_to_ignore
                    ]
                    non_null_values_from_parents = [value for value in values_from_parents if not None]
                    explicit_from_parents = functools_reduce(lambda x, y: x or y, non_null_values_from_parents, None)
                    _set_field(block_key, explicit_from_parents)

    @classmethod
    def _collect_max_scores(cls, block_structure):
        """
        Collect the `max_score` for every block in the provided `block_structure`.
        """
        for block_locator in block_structure.post_order_traversal():
            block = block_structure.get_xblock(block_locator)
            if getattr(block, 'has_score', False):
                cls._collect_max_score(block_structure, block)

    @classmethod
    def _collect_max_score(cls, block_structure, module):
        """
        Collect the `max_score` from the given module, storing it as a
        `transformer_block_field` associated with the `GradesTransformer`.
        """
        max_score = module.max_score()
        block_structure.set_transformer_block_field(module.location, cls, 'max_score', max_score)
        if max_score is None:
            log.warning("GradesTransformer: max_score is None for {}".format(module.location))

    @classmethod
    def _collect_grading_policy_hash(cls, block_structure):
        """
        Collect a hash of the course's grading policy, storing it as a
        `transformer_block_field` associated with the `GradesTransformer`.
        """
        course_location = block_structure.root_block_usage_key
        course_block = block_structure.get_xblock(course_location)
        block_structure.set_transformer_block_field(
            course_block.location,
            cls,
            "grading_policy_hash",
            cls.grading_policy_hash(course_block),
        )

    @staticmethod
    def _iter_scorable_xmodules(block_structure):
        """
        Loop through all the blocks locators in the block structure, and
        retrieve the module (XModule or XBlock) associated with that locator.

        For implementation reasons, we need to pull the max_score from the
        XModule, even though the data is not user specific.  Here we bind the
        data to a SystemUser.
        """
        for block_locator in block_structure.post_order_traversal():
            block = block_structure.get_xblock(block_locator)
            if getattr(block, 'has_score', False):
                yield block