# Copyright 2013 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Internal implementation details of the peer review subsystem.

Public classes, including domain objects, can be found in domain.py and
models/student_work.py. Entities declared here should not be used by external
clients.
"""

__author__ = [
    'johncox@google.com (John Cox)',
]

from models import counters
from models import models
from models import student_work
from modules.review import domain
from google.appengine.ext import db

COUNTER_INCREMENT_COUNT_COUNT_AGGREGATE_EXCEEDED_MAX = counters.PerfCounter(
    'gcb-pr-increment-count-count-aggregate-exceeded-max',
    ('number of times increment_count() failed because the new aggregate of '
     'the counts would have exceeded domain.MAX_UNREMOVED_REVIEW_STEPS'))


class ReviewSummary(student_work.BaseEntity):
    """Object that tracks the aggregate state of reviews for a submission."""

    # UTC last modification timestamp.
    change_date = db.DateTimeProperty(auto_now=True, required=True)
    # UTC create date.
    create_date = db.DateTimeProperty(auto_now_add=True, required=True)

    # Strong counters. Callers should never manipulate these directly. Instead,
    # use decrement|increment_count.
    # Count of ReviewStep entities for this submission currently in state
    # STATE_ASSIGNED.
    assigned_count = db.IntegerProperty(default=0, required=True)
    # Count of ReviewStep entities for this submission currently in state
    # STATE_COMPLETED.
    completed_count = db.IntegerProperty(default=0, required=True)
    # Count of ReviewStep entities for this submission currently in state
    # STATE_EXPIRED.
    expired_count = db.IntegerProperty(default=0, required=True)

    # Key of the student who wrote the submission being reviewed.
    reviewee_key = student_work.KeyProperty(
        kind=models.Student.kind(), required=True)
    # Key of the submission being reviewed.
    submission_key = student_work.KeyProperty(
        kind=student_work.Submission.kind(), required=True)
    # Identifier of the unit this review is a part of.
    unit_id = db.StringProperty(required=True)

    def __init__(self, *args, **kwargs):
        """Constructs a new ReviewSummary."""
        assert not kwargs.get('key_name'), (
            'Setting key_name manually not supported')
        submission_key = kwargs.get('submission_key')
        assert submission_key, 'Missing required submission_key property'
        kwargs['key_name'] = self.key_name(submission_key)
        super(ReviewSummary, self).__init__(*args, **kwargs)

    @classmethod
    def key_name(cls, submission_key):
        """Creates a key_name string for datastore operations."""
        return '(review_summary:%s)' % submission_key.id_or_name()

    @classmethod
    def safe_key(cls, db_key, transform_fn):
        _, _, unit_id, unsafe_reviewee_key_name = cls._split_key(db_key.name())
        unsafe_reviewee_key = db.Key.from_path(
            models.Student.kind(), unsafe_reviewee_key_name)
        unsafe_submission_key = student_work.Submission.get_key(
            unit_id, unsafe_reviewee_key)
        safe_submission_key = student_work.Submission.safe_key(
            unsafe_submission_key, transform_fn)
        return db.Key.from_path(cls.kind(), cls.key_name(safe_submission_key))

    def _check_count(self):
        count_sum = (
            self.assigned_count + self.completed_count + self.expired_count)
        if count_sum >= domain.MAX_UNREMOVED_REVIEW_STEPS:
            COUNTER_INCREMENT_COUNT_COUNT_AGGREGATE_EXCEEDED_MAX.inc()
            raise db.BadValueError(
                'Unable to increment %s to %s; max is %s' % (
                    self.kind(), count_sum, domain.MAX_UNREMOVED_REVIEW_STEPS))

    def decrement_count(self, state):
        """Decrements the count for the given state enum; does not save.

        Args:
            state: string. State indicating counter to decrement; must be one of
                domain.REVIEW_STATES.

        Raises:
            ValueError: if state not in domain.REVIEW_STATES.
        """
        if state == domain.REVIEW_STATE_ASSIGNED:
            self.assigned_count -= 1
        elif state == domain.REVIEW_STATE_COMPLETED:
            self.completed_count -= 1
        elif state == domain.REVIEW_STATE_EXPIRED:
            self.expired_count -= 1
        else:
            raise ValueError('%s not in %s' % (state, domain.REVIEW_STATES))

    def increment_count(self, state):
        """Increments the count for the given state enum; does not save.

        Args:
            state: string. State indicating counter to increment; must be one of
                domain.REVIEW_STATES.

        Raises:
            db.BadValueError: if incrementing the counter would cause the sum of
               all *_counts to exceed domain.MAX_UNREMOVED_REVIEW_STEPS.
            ValueError: if state not in domain.REVIEW_STATES
        """
        if state not in domain.REVIEW_STATES:
            raise ValueError('%s not in %s' % (state, domain.REVIEW_STATES))

        self._check_count()

        if state == domain.REVIEW_STATE_ASSIGNED:
            self.assigned_count += 1
        elif state == domain.REVIEW_STATE_COMPLETED:
            self.completed_count += 1
        elif state == domain.REVIEW_STATE_EXPIRED:
            self.expired_count += 1

    def for_export(self, transform_fn):
        model = super(ReviewSummary, self).for_export(transform_fn)
        model.reviewee_key = models.Student.safe_key(
            model.reviewee_key, transform_fn)
        model.submission_key = student_work.Submission.safe_key(
            model.submission_key, transform_fn)
        return model


class ReviewStep(student_work.BaseEntity):
    """Object that represents a single state of a review."""

    # Audit trail information.

    # Identifier for the kind of thing that did the assignment. Used to
    # distinguish between assignments done by humans and those done by the
    # review subsystem.
    assigner_kind = db.StringProperty(
        choices=domain.ASSIGNER_KINDS, required=True)
    # UTC last modification timestamp.
    change_date = db.DateTimeProperty(auto_now=True, required=True)
    # UTC create date.
    create_date = db.DateTimeProperty(auto_now_add=True, required=True)

    # Repeated data to allow filtering/ordering in queries.

    # Key of the submission being reviewed.
    submission_key = student_work.KeyProperty(
        kind=student_work.Submission.kind(), required=True)
    # Unit this review step is part of.
    unit_id = db.StringProperty(required=True)

    # State information.

    # State of this review step.
    state = db.StringProperty(choices=domain.REVIEW_STATES, required=True)
    # Whether or not the review has been removed. By default removed entities
    # are ignored for most queries.
    removed = db.BooleanProperty(default=False)

    # Pointers that tie the work and people involved together.

    # Key of the Review associated with this step.
    review_key = student_work.KeyProperty(kind=student_work.Review.kind())
    # Key of the associated ReviewSummary.
    review_summary_key = student_work.KeyProperty(kind=ReviewSummary.kind())
    # Key of the Student being reviewed.
    reviewee_key = student_work.KeyProperty(kind=models.Student.kind())
    # Key of the Student doing this review.
    reviewer_key = student_work.KeyProperty(kind=models.Student.kind())

    def __init__(self, *args, **kwargs):
        """Constructs a new ReviewStep."""
        assert not kwargs.get('key_name'), (
            'Setting key_name manually not supported')
        reviewer_key = kwargs.get('reviewer_key')
        submission_key = kwargs.get('submission_key')
        assert reviewer_key, 'Missing required reviewer_key property'
        assert submission_key, 'Missing required submission_key property'
        kwargs['key_name'] = self.key_name(submission_key, reviewer_key)
        super(ReviewStep, self).__init__(*args, **kwargs)

    @classmethod
    def key_name(cls, submission_key, reviewer_key):
        """Creates a key_name string for datastore operations."""
        return '(review_step:%s:%s)' % (
            submission_key.id_or_name(), reviewer_key.id_or_name())

    @classmethod
    def safe_key(cls, db_key, transform_fn):
        """Constructs a version of the entitiy's key that is safe for export."""
        cls._split_key(db_key.name())
        name = db_key.name().strip('()')
        unsafe_submission_key_name, unsafe_reviewer_id_or_name = name.split(
            ':', 1)[1].rsplit(':', 1)
        unsafe_reviewer_key = db.Key.from_path(
            models.Student.kind(), unsafe_reviewer_id_or_name)
        safe_reviewer_key = models.Student.safe_key(
            unsafe_reviewer_key, transform_fn)

        # Treating as module-protected. pylint: disable-msg=protected-access
        _, unit_id, unsafe_reviewee_key_name = (
            student_work.Submission._split_key(unsafe_submission_key_name))
        unsafe_reviewee_key = db.Key.from_path(
            models.Student.kind(), unsafe_reviewee_key_name)
        unsafe_submission_key = student_work.Submission.get_key(
            unit_id, unsafe_reviewee_key)
        safe_submission_key = student_work.Submission.safe_key(
            unsafe_submission_key, transform_fn)

        return db.Key.from_path(
            cls.kind(), cls.key_name(safe_submission_key, safe_reviewer_key))

    def for_export(self, transform_fn):
        """Creates a version of the entity that is safe for export."""
        model = super(ReviewStep, self).for_export(transform_fn)
        model.review_key = student_work.Review.safe_key(
            model.review_key, transform_fn)
        model.review_summary_key = ReviewSummary.safe_key(
            model.review_summary_key, transform_fn)
        model.reviewee_key = models.Student.safe_key(
            model.reviewee_key, transform_fn)
        model.reviewer_key = models.Student.safe_key(
            model.reviewer_key, transform_fn)
        model.submission_key = student_work.Submission.safe_key(
            model.submission_key, transform_fn)
        return model
