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

/**
 * Privacy Subsystem implementation for mod_assignment.
 *
 * @package    mod_assignment
 * @copyright  2018 Zig Tan <zig@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace mod_assignment\privacy;

use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\helper;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\userlist;

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

global $CFG;
require_once($CFG->dirroot . '/mod/assignment/lib.php');

/**
 * Implementation of the privacy subsystem plugin provider for mod_assignment.
 *
 * @copyright  2018 Zig Tan <zig@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class provider implements
    \core_privacy\local\metadata\provider,
    \core_privacy\local\request\plugin\provider,
    \core_privacy\local\request\user_preference_provider,
    \core_privacy\local\request\core_userlist_provider {

    /**
     * Return the fields which contain personal data.
     *
     * @param collection $collection a reference to the collection to use to store the metadata.
     * @return collection the updated collection of metadata items.
     */
    public static function get_metadata(collection $collection) : collection {
        $collection->add_database_table(
            'assignment_submissions',
            [
                'userid' => 'privacy:metadata:assignment_submissions:userid',
                'timecreated' => 'privacy:metadata:assignment_submissions:timecreated',
                'timemodified' => 'privacy:metadata:assignment_submissions:timemodified',
                'numfiles' => 'privacy:metadata:assignment_submissions:numfiles',
                'data1' => 'privacy:metadata:assignment_submissions:data1',
                'data2' => 'privacy:metadata:assignment_submissions:data2',
                'grade' => 'privacy:metadata:assignment_submissions:grade',
                'submissioncomment' => 'privacy:metadata:assignment_submissions:submissioncomment',
                'teacher' => 'privacy:metadata:assignment_submissions:teacher',
                'timemarked' => 'privacy:metadata:assignment_submissions:timemarked',
                'mailed' => 'privacy:metadata:assignment_submissions:mailed'
            ],
            'privacy:metadata:assignment_submissions'
        );

        // Legacy mod_assignment preferences from Moodle 2.X.
        $collection->add_user_preference('assignment_filter', 'privacy:metadata:assignmentfilter');
        $collection->add_user_preference('assignment_mailinfo', 'privacy:metadata:assignmentmailinfo');
        $collection->add_user_preference('assignment_perpage', 'privacy:metadata:assignmentperpage');
        $collection->add_user_preference('assignment_quickgrade', 'privacy:metadata:assignmentquickgrade');

        return $collection;
    }

    /**
     * Get the list of contexts that contain user information for the specified user.
     *
     * @param int $userid the userid.
     * @return contextlist the list of contexts containing user info for the user.
     */
    public static function get_contexts_for_userid(int $userid) : contextlist {
        $contextlist = new contextlist();

        $sql = "SELECT DISTINCT
                       ctx.id
                  FROM {context} ctx
                  JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmodule
                  JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
                  JOIN {assignment} a ON cm.instance = a.id
                  JOIN {assignment_submissions} s ON s.assignment = a.id
                 WHERE s.userid = :userid
                    OR s.teacher = :teacher";

        $params = [
            'contextmodule'  => CONTEXT_MODULE,
            'modulename'    => 'assignment',
            'userid'        => $userid,
            'teacher'       => $userid
        ];

        $contextlist->add_from_sql($sql, $params);

        return $contextlist;
    }

    /**
     * Get the list of users who have data within a context.
     *
     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
     */
    public static function get_users_in_context(userlist $userlist) {
        $context = $userlist->get_context();
        if ($context->contextlevel != CONTEXT_MODULE) {
            return;
        }

        $params = [
            'modulename' => 'assignment',
            'contextlevel' => CONTEXT_MODULE,
            'contextid' => $context->id
        ];
        $sql = "SELECT s.userid
                  FROM {assignment_submissions} s
                  JOIN {assignment} a ON s.assignment = a.id
                  JOIN {modules} m ON m.name = :modulename
                  JOIN {course_modules} cm ON a.id = cm.instance AND cm.module = m.id
                  JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
                 WHERE ctx.id = :contextid
        ";
        $userlist->add_from_sql('userid', $sql, $params);

        $sql = "SELECT s.teacher
                  FROM {assignment_submissions} s
                  JOIN {assignment} a ON s.assignment = a.id
                  JOIN {modules} m ON m.name = :modulename
                  JOIN {course_modules} cm ON a.id = cm.instance AND cm.module = m.id
                  JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
                 WHERE ctx.id = :contextid
        ";
        $userlist->add_from_sql('teacher', $sql, $params);
    }

    /**
     * Export personal data for the given approved_contextlist.
     * User and context information is contained within the contextlist.
     *
     * @param approved_contextlist $contextlist a list of contexts approved for export.
     */
    public static function export_user_data(approved_contextlist $contextlist) {
        if (empty($contextlist->count())) {
            return;
        }

        $user = $contextlist->get_user();

        foreach ($contextlist->get_contexts() as $context) {
            if ($context->contextlevel != CONTEXT_MODULE) {
                continue;
            }

            // Cannot make use of helper::export_context_files(), need to manually export assignment details.
            $assignmentdata = self::get_assignment_by_context($context);

            // Get assignment details object for output.
            $assignment = self::get_assignment_output($assignmentdata);
            writer::with_context($context)->export_data([], $assignment);

            // Check if the user has marked any assignment's submissions to determine assignment submissions to export.
            $teacher = (self::has_marked_assignment_submissions($assignmentdata->id, $user->id) == true) ? true : false;

            // Get the assignment submissions submitted by & marked by the user for an assignment.
            $submissionsdata = self::get_assignment_submissions_by_assignment($assignmentdata->id, $user->id, $teacher);

            foreach ($submissionsdata as $submissiondata) {
                // Default subcontext path to export assignment submissions submitted by the user.
                $subcontexts = [
                    get_string('privacy:submissionpath', 'mod_assignment')
                ];

                if ($teacher == true) {
                    if ($submissiondata->teacher == $user->id) {
                        // Export assignment submissions that have been marked by the user.
                        $subcontexts = [
                            get_string('privacy:markedsubmissionspath', 'mod_assignment'),
                            transform::user($submissiondata->userid)
                        ];
                    }
                }

                // Get assignment submission details object for output.
                $submission = self::get_assignment_submission_output($submissiondata);
                $itemid = $submissiondata->id;

                writer::with_context($context)
                    ->export_data($subcontexts, $submission)
                    ->export_area_files($subcontexts, 'mod_assignment', 'submission', $itemid);
            }
        }
    }

    /**
     * Stores the user preferences related to mod_assign.
     *
     * @param  int $userid The user ID that we want the preferences for.
     */
    public static function export_user_preferences(int $userid) {
        $context = \context_system::instance();
        $assignmentpreferences = [
            'assignment_filter' => [
                'string' => get_string('privacy:metadata:assignmentfilter', 'mod_assignment'),
                'bool' => false
            ],
            'assignment_mailinfo' => [
                'string' => get_string('privacy:metadata:assignmentmailinfo', 'mod_assignment'),
                'bool' => false
            ],
            'assignment_perpage' => [
                'string' => get_string('privacy:metadata:assignmentperpage', 'mod_assignment'),
                'bool' => false
            ],
            'assignment_quickgrade' => [
                'string' => get_string('privacy:metadata:assignmentquickgrade', 'mod_assignment'),
                'bool' => false
            ],
        ];
        foreach ($assignmentpreferences as $key => $preference) {
            $value = get_user_preferences($key, null, $userid);
            if ($preference['bool']) {
                $value = transform::yesno($value);
            }
            if (isset($value)) {
                writer::with_context($context)
                    ->export_user_preference('mod_assignment', $key, $value, $preference['string']);
            }
        }
    }

    /**
     * Delete all data for all users in the specified context.
     *
     * @param \context $context the context to delete in.
     */
    public static function delete_data_for_all_users_in_context(\context $context) {
        global $DB;

        if ($context->contextlevel == CONTEXT_MODULE) {
            // Delete all assignment submissions for the assignment associated with the context module.
            $assignment = self::get_assignment_by_context($context);
            if ($assignment != null) {
                $DB->delete_records('assignment_submissions', ['assignment' => $assignment->id]);

                // Delete all file uploads associated with the assignment submission for the specified context.
                $fs = get_file_storage();
                $fs->delete_area_files($context->id, 'mod_assignment', 'submission');
            }
        }
    }

    /**
     * Delete all user data for the specified user, in the specified contexts.
     *
     * @param approved_contextlist $contextlist a list of contexts approved for deletion.
     */
    public static function delete_data_for_user(approved_contextlist $contextlist) {
        global $DB;

        if (empty($contextlist->count())) {
            return;
        }

        $userid = $contextlist->get_user()->id;

        // Only retrieve assignment submissions submitted by the user for deletion.
        $assignmentsubmissionids = array_keys(self::get_assignment_submissions_by_contextlist($contextlist, $userid));
        $DB->delete_records_list('assignment_submissions', 'id', $assignmentsubmissionids);

        // Delete all file uploads associated with the assignment submission for the user's specified list of contexts.
        $fs = get_file_storage();
        foreach ($contextlist->get_contextids() as $contextid) {
            foreach ($assignmentsubmissionids as $submissionid) {
                $fs->delete_area_files($contextid, 'mod_assignment', 'submission', $submissionid);
            }
        }
    }

    /**
     * Delete multiple users within a single context.
     *
     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
     */
    public static function delete_data_for_users(approved_userlist $userlist) {
        global $DB;

        $context = $userlist->get_context();
        // If the context isn't for a module then return early.
        if ($context->contextlevel != CONTEXT_MODULE) {
            return;
        }
        // Fetch the assignment.
        $assignment = self::get_assignment_by_context($context);
        $userids = $userlist->get_userids();

        list($inorequalsql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
        $params['assignmentid'] = $assignment->id;

        // Get submission ids.
        $sql = "
            SELECT s.id
            FROM {assignment_submissions} s
            JOIN {assignment} a ON s.assignment = a.id
            WHERE a.id = :assignmentid
            AND s.userid $inorequalsql
        ";

        $submissionids = $DB->get_records_sql($sql, $params);
        list($submissionidsql, $submissionparams) = $DB->get_in_or_equal(array_keys($submissionids), SQL_PARAMS_NAMED);
        $fs = get_file_storage();
        $fs->delete_area_files_select($context->id, 'mod_assignment', 'submission', $submissionidsql, $submissionparams);
        // Delete related tables.
        $DB->delete_records_list('assignment_submissions', 'id', array_keys($submissionids));
    }

    // Start of helper functions.

    /**
     * Helper function to check if a user has marked assignment submissions for a given assignment.
     *
     * @param int $assignmentid The assignment ID to check if user has marked associated submissions.
     * @param int $userid       The user ID to check if user has marked associated submissions.
     * @return bool             If user has marked associated submissions returns true, otherwise false.
     * @throws \dml_exception
     */
    protected static function has_marked_assignment_submissions($assignmentid, $userid) {
        global $DB;

        $params = [
            'assignment' => $assignmentid,
            'teacher'    => $userid
        ];

        $sql = "SELECT count(s.id) as nomarked
                  FROM {assignment_submissions} s
                 WHERE s.assignment = :assignment
                   AND s.teacher = :teacher";

        $results = $DB->get_record_sql($sql, $params);

        return ($results->nomarked > 0) ? true : false;
    }

    /**
     * Helper function to return assignment for a context module.
     *
     * @param object $context   The context module object to return the assignment record by.
     * @return mixed            The assignment details or null record associated with the context module.
     * @throws \dml_exception
     */
    protected static function get_assignment_by_context($context) {
        global $DB;

        $params = [
            'modulename' => 'assignment',
            'contextmodule' => CONTEXT_MODULE,
            'contextid' => $context->id
        ];

        $sql = "SELECT a.id,
                       a.name,
                       a.intro,
                       a.assignmenttype,
                       a.grade,
                       a.timedue,
                       a.timeavailable,
                       a.timemodified
                  FROM {assignment} a
                  JOIN {course_modules} cm ON a.id = cm.instance
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
                  JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule
                 WHERE ctx.id = :contextid";

        return $DB->get_record_sql($sql, $params);
    }

    /**
     * Helper function to return assignment submissions submitted by / marked by a user and their contextlist.
     *
     * @param object $contextlist   Object with the contexts related to a userid to retrieve assignment submissions by.
     * @param int $userid           The user ID to find assignment submissions that were submitted by.
     * @param bool $teacher         The teacher status to determine if marked assignment submissions should be returned.
     * @return array                Array of assignment submission details.
     * @throws \coding_exception
     * @throws \dml_exception
     */
    protected static function get_assignment_submissions_by_contextlist($contextlist, $userid, $teacher = false) {
        global $DB;

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);

        $params = [
            'contextmodule' => CONTEXT_MODULE,
            'modulename' => 'assignment',
            'userid' => $userid
        ];

        $sql = "SELECT s.id as id,
                       s.assignment as assignment,
                       s.numfiles as numfiles,
                       s.data1 as data1,
                       s.data2 as data2,
                       s.grade as grade,
                       s.submissioncomment as submissioncomment,
                       s.teacher as teacher,
                       s.timemarked as timemarked,
                       s.timecreated as timecreated,
                       s.timemodified as timemodified
                  FROM {context} ctx
                  JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmodule
                  JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
                  JOIN {assignment} a ON cm.instance = a.id
                  JOIN {assignment_submissions} s ON s.assignment = a.id
                 WHERE (s.userid = :userid";

        if ($teacher == true) {
            $sql .= " OR s.teacher = :teacher";
            $params['teacher'] = $userid;
        }

        $sql .= ")";

        $sql .= " AND ctx.id {$contextsql}";
        $params += $contextparams;

        return $DB->get_records_sql($sql, $params);
    }

    /**
     * Helper function to retrieve assignment submissions submitted by / marked by a user for a specific assignment.
     *
     * @param int $assignmentid     The assignment ID to retrieve assignment submissions by.
     * @param int $userid           The user ID to retrieve assignment submissions submitted / marked by.
     * @param bool $teacher         The teacher status to determine if marked assignment submissions should be returned.
     * @return array                Array of assignment submissions details.
     * @throws \dml_exception
     */
    protected static function get_assignment_submissions_by_assignment($assignmentid, $userid, $teacher = false) {
        global $DB;

        $params = [
            'assignment' => $assignmentid,
            'userid' => $userid
        ];

        $sql = "SELECT s.id as id,
                       s.assignment as assignment,
                       s.numfiles as numfiles,
                       s.data1 as data1,
                       s.data2 as data2,
                       s.grade as grade,
                       s.submissioncomment as submissioncomment,
                       s.teacher as teacher,
                       s.timemarked as timemarked,
                       s.timecreated as timecreated,
                       s.timemodified as timemodified,
                       s.userid as userid
                  FROM {assignment_submissions} s
                 WHERE s.assignment = :assignment
                   AND (s.userid = :userid";

        if ($teacher == true) {
            $sql .= " OR s.teacher = :teacher";
            $params['teacher'] = $userid;
        }

        $sql .= ")";

        return $DB->get_records_sql($sql, $params);
    }

    /**
     * Helper function generate assignment output object for exporting.
     *
     * @param object $assignmentdata    Object containing assignment data.
     * @return object                   Formatted assignment output object for exporting.
     */
    protected static function get_assignment_output($assignmentdata) {
        $assignment = (object) [
            'name' => $assignmentdata->name,
            'intro' => $assignmentdata->intro,
            'assignmenttype' => $assignmentdata->assignmenttype,
            'grade' => $assignmentdata->grade,
            'timemodified' => transform::datetime($assignmentdata->timemodified)
        ];

        if ($assignmentdata->timeavailable != 0) {
            $assignment->timeavailable = transform::datetime($assignmentdata->timeavailable);
        }

        if ($assignmentdata->timedue != 0) {
            $assignment->timedue = transform::datetime($assignmentdata->timedue);
        }

        return $assignment;
    }

    /**
     * Helper function generate assignment submission output object for exporting.
     *
     * @param object $submissiondata    Object containing assignment submission data.
     * @return object                   Formatted assignment submission output for exporting.
     */
    protected static function get_assignment_submission_output($submissiondata) {
        $submission = (object) [
            'assignment' => $submissiondata->assignment,
            'numfiles' => $submissiondata->numfiles,
            'data1' => $submissiondata->data1,
            'data2' => $submissiondata->data2,
            'grade' => $submissiondata->grade,
            'submissioncomment' => $submissiondata->submissioncomment,
            'teacher' => transform::user($submissiondata->teacher)
        ];

        if ($submissiondata->timecreated != 0) {
            $submission->timecreated = transform::datetime($submissiondata->timecreated);
        }

        if ($submissiondata->timemarked != 0) {
            $submission->timemarked = transform::datetime($submissiondata->timemarked);
        }

        if ($submissiondata->timemodified != 0) {
            $submission->timemodified = transform::datetime($submissiondata->timemodified);
        }

        return $submission;
    }
}
