# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Harvard, edX & OpenCraft
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3".  If not, see <http://www.gnu.org/licenses/>.
#

import json
import logging

from lazy.lazy import lazy

from xblock.core import XBlock
from xblock.fields import String, Scope
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
    StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
)
from .sub_api import sub_api


loader = ResourceLoader(__name__)

log = logging.getLogger(__name__)


# Make '_' a no-op so we can scrape strings
def _(text):
    return text


def _normalize_id(key):
    """
    Helper method to normalize a key to avoid issues where some keys have version/branch and others don't.
    e.g. self.scope_ids.usage_id != self.runtime.get_block(self.scope_ids.usage_id).scope_ids.usage_id
    """
    if hasattr(key, "for_branch"):
        key = key.for_branch(None)
    if hasattr(key, "for_version"):
        key = key.for_version(None)
    return key


@XBlock.needs('i18n')
@XBlock.wants('user')
class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock):
    """
    XBlock that displays plot that summarizes answers to scale and/or rating questions.
    """

    CATEGORY = 'sb-plot'
    STUDIO_LABEL = _(u"Plot")

    # Settings
    display_name = String(
        display_name=_("Plot title"),
        default="Plot",
        scope=Scope.content
    )

    plot_label = String(
        display_name=_("Plot label"),
        help=_("Label for default overlay that shows student's answers to scale questions"),
        default="yours",
        scope=Scope.content
    )

    point_color_default = String(
        display_name=_("Point color (default overlay)"),
        help=_("Point color to use for default overlay"),
        default="green",
        scope=Scope.content
    )

    point_color_average = String(
        display_name=_("Point color (average overlay)"),
        help=_("Point color to use for average overlay"),
        default="blue",
        scope=Scope.content
    )

    q1_label = String(
        display_name=_("Quadrant I"),
        help=_(
            "Label for the first quadrant. "
            "Plot uses counter-clockwise numbering starting in the top right quadrant."
        ),
        default="Q1",
        scope=Scope.content
    )

    q2_label = String(
        display_name=_("Quadrant II"),
        help=_(
            "Label for the second quadrant. "
            "Plot uses counter-clockwise numbering starting in the top right quadrant."
        ),
        default="Q2",
        scope=Scope.content
    )

    q3_label = String(
        display_name=_("Quadrant III"),
        help=_(
            "Label for the third quadrant. "
            "Plot uses counter-clockwise numbering starting in the top right quadrant."
        ),
        default="Q3",
        scope=Scope.content
    )

    q4_label = String(
        display_name=_("Quadrant IV"),
        help=_(
            "Label for the fourth quadrant. "
            "Plot uses counter-clockwise numbering starting in the top right quadrant."
        ),
        default="Q4",
        scope=Scope.content
    )

    claims = String(
        display_name=_("Claims and associated questions"),
        help=_(
            'Claims and questions that should be included in the plot. '
            'Each line defines a triple of the form "claim, q1, q2", '
            'where "claim" is arbitrary text that represents a claim, '
            'and "q1" and "q2" are IDs of scale or rating questions. '
        ),
        default="",
        multiline_editor=True,
        resettable_editor=False
    )

    editable_fields = (
        'display_name', 'plot_label', 'point_color_default', 'point_color_average',
        'q1_label', 'q2_label', 'q3_label', 'q4_label', 'claims'
    )

    @lazy
    def course_key_str(self):
        location = _normalize_id(self.location)
        return unicode(location.course_key)

    @property
    def default_claims(self):
        return self._get_claims(self._get_default_response)

    @property
    def average_claims(self):
        return self._get_claims(self._get_average_response)

    def _get_claims(self, response_function):
        if not self.claims:
            return []

        mentoring_block = self.get_parent().get_parent()
        question_ids, questions = mentoring_block.question_ids, mentoring_block.questions
        claims = []
        for line in self.claims.split('\n'):
            claim, q1, q2 = line.split(', ')
            r1, r2 = None, None
            for question_id, question in zip(question_ids, questions):
                if question.url_name == q1:
                    r1 = response_function(question, question_id)
                if question.url_name == q2:
                    r2 = response_function(question, question_id)
                if r1 is not None and r2 is not None:
                    break
            claims.append([claim, r1, r2])

        return claims

    def _get_default_response(self, question, question_id):
        # 1. Obtain block_type for question
        question_type = question.scope_ids.block_type
        # 2. Obtain latest submission for question
        student_dict = {
            'student_id': self.runtime.anonymous_student_id,
            'course_id': self.course_key_str,
            'item_id': question_id,
            'item_type': question_type,
        }
        submissions = sub_api.get_submissions(student_dict, limit=1)
        # 3. Extract response from latest submission for question
        answer_cache = {}
        for submission in submissions:
            answer = self._get_answer(question, submission, answer_cache)
            return int(answer)

    def _get_average_response(self, question, question_id):
        # 1. Obtain block_type for question
        question_type = question.scope_ids.block_type
        # 2. Obtain latest submissions for question
        submissions = sub_api.get_all_submissions(self.course_key_str, question_id, question_type)
        # 3. Extract responses from latest submissions for question and sum them up
        answer_cache = {}
        response_total = 0
        num_submissions = 0  # Can't use len(submissions) because submissions is a generator
        for submission in submissions:
            answer = self._get_answer(question, submission, answer_cache)
            response_total += int(answer)
            num_submissions += 1
        # 4. Calculate average response for question
        if num_submissions:
            return response_total / float(num_submissions)

    def _get_answer(self, block, submission, answer_cache):
        """
        Return answer associated with `submission` to `block`.

        `answer_cache` is a dict that is reset for each block.
        """
        answer = submission['answer']
        # Convert from answer ID to answer label
        if answer not in answer_cache:
            answer_cache[answer] = block.get_submission_display(answer)
        return answer_cache[answer]

    def default_claims_json(self):
        return json.dumps(self.default_claims)

    def average_claims_json(self):
        return json.dumps(self.average_claims)

    @XBlock.json_handler
    def get_data(self, data, suffix):
        return {
            'default_claims': self.default_claims,
            'average_claims': self.average_claims,
        }

    @property
    def allowed_nested_blocks(self):
        """
        Returns a list of allowed nested XBlocks. Each item can be either

        * An XBlock class
        * A NestedXBlockSpec

        If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
        NestedXBlockSpec allows explicitly setting disabled/enabled state,
        disabled reason (if any) and single/multiple instances.
        """
        return [PlotOverlayBlock]

    @lazy
    def overlay_ids(self):
        """
        Get the usage_ids of all of this XBlock's children that are overlays.
        """
        return [
            _normalize_id(child_id) for child_id in self.children if
            child_isinstance(self, child_id, PlotOverlayBlock)
        ]

    @lazy
    def overlays(self):
        """
        Get the overlay children of this block.
        """
        return [self.runtime.get_block(overlay_id) for overlay_id in self.overlay_ids]

    @lazy
    def overlay_data(self):
        if not self.claims:
            return []

        overlay_data = []
        claims = self.claims.split('\n')
        for index, overlay in enumerate(self.overlays):
            claims_json = []
            if overlay.claim_data:
                claim_data = overlay.claim_data.split('\n')
                for claim, data in zip(claims, claim_data):
                    claim = claim.split(', ')[0]
                    r1, r2 = data.split(', ')
                    claims_json.append([claim, int(r1), int(r2)])
            claims_json = json.dumps(claims_json)
            overlay_data.append({
                'plot_label': overlay.plot_label,
                'point_color': overlay.point_color,
                'description': overlay.description,
                'citation': overlay.citation,
                'claims_json': claims_json,
                'position': index,
            })
        return overlay_data

    @lazy
    def claims_display(self):
        if not self.claims:
            return []

        claims = []
        for claim in self.claims.split('\n'):
            claim, q1, q2 = claim.split(', ')
            claims.append([claim, q1, q2])
        return claims

    def author_preview_view(self, context):
        context['self'] = self
        fragment = Fragment()
        fragment.add_content(loader.render_template('templates/html/plot_preview.html', context))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/plot-preview.css'))
        if self.overlay_ids:
            fragment.add_content(
                u"<p>{}</p>".format(
                    _(u"In addition to the default and average overlays the plot includes the following overlays:")
                ))
            for overlay in self.overlays:
                overlay_fragment = self._render_child_fragment(overlay, context, view='mentoring_view')
                fragment.add_frag_resources(overlay_fragment)
                fragment.add_content(overlay_fragment.content)
        return fragment

    def mentoring_view(self, context):
        return self.student_view(context)

    def student_view(self, context=None):
        """ Student View """
        context = context.copy() if context else {}
        context['hide_header'] = True
        context['self'] = self
        fragment = Fragment()
        fragment.add_content(loader.render_template('templates/html/plot.html', context))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/plot.css'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/d3.min.js'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot.js'))
        fragment.initialize_js('PlotBlock')
        return fragment

    def author_edit_view(self, context):
        """
        Add some HTML to the author view that allows authors to add child blocks.
        """
        context['wrap_children'] = {
            'head': u'<div class="mentoring">',
            'tail': u'</div>'
        }
        fragment = super(PlotBlock, self).author_edit_view(context)
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot_edit.js'))
        fragment.initialize_js('PlotEdit')
        return fragment


@XBlock.needs('i18n')
class PlotOverlayBlock(StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlock):
    """
    XBlock that represents a user-defined overlay for a plot block.
    """

    CATEGORY = 'sb-plot-overlay'
    STUDIO_LABEL = _(u"Plot Overlay")

    # Settings
    display_name = String(
        display_name=_("Overlay title"),
        default="Overlay",
        scope=Scope.content
    )

    plot_label = String(
        display_name=_("Plot label"),
        help=_("Label for button that allows to toggle visibility of this overlay"),
        default="",
        scope=Scope.content
    )

    point_color = String(
        display_name=_("Point color"),
        help=_("Point color to use for this overlay"),
        default="",
        scope=Scope.content
    )

    description = String(
        display_name=_("Description"),
        help=_("Description of this overlay (optional)"),
        default="",
        scope=Scope.content
    )

    citation = String(
        display_name=_("Citation"),
        help=_("Source of data belonging to this overlay (optional)"),
        default="",
        scope=Scope.content
    )

    claim_data = String(
        display_name=_("Claim data"),
        help=_(
            'Claim data to include in this overlay. '
            'Each line defines a tuple of the form "q1, q2", '
            'where "q1" is the value associated with the first scale or rating question, '
            'and "q2" is the value associated with the second scale or rating question. '
            'Note that data will be associated with claims in the order that they are defined in the parent plot.'
        ),
        default="",
        multiline_editor=True,
        resettable_editor=False
    )

    editable_fields = (
        "plot_label", "point_color", "description", "citation", "claim_data"
    )

    def validate_field_data(self, validation, data):
        """
        Validate this block's field data.
        """
        super(PlotOverlayBlock, self).validate_field_data(validation, data)

        def add_error(msg):
            validation.add(ValidationMessage(ValidationMessage.ERROR, msg))

        if not data.plot_label.strip():
            add_error(_(u"No plot label set. Button for toggling visibility of this overlay will not have a label."))
        if not data.point_color.strip():
            add_error(_(u"No point color set. This overlay will not work correctly."))

        # If parent plot is associated with one or more claims, prompt user to add claim data
        parent = self.get_parent()
        if parent.claims.strip() and not data.claim_data.strip():
            add_error(_(u"No claim data provided. This overlay will not work correctly."))

    def author_preview_view(self, context):
        return self.student_view(context)

    def mentoring_view(self, context):
        context = context.copy() if context else {}
        context['hide_header'] = True
        return self.author_preview_view(context)

    def student_view(self, context):
        context['self'] = self
        fragment = Fragment()
        fragment.add_content(loader.render_template('templates/html/overlay.html', context))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/overlay.css'))
        return fragment
