<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Drag-and-drop markers question definition class.
 *
 * @package    qtype_ddmarker
 * @copyright  2012 The Open University
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */


defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot . '/question/type/ddimageortext/questionbase.php');
require_once($CFG->dirroot . '/question/type/ddmarker/shapes.php');


/**
 * Represents a drag-and-drop markers question.
 *
 * @copyright  2009 The Open University
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class qtype_ddmarker_question extends qtype_ddtoimage_question_base {

    public $showmisplaced;

    public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
        if ($filearea == 'bgimage') {
            $validfilearea = true;
        } else {
            $validfilearea = false;
        }
        if ($component == 'qtype_ddmarker' && $validfilearea) {
            $question = $qa->get_question();
            $itemid = reset($args);
            return $itemid == $question->id;
        } else {
            return parent::check_file_access($qa, $options, $component,
                                                                $filearea, $args, $forcedownload);
        }
    }
    /**
     * Get a choice identifier
     *
     * @param int $choice stem number
     * @return string the question-type variable name.
     */
    public function choice($choice) {
        return 'c' . $choice;
    }

    public function get_expected_data() {
        $vars = array();
        foreach ($this->choices[1] as $choice => $notused) {
            $vars[$this->choice($choice)] = PARAM_NOTAGS;
        }
        return $vars;
    }
    public function is_complete_response(array $response) {
        foreach ($this->choices[1] as $choiceno => $notused) {
            if (isset($response[$this->choice($choiceno)])
                                            && '' != trim($response[$this->choice($choiceno)])) {
                return true;
            }
        }
        return false;
    }
    public function is_gradable_response(array $response) {
        return $this->is_complete_response($response);
    }
    public function is_same_response(array $prevresponse, array $newresponse) {
        foreach ($this->choices[1] as $choice => $notused) {
            $fieldname = $this->choice($choice);
            if (!$this->arrays_same_at_key_integer(
                    $prevresponse, $newresponse, $fieldname)) {
                return false;
            }
        }
        return true;
    }
    /**
     * Tests to see whether two arrays have the same set of coords at a particular key. Coords
     * can be in any order.
     * @param array $array1 the first array.
     * @param array $array2 the second array.
     * @param string $key an array key.
     * @return bool whether the two arrays have the same set of coords (or lack of them)
     * for a given key.
     */
    public function arrays_same_at_key_integer(
            array $array1, array $array2, $key) {
        if (array_key_exists($key, $array1)) {
            $value1 = $array1[$key];
        } else {
            $value1 = '';
        }
        if (array_key_exists($key, $array2)) {
            $value2 = $array2[$key];
        } else {
            $value2 = '';
        }
        $coords1 = explode(';', $value1);
        $coords2 = explode(';', $value2);
        if (count($coords1) !== count($coords2)) {
            return false;
        } else if (count($coords1) === 0) {
            return true;
        } else {
            $valuesinbotharrays = $this->array_intersect_fixed($coords1, $coords2);
            return (count($valuesinbotharrays) == count($coords1));
        }
    }

    /**
     *
     * This function is a variation of array_intersect that checks for the existence of duplicate
     * array values too.
     * @author dml at nm dot ru (taken from comments on php manual)
     * @param array $array1
     * @param array $array2
     * @return bool whether array1 and array2 contain the same values including duplicate values
     */
    protected function array_intersect_fixed($array1, $array2) {
        $result = array();
        foreach ($array1 as $val) {
            if (($key = array_search($val, $array2, true)) !== false) {
                 $result[] = $val;
                 unset($array2[$key]);
            }
        }
        return $result;
    }


    public function get_validation_error(array $response) {
        if ($this->is_complete_response($response)) {
            return '';
        }
        return get_string('pleasedragatleastonemarker', 'qtype_ddmarker');
    }

    public function get_num_parts_right(array $response) {
        $chosenhits = $this->choose_hits($response);
        $divisor = max(count($this->rightchoices), $this->total_number_of_items_dragged($response));
        return array(count($chosenhits), $divisor);
    }

    /**
     * Choose hits to maximize grade where drop targets may have more than one hit and drop targets
     * can overlap.
     * @param array $response
     * @return array chosen hits
     */
    protected function choose_hits(array $response) {
        $allhits = $this->get_all_hits($response);
        $chosenhits = array();
        foreach ($allhits as $placeno => $hits) {
            foreach ($hits as $itemno => $hit) {
                $choice = $this->get_right_choice_for($placeno);
                $choiceitem = "$choice $itemno";
                if (!in_array($choiceitem, $chosenhits)) {
                    $chosenhits[$placeno] = $choiceitem;
                    break;
                }
            }
        }
        return $chosenhits;
    }
    public function total_number_of_items_dragged(array $response) {
        $total = 0;
        foreach ($this->choiceorder[1] as $choice) {
            $choicekey = $this->choice($choice);
            if (array_key_exists($choicekey, $response) && trim($response[$choicekey] !== '')) {
                $total += count(explode(';', $response[$choicekey]));
            }
        }
        return $total;
    }

    /**
     * Get's an array of all hits on drop targets. Needs further processing to find which hits
     * to select in the general case that drop targets may have more than one hit and drop targets
     * can overlap.
     * @param array $response
     * @return array all hits
     */
    protected function get_all_hits(array $response) {
        $hits = array();
        foreach ($this->places as $placeno => $place) {
            $rightchoice = $this->get_right_choice_for($placeno);
            $rightchoicekey = $this->choice($rightchoice);
            if (!array_key_exists($rightchoicekey, $response)) {
                continue;
            }
            $choicecoords = $response[$rightchoicekey];
            $coords = explode(';', $choicecoords);
            foreach ($coords as $itemno => $coord) {
                if (trim($coord) === '') {
                    continue;
                }
                $pointxy = explode(',', $coord);
                $pointxy[0] = round($pointxy[0]);
                $pointxy[1] = round($pointxy[1]);
                if ($place->drop_hit($pointxy)) {
                    if (!isset($hits[$placeno])) {
                        $hits[$placeno] = array();
                    }
                    $hits[$placeno][$itemno] = $coord;
                }
            }
        }
        // Reverse sort in order of number of hits per place (if two or more
        // hits per place then we want to make sure hits do not hit elsewhere).
        $sortcomparison = function ($a1, $a2){
            return (count($a1) - count($a2));
        };
        uasort($hits, $sortcomparison);
        return $hits;
    }

    public function get_right_choice_for($place) {
        $group = $this->places[$place]->group;
        foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
            if ($this->rightchoices[$place] == $choiceid) {
                return $choicekey;
            }
        }
        return null;
    }
    public function grade_response(array $response) {
        list($right, $total) = $this->get_num_parts_right($response);
        $fraction = $right / $total;
        return array($fraction, question_state::graded_state_for_fraction($fraction));
    }

    public function compute_final_grade($responses, $totaltries) {
        $maxitemsdragged = 0;
        $wrongtries = array();
        foreach ($responses as $i => $response) {
            $maxitemsdragged = max($maxitemsdragged,
                                                $this->total_number_of_items_dragged($response));
            $hits = $this->choose_hits($response);
            foreach ($hits as $place => $choiceitem) {
                if (!isset($wrongtries[$place])) {
                    $wrongtries[$place] = $i;
                }
            }
            foreach ($wrongtries as $place => $notused) {
                if (!isset($hits[$place])) {
                    unset($wrongtries[$place]);
                }
            }
        }
        $numtries = count($responses);
        $numright = count($wrongtries);
        $penalty = array_sum($wrongtries) * $this->penalty;
        $grade = ($numright - $penalty) / (max($maxitemsdragged, count($this->places)));
        return $grade;
    }
    public function clear_wrong_from_response(array $response) {
        $hits = $this->choose_hits($response);

        $cleanedresponse = array();
        foreach ($response as $choicekey => $coords) {
            $choice = (int)substr($choicekey, 1);
            $choiceresponse = array();
            $coordparts = explode(';', $coords);
            foreach ($coordparts as $itemno => $coord) {
                if (in_array("$choice $itemno", $hits)) {
                    $choiceresponse[] = $coord;
                }
            }
            $cleanedresponse[$choicekey] = join(';', $choiceresponse);
        }
        return $cleanedresponse;
    }
    public function get_wrong_drags(array $response) {
        $hits = $this->choose_hits($response);
        $wrong = array();
        foreach ($response as $choicekey => $coords) {
            $choice = (int)substr($choicekey, 1);
            if ($coords != '') {
                $coordparts = explode(';', $coords);
                foreach ($coordparts as $itemno => $coord) {
                    if (!in_array("$choice $itemno", $hits)) {
                        $wrong[] = $this->get_selected_choice(1, $choice)->text;
                    }
                }
            }
        }
        return $wrong;
    }


    public function get_drop_zones_without_hit(array $response) {
        $hits = $this->choose_hits($response);

        $nohits = array();
        foreach ($this->places as $placeno => $place) {
            $choice = $this->get_right_choice_for($placeno);
            if (!isset($hits[$placeno])) {
                $nohit = new stdClass();
                $nohit->coords = $place->coords;
                $nohit->shape = $place->shape->name();
                $nohit->markertext = $this->choices[1][$this->choiceorder[1][$choice]]->text;
                $nohits[] = $nohit;
            }
        }
        return $nohits;
    }

    public function classify_response(array $response) {
        $parts = array();
        $hits = $this->choose_hits($response);
        foreach ($this->places as $placeno => $place) {
            if (isset($hits[$placeno])) {
                $shuffledchoiceno = $this->get_right_choice_for($placeno);
                $choice = $this->get_selected_choice(1, $shuffledchoiceno);
                $parts[$placeno] = new question_classified_response(
                                                    $choice->no,
                                                    $choice->summarise(),
                                                    1 / count($this->places));
            } else {
                $parts[$placeno] = question_classified_response::no_response();
            }
        }
        return $parts;
    }

    public function get_correct_response() {
        $responsecoords = array();
        foreach ($this->places as $placeno => $place) {
            $rightchoice = $this->get_right_choice_for($placeno);
            if ($rightchoice !== null) {
                $rightchoicekey = $this->choice($rightchoice);
                $correctcoords = $place->correct_coords();
                if ($correctcoords !== null) {
                    if (!isset($responsecoords[$rightchoicekey])) {
                        $responsecoords[$rightchoicekey] = array();
                    }
                    $responsecoords[$rightchoicekey][] = join(',', $correctcoords);
                }
            }
        }
        $response = array();
        foreach ($responsecoords as $choicekey => $coords) {
            $response[$choicekey] = join(';', $coords);
        }
        return $response;
    }

    public function get_right_answer_summary() {
        $placesummaries = array();
        foreach ($this->places as $placeno => $place) {
            $shuffledchoiceno = $this->get_right_choice_for($placeno);
            $choice = $this->get_selected_choice(1, $shuffledchoiceno);
            $placesummaries[] = '{'.$place->summarise().' -> '.$choice->summarise().'}';
        }
        return join(', ', $placesummaries);
    }

    public function summarise_response(array $response) {
        $hits = $this->choose_hits($response);
        $goodhits = array();
        foreach ($this->places as $placeno => $place) {
            if (isset($hits[$placeno])) {
                $shuffledchoiceno = $this->get_right_choice_for($placeno);
                $choice = $this->get_selected_choice(1, $shuffledchoiceno);
                $goodhits[] = "{".$place->summarise()." -> ". $choice->summarise(). "}";
            }
        }
        if (count($goodhits) == 0) {
            return null;
        }
        return implode(', ', $goodhits);
    }

    public function get_random_guess_score() {
        return null;
    }
}

/**
 * Represents one of the choices (draggable markers).
 *
 * @copyright  2009 The Open University
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class qtype_ddmarker_drag_item {
    /** @var string Label for the drag item */
    public $text;

    /** @var int Number of the item */
    public $no;

    /** @var int Group of the item */
    public $infinite;

    /** @var int Number of drags */
    public $noofdrags;

    /**
     * Drag item object setup.
     *
     * @param string $label The label text of the drag item
     * @param int $no Which number drag item this is
     * @param bool $infinite True if the item can be used an unlimited number of times
     * @param int $noofdrags
     */
    public function __construct($label, $no, $infinite, $noofdrags) {
        $this->text = $label;
        $this->infinite = $infinite;
        $this->no = $no;
        $this->noofdrags = $noofdrags;
    }

    /**
     * Returns the group of this item.
     *
     * @return int
     */
    public function choice_group() {
        return 1;
    }

    /**
     * Creates summary text of for the drag item.
     *
     * @return string
     */
    public function summarise() {
        return $this->text;
    }
}
/**
 * Represents one of the places (drop zones).
 *
 * @copyright  2009 The Open University
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class qtype_ddmarker_drop_zone {
    /** @var int Group of the item */
    public $group = 1;

    /** @var int Number of the item */
    public $no;

    /** @var object Shape of the item */
    public $shape;

    /** @var array Location of the item */
    public $coords;

    /**
     * Setup a drop zone object.
     *
     * @param int $no Which number drop zone this is
     * @param int $shape Shape of the drop zone
     * @param array $coords Coordinates of the zone
     */
    public function __construct($no, $shape, $coords) {
        $this->no = $no;
        $this->shape = qtype_ddmarker_shape::create($shape, $coords);
        $this->coords = $coords;
    }

    /**
     * Creates summary text of for the drop zone
     *
     * @return string
     */
    public function summarise() {
        return get_string('summariseplaceno', 'qtype_ddmarker', $this->no);
    }

    /**
     * Indicates if the it coordinates are in this drop zone.
     *
     * @param array $xy Array of X and Y location
     * @return bool
     */
    public function drop_hit($xy) {
        return $this->shape->is_point_in_shape($xy);
    }

    /**
     * Gets the center point of this zone
     *
     * @return array X and Y location
     */
    public function correct_coords() {
        return $this->shape->center_point();
    }
}
