<?php

/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2016 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
 *
 */

namespace oat\taoQtiTest\models;

use qtism\common\enums\BaseType;
use qtism\common\enums\Cardinality;
use qtism\common\collections\IdentifierCollection;
use qtism\data\AssessmentTest;
use qtism\data\state\OutcomeDeclaration;
use qtism\data\state\DefaultValue;
use qtism\data\state\ValueCollection;
use qtism\data\state\Value;
use qtism\data\rules\SetOutcomeValue;
use qtism\data\rules\OutcomeRuleCollection;
use qtism\data\rules\OutcomeRule;
use qtism\data\processing\OutcomeProcessing;
use qtism\data\expressions\ExpressionCollection;
use qtism\data\expressions\NumberCorrect;
use qtism\data\expressions\TestVariables;
use qtism\data\expressions\operators\Sum;

/**
 * Utility class for Test Category Rules Generation.
 *
 * Provides utility methods supporting Test Category Rules Generation process.
 */
class TestCategoryRulesUtils
{
    const NUMBER_ITEMS_SUFFIX = '_CATEGORY_NUMBER_ITEMS';
    const NUMBER_CORRECT_SUFFIX = '_CATEGORY_NUMBER_CORRECT';
    const TOTAL_SCORE_SUFFIX = '_CATEGORY_TOTAL_SCORE';
    
    /**
     * Extract all categories from a given QTI-SDK AssessmentTest object.
     *
     * This method will extract all category identifiers found as assessmentItemRef
     * category identifiers from a given $test object.
     *
     * An optional $exclusion array can be given as a second parameter. Each of the Pearl
     * Compatible Regular Expression will be applied on the categories to be extracted. If
     * one of them matches a category, it will be excluded from the extraction process.
     *
     * Identifiers returned will be unique.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param array $exclusion (optional) An optional array of Pearl Compatible Regular Expressions.
     * @return array An array of QTI category identifiers.
     */
    public static function extractCategories(AssessmentTest $test, array $exclusion = [])
    {
        $categories = [];
        
        $assessmentItemRefs = $test->getComponentsByClassName('assessmentItemRef');
        foreach ($assessmentItemRefs as $assessmentItemRef) {
            $assessmentItemRefCategories = $assessmentItemRef->getCategories()->getArrayCopy();
            
            $count = count($assessmentItemRefCategories);
            for ($i = 0; $i < $count; $i++) {
                foreach ($exclusion as $excl) {
                    if (@preg_match($excl, $assessmentItemRefCategories[$i]) === 1) {
                        unset($assessmentItemRefCategories[$i]);
                        break;
                    }
                }
            }
            
            $categories = array_merge($categories, $assessmentItemRefCategories);
        }
        
        return array_unique($categories);
    }
    
    /**
     * Append a variable dedicated to counting number of items related to a given category.
     *
     * This method will append a QTI outcome variable dedicated to count the number of items
     * related to a given QTI $category, to a given QTI $test.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $category A QTI category identifier.
     * @return string the identifier of the created QTI outcome variable.
     */
    public static function appendNumberOfItemsVariable(AssessmentTest $test, $category)
    {
        $varName = strtoupper($category) . self::NUMBER_ITEMS_SUFFIX;
        self::appendOutcomeDeclarationToTest($test, $varName, BaseType::INTEGER, self::countNumberOfItemsWithCategory($test, $category));
        
        return $varName;
    }
    
    /**
     * Append a variable dedicated to counting number of correctly responded items related to a given category.
     *
     * This method will append a QTI outcome variable dedicated to count the number of items that are correctly responded
     * related to a given QTI $category, to a given QTI $test.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $category A QTI category identifier.
     * @return string the identifier of the created QTI outcome variable.
     */
    public static function appendNumberCorrectVariable(AssessmentTest $test, $category)
    {
        $varName = strtoupper($category) . self::NUMBER_CORRECT_SUFFIX;
        self::appendOutcomeDeclarationToTest($test, $varName, BaseType::INTEGER);
        
        return $varName;
    }
    
    /**
     * Append a variable dedicated to store the total score items related to a given category.
     *
     * This method will append a QTI outcome variable dedicated to store the total score of items
     * related to a given QTI $category, to a given QTI $test.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $category A QTI category identifier.
     * @return string the identifier of the created QTI outcome variable.
     */
    public static function appendTotalScoreVariable(AssessmentTest $test, $category)
    {
        $varName = strtoupper($category) . self::TOTAL_SCORE_SUFFIX;
        self::appendOutcomeDeclarationToTest($test, $varName, BaseType::FLOAT);

        return $varName;
    }
    
    /**
     * Append the outcome processing rules to populate an outcome variable with the number of items correctly responded related to a given category.
     *
     * This method will append a QTI outcome processing to a given QTI-SDK AssessmentTest $test, dedicated to count the number
     * of correctly responded items related to a given QTI $category.
     *
     * In case of an outcome processing rule targetting a variable name $varName already exists in the test, the outcome
     * processing rule is not appended to the test.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $category A QTI category identifier.
     * @param string $varName The QTI identifier of the variable to be populated by the outcome processing rule.
     */
    public static function appendNumberCorrectOutcomeProcessing(AssessmentTest $test, $category, $varName)
    {
        if (self::isVariableSetOutcomeValueTarget($test, $varName) === false) {
            $numberCorrectExpression = new NumberCorrect();
            $numberCorrectExpression->setIncludeCategories(
                new IdentifierCollection(
                    [$category]
                )
            );
            
            $setOutcomeValue = new SetOutcomeValue(
                $varName,
                $numberCorrectExpression
            );
            
            self::appendOutcomeRule($test, $setOutcomeValue);
        }
    }
    
    /**
     * Append the outcome processing rules to populate an outcome variable with total score of items related to a given category.
     *
     * This method will append a QTI outcome processing to a given QTI-SDK AssessmentTest $test, dedicated to store
     * the total score of items related to a given QTI $category.
     *
     * In case of the $weightIdentifier argument is given, the score will consider weights defined at the assessmentItemRef
     * level identified by $weightIdentifier. Otherwise, no weights are taken into account while computing total scores.
     *
     * In case of an outcome processing rule targetting a variable name $varName already exists in the test, the outcome
     * processing rule is not appended to the test.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $category A QTI category identifier.
     * @param string $varName The QTI identifier of the variable to be populated by the outcome processing rule.
     * @param string $scoreIdentifier (optional) An optional QTI identifier to be used as items' score variable (defaults to "SCORE").
     * @param string $weightIdentifier (optional) An optional QTI identifier to be used as items' weight to be considered for total score. (defaults to empty string).
     */
    public static function appendTotalScoreOutcomeProcessing(AssessmentTest $test, $category, $varName, $scoreVariableIdentifier = 'SCORE', $weightIdentifier = '')
    {
        if (self::isVariableSetOutcomeValueTarget($test, $varName) === false) {
            $testVariablesExpression = new TestVariables($scoreVariableIdentifier, BaseType::FLOAT);
            $testVariablesExpression->setWeightIdentifier($weightIdentifier);
            $testVariablesExpression->setIncludeCategories(
                new IdentifierCollection(
                    [$category]
                )
            );
            
            $setOutcomeValue = new SetOutcomeValue(
                $varName,
                new Sum(
                    new ExpressionCollection(
                        [
                            $testVariablesExpression
                        ]
                    )
                )
            );
            
            self::appendOutcomeRule($test, $setOutcomeValue);
        }
    }
    
    /**
     * Append an outcome declaration to a test.
     *
     * This method will append an outcome declaration with identifier $varName, single cardinality
     * to a given QTI-SDK AssessmentTest $test object.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $varName The variable name to be used for the outcome declaration.
     * @param integer $baseType A QTI-SDK Base Type.
     * @param mixed (optional) A default value for the variable.
     */
    public static function appendOutcomeDeclarationToTest(AssessmentTest $test, $varName, $baseType, $defaultValue = null)
    {
        $outcomeDeclarations = $test->getOutcomeDeclarations();
        $outcome = new OutcomeDeclaration($varName, $baseType, Cardinality::SINGLE);
        
        if ($defaultValue !== null) {
            $outcome->setDefaultValue(
                new DefaultValue(
                    new ValueCollection(
                        [
                            new Value(
                                $defaultValue,
                                $baseType
                            )
                        ]
                    )
                )
            );
        }
        
        $outcomeDeclarations->attach($outcome);
    }
    
    /**
     * Count the number of items in a test that belong to a given category.
     *
     * This method will count the number of items in a test that belong to a given category.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $category a QTI category identifier.
     * @return integer The number of items that belong to $category.
     */
    public static function countNumberOfItemsWithCategory(AssessmentTest $test, $category)
    {
        $count = 0;
        
        $assessmentItemRefs = $test->getComponentsByClassName('assessmentItemRef');
        foreach ($assessmentItemRefs as $assessmentItemRef) {
            if (in_array($category, $assessmentItemRef->getCategories()->getArrayCopy()) === true) {
                $count++;
            }
        }
        
        return $count;
    }
    
    /**
     * Know whether or not a variable is the target of an existing setOutcomeValue QTI rule.
     *
     * This method enables the client code to know whether or not a variable with identifier
     * $varName is the target of an existing setOutcomeValue QTI rule with a given
     * AssessmentTest $test object.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param string $varName A QTI variable identifier.
     * @return boolean
     */
    public static function isVariableSetOutcomeValueTarget(AssessmentTest $test, $varName)
    {
        $setOutcomeValues = $test->getComponentsByClassName('setOutcomeValue');
        foreach ($setOutcomeValues as $setOutcomeValue) {
            if ($setOutcomeValue->getIdentifier() === $varName) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Append a QTI-SDK OutcomeRule object in an AssessmentTest's OutcomeProcessing.
     *
     * In case of no OutcomeProcessing is set yet for the AssessmentTest $test object,
     * it will be automatically created, with the OutcomeRule $rule as its first
     * rule. Otherwise, the OutcomeRule $rule is simply appended to the existing
     * OutcomeProcessing object.
     *
     * @param qtism\data\AssessmentTest $test A QTI-SDK AssessmentTest object.
     * @param qtism\data\rules\OutcomeRule A QTI-SDK OutcomeRule object.
     */
    private static function appendOutcomeRule(AssessmentTest $test, OutcomeRule $rule)
    {
        $outcomeProcessing = $test->getOutcomeProcessing();
        if ($outcomeProcessing === null) {
            $test->setOutcomeProcessing(
                new OutcomeProcessing(
                    new OutcomeRuleCollection(
                        [
                            $rule
                        ]
                    )
                )
            );
        } else {
            $outcomeProcessing->getOutcomeRules()[] = $rule;
        }
    }
}