<?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/>.

/**
 * Contains the class for building the user's activity completion details.
 *
 * @package   core_completion
 * @copyright 2021 Jun Pataleta <jun@moodle.com>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

declare(strict_types = 1);

namespace core_completion;

use cm_info;
use completion_info;

/**
 * Class for building the user's activity completion details.
 *
 * @package   core_completion
 * @copyright 2021 Jun Pataleta <jun@moodle.com>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class cm_completion_details {
    /** @var completion_info The completion info instance for this cm's course. */
    protected $completioninfo = null;

    /** @var object The completion data. */
    protected $completiondata = null;

    /** @var cm_info The course module information. */
    protected $cminfo = null;

    /** @var int The user ID. */
    protected $userid = 0;

    /** @var bool Whether to return automatic completion details. */
    protected $returndetails = true;

    /** @var activity_custom_completion Activity custom completion object. */
    protected $cmcompletion = null;

    /**
     * Constructor.
     *
     * @param completion_info $completioninfo The completion info instance for this cm's course.
     * @param cm_info $cminfo The course module information.
     * @param int $userid The user ID.
     * @param bool $returndetails Whether to return completion details or not.
     */
    public function __construct(completion_info $completioninfo, cm_info $cminfo, int $userid, bool $returndetails = true) {
        $this->completioninfo = $completioninfo;
        // We need to pass wholecourse = true here for better performance. All the course's completion data for the current
        // logged-in user will get in a single query instead of multiple queries and loaded to cache.
        $this->completiondata = $completioninfo->get_data($cminfo, true, $userid);
        $this->cminfo = $cminfo;
        $this->userid = $userid;
        $this->returndetails = $returndetails;
        $cmcompletionclass = activity_custom_completion::get_cm_completion_class($this->cminfo->modname);
        if ($cmcompletionclass) {
            $this->cmcompletion = new $cmcompletionclass(
                $this->cminfo,
                $this->userid,
                $completioninfo->get_core_completion_state($cminfo, $userid)
            );
        }
    }

    /**
     * Fetches the completion details for a user.
     *
     * @return array An array of completion details for a user containing the completion requirement's description and status.
     * @throws \coding_exception
     */
    public function get_details(): array {
        if (!$this->is_automatic()) {
            // No details need to be returned for modules that don't have automatic completion tracking enabled.
            return [];
        }

        if (!$this->returndetails) {
            // We don't need to return completion details.
            return [];
        }

        $completiondata = $this->completiondata;
        $hasoverride = !empty($this->overridden_by());

        $details = [];

        // Completion rule: Student must view this activity.
        if (!empty($this->cminfo->completionview)) {
            if (!$hasoverride) {
                $status = COMPLETION_INCOMPLETE;
                if ($completiondata->viewed == COMPLETION_VIEWED) {
                    $status = COMPLETION_COMPLETE;
                }
            } else {
                $status = $completiondata->completionstate;
            }

            $details['completionview'] = (object)[
                'status' => $status,
                'description' => get_string('detail_desc:view', 'completion'),
            ];
        }

        // Completion rule: Student must receive a grade.
        if (!is_null($this->cminfo->completiongradeitemnumber)) {
            if (!$hasoverride) {
                $status = $completiondata->completiongrade ?? COMPLETION_INCOMPLETE;
            } else {
                $status = $completiondata->completionstate;
            }

            $details['completionusegrade'] = (object)[
                'status' => $status,
                'description' => get_string('detail_desc:receivegrade', 'completion'),
            ];

            if (!is_null($this->cminfo->completionpassgrade) && $this->cminfo->completionpassgrade) {
                $details['completionpassgrade'] = (object)[
                    'status' => $completiondata->passgrade ?? COMPLETION_INCOMPLETE,
                    'description' => get_string('detail_desc:receivepassgrade', 'completion'),
                ];
            }
        }

        if ($this->cmcompletion) {
            if (isset($completiondata->customcompletion)) {
                foreach ($completiondata->customcompletion as $rule => $status) {
                    $details[$rule] = (object)[
                        'status' => !$hasoverride ? $status : $completiondata->completionstate,
                        'description' => $this->cmcompletion->get_custom_rule_description($rule),
                    ];
                }

                $details = $this->sort_completion_details($details);
            }
        }

        return $details;
    }

    /**
     * Sort completion details in the order specified by the activity's custom completion implementation.
     *
     * @param array $details The completion details to be sorted.
     * @return array
     * @throws \coding_exception
     */
    protected function sort_completion_details(array $details): array {
        $sortorder = $this->cmcompletion->get_sort_order();
        $sorteddetails = [];

        foreach ($sortorder as $sortedkey) {
            if (isset($details[$sortedkey])) {
                $sorteddetails[$sortedkey] = $details[$sortedkey];
            }
        }

        // Make sure the sorted list includes all of the conditions that were set.
        if (count($sorteddetails) < count($details)) {
            $exceptiontext = get_class($this->cmcompletion) .'::get_sort_order() is missing one or more completion conditions.' .
                ' All custom and standard conditions that apply to this activity must be listed.';
            throw new \coding_exception($exceptiontext);
        }

        return $sorteddetails;
    }

    /**
     * Fetches the overall completion state of this course module.
     *
     * @return int The overall completion state for this course module.
     */
    public function get_overall_completion(): int {
        return (int)$this->completiondata->completionstate;
    }

    /**
     * Returns whether the overall completion state of this course module should be marked as complete or not.
     * This is based on the completion settings of the course module, so when the course module requires a passing grade,
     * it will only be marked as complete when the user has passed the course module. Otherwise, it will be marked as complete
     * even when the user has failed the course module.
     *
     * @return bool True when the module can be marked as completed.
     */
    public function is_overall_complete(): bool {
        $completionstates = [];
        if ($this->is_manual()) {
            $completionstates = [COMPLETION_COMPLETE];
        } else if ($this->is_automatic()) {
            // Successfull completion states depend on the completion settings.
            if (property_exists($this->completiondata, 'customcompletion') && !empty($this->completiondata->customcompletion)) {
                // If the module has any failed custom completion rule the state could be COMPLETION_COMPLETE_FAIL.
                $completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS];
            } else if (isset($this->completiondata->passgrade)) {
                // Passing grade is required. Don't mark it as complete when state is COMPLETION_COMPLETE_FAIL.
                $completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS];
            } else {
                // Any grade is required. Mark it as complete even when state is COMPLETION_COMPLETE_FAIL.
                $completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL];
            }
        }

        return in_array($this->get_overall_completion(), $completionstates);
    }

    /**
     * Whether this activity module has completion enabled.
     *
     * @return bool
     */
    public function has_completion(): bool {
        return $this->completioninfo->is_enabled($this->cminfo) != COMPLETION_DISABLED;
    }

    /**
     * Whether this activity module instance tracks completion automatically.
     *
     * @return bool
     */
    public function is_automatic(): bool {
        return $this->cminfo->completion == COMPLETION_TRACKING_AUTOMATIC;
    }

    /**
     * Whether this activity module instance tracks completion manually.
     *
     * @return bool
     */
    public function is_manual(): bool {
        return $this->cminfo->completion == COMPLETION_TRACKING_MANUAL;
    }

    /**
     * Fetches the user ID that has overridden the completion state of this activity for the user.
     *
     * @return int|null
     */
    public function overridden_by(): ?int {
        return isset($this->completiondata->overrideby) ? (int)$this->completiondata->overrideby : null;
    }

    /**
     * Checks whether completion is being tracked for this user.
     *
     * @return bool
     */
    public function is_tracked_user(): bool {
        return $this->completioninfo->is_tracked_user($this->userid);
    }

    /**
     * Determine whether to show the manual completion or not.
     *
     * @return bool
     */
    public function show_manual_completion(): bool {
        global $PAGE;

        if (!$this->is_manual()) {
            return false;
        }

        if ($PAGE->context->contextlevel == CONTEXT_MODULE) {
            // Manual completion should always be shown on the activity page.
            return true;
        } else {
            $course = $this->cminfo->get_course();
            if ($course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS) {
                return true;
            } else if ($this->cmcompletion) {
                return $this->cmcompletion->manual_completion_always_shown();
            }
        }

        return false;
    }

    /**
     * Completion state timemodified
     *
     * @return int timestamp
     */
    public function get_timemodified(): int {
        return (int)$this->completiondata->timemodified;
    }

    /**
     * Generates an instance of this class.
     *
     * @param cm_info $cminfo The course module info instance.
     * @param int $userid The user ID that we're fetching completion details for.
     * @param bool $returndetails  Whether to return completion details or not.
     * @return cm_completion_details
     */
    public static function get_instance(cm_info $cminfo, int $userid, bool $returndetails = true): cm_completion_details {
        $course = $cminfo->get_course();
        $completioninfo = new \completion_info($course);
        return new self($completioninfo, $cminfo, $userid, $returndetails);
    }
}
