<?php
/**
 * Lib of common functions for ned_teacher_tools, ned_student_menu, may be more later
 *
 * @package    block_ned_teacher_tools
 * @subpackage NED
 * @copyright  2020 NED {@link http://ned.ca}
 * @author     NED {@link http://ned.ca}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace block_ned_teacher_tools;
use block_ned_teacher_tools\shared_lib as SH;
use block_ned_teacher_tools\deadline_manager as DM;
use local_ned_controller\grade_info;
use local_ned_controller\marking_manager\marking_manager as MM;
use local_ned_controller\tt_config_manager as CM;

const PLUGIN = 'ned_teacher_tools';

const PLUGIN_TYPE = 'block';
const PLUGIN_NAME = PLUGIN_TYPE . '_' . PLUGIN;
const PLUGIN_URL = '/blocks/' . PLUGIN . '/';
const PLUGIN_CAPABILITY = PLUGIN_TYPE . '/' . PLUGIN . ':';
const PLUGIN_PATH = __DIR__;
const DIRROOT = __DIR__ . '/../..';

const TT = 'ned_teacher_tools';
const TTX = 'ned_teacher_tools_x';
const SM = 'ned_student_menu';

const PLUGIN_TT = 'block_' . TT;
const PLUGIN_TTX = 'block_' . TTX;
const PLUGIN_SM = 'block_' . SM;

const is_TT = PLUGIN_NAME == PLUGIN_TT;
const is_TTX = PLUGIN_NAME == PLUGIN_TTX;
const is_aTT = is_TT || is_TTX;
const is_SM = PLUGIN_NAME == PLUGIN_SM;

const ACTIVITYSETTING = '1';
const PASSGRADEPERCENT = '2';

const RESUBMISSIONS_ALL = 0;
const RESUBMISSIONS_MADE_BY_RM = 10;
const RESUBMISSIONS_MADE_BY_RM_CONTROLLED_NOT_BY_RM = 11;
const RESUBMISSIONS_MADE_BY_RM_CONTROLLED_BY_RM = 12;
const RESUBMISSIONS_MADE_NOT_BY_RM = 20;
const RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_NOT_BY_RM = 21;
const RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_BY_RM = 22;
const RESUBMISSIONS_KEYS = [
    RESUBMISSIONS_ALL,
    RESUBMISSIONS_MADE_BY_RM, RESUBMISSIONS_MADE_BY_RM_CONTROLLED_NOT_BY_RM, RESUBMISSIONS_MADE_BY_RM_CONTROLLED_BY_RM,
    RESUBMISSIONS_MADE_NOT_BY_RM, RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_NOT_BY_RM, RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_BY_RM
];

/**
 * Also note, that format grade "none" can have format as "/X" where X is max grade of activity
 * @see \block_ned_teacher_tools\get_formatted_grade_by_grade_item()
 */
const F_GRADE_NONE = '-';
const F_TIME_NONE = '-';

const SP_ACTION_REMOVESUBMISSION = 'removesubmissiondata';
const SP_ACTION_ALLOWRESUBMISSION = 'allowresubmission';
const SP_ACTION_ALLOWREVERT2DRAFT = 'allowungraded2draft';
const SP_ACTION_EXCLUDEGRADE = 'excludegrade';
const SP_ACTION_REFRESHGRADE = 'refreshgrade';
const SP_ACTIONS = [
    SP_ACTION_REMOVESUBMISSION, SP_ACTION_ALLOWRESUBMISSION, SP_ACTION_ALLOWREVERT2DRAFT, SP_ACTION_EXCLUDEGRADE, SP_ACTION_REFRESHGRADE
];
const SP_ADMIN_ACTIONS = [SP_ACTION_EXCLUDEGRADE, SP_ACTION_REFRESHGRADE];

const VIEW_USERS_SUSPENDED_NONE = 0;
const VIEW_USERS_SUSPENDED_ENROLLMENTS = 1;
const VIEW_USERS_SUSPENDED_ACCOUNTS = 2;
const VIEW_USERS_SUSPENDED_BOTH = 3;

const ROLE_OT = 'online-teacher';
const ROLE_CT = 'classroom-teacher';
const ROLE_STUDENT = 'student';

const GROUP_ALL = 0;
const GROUP_NONE = -1;
const GROUPS = [GROUP_ALL, GROUP_NONE];

const TAG_MIDTERM = 'Midterm point';
const TAG_FORMATIVE = 'Formative';
const TAG_SUMMATIVE =  'Summative';
const IMPORTANT_TAGS = [
    'midterm' => TAG_MIDTERM,
    'formative' => TAG_FORMATIVE,
    'summative' => TAG_SUMMATIVE,
];

require_once(DIRROOT . '/course/format/singleactivity/lib.php');
require_once(DIRROOT . '/lib/classes/user.php');
require_once(DIRROOT . '/lib/weblib.php');
require_once(PLUGIN_PATH . '/classes/activity_status.php');
require_once(PLUGIN_PATH . '/common_data_utils.php');

require_once(DIRROOT . '/mod/assign/locallib.php');

/**
 * @param \cm_info|\stdClass  $activity
 * @param                     $userid
 * @param bool                $showkicagrades
 * @param bool                $deadlinemanagerinstalled
 * @param null|\stdClass      $rm_config                       - {@see \local_ned_controller\shared\tt_and_sm::get_rm_config()}
 *                                                             {@see \local_ned_controller\shared\tt_and_sm::update_rm_config}
 * @param string              $mod_status
 * @param array               $tags
 * @param object|null         $mm_data
 * @param int|string          $timezone
 * @param bool|null           $cap_can_view_grades_before_midn - can or not user see grades before midnight
 * @param bool|null           $cap_can_view_sp_graders         - can or not user see grader's name
 * @param bool                $can_award_extension_after_last_activity
 *
 * @return array of \html_table_row
 */
function render_activity_row($activity, $userid=null, $showkicagrades=false,
        $deadlinemanagerinstalled=false, $rm_config=null, $mod_status='', $tags=[], $mm_data=null, $timezone=null,
        $cap_can_view_grades_before_midn=null, $cap_can_view_sp_graders=null, $can_award_extension_after_last_activity=true){
    /** @var \stdClass|\local_ned_controller\marking_manager\mm_data_by_user|null $mm_data */
    $userid = SH::get_userid_or_global($userid);
    $rows = [];
    $courseid = $activity->course;
    $kicagradingurl = null;
    if ($activity->modname === SH::QUIZ){
        if ($tags && in_array('KICA', $tags) && class_exists('\\local_kicaquizgrading\\helper')){
            $kicagradingurl = \local_kicaquizgrading\helper::get_kica_grader_link_from_cm($activity, $userid);
        }
    }
    $block_config = get_site_and_course_block_config($courseid, PLUGIN);
    $course_context = SH::ctx($courseid);
    $cap_can_view_grades_before_midn = $cap_can_view_grades_before_midn ?? SH::cap_can_view_grades_before_midn($course_context);
    $cap_can_view_sp_graders = $cap_can_view_sp_graders ??
        (SH::is_tt_exists() && has_capability('block/ned_teacher_tools:spcanseegrader', $course_context));
    $add_row_main_class = [];
    $offset_row = 1;
    if (!($block_config->showduedatecolumn ?? true)){
        $offset_row = 0;
    }
    $now = time();
    // some short functions
    $is_fgrade_none = function($fgrade){
        if (!is_string($fgrade)) return false;
        if (!isset($fgrade[0])){
            // normally shouldn't be such situation, as $fgrade should be none-empty string
            // note, that "0" - is not none grade
            return true;
        }
        return $fgrade === F_GRADE_NONE || $fgrade[0] == '/'; // "/X" - is also none grade
    };
    $format_submit_time = function($submit_time, $deadline_time=null, $draft=false, $final_deadline=null) use ($mm_data, $timezone){
        $now = time();
        $submit_time_f = [];
        if ($deadline_time && (!$submit_time && $now > $deadline_time)){
            $title = str('duedate');
            if ($draft){
                $submit_time_f[] = SH::icon_get_nge_by_status(SH::STATUS_DRAFT).' ';
            }

            if ($final_deadline && $now < $final_deadline){
                $submit_time_f[] = fa('', 'L', $title);
            } else {
                $submit_time_f[] = fa('fa-calendar-times-o', '', $title);
            }
        } elseif ($deadline_time && ($submit_time && $submit_time > $deadline_time)){
            $submit_time_f[] = SH::ned_date_tz($submit_time, $timezone, '') . ' ('.SH::str('latesubmission').')';
        } elseif ($draft){
            $submit_time_f[] = SH::icon_get_nge_by_status(SH::STATUS_DRAFT);
        } elseif (!$submit_time){
            $submit_time_f[] = SH::icon_get_nge_by_status(SH::STATUS_UNSUBMITTED);
        } else {
            $submit_time_f[] = SH::ned_date_tz($submit_time, $timezone,'');
        }

        if (is_aTT){
            $NGC = SH::$ned_grade_controller;
            $ngc_status = $mm_data->ngc_status ?? 0;
            if (($ngc_status == $NGC::ST_PAUSED || $ngc_status == $NGC::ST_OBSOLETED) && $mm_data->ngc_record){
                $submit_time_f[] = SH::link(SH::ngc_record_view_url($mm_data->ngc_record),
                    fa('fa-pause-circle-o text-warning', '', SH::$C::str($NGC::STATUSES[$ngc_status])));
            }
        }

        return empty($submit_time_f) ? F_TIME_NONE : SH::arr2str($submit_time_f);
    };
    $format_grade_time = function($grade_time, $is_last_row=true)
            use ($activity, $userid, $mm_data, $cap_can_view_grades_before_midn, $cap_can_view_sp_graders, $timezone){
        if (SH::grade_is_hidden_now_before_midn($activity, $userid, $mm_data->first_last_grade_time ?? null, $cap_can_view_grades_before_midn)){
            return F_GRADE_NONE;
        }

        if ($is_last_row && !empty($mm_data->first_last_grade_time)){
            $grade_time = SH::grade_get_shown_midn_grade_time($activity, $userid, $mm_data->first_last_grade_time, $cap_can_view_grades_before_midn);
            $fgrade_time = SH::ned_date_tz($grade_time, $timezone);

            // add "graded by" or "updated by"
            $add_to_grade_time = '';
            $show_update_time = !empty($mm_data->grade_time) && ($mm_data->grade_time - $grade_time) > MINSECS; // $mm_data->grade_time > $grade_time

            if ($cap_can_view_sp_graders){
                if (!empty($mm_data->graderid) && $mm_data->graderid != $userid){
                    $add_to_grade_time = SH::str($show_update_time ? 'updatedby' : 'gradedby');
                    $add_to_grade_time .= ' '.SH::q_user_link($mm_data->graderid, $activity->course);
                }
            }

            if ($show_update_time){
                if (empty($add_to_grade_time)){
                    $add_to_grade_time = SH::str('updated');
                }
                $add_to_grade_time .= ': '.SH::ned_date_tz($mm_data->grade_time, $timezone);
            }

            if (!empty($add_to_grade_time)){
                $fgrade_time .= SH::div('('.$add_to_grade_time.')', 'font-italic');
            }
        } else {
            $grade_time = SH::grade_get_shown_midn_grade_time($activity, $userid, $grade_time, $cap_can_view_grades_before_midn);
            $fgrade_time = SH::ned_date_tz($grade_time, $timezone);
        }

        return $fgrade_time;
    };
    $format_grade_raw = function($status, $f_grade, $formative=false, $mm_data=null)
        use ($activity, $userid, $cap_can_view_grades_before_midn){
        /** @var object|\local_ned_controller\marking_manager\mm_data_by_user $mm_data */
        if (empty($mm_data->excluded) && !$formative &&
            // pass if $f_grade is "/X", so don't use $is_fgrade_none() here
            ($f_grade != F_GRADE_NONE && $status != SH::STATUS_WAITINGFORGRADE) &&
            !SH::grade_is_hidden_now_before_midn($activity, $userid, $mm_data->first_last_grade_time ?? null, $cap_can_view_grades_before_midn))
        {
            return $f_grade;
        }

        if ($mm_data){
            return SH::icon_get_nge_by_mm($mm_data);
        }

        return SH::icon_get_nge_by_status($status);
    };
    $format_grade = function($status, $f_grade, $formative=false, $mm_data=null)
        use ($format_grade_raw, $activity, $userid, $kicagradingurl, $courseid, $cap_can_view_grades_before_midn){

        /** @var \local_ned_controller\marking_manager\mm_data_by_activity_user|object $mm_data */
        $NGC = SH::$ned_grade_controller;
        $postfix = '';

        if (!empty($mm_data->ngc_status) && !empty($mm_data->ngc_grade_type) &&
            !SH::grade_is_hidden_now_before_midn($activity, $userid, $mm_data->first_last_grade_time ?? null, $cap_can_view_grades_before_midn) &&
            ($mm_data->ngc_status == $NGC::ST_DONE || $mm_data->ngc_status == $NGC::ST_OBSOLETED)){

            if (!empty($mm_data->ngc_grade_change) && $mm_data->ngc_grade_type == $NGC::GT_DEDUCTION){
                $ngc_record = SH::$ned_grade_controller::get_ngc_record_by_mm_data($mm_data, $courseid);
                $postfix = SH::get_grade_deduction_postfix_by_ngc_or_reason($ngc_record);
            } elseif ($mm_data->ngc_grade_type == $NGC::GT_AWARD_ZERO){
                $zero_icon = SH::icon_get_nge($mm_data, SH::STATUS_NGC_ZERO);
                if ($mm_data->ngc_status == $NGC::ST_DONE){
                    return $zero_icon;
                }
                // if $NGC::ST_OBSOLETED
                $postfix = $zero_icon;
            }
        }

        $f_grade = $format_grade_raw($status, $f_grade, $formative, $mm_data);
        if ($mm_data->overridden ?? false){
            $f_grade .= fa('fa-lock', '', str('gradeoverridden'));
        }
        if (!empty($kicagradingurl)){
            $f_grade = link($kicagradingurl, $f_grade);
        }
        if (!empty($postfix)){
            $f_grade .= SH::HTML_SPACE.$postfix;
        }

        return $f_grade;
    };
    $check_classes = function($status, $add_row_main_class=[], $submit_time_raw=0, $mm_data=null){
        $g_class = ['activity-grade', $status];
        if ($submit_time_raw){
            $add_row_main_class[] = SH::STATUS_SUBMITTED;
        }
        if ($mm_data){
            if ($mm_data->unmarked ?? false){
                $add_row_main_class[] = SH::STATUS_UNMARKED;
            }
            if ($mm_data->shouldpass ?? false){
                // add green/red background to grade cell
                switch ($status){
                    case SH::STATUS_COMPLETED:
                    case SH::STATUS_GRADED_KICA_SUMMATIVE:
                    case SH::STATUS_GRADED_KICA_FORMATIVE:
                        $g_class[] = 'passing';
                        break;
                    case SH::STATUS_INCOMPLETED:
                    case SH::STATUS_INCOMPLETED_SUMMATIVE:
                    case SH::STATUS_INCOMPLETED_FORMATIVE:
                    case SH::STATUS_KICA_ZEROGRADE:
                    case SH::STATUS_NGC_ZERO:
                        $g_class[] = 'failing';
                        break;
                }
            }
        }
        return [$g_class, $add_row_main_class];
    };

    $main_tag = null;
    $important_tags = [];
    $params = [];
    $text_to_grade = '';
    $timer_span = '';
    $add_text_to_grade = function($last_attempt=true)
    use (&$mm_data, &$text_to_grade, &$activity, &$important_tags, &$timer_span, &$course_context, &$userid){
        $add_to_menu = [];
        $fa_icons = [
            SP_ACTION_REMOVESUBMISSION => 'fa-trash-o',
            SP_ACTION_ALLOWRESUBMISSION => 'fa-plus-square',
            SP_ACTION_ALLOWREVERT2DRAFT => 'fa-floppy-o',
            SP_ACTION_EXCLUDEGRADE => 'fa-first-order',
            SP_ACTION_REFRESHGRADE => 'fa-refresh',
        ];

        if (is_TT && $last_attempt){
            $url = clone(SH::page()->url);
            $url->param('cmid', $activity->id);

            foreach (SP_ACTIONS as $sp_action){
                if ($sp_action == SP_ACTION_EXCLUDEGRADE){
                    if (!isset($mm_data->gradepass) || !empty($mm_data->excluded)){
                        continue;
                    }
                } elseif ($sp_action == SP_ACTION_REFRESHGRADE) {
                    if (!isset($mm_data->gradepass) || !empty($mm_data->excluded) || !empty($mm_data->overridden)){
                        continue;
                    }
                } elseif ($activity->modname != SH::ASSIGN){
                    continue;
                }

                if ($sp_action == SP_ACTION_ALLOWRESUBMISSION && !in_array(TAG_SUMMATIVE, $important_tags)){
                    continue;
                }
                if ($sp_action == SP_ACTION_ALLOWREVERT2DRAFT && (!($mm_data->unmarked ?? false) || ($mm_data->draft ?? false))){
                    continue;
                }

                if (in_array($sp_action, SP_ADMIN_ACTIONS)){
                    if (!is_siteadmin()) continue;
                } else {
                    if (!SH::has_capability($sp_action, $course_context)) continue;
                }
                $url->param('action', $sp_action);
                $add_to_menu[] = [clone($url), [], $sp_action, $fa_icons[$sp_action] ?? null];
            }

            $text_to_grade .= SH::get_activity_menu($activity, $userid, $add_to_menu, true);
        }

        if (!empty($timer_span) && $last_attempt){
            $text_to_grade .= $timer_span;
        }

        if (empty($text_to_grade)){
            return '';
        }
        $res = SH::div($text_to_grade, 'text-to-grade');
        $text_to_grade = '';
        return $res;
    };

    if ($block_config->showformativesummativeicons ?? true) {
        if ($mm_data){
            foreach (IMPORTANT_TAGS as $tag_key => $tag){
                $is_tag = 'is_'.$tag_key;
                if ($mm_data->$is_tag ?? false){
                    $main_tag = $main_tag ?? $tag;
                    $important_tags[] = $tag;
                }
            }
        } else {
            foreach (IMPORTANT_TAGS as $tag){
                if (in_array($tag, $tags) !== false){
                    $main_tag = $main_tag ?? $tag;
                    $important_tags[] = $tag;
                }
            }
        }
    }

    if ($main_tag){
        $params = ['data-tags' => $main_tag, 'title' => $main_tag];
    }

    // deadline
    $finaldeadline = null;
    $dm_module = $deadlinemanagerinstalled ? DM::get_dmm_by_cm($activity, $courseid) : null;
    if ($mm_data){
        $finaldeadline = $mm_data->duedate ?? null;
    } elseif ($dm_module){
        $finaldeadline = $dm_module->get_user_final_deadline($userid);
    } else {
        $finaldeadline = SH::get_deadline_by_cm($activity, $userid, $courseid);
    }
    $text_2 = SH::ned_date_tz($finaldeadline, $timezone);
    if ($finaldeadline && $finaldeadline > $now){
        $timer_span = SH::span('', 'timer duedate-time',
            ['data-datetime' => $finaldeadline, 'title' => str('duedate')]);
    }

    // extension
    if (is_TT && !empty($dm_module) && !empty($mm_data)){
        [$use_extension, $numberofextensions, $can_add_extension, $showextensionicon] =
            DM::get_activity_extension_data($activity, $userid, null, null, $finaldeadline, true, false, $courseid);
        if ($use_extension){
            $text_2 .= SH::render_from_template('deadline_manager_user_extension_button', [
                'numberofextensions' => $numberofextensions,
                'showextensionicon' => $showextensionicon,
                'can_add_extension' => $can_add_extension && $can_award_extension_after_last_activity,
                'userid' => $userid,
                'cmid' => $activity->id,
            ]);
        }
    }

    // activity name & tags
    $activity_name_add = '';
    if ($main_tag == TAG_MIDTERM){
        $add_row_main_class[] = 'row-midterm-point';
    }

    if (isset($mm_data->gradepass) && ($mm_data->excluded ?? 0)){
        $add_row_main_class[] = SH::STATUS_EXCLUDED;
    }

    $lastactivitytag = '';
    if (in_array(SH::TAG_LAST_ACTIVITY, $tags)) {
        $lastactivitytag = SH::tag('span', SH::TAG_LAST_ACTIVITY,
            array('class' => 'badge badge-info ml-1'));
    }

    $output = row(array_fill(0, $offset_row + 4, ''));
    $output->cells[0] = cell($activity_name_add . SH::q_cm_student_link($activity, $userid, $courseid) . $lastactivitytag,
        'activity-name add-tag-icon', $params);

    if (!empty($offset_row)){
        $output->cells[1] = cell($text_2, 'activity-type');
    }

    // submissions
    $submission_info = new \local_ned_controller\mod_assign\assign_info($activity, $userid);
    if ($submission_info->exist && $submission_info->submissions_count > 0){
        if (empty($offset_row)){
            $output->cells[0]->rowspan =  $submission_info->submissions_count;
        } else {
            $output->cells[0]->rowspan = $output->cells[1]->rowspan = $submission_info->submissions_count;
        }
        // it's need to sorting, otherwise we can make just empty cells here
        if (empty($offset_row)){
            $hidden_cells = [cell($output->cells[0]->text, 'display-none')];}
        else {
            $hidden_cells = [cell($output->cells[0]->text, 'display-none'), cell($output->cells[1]->text, 'display-none')];
        }
        $submitted_sort = 0;
        $graded_time_sort = 0;
        $grade_sort = 0;
        $rm_is_on = $rm_config && isset($rm_config->assignments[$activity->id]) && $rm_config->assignments[$activity->id];
        foreach ($submission_info->submissions as $num => $submission){
            $grade_params = [];
            $add_row_class = $add_row_main_class;
            $add_row_attr = [];
            $is_last_row = $num == ($submission_info->submissions_count - 1);
            $is_first_row = $num == 0;

            if ($is_first_row){
                $row = $output;
                if (empty($offset_row)) {
                    $row->cells = [$output->cells[0]];
                } else {
                    $row->cells = [$output->cells[0], $output->cells[1]];
                }
            } else {
                $row = row($hidden_cells);
            }

            if ($is_last_row){
                $submit_time = $submission_info->last_submit_time;
                $grade_time = $submission_info->first_last_grade_time;
                $rawgrade = $submission_info->last_grade;
            } else {
                $submit_time = $submission->submitted ? $submission->submit_time : null;
                $grade_time = $submission->graded ? $submission->grade_time : null;
                $rawgrade = $submission->graded ? $submission->grade : null;
            }

            $deduction = (($mm_data->ngc_status ?? 0) == SH::$ned_grade_controller::ST_DONE) ? ($mm_data->ngc_grade_change ?? 0) : 0;
            [$g_percenta, $g_format, $contribution, $status, $gradetimecreated, $fgrade_time] =
                get_formatted_grade_by_grade_item($userid, $submission_info->grade_item, $showkicagrades, $rawgrade,
                    null, null, null, null, $is_last_row, $deduction);

            if ($is_last_row){
                [$submitted_sort, $graded_time_sort, $grade_sort] = SH::val2int_multi($submit_time, $grade_time, $g_percenta);
                $submit_time_f = $format_submit_time($submit_time, $finaldeadline, $submission->draft, $mm_data->cutoffdate ?? 0);
                $status = $mm_data ? SH::get_status_by_mm_data($mm_data) : $mod_status;
                [$g_class, $add_row_class] = $check_classes($status, $add_row_class, $submit_time, $mm_data);
            } else {
                $add_row_class[] = 'previous-attempt';
                $submit_time_f = SH::ned_date_tz($submit_time, $timezone);
                $g_class = '';
            }

            if (!$is_first_row){
                $add_row_class[] = 'resubmission-attempt';
                if (!$submit_time && $finaldeadline && $now > $finaldeadline){
                    $add_row_class[] = 'resubmission-due';
                }
            }

            if (($submit_time || $grade_time) && $is_fgrade_none($g_format)){
                if (!$is_last_row && isset($rawgrade) && $rawgrade != -1){
                    // prev submission grade is hidden
                    $grade_time_f = F_TIME_NONE;
                    $g_format = '';
                } elseif ($mm_data){
                    $grade_time_f = F_TIME_NONE;
                    $g_format = $format_grade($status, SH::icon_get_nge_by_status($status), false, $mm_data);
                } else {
                    $grade_time_f = SH::icon_get_nge_by_status(SH::STATUS_SUBMITTED);
                    $g_format = $format_grade($status, $grade_time_f, false, $mm_data);
                }
                $graded_time_sort = $grade_sort = 0;
            } else {
                $g_format = $format_grade($status, $g_format, $main_tag == TAG_FORMATIVE, $is_last_row ? $mm_data : null);
                $grade_time_f = $format_grade_time($grade_time, $is_last_row);
            }

            if ($rm_is_on){
                $resubmission_time_start = null;
                $ri_class = 'resubmission-info rm-i ';
                $add_button = false;
                $add_time = false;
                $add_remove_link = false;
                $show_rm = true;
                $title = '';
                if (!$is_last_row){
                    if ($submission_info->submissions[$num+1]->user_sign){
                        $ri_class .= 'rm-done';
                        $title = str('resubmission_legend:done');
                    } else {
                        $ri_class .= 'rm-free';
                        $title = str('resubmission_legend:free');
                    }
                } else {
                    if (!$submit_time && !$submission->draft && !$submission_info->last_grade_time){
                        if (!$is_first_row){
                            $ri_class = 'resubmission-info rm-remove';
                            $add_remove_link = true;
                        }
                    }

                    $resubmission_time_start = SH::grade_get_midn_grade_time($submission_info->first_last_grade_time);
                    // in there is no real grade, or user can't see it now
                    if (!$submission_info->first_last_grade_time || !$resubmission_time_start || $resubmission_time_start > $now){
                        // resubmission timer don't start yet
                        if (!$submit_time){
                            $ri_class = '';
                        } elseif (!$add_remove_link){
                            $ri_class .= 'rm-clock';
                            $title = str('resubmission_legend:clock');
                        }
                    } elseif ($rm_config->max_time && ($now > ($resubmission_time_start + $rm_config->max_time))){
                        // resubmission timer was ended
                        $ri_class .= 'rm-lock';
                        $title = str('resubmission_legend:lock');
                    } else {
                        // resubmission timer is now!
                        $can_ressubmit = $rm_config->can_ressubmit &&
                            !($rm_config->max_attempts && $rm_config->max_attempts <= $submission_info->user_signed_submissions_count);
                        if ($can_ressubmit){
                            $add_time = true;
                            $errors = $rm_config->pp_error;
                            if (!empty($errors)){
                                $ri_class .= 'rm-not-allowed';
                                $title = join('&#13;', $errors);
                            } else {
                                $ri_class .= 'rm-unlock';
                                $title = str('resubmit_title');
                                $add_button = true;
                            }
                        } else {
                            $show_rm = false;
                        }
                    }
                }

                if ($show_rm){
                    $add_text = '';
                    if (is_SM && $add_button){
                        $input_params = ['href' => new \moodle_url('/blocks/ned_student_menu/ask_to_resubmit.php', ['id' => $activity->id])];
                        $add_text .= SH::html_input('submit', str('resubmit'), $input_params, 'id_submitbutton');
                    }
                    if ($add_time && $resubmission_time_start){
                        $timer_span = SH::span('', 'timer resubmission-time',
                            ['data-resubmission_time_start' => $resubmission_time_start, 'title' => str('resubmit_time_title')]);
                    }
                    if (is_SM && $add_remove_link){
                        $add_text .= SH::link(['~/unused_resubmission_remove.php', ['cmid' => $activity->id, 'userid' => $userid]],
                            ' ', 'btn btn-primary fa fa-trash', ['title' => str('cancelresubmissionrequest')]);
                    }
                    $text_to_grade .= SH::div($add_text, $ri_class, ['title' => $title]);
                }
            }

            $row->cells[$offset_row + 1] = cell($submit_time_f, 'activity-submitted');
            $row->cells[$offset_row + 2] = cell($grade_time_f,'activity-graded');
            if (has_capability('block/ned_teacher_tools:canviewrapsreport', SH::ctx($courseid))) {
                list($similarityscore, $scores) = utils::render_copyleaks_score($activity, $userid, 'similarityscore', true);
                $row->cells[$offset_row + 3] = cell($similarityscore, '', []);
                list($aiscore, $scores) = utils::render_copyleaks_score($activity, $userid, 'aiscore', true);
                $row->cells[$offset_row + 4] = cell($aiscore, '', []);
                $row->cells[$offset_row + 5] = cell($g_format.$add_text_to_grade($is_last_row), $g_class, $grade_params);
            } else {
                $row->cells[$offset_row + 3] = cell($g_format.$add_text_to_grade($is_last_row), $g_class, $grade_params);
            }
            $row->attributes['class'] .= join(' ', $add_row_class);
            $row->attributes = array_merge($row->attributes, $add_row_attr);
            $rows[$num] = $row;
        }
        // add for all cells of one activity the same sort order (of last submission)
        foreach ($rows as $row){
            $row->cells[$offset_row + 1]->attributes['data-sort-value'] = $submitted_sort;
            $row->cells[$offset_row + 2]->attributes['data-sort-value'] = $graded_time_sort;
            $row->cells[$offset_row + 3]->attributes['data-sort-value'] = $grade_sort;
        }

    } else {
        [$g_percenta, $g_format, $contribution, $status, $g_create, $g_time] =
            get_formatted_grade($activity, $userid, null, $showkicagrades, $mm_data);

        if ($mm_data || !is_null($g_create)){
            $submit_time_raw = $mm_data->submit_time ?? ($g_create ?: 0);
            $submit_time_f = $format_submit_time($submit_time_raw, $finaldeadline, false, $mm_data->cutoffdate ?? 0);
            $status = $mm_data ? $mod_status : $status;
        } else {
            $submit_time_raw = 0;
            $submit_time_f = F_TIME_NONE;
        }

        [$g_class, $add_row_main_class] = $check_classes($status, $add_row_main_class, $submit_time_raw, $mm_data);
        $g_format = $format_grade($mod_status, $g_format, $main_tag == TAG_FORMATIVE, $mm_data);
        $grade_time_f = $format_grade_time($g_time);
        $output->cells[$offset_row + 1] = cell($submit_time_f, 'activity-submitted', ['data-sort-value' => (int)$submit_time_raw]);
        $output->cells[$offset_row + 2] = cell($grade_time_f, 'activity-graded', ['data-sort-value' => (int)$g_time]);
        if (has_capability('block/ned_teacher_tools:canviewrapsreport', SH::ctx($courseid))) {
            list($similarityscore, $scores) = utils::render_copyleaks_score($activity, $userid, 'similarityscore', true);
            $output->cells[$offset_row + 3] = cell($similarityscore, '', []);
            list($aiscore, $scores) = utils::render_copyleaks_score($activity, $userid, 'aiscore', true);
            $output->cells[$offset_row + 4] = cell($aiscore, '', []);
            $output->cells[$offset_row + 5] = cell($g_format . $add_text_to_grade(), $g_class, ['data-sort-value' => $g_percenta]);
        } else {
            $output->cells[$offset_row + 3] = cell($g_format . $add_text_to_grade(), $g_class, ['data-sort-value' => $g_percenta]);
        }
        $output->attributes['class'] .= join(' ', $add_row_main_class);
        $rows[0] = $output;
    }

    return $rows;
}

/**
 * @param                  $userid
 * @param                  $gradeitem
 * @param bool             $showkicagrades
 * @param null|\int|\float $finalgrade
 * @param \mixed           $reporttable
 * @param null             $gradetimecreated
 * @param null             $gradetime
 * @param \stdClass|\local_ned_controller\marking_manager\mm_data_by_user|null $mm_data
 * @param bool             $final_attempt
 * @param int              $deduction
 *
 * @return array [$gradepercentage, $gradeformatted, $contributiontocoursetotal, $status, $gradetimecreated, $gradetime]
 */
function get_formatted_grade_by_grade_item($userid, $gradeitem, $showkicagrades=false, $finalgrade=null, $reporttable=null,
    $gradetimecreated=null, $gradetime=null, $mm_data=null, $final_attempt=true, $deduction=0){

    $gradepercentage = 0;
    $gradeformatted = F_GRADE_NONE;
    $contributiontocoursetotal = '-';
    $status = 'notgraded';
    $default = [$gradepercentage, $gradeformatted, $contributiontocoursetotal, $status, $gradetimecreated, $gradetime];

    if (!$gradeitem) return $default;

    $grademax = $gradeitem->grademax;
    $userid = SH::get_userid_or_global($userid);
    if (!is_kica_exists()){
        $showkicagrades = false;
    }
    $config = get_config(is_aTT ? PLUGIN_NAME : PLUGIN_TT);
    $get_usual_grades = !$showkicagrades;
    $kicaitem = null;

    if ($showkicagrades) {
        $kica = SH::get_kica($gradeitem->courseid);
        if ($kica){
            $kicaitem = SH::ki_get_by_grade_item($gradeitem->id);
        }
        if ($kica && !empty($kicaitem->id)){
            $kicagrade = SH::kg_get_by_userid_itemid($userid, $kicaitem);
            if (!empty($kicagrade->id)){
                $finalgrade = $kicagrade->get_finalgrade();
                $default[4] = $gradetimecreated = $kicagrade->timecreated;
                if (isset($mm_data->first_last_grade_time)){
                    $gradetime = $mm_data->first_last_grade_time;
                } else {
                    $gradetime = $kicagrade->timemodified;
                }
                $default[5] = $gradetime;
            } else {
                $finalgrade = null;
                $default[4] = $gradetimecreated = null;
                $default[5] = $gradetime = null;
            }

            if (is_null($finalgrade)) return $default;

            $grademax = $kicaitem->knowledge + $kicaitem->inquiry + $kicaitem->communication + $kicaitem->application;
        } else {
            $get_usual_grades = true;
        }
    }

    if ($get_usual_grades) {
        if ($showkicagrades){
            if ($mm_data){
                $default[4] = $gradetimecreated = $mm_data->submit_time;
                $default[5] = $gradetime = $mm_data->first_last_grade_time;
                $finalgrade = $mm_data->finalgrade;
            } else {
                $grade = grade_info::get_grade_grade_s($gradeitem->id, $userid);
                if ($grade){
                    $default[4] = $gradetimecreated = $grade->timecreated;
                    $default[5] = $gradetime = $grade->timemodified;
                    $finalgrade = $grade->finalgrade;
                }
            }
        }

        if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
            $gradeformatted = F_GRADE_NONE;
        } else if ($gradeitem->gradetype == GRADE_TYPE_VALUE) {
            $default[1] = $gradeformatted =  $grademax > 0 ? '/' . round($grademax) : F_GRADE_NONE;
        }

        if (is_null($finalgrade)) return $default;

        if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
            if (!empty($gradeitem->scaleid)) {
                $scale = grade_info::get_scale_s($gradeitem->scaleid);
                $gradeval = (int)$finalgrade; // scales use only integers
                $scales = explode(",", $scale->scale);
                $gradeformatted = $gradeval ? $scales[($gradeval-1)] : 0;
                $gradepercentage = -$gradeval;
            }
        }

        if ($reporttable) {
            foreach ($reporttable as $item) {
                if (isset($item['contributiontocoursetotal'])
                    && (strpos($item['contributiontocoursetotal']['headers'], 'row_'.$gradeitem->id.'_'.$userid) !== false))
                    $contributiontocoursetotal = $item['contributiontocoursetotal']['content'];
            }
        }
    }

    // TODO Find the way to get KICA grades for previous submissions
    if ($showkicagrades && !$final_attempt) {
        $default[1] = '';
        return $default;
    }

    $show_as_kica = $showkicagrades && !$get_usual_grades;

    if ($show_as_kica || $gradeitem->gradetype == GRADE_TYPE_VALUE){
        if ($grademax > 0){
            $finalgrade_orig = null;
            if (!empty($kicagrade->id) && $kicagrade->has_deduction()){
                $finalgrade_orig = $kicagrade->get_original_finalgrade();
            } elseif ($deduction){
                $finalgrade_orig = $finalgrade;
                $finalgrade = $finalgrade_orig * (1 - $deduction/100);
            }

            if (!is_null($finalgrade_orig)){
                $finalgrade_orig = SH::tag('del', $finalgrade_orig, 'dimmed_text') . ' ';
            } else {
                $finalgrade_orig = '';
            }

            $gradepercentage = round(($finalgrade / $grademax) * 100);
            $gradeformatted = $finalgrade_orig . round($finalgrade) . '/' . round($grademax) . ' (' . $gradepercentage . '%)';
        } else {
            $gradepercentage = 1;
            $gradeformatted = F_GRADE_NONE;
        }

        if ($config->passgrade == ACTIVITYSETTING) {
            if ($gradeitem->gradepass > 0) {
                if ($gradeitem->gradepass <= $finalgrade) {
                    $status = 'passing';
                } else {
                    $status = 'failing';
                }
            };
        } else if ($config->passgrade == PASSGRADEPERCENT) {
            if ($config->passgradepercent <= $gradepercentage) {
                $status = 'passing';
            } else {
                $status = 'failing';
            }
        }
    }

    if ($contributiontocoursetotal == '0.00 %') {
        $contributiontocoursetotal = '-';
    }
    return [$gradepercentage, $gradeformatted, $contributiontocoursetotal, $status, $gradetimecreated, $gradetime];
}

/**
 * @param \cm_info|\stdClass $mod
 * @param int                $userid
 * @param null               $reporttable
 * @param bool               $showkicagrades
 * @param \stdClass|\local_ned_controller\marking_manager\mm_data_by_user|null $mm_data
 *
 * @return array [$gradepercentage, $gradeformatted, $contributiontocoursetotal, $status, $gradetimecreated, $gradetime]
 */
function get_formatted_grade($mod, $userid=0, $reporttable=null, $showkicagrades=false, $mm_data=null) {
    global $USER;

    if (!$userid) {
        $userid = $USER->id;
    }
    $gradetimecreated = $gradetime = $finalgrade = null;
    $gradeitem = grade_info::get_grade_item_by_mod_s($mod);

    if ($gradeitem){
        if (!$showkicagrades){
            if ($mm_data){
                $gradetimecreated = $mm_data->submit_time;
                $gradetime = $mm_data->first_last_grade_time ?? $mm_data->grade_time;
                $finalgrade = $mm_data->finalgrade;
            } else {
                $grade = grade_info::get_grade_grade_s($gradeitem->id, $userid);
                if ($grade){
                    $gradetimecreated = $grade->timecreated;
                    $gradetime = $grade->timemodified;
                    $finalgrade = $grade->finalgrade;
                }
            }
        }
    }

    if (isset($gradeitem->id) && !is_null($gradeitem->id)) {
        [$gradepercentage, $gradeformatted, $contributiontocoursetotal, $status, $gradetimecreated, $gradetime] =
            get_formatted_grade_by_grade_item($userid, $gradeitem, $showkicagrades, $finalgrade, $reporttable, $gradetimecreated, $gradetime, $mm_data);
        return array($gradepercentage, $gradeformatted, $contributiontocoursetotal, $status, $gradetimecreated, $gradetime);
    }
    return array(null, null, null, null, null, null);
}

/**
 * @param $courseid
 * @param $userid
 *
 * @return \cm_info[]
 */
function get_gradable_activities($courseid, $userid) {
    static $_data = [];

    if (!isset($_data[$courseid][$userid])){
        $modinfo = get_fast_modinfo($courseid, $userid);
        $activities = $modinfo->get_cms();

        foreach ($activities as $key => $mod) {
            $modulecontext = \context_module::instance($mod->id);
            if (!$mod->visible && !has_capability('moodle/course:viewhiddenactivities', $modulecontext, $userid) ||
                !has_capability('mod/assign:submit', $modulecontext, $userid)) {
                unset($activities[$key]);
                continue;
            }
            $instance = get_module_db($mod->course, $mod->modname, $mod->instance);
            if (!$instance || (($mod->modname == 'forum') && !($instance->assessed > 0))) {
                unset($activities[$key]);
                continue;
            }
            if (!$item = get_grade_item($courseid, $mod->modname, $mod->instance)) {
                unset($activities[$key]);
                continue;
            }
        }
        $_data[$courseid][$userid] = $activities;
    }

    return $_data[$courseid][$userid];
}

/**
 * @param $assignid
 *
 * @return bool
 * @throws \dml_exception
 */
function is_offline_assign($assignid) {
    global $DB;

    $sql = /** @lang text */
        "SELECT 1
              FROM {assign_plugin_config} pc
             WHERE pc.assignment = ?
               AND pc.subtype = 'assignsubmission' 
               AND pc.value = 1 
               AND pc.plugin <> 'comments'";

    return !$DB->record_exists_sql($sql, array($assignid));
}

/**
 * @param $mod
 * @param $userid
 *
 * @return bool
 * @throws \dml_exception
 */
function assignment_status($mod, $userid) {
    global $CFG, $DB, $SESSION;

    require_once($CFG->dirroot . '/mod/assign/locallib.php');

    if (isset($SESSION->completioncache)) {
        unset($SESSION->completioncache);
    }

    if ($mod->modname == 'assignment') {
        return false;
    } else if ($mod->modname == SH::ASSIGN) {
        if (!($assignment = get_module_db($mod->course, $mod->modname, $mod->instance))) {
            return false;
        }

        $offlineassignment = is_offline_assign($assignment->id);
        $submissions = $DB->get_records('assign_submission', [
            'assignment' => $assignment->id,
            'userid'     => $userid
        ], 'attemptnumber DESC', '*', 0, 1);
        if (empty($submissions)){
            $submission = new \stdClass();
            $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
            $submission->timemodified = 0;
            $submission->attemptnumber = 0;
            $attemptnumber = 0;
        } else {
            $submission = reset($submissions);
            $attemptnumber = $submission->attemptnumber;
            if (($submission->status == 'reopened') && ($submission->attemptnumber > 0)) {
                $attemptnumber = $submission->attemptnumber - 1;
            }
        }

        $submissionisgraded = $DB->get_records('assign_grades',
            ['assignment' => $assignment->id, 'userid' => $userid, 'attemptnumber' => $attemptnumber],
            'attemptnumber DESC', '*', 0, 1);
        if ($submissionisgraded){
            $submissionisgraded = reset($submissionisgraded);
            if ($submissionisgraded->grade > -1) {
                if (($submission->timemodified > $submissionisgraded->timemodified)
                    || ($submission->attemptnumber > $submissionisgraded->attemptnumber)) {
                    $graded = false;
                } else {
                    $graded = true;
                }
            } else {
                $graded = false;
            }
        } else {
            [$gradepercentage, $gradeformatted, $contributiontocoursetotal, $status] = get_formatted_grade($mod, 0);
            if ($status !== 'notgraded'){
                $graded = true;
            } else {
                $graded = false;
            }
        }

        if ($submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT){
            if ($graded){
                return SH::STATUS_SUBMITTED;
            } else {
                return SH::STATUS_DRAFT;
            }
        } elseif ($submission->status == ASSIGN_SUBMISSION_STATUS_REOPENED){
            return SH::STATUS_SUBMITTED;
        } elseif ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED){
            if ($graded) {
                return SH::STATUS_SUBMITTED;
            } else {
                return SH::STATUS_WAITINGFORGRADE;
            }
        } elseif ($submission->status == ASSIGN_SUBMISSION_STATUS_NEW){
            if ($graded){
                if ($offlineassignment){
                    return 'offlinegraded';
                } else {
                    return 'gradeoverride';
                }
            }
        }
    }

    return false;
}

/**
 * @param $itemid
 * @param $userid
 *
 * @return bool|float
 * @throws \dml_exception
 */
function gradebook_grade ($itemid, $userid) {
    if ($grade = get_grade_grade ($userid, $itemid)) {
        return $grade->finalgrade;
    } else {
        return false;
    }
}

/**
 * Return config for block on course page by course id
 * Don't use it in a course loop!
 *
 * @param        $course_id
 * @param string $plugin_name
 *
 * @return bool|\stdClass
 */
function get_block_config($course_id, $plugin_name='ned_student_menu'){
    return SH::get_block_config($course_id, $plugin_name);
}

/**
 * Save config of block for course page by course id
 * Don't use it in a course loop!
 *
 * @param        $course_id
 * @param string $plugin_name
 * @param        $config
 *
 * @return bool
 */
function save_block_config($course_id, $plugin_name='ned_student_menu', $config=null){
    return SH::save_block_config($course_id, $plugin_name, $config);
}

/**
 * Get block config for course, based on site block config and course block settings
 *
 * @param        $course_id
 * @param string $plugin_name
 *
 * @return object
 */
function get_site_and_course_block_config($course_id, $plugin_name='ned_teacher_tools_x'){
    return SH::get_site_and_course_block_config($course_id, $plugin_name);
}


/**
 * Return div block with activity links to Student progress page
 *
 * @param array|int[]     $activity_stats - [$completed, $incompleted, $saved, $notattempted, $waitingforgrade]
 * @param \moodle_url|string $base_url
 * @param array $url_params
 * @param int $fs_type
 *
 * @return string
 */
function get_activity_menu($activity_stats, $base_url, $url_params, $fs_type=USUAL_ACTIVITY){
    [$completed, $incompleted, $saved, $notattempted, $waitingforgrade] = $activity_stats;

    $link = function($url_status, $number_activity, $key_activity=null) use ($base_url, $url_params, $fs_type){
        $key_activity = $key_activity ? $key_activity : $url_status;
        $text = $number_activity .' '. SH::str($key_activity);
        return SH::link([$base_url, array_merge($url_params, ['show' => $url_status, 'fs_type' => $fs_type])], $text);
    };

    $div = function($status='', $num=0, $url_status='') use ($link){
        $url_status = empty($url_status) ? $status : $url_status;
        $img = empty($status) ? '' : SH::span(SH::icon_get_nge_by_status($status), 'mr-2');
        $text = $link($url_status, $num, $status);

        return SH::div($img . $text, 'ned-activity-link');
    };

    $content = '';

    // Completed.
    if (!is_null($completed)) {
        switch ($fs_type){
            case FORMATIVE_ACTIVITY:
                $st = SH::STATUS_GRADED_KICA_FORMATIVE; break;
            case SUMMATIVE_ACTIVITY:
                $st = SH::STATUS_GRADED_KICA_SUMMATIVE; break;
            default:
                $st = SH::STATUS_COMPLETED; break;
        }
        $content .= $div($st, $completed);
    }

    // Incompleted.
    if (!is_null($incompleted)){
        switch ($fs_type){
            case FORMATIVE_ACTIVITY:
                $st = SH::STATUS_INCOMPLETED_FORMATIVE; break;
            case SUMMATIVE_ACTIVITY:
                $st = SH::STATUS_INCOMPLETED_SUMMATIVE; break;
            default:
                $st = SH::STATUS_INCOMPLETED; break;
        }
        $content .= $div($st, $incompleted);
    }

    // Draft.
    if (!empty($saved)){
        $content .= $div(SH::STATUS_DRAFT, $saved);
    }

    // Not Attempted.
    if (!is_null($notattempted)){
        $content .= $div(SH::STATUS_NOTATTEMPTED, $notattempted);
    }
    // Waiting for grade.
    if (!is_null($waitingforgrade)){
        switch ($fs_type){
            case FORMATIVE_ACTIVITY:
                $st = SH::STATUS_UNGRADED_KICA_FORMATIVE; break;
            case SUMMATIVE_ACTIVITY:
                $st = SH::STATUS_UNGRADED_KICA_SUMMATIVE; break;
            default:
                $st = SH::STATUS_WAITINGFORGRADE; break;
        }
        $content .=  $div($st, $waitingforgrade);
    }

    return $content;
}

/**
 * Create tag ulr-select
 *
 * @param $url
 * @param $tags
 * @param $tag
 *
 * @return string
 */
function tags_print_course_menu($url, $tags, $tag){
    global $OUTPUT;

    $def_tags = ['0' => str('alltags'), '-1' => str('nonetags')];
    ksort($tags);
    $select = new \single_select($url, 'tag', $def_tags + $tags, $tag, null, 'selecttag');
    $select->label = \html_writer::tag('i','',
        ['class' => 'fa fa-tags', 'title' => str('choosetag')]);
    $output = \html_writer::div($OUTPUT->render($select), 'groupselector tag-selector');

    return $output;
}

/**
 * Render student progress page, return nothing
 *  none capability checks, do it before, if you need
 * @param        $course
 * @param        $user
 * @param        $show
 * @param        $fs_type
 * @param null   $groupid
 * @param null   $tag
 * @param string $studentselector
 * @param string $extra
 *
 * @throws \coding_exception
 * @throws \dml_exception
 * @throws \moodle_exception
 */
function render_student_progress_page($course, $user, $show, $fs_type, $groupid=null, $tag=null, $studentselector='', $extra=''){
    global $OUTPUT, $PAGE;

    if (!$course){
        SH::print_error('There is wrong course!');
    }

    $school_time_zone = null;
    $school = SH::get_user_school($user);
    if (class_exists('\local_schoolmanager\school') && $school instanceof \local_schoolmanager\school){
        $timezone = $school_time_zone = $school->get_timezone();
    } elseif (!empty($school->timezone)){
        $timezone = $school_time_zone = $school->timezone;
    } elseif (!empty($user->timezone)){
        $timezone = $user->timezone;
    } else {
        $timezone = SH::NED_TIMEZONE;
    }

    $courseid = $course->id;
    $userid = $user ? $user->id : null;
    $now = time();
    $show_keys = [
        activity_status::STATUS_ALL, activity_status::STATUS_COMPLETED, activity_status::STATUS_INCOMPLETED,
        activity_status::STATUS_DRAFT, activity_status::STATUS_NOTATTEMPTED, activity_status::STATUS_WAITINGFORGRADE
    ];

    $check_show = isset_in_list($show_keys, $show, false);
    if (!$check_show){
        $show = isset_in_list($show_keys, activity_status::get_category_status($show), activity_status::STATUS_ALL);
    }

    $fs_type = isset_in_list([SUMMATIVE_ACTIVITY, FORMATIVE_ACTIVITY], $fs_type, USUAL_ACTIVITY);
    $add_body_class = 'ned-student-progress path-blocks-NED';

    $base_url = PLUGIN_URL . 'student_progress.php';
    $base_url_param = ['courseid' => $courseid, 'show' => $show, 'fs_type' => $fs_type];
    $cmid = 0;

    if(is_aTT){
        $base_url_param = array_merge($base_url_param, ['group' => $groupid, 'user' => $userid, 'tag' => $tag, 'prevpage'=>'student_progress']);
        $cmid = optional_param('cmid', 0, PARAM_INT);
    }

    $called_from_somewhere = $PAGE->has_set_url();
    $result = '';

    $print_result = function ($result) use ($OUTPUT, $course, $called_from_somewhere, $extra) {
        if (!$called_from_somewhere){
            echo $OUTPUT->header();
        }

        echo $extra;
        echo \html_writer::div($result,'', ['id' => 'mark-interface']);

        if (!$called_from_somewhere){
            echo $OUTPUT->footer($course);
        }
    };

    // Page settings
    if (!$called_from_somewhere){
        $PAGE->set_url($base_url, $base_url_param);
        $PAGE->navbar->add(str('studentprogress'),
            new \moodle_url($base_url, array_diff_key($base_url_param, ['show' => 0, 'fs_type' => 0, 'tag' => 0])));
        if (is_aTT && $userid){
            $PAGE->navbar->add(fullname($user), new \moodle_url('/user/view.php', ['id' => $userid, 'course' => $courseid]));
        }
        $title = str('studentprogress');
        $PAGE->set_title($title . '');
        $PAGE->set_heading($course->fullname . ': ' . $title);
        $PAGE->add_body_class($add_body_class);
    } else {
        $result .= \html_writer::script("window.document.body.className += ' $add_body_class';");
    }

    if (!$userid || $groupid == GROUP_NONE){
        $result .= \html_writer::div(str('noactivity'), 'no-activity');
        $print_result($result);
        return;
    }

    // should be checked ASAP after PAGE is ready, as we may redirected from here
    _student_progress_page_action($courseid, $userid);
    _student_progress_page_scripts();

    // Prepare data
    $mm_data_list = [];
    $get_mm_data = function($key) use (&$mm_data_list, $userid){
        return $mm_data_list[$key][$userid] ?? null;
    };
    grade_info::prepare_s($courseid, $userid);
    \local_ned_controller\mod_assign\assign_info::prepare_submission_info_records($courseid, $userid);
    $sm_config = get_site_and_course_block_config($courseid, SM);
    $tt_config = get_site_and_course_block_config($courseid, TT);

    $block_config = is_aTT ? $tt_config : $sm_config;
    $rm_config = SH::get_rm_config_by_courseid($courseid, $tt_config);

    $deadlinemanagerinstalled = !empty(\core_plugin_manager::instance()->get_plugin_info(is_aTT ? PLUGIN_NAME : PLUGIN_TT));
    $kica = get_kica_if_exists($courseid);
    $participation_power = $summative_score = $formative_score = $divided_score = $submissions_count = $max_course_attempts = null;
    $pp_status = '';
    $rm_check_pp = !empty($rm_config->check_pp);
    $show_pp = $tt_config->participationpower_option ?? false;
    $cap_can_view_grades_before_midn = SH::cap_can_view_grades_before_midn($course);
    $cap_can_view_sp_graders = SH::is_tt_exists() && has_capability('block/ned_teacher_tools:spcanseegrader', SH::ctx($courseid));

    $table = new \html_table();
    $table->id = 'datatable';
    $table->attributes = ['class' => 'activity-table student-progress'];

    $MM = MM::get_MM_by_params(['course' => $course, 'set_students' => [$userid => $user]]);
    $filter = [MM::ST_ALL, MM::BY_ACTIVITY, MM::BY_USER, MM::GET_TAGS, MM::USE_DEADLINE, MM::USE_KICA];
    $MM->filter_hide_grades_before_midn_if_need($filter);
    $mm_data_list = $MM->get_data_all($filter);
    if (is_aTT){
        $activities = SH::get_important_activities_by_users_and_compare_list($courseid, null, $userid, $mm_data_list, true);
    } else {
        $activities = SH::get_important_activities_by_compare_list($courseid, $userid, $mm_data_list, true);
    }

    $dm_entity = DM::get_dm_entity($course, $groupid, $user);
    $can_award_extension_after_last_activity = $dm_entity->can_award_extension_after_last_activity();

    // user activity status
    $activities_info = new activity_status();
    if (!empty($activities)) {
        $activities_info->check_activities_list($activities, $userid, false, $kica, $mm_data_list);
        switch($fs_type){
            case FORMATIVE_ACTIVITY:
                $current_info = $activities_info->get_formative_activity_status();
                break;
            case SUMMATIVE_ACTIVITY:
                $current_info = $activities_info->get_summative_activity_status();
                break;
            default:
                $current_info = $activities_info;
        }

        if ($rm_check_pp || $show_pp){
            [$summative_score, $formative_score, $participation_power, $divided_score, $text_status] = $activities_info->get_full_pp_data();
            $pp_status = 'pp-' . $text_status;
        }
    } else {
        $current_info = $activities_info;
    }

    // Create menu section & options which depend on $rm_config

    $is_view_grade = true;

    $course_grade = SH::get_course_grade($courseid, $userid);
    $general_info = '';
    if (is_aTT) {
        $plugincfg = get_config(PLUGIN_NAME);
        /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
        $course_users_status = new \block_ned_teacher_tools\course_users_status($courseid);
        $users_statuses = $course_users_status->get_users_statuses($userid);
        $enable_course_status_controller_on_this_course = $course_users_status->enable_on_course();
        $course_status_controller = $plugincfg->enablecoursecompletionstatuscontroller && $enable_course_status_controller_on_this_course;
        if ($course_status_controller && !is_siteadmin()) {
            if (array_key_exists($userid, $users_statuses) && !empty($users_statuses[$userid]) && !empty($users_statuses[$userid]->status)) {
                /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
                $is_view_grade = \block_ned_teacher_tools\get_check_permission_view_grade($courseid, $users_statuses[$userid]->status);
            }
        }

        $user_link = link(['/user/view.php', ['id' => $userid, 'course' => $courseid]], fullname($user), 'gi-value student');
        $general_info .= \html_writer::div(
            \html_writer::span(str('student'), 'gi-label') .
            SH::get_profile_with_menu_flag($userid, $courseid, $user_link)
        );
    }

    // Grade as link to gradebook
    if ($sm_config->replacewithkicagrade && utils::kica_gradebook_enabled($courseid)) {
        $gradebook_url = new \moodle_url('/local/kica/grade_user.php', ['courseid' => $courseid, 'group' => $groupid, 'participant' => $userid]);
    } else {
        $gradebook_url = new \moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'group' => $groupid, 'userid' => $userid]);
    }
    if ($is_view_grade) {
        $general_info .= \html_writer::div(
            link($gradebook_url,
                \html_writer::span(str('currentgrade'), 'gi-label') .
                \html_writer::span($course_grade, 'gi-value'), '', ['title' => str('opensmth', str('gradebook'))]
            )
        );
    } else {
        $general_info .= \html_writer::div(\html_writer::span(str('currentgrade'), 'gi-label') .
            \html_writer::span(fa('tt-status-icon tt-si-waitingfinalgrade'), 'gi-value'), 'review-modal-message-text'
        );
    }

    if ($rm_config || $show_pp){
        if ($rm_config){
            $submissions_count = \local_ned_controller\mod_assign\assign_info::get_course_submissions_count($courseid, $userid);
            $rm_config = SH::update_rm_config($rm_config, $submissions_count, $participation_power);
            $table->attributes['data-resubmit_max_time'] = $rm_config->max_time;
        }

        if (($rm_config->show_pp ?? false) || $show_pp){
            if ($summative_score){
                $pp_value = \html_writer::span( \html_writer::tag('i',' ', ['class' => 'fa fa-rocket']) .
                        \html_writer::span(str($pp_status), 'pp-status'),
                        "gi-value participationpower $pp_status", ['title' => $participation_power]) .
                    \html_writer::link('#', ' ',
                        [
                            'class' => 'participationpower-help',
                            'data-data' => "$summative_score $formative_score $divided_score $participation_power $pp_status",
                            'data-course' => $course->shortname
                        ]
                    );
            } else {
                $pp_value = '-';
            }
            $general_info .= \html_writer::div(\html_writer::span(SH::$C::str('participationpower'), 'gi-label') . $pp_value);
        }

        if ($rm_config){
            // if $rm_config->max_course_attempts == 0, then no resubmission limit
            $max_course_attempts = $rm_config->max_course_attempts ?: '∞';
            $general_info .= \html_writer::div(
                \html_writer::span(str('resubmissionused'), 'gi-label') .
                \html_writer::span($submissions_count . '/' . $max_course_attempts, 'gi-value')
            );
        }

        if (is_aTT && class_exists('\report_ghs\helper')){
            // Deadline extensions.
            $contextsystem = \context_system::instance();
            if (has_capability('report/ghs:viewactivityextensions', $contextsystem)
                || \report_ghs\helper::has_capability_in_any_course('report/ghs:viewactivityextensions')) {

                $cls = '';
                if (!SH::db()->record_exists('block_ned_teacher_tools_exte', ['courseid' => $courseid, 'userid' => $userid])) {
                    $cls = 'dimmed_text';
                }
                $url_params = [
                    'filtercourse' => $courseid,
                    'filterstudent' => $userid
                ];
                $extension_url = new \moodle_url('//report/ghs/ghs_activity_extension.php', $url_params);

                $general_info .= \html_writer::div(
                    link($extension_url,
                        \html_writer::span(str('deadlineextentions', null, 'local_schoolmanager'), ''), $cls, ['target' => '_blank']
                    )
                );
            }
        }
    }

    $tag_options = [];
    if (is_aTT && ($tt_config->showtagsfilter ?? true)){
        $context = \context_course::instance($courseid);
        $contexts = $context->get_child_contexts();
        $contexts[] = $context;
        $tags = \core_tag_tag::get_tags_by_area_in_contexts('core', 'course_modules', $contexts);
        foreach ($tags as $_tag){
            $tag_options[$_tag->id] = $_tag->get_display_name(true);
        }
        $studentselector .= tags_print_course_menu(new \moodle_url($base_url, $base_url_param), $tag_options, $tag);
    }

    $menu_section = function($content, $title='', $title_link=false, $add_class=''){
        $add_class = $add_class ?: $title;
        if ($title_link){
            $title = $title ? link($title_link, str($title), 'sp-title') : '';
        } else {
            $title = $title ? SH::div(str($title), 'sp-title') : '';
        }
        return SH::div($title . SH::arr2str($content), 'sp-section ' . $add_class);
    };

    $sp_menu = '';
    if ($block_config->showgeneralinfosection ?? true) {
        $sp_menu .= $menu_section($general_info, 'generalinfo');
    }
    $url_params = $base_url_param;
    $url_params['show'] = activity_status::STATUS_ALL;

    $time_zone_info = [];
    if ($school_time_zone){
        $msg_key = 'timezone_info_school';
    } elseif (!empty($user->timezone)){
        $msg_key = 'timezone_info_student';
    } else {
        $msg_key = 'timezone_info_server';
    }
    $tz_name = \core_date::get_user_timezone($timezone);
    $time_zone_info[] = SH::div(SH::str($msg_key, ['timezone' => $tz_name, 'now' => SH::ned_date_tz($now, $timezone)]));
    $viewer_tz = \core_date::get_user_timezone(null);
    if ($viewer_tz != $tz_name){
        $time_zone_info[] = SH::div(SH::str('timezone_info_user',
            ['timezone' => $viewer_tz, 'now' => SH::ned_date_tz($now, null)]));
    }
    $sp_menu .= $menu_section($time_zone_info, 'timezoneinfo');

    if ($tt_config->participationpower_option ?? 0){
        if ($block_config->showsummativeactivitiessection ?? true) {
            $act_menu = get_activity_menu($activities_info->get_summative_list(), $base_url, $url_params, SUMMATIVE_ACTIVITY);
            $sp_menu .= $menu_section($act_menu, 'summativeactivities', [$base_url, $url_params]);
        }
        if ($block_config->showformativeactivitiessection ?? true) {
            $act_menu = get_activity_menu($activities_info->get_formative_list(), $base_url, $url_params, FORMATIVE_ACTIVITY);
            $sp_menu .= $menu_section($act_menu, 'formativeactivities', [$base_url, $url_params]);
        }
    } else {
        $act_menu =  get_activity_menu($activities_info->get_list(), $base_url, $url_params, USUAL_ACTIVITY);
        $sp_menu .= $menu_section($act_menu, 'studentprogress', [$base_url, $url_params]);
    }

    // Badges

    $table_badges = '';
    $badge = function($label, $url_params, $add_class='', $attr=[]) use ($base_url){
        $attr['title'] = isset($attr['title']) ? $attr['title'] : str('removefilter');
        return SH::link([$base_url, $url_params], $label, 'ned-badge ' . $add_class, $attr);
    };

    if ($show !== activity_status::STATUS_ALL){
        $table_badges .= $badge($show, array_merge($base_url_param, ['show' => 'all']));
    }

    if ($fs_type){
        $label = $fs_type == SUMMATIVE_ACTIVITY ? 'summativeactivities' : 'formativeactivities';
        $table_badges .= $badge($label, array_merge($base_url_param, ['fs_type' => USUAL_ACTIVITY]));
    }

    if ($rm_config){
        $max_time = $tt_config->daysforresubmission ? $tt_config->daysforresubmission * 24 : '∞';
        $table_badges .= SH::link(['#'], 'resubmission_modal:link', 'resubmission-help',
            ['data-data' => "$max_time $rm_config->show_pp $participation_power $submissions_count $max_course_attempts"]);
    }

    // Fill the main table

    if(!empty($activities)){
        $table->head = [cell(str('activityname'), 'activity-name')];

        if ($block_config->showduedatecolumn ?? true){
            $table->head[] = cell(str('duedate'), 'activity-type');
        }
        $table->head[] = cell(str('submitted'), 'activity-submitted');
        $table->head[] = cell(str('marked'), 'activity-graded');
        if (has_capability('block/ned_teacher_tools:canviewrapsreport', SH::ctx($courseid))) {
            $attributes = ['data-title' => SH::str('plagiarismscore'), 'data-toggle' => 'tooltip', 'data-placement' => 'top'];
            $table->head[] = cell(str('ps'), 'activity-ps', $attributes);
            $attributes['data-title'] = SH::str('aicontentscore');
            $table->head[] = cell(str('aic'), 'activity-aic', $attributes);
        }
        $table->head[] = cell(str('yourgrade'), 'activity-grade');

        $tag_exist = $tag < 1 || isset($tag_options[$tag]);
        foreach ($activities as $activity) {
            $status = $current_info->get_status($activity->id);
            if (!$current_info::is_this_status($status, $show)) {
                continue;
            }

            $tags = $current_info->get_tags($activity->id);
            if (is_aTT){
                if ($tag != 0 && $tag_exist && !($tag == -1 && empty($tags)) &&
                    !$current_info->check_tag_by_name($activity->id, $tag_options[$tag] ?? '')){
                    continue;
                }
                if ($cmid && $cmid != $activity->id){
                    continue;
                }
            }

            $table->data = array_merge($table->data,
                render_activity_row($activity, $userid, (bool)$kica, $deadlinemanagerinstalled,
                    $rm_config, $status, $tags, $get_mm_data($activity->id), $timezone, $cap_can_view_grades_before_midn, $cap_can_view_sp_graders,
                    $can_award_extension_after_last_activity));
        }
    }

    // Put together page content
    if (is_aTT){
        $result .= SH::div($studentselector, 'student-selector-wrapper');
    }
    $result .= SH::div($sp_menu, 'sp-menu') . SH::div($table_badges, 'ned-table-badges');

    if (!empty($table->data)){
        $result .= SH::render_table($table);
    } else {
        if ($show != activity_status::STATUS_ALL || $fs_type){
            $result .= SH::div(str('noactivitybyfilters'), 'no-activity');
        } else {
            $result .= SH::div(str('noactivity'), 'no-activity');
        }
    }

    if ($rm_config){
        $result .= SH::div(
            SH::$C::render_from_template('resubmission_legend',
                ['title' => str('iconlegend'), 'classname' => 'description']),
            'resubmission-legend');
    }

    // Render page
    $print_result($result);
}


/**
 * Load scripts for the student_progress page
 */
function _student_progress_page_scripts(){
    global $PAGE;

    SH::$C::js_call_amd('add_sorter', 'add_sort', ['#datatable']);
    SH::$C::js_call_amd('student_progress', 'init', [$PAGE->context->id]);
    SH::$C::js_call_amd('add_timer', 'add', ['#mark-interface table .timer[data-datetime]']);
    SH::$C::js_call_amd('participationpower', 'init', ['#mark-interface a.participationpower-help']);
    if (!is_aTT){
        return;
    }

    SH::js_call_amd('reviewmodalmessage', 'init');

    // notification template for remove submission feature
    $note = new \local_ned_controller\support\ned_notify_simple();
    $note->set_type($note::TYPE_NOTICE);
    $note->cancel_button = true;
    $note->ok_text = get_string('continue');
    SH::js_call_amd('student_progress_menu', 'init', [$note->js_export()]);
}

/**
 * Check actions for the student_progress page
 * Note: return void, but can redirect page
 *
 * @param $courseid
 * @param $userid
 *
 * @noinspection PhpUndefinedVariableInspection
 */
function _student_progress_page_action($courseid, $userid){
    if (!is_aTT){
        return;
    }

    $action = optional_param('action', '', PARAM_TEXT);
    $action_cmid = optional_param('cmid', 0, PARAM_INT);
    if (empty($action) || empty($action_cmid) || empty($courseid) || empty($userid) || !in_array($action, SP_ACTIONS)){
        return;
    }

    $url = clone(SH::page()->url);
    $url->remove_params(['action', 'cmid']);

    if (in_array($action, SP_ADMIN_ACTIONS)){
        $has_capability = is_siteadmin();
    } else {
        $course_context = \context_course::instance($courseid);
        $has_capability = SH::has_capability($action, $course_context);
    }
    if (!$has_capability){
        redirect($url, SH::str('actiondenied'), null, \core\notification::ERROR);
        return;
    }

    if ($action != SP_ACTION_EXCLUDEGRADE && $action != SP_ACTION_REFRESHGRADE){
        $ned_assign = SH::get_assign_by_cm($action_cmid, $courseid);
        if (!$ned_assign->is_valid($courseid)){
            return;
        }

        $cm = $ned_assign->get_course_module();
    } else {
        $cm = SH::get_cm_by_cmid($action_cmid, $courseid);
        if (!$cm || $cm->course != $courseid){
            return;
        }
    }

    $success = null;
    $messages = [
        SP_ACTION_REMOVESUBMISSION => [
            true    => 'submissionremoved',
            false   => 'submissioncantremoved',
        ],
        SP_ACTION_ALLOWRESUBMISSION => [
            true    => 'resubmissionadded',
            false   => 'resubmissioncantadded',
        ],
        SP_ACTION_ALLOWREVERT2DRAFT => [
            true    => 'ungraded2draft_done',
            false   => 'ungraded2draft_fail',
        ],
        SP_ACTION_EXCLUDEGRADE => [
            true    => 'excludegrade_done',
            false   => 'excludegrade_fail',
        ],
        SP_ACTION_REFRESHGRADE=> [
            true    => 'refreshgrade_done',
            false   => 'refreshgrade_fail',
        ],
    ];
    $message = null;

    switch ($action){
        case SP_ACTION_REMOVESUBMISSION:
            $success = $ned_assign->remove_last_assign_submission($userid);
            break;
        case SP_ACTION_ALLOWRESUBMISSION:
            $due_days = optional_param('duedays', 0, PARAM_INT);
            $not_credit = optional_param('notcredit', false, PARAM_BOOL);
            $set_duedate = $due_days ? (time() + $due_days*DAYSECS) : 0;
            $success = $ned_assign->force_add_attempt($userid, $not_credit ? null : $userid, $set_duedate);
            break;
        case SP_ACTION_ALLOWREVERT2DRAFT:
            $success = false;
            $submission = $ned_assign->get_needed_user_submission($userid);
            if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) break;

            $grade = null;
            $gg = $ned_assign->get_grade_grade($userid, false);
            if ($gg){
                $grade = $gg->overridden ? $gg->finalgrade : $gg->rawgrade;
            }
            if (!is_null($grade)) break;

            $res = $ned_assign->submitted2draft($userid, false);
            if (is_string($res)){
                $success = false;
                $message = $res;
            } else {
                $success = true;
            }
            break;
        case SP_ACTION_EXCLUDEGRADE:
            $grade = SH::get_grade_grade($cm, $userid, true, true);
            if (empty($grade)){
                $success = false;
                break;
            }

            $grade->excluded = time();
            $success = SH::grade_grade_update($cm, $grade);
            break;
        case SP_ACTION_REFRESHGRADE:
            $success = SH::grade_grade_refresh($cm, $userid);
            break;
    }

    if (is_null($success)){
        return;
    }

    $success = (bool)$success;
    if (empty($message)){
        $message = $messages[$action][$success] ?? '';
        if (empty($message)){
            return;
        }

        $message = str($message, $cm->name);
    }

    $message_type = $success ? \core\notification::SUCCESS : \core\notification::WARNING;
    redirect($url, $message, null, $message_type);
}

/**
 * Sort activities in moodle gradebook order (by \grade_item->sortorder)
 * @param \cm_info[] $activities
 *
 * @return \cm_info[] - [\grade_item->sortorder => $mod]
 */
function sort_activities_as_gradebook($activities){
    foreach ($activities as $mod){
        if (!isset($courseid)){
            $courseid = $mod->get_course()->id;
        }
        $grade_item = get_grade_item($courseid, $mod->modname, $mod->instance);
        if ($grade_item){
            $activities_by_sortorder[$grade_item->sortorder] = $mod;
        }
    }
    ksort($activities_by_sortorder);
    return $activities_by_sortorder;
}


// Small functions, which often used locally

/**
 * Moodle get_string for this plugin
 *
 * @param      $identifier
 * @param null $params
 * @param null $plugin
 *
 * @return string
 */
function str($identifier, $params=null, $plugin=null){
    return SH::str($identifier, $params, $plugin);
}

/**
 * Moodle get_string for this plugin, but with check of existing such string
 *
 * @param array|string $identifier_params - [$identifier, $params, $plugin] for str(), or only $identifier as string
 * @param null  $def - if there are no $identifier, use $def
 *
 * @return string
 */
function str_check($identifier_params=[], $def=null){
    static $string_manager = null;
    $identifier_params_def = ['', null, null];
    if (!$string_manager){
        $string_manager = get_string_manager();
    }

    if (is_array($identifier_params)){
        $identifier_params = $identifier_params + $identifier_params_def;
    } else {
        $identifier_params = [$identifier_params] + $identifier_params_def;
    }
    [$identifier, $params, $plugin] = $identifier_params;
    return SH::str_check($identifier, $params, $def, $plugin);
}

/**
 * @param            $check
 * @param int|null   $data
 * @param string     $format   - see format of strftime() function
 * @param int|string $timezone - by default use $USER timezone
 *
 * @return string
 */
function ned_date($check, $data=null, $format=SH::DT_FORMAT_FULL, $timezone=null){
    return SH::ned_date($check, F_TIME_NONE, $data, $format, $timezone);
}

/**
 * @param string|\moodle_url|array  $url_params - if it's array, that used [$url_text='', $params=null, $anchor=null]
 * @param string $text
 * @param string|array $class
 * @param array  $attr
 *
 * @return string
 * @throws \moodle_exception
 */
function link($url_params='', $text='', $class=null, $attr=[]){
    if ($url_params instanceof \moodle_url) {
        $m_url = $url_params;
    } else {
        if (is_string($url_params)){
            [$t_url, $params, $anchor] = [$url_params, null, null];
        } else {
            [$t_url, $params, $anchor] = $url_params + ['', null, null];
        }

        $m_url = new \moodle_url($t_url, $params, $anchor);
    }

    $attr['class'] = (isset($attr['class'])) ? $attr['class'] : '';
    if (is_array($class)){
        $class = join(' ', $class);
    }
    if (is_null($class) && strpos($text, ' ') === false){
        $class = $text;
    }
    $attr['class'].= $class ?: '';

    if (!empty($text) and get_string_manager()->string_exists($text, PLUGIN_NAME)) {
        $text = str($text);
    }

    return \html_writer::link($m_url, $text, $attr);
}

/**
 * @param string $filename
 * @param string $class
 * @param string $plugin
 * @param array  $attr
 *
 * @return string
 */
function img($filename, $class='icon', $plugin='moodle', $attr=[]){
    global $OUTPUT;
    $plugin = is_null($plugin) ? PLUGIN_NAME : $plugin;
    $url = '/blocks/' . PLUGIN . '/pix/' . $filename;
    $url = file_exists(DIRROOT . $url) ? $url :
        (file_exists(DIRROOT . $filename) ? $filename : $OUTPUT->image_url($filename, $plugin));
    $attr['class'] = (isset($attr['class'])) ? $attr['class'] : '';
    $attr['class'].= $class;
    $alt = isset2($attr, ['alt'], isset2($attr, ['title'], ''));
    return \html_writer::img(new \moodle_url($url), $alt, $attr);
}

/**
 * Return html fa (<i>) element with fa (and $class) class
 *
 * @param string|array $class
 * @param string $content
 * @param string $title
 * @param array  $attr
 *
 * @return string
 */
function fa($class='', $content='', $title='', $attr=[]){
    return SH::fa($class, $content, $title, $attr);
}

/**
 * Return html row
 *
 * @param array  $cells
 * @param array|string $class
 * @param array  $attr
 *
 * @return \html_table_row
 */
function row($cells=null, $class='', $attr=null){
    return SH::row($cells, $class, $attr);
}

/**
 * Return html cell
 *
 * @param string|array $text
 * @param string|array $class
 * @param array  $attr
 *
 * @return \html_table_cell
 */
function cell($text=null, $class='', $attr=null){
    return SH::cell($text, $class, $attr);
}

/**
 * Redirect with page catch
 *
 * @param \moodle_url|string $url
 * @param string             $message
 * @param int                $delay
 * @param string             $messagetype
 */
function redirect($url, $message='', $delay=null, $messagetype=SH::NOTIFY_INFO){
    SH::redirect($url, $message, $delay, $messagetype);
    die;
}

/**
 * Print notification and link "continue" as redirect attempt
 *
 * @param        $url
 * @param string $message
 * @param string $messagetype
 * @param bool   $return
 *
 * @return string
 */
function redirect_continue($url, $message='', $messagetype=SH::NOTIFY_INFO, $return=false){
    return SH::redirect_continue($url, $message, $messagetype, $return);
}

/**
 * @param null $made_by_rm - true if only made_by_rm, false if only not_made_by_rm
 * @param null $courseid
 *
 * @return array [$courses_by_cat[], $records_by_cat[]]
 */
function get_unused_resubmissions($made_by_rm=null, $courseid=null){
    global $DB;

    $additional_g_g_h_condition = '';
    if (!is_null($made_by_rm)){
        if ($made_by_rm){
            $additional_g_g_h_condition = "AND g_g_h.loggeduser = a_s.userid";
        } else {
            $additional_g_g_h_condition = "AND g_g_h.loggeduser <> a_s.userid";
        }
    }

    $additional_where = '';
    if ($courseid){
        $additional_where .= "AND c.id = $courseid";
    }

    $sql = "SELECT g_g_h.id, 
        CONCAT(u.firstname, ' ', u.lastname) AS 'username',
        c.fullname AS 'course',
        a.name AS 'assign',
        
        u.id AS 'userid',
        c.id AS 'courseid',
        a.id AS 'activityid',
        cm.id AS 'cmid',
        g_g_h.loggeduser = u.id AS 'made_by_rm',
        a_s.timecreated,
        
        a_s.attemptnumber AS 'Attempt'
        FROM {assign_submission} AS a_s
        LEFT JOIN {assign} AS a
            ON a_s.assignment = a.id
        LEFT JOIN {grade_items} AS g_i
            ON a_s.assignment = g_i.iteminstance
            AND g_i.itemtype = \"mod\"
            AND g_i.itemmodule = \"assign\"
            AND g_i.courseid = a.course
        LEFT JOIN {assign_grades} a_g
            ON a_s.assignment = a_g.assignment
            AND a_g.userid = a_s.userid
            AND a_g.attemptnumber = a_s.attemptnumber
        LEFT JOIN {grade_grades_history} AS g_g_h
            ON g_i.id = g_g_h.itemid
            AND g_g_h.timemodified = a_s.timecreated
            AND g_g_h.source = \"mod/assign\"
            AND g_g_h.userid = a_s.userid
            AND g_g_h.usermodified = a_s.userid
            $additional_g_g_h_condition
        LEFT JOIN {course} AS c 
            ON c.id = a.course
        LEFT JOIN {user} AS u
            ON u.id = a_s.userid
        LEFT JOIN {context} AS cntxt
            ON (cntxt.contextlevel = 50 
            AND cntxt.instanceid = c.id)
        LEFT JOIN {role_assignments} AS ra
            ON ra.userid = u.id 
            AND ra.contextid = cntxt.id
        LEFT JOIN {role_assignments} AS ra2
            ON ra2.userid = u.id 
            AND ra2.contextid = cntxt.id
            AND ra2.id > ra.id
        LEFT JOIN {role} AS r 
            ON r.id = ra.roleid
        
        LEFT JOIN {course_modules} AS cm
            ON cm.course = c.id
            AND cm.instance = a.id
        
        WHERE a_s.status = 'reopened' 
            AND a_s.latest = '1' 
            AND r.shortname = 'student' 
            AND ra2.id IS NULL
            AND g_g_h.id IS NOT NULL 
            AND (a_g.timemodified IS NULL OR a_g.grade IS NULL OR a_g.grade <= 0)
            $additional_where
        ORDER BY c.id, g_g_h.id
    ";
    $records = $DB->get_records_sql($sql);
    $courses = [];
    $records_by_cat = [];
    $courses_by_cat = [];
    $unic = [];
    foreach (RESUBMISSIONS_KEYS as $KEY){
        $records_by_cat[$KEY] = [];
        $courses_by_cat[$KEY] = [];
    }
    foreach ($records as $record){
        if (isset($unic[$record->userid][$record->cmid])){
            continue;
        }
        $unic[$record->userid][$record->cmid] = true;

        if (!isset($courses[$record->courseid])){
            $courses[$record->courseid] = CM::check_and_get_resubmission_activities($record->courseid);
        }

        $record->controlled_rm = $courses[$record->courseid] && isset($courses[$record->courseid][$record->cmid]) &&
            $courses[$record->courseid][$record->cmid];
        if ($record->made_by_rm){
            $records_by_cat[RESUBMISSIONS_MADE_BY_RM][$record->id] = $record;
            $courses_by_cat[RESUBMISSIONS_MADE_BY_RM][$record->courseid] = $record->course;
            if ($record->controlled_rm){
                $records_by_cat[RESUBMISSIONS_MADE_BY_RM_CONTROLLED_BY_RM][$record->id] = $record;
                $courses_by_cat[RESUBMISSIONS_MADE_BY_RM_CONTROLLED_BY_RM][$record->courseid] = $record->course;
            } else {
                $records_by_cat[RESUBMISSIONS_MADE_BY_RM_CONTROLLED_NOT_BY_RM][$record->id] = $record;
                $courses_by_cat[RESUBMISSIONS_MADE_BY_RM_CONTROLLED_NOT_BY_RM][$record->courseid] = $record->course;
            }
        } else {
            $records_by_cat[RESUBMISSIONS_MADE_NOT_BY_RM][$record->id] = $record;
            $courses_by_cat[RESUBMISSIONS_MADE_NOT_BY_RM][$record->courseid] = $record->course;
            if ($record->controlled_rm){
                $records_by_cat[RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_BY_RM][$record->id] = $record;
                $courses_by_cat[RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_BY_RM][$record->courseid] = $record->course;
            } else {
                $records_by_cat[RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_NOT_BY_RM][$record->id] = $record;
                $courses_by_cat[RESUBMISSIONS_MADE_NOT_BY_RM_CONTROLLED_NOT_BY_RM][$record->courseid] = $record->course;
            }
        }


        $records_by_cat[RESUBMISSIONS_ALL][$record->id] = $record;
        $courses_by_cat[RESUBMISSIONS_ALL][$record->courseid] = $record->course;
    }

    return [$courses_by_cat, $records_by_cat];
}

/**
 * Remove last submission and try to revert grade to previous state
 *  return false if can do nothing, true if can at least change assign_submission
 *
 * WARNING: There are none security check
 *
 * @param \cm_info|numeric $cm_or_id
 * @param int|string       $userid
 * @param null|string      $check_status - string status, if you wish check submission
 * @param object|numeric   $courseorid   - Optional course object (or its id) if loaded, improves optimization if $cm_or_id is represented as ID
 *
 * @return bool
 */
function remove_last_submission($cm_or_id, $userid, $check_status=null, $courseorid=null){
    $ned_assign = SH::get_assign_by_cm($cm_or_id, $courseorid);
    return $ned_assign->remove_last_assign_submission($userid, $check_status);
}

/**
 * Return html checkbox as checkbox & link (the same to single selector)
 *
 * @param             $value
 * @param \moodle_url $url
 * @param string      $name
 * @param string      $text
 * @param string      $class
 * @param null        $attributes
 *
 * @return string
 */
function single_checkbox($value, \moodle_url $url, $name, $text='', $class='', $attributes=null){
    $url = new \moodle_url($url);
    $url->param($name, (int)(!$value));
    $checkbox =  \html_writer::checkbox($name, $value, $value, $text,
        ['onclick' => "window.location.href = '{$url->out(false)}';"]);
    return \html_writer::div($checkbox, $class, $attributes);
}

/**
 * Return string selector if $render=true, \single_select otherwise
 *
 * @param        $url
 * @param        $name
 * @param array  $options
 * @param string $selected
 * @param null   $label
 * @param null   $formid
 * @param null   $nothing
 * @param array  $attributes
 * @param bool   $render
 *
 * @return \single_select | string
 */
function single_select($url, $name, $options=[], $selected='', $label=null, $formid=null, $nothing=null, $attributes=[], $render=true){
    return SH::single_select($url, $name, $options, $selected, $label, $attributes, $formid, $nothing, $render);
}

function choose_students_by_group($allstudents, $groups, $groupid){
    $enrolledusers = $allstudents;
    if ($groupid == GROUP_NONE){
        $enrolledusers = [];
    } elseif($groupid != GROUP_ALL){
        $enrolledusers = isset($groups[$groupid]) ? $groups[$groupid]->users : [];
    }
    return $enrolledusers;
}

/**
 * Return grade for given user or all users.
 *
 * @global object
 * @global object
 * @param object $forum
 * @param int $userid optional user id, 0 means all users
 * @return array array of grades, false if none
 */
function forum_get_user_grades($forum, $userid = 0) {
    global $CFG;

    require_once($CFG->dirroot.'/rating/lib.php');

    $ratingoptions = new \stdClass;
    $ratingoptions->component = 'mod_forum';
    $ratingoptions->ratingarea = 'post';

    //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
    $ratingoptions->modulename = 'forum';
    $ratingoptions->moduleid   = $forum->id;
    $ratingoptions->userid = $userid;
    $ratingoptions->aggregationmethod = $forum->assessed;
    $ratingoptions->scaleid = $forum->scale;
    $ratingoptions->itemtable = 'forum_posts';
    $ratingoptions->itemtableusercolumn = 'userid';

    $rm = new \rating_manager();
    return $rm->get_user_grades($ratingoptions);
}

/**
 * Return grade function for mod, if exist, null otherwise
 * @param \cm_info|\stdClass $mod
 *
 * @return string|null
 */
function get_grade_function($mod){
    global $CFG;
    $gradefunction = $mod->modname . "_get_user_grades";
    $local_gradefunction = '\\' . PLUGIN_NAME . '\\' . $gradefunction;
    if(function_exists($local_gradefunction)){
        return $local_gradefunction;
    }

    $libfile = "$CFG->dirroot/mod/$mod->modname/lib.php";
    if (!file_exists($libfile)){
        return null;
    }

    require_once($libfile);
    return function_exists($gradefunction) ? $gradefunction : null;
}

/**
 * Check $obj on isset by all $keys
 *  Return value, if it exists, $def otherwise
 * @param       $obj
 * @param array $keys
 * @param null  $def
 *
 * @return array|mixed|null
 */
function isset2($obj, $keys=[], $def=null){
    return SH::isset2($obj, $keys, $def);
}

/**
 * Return $key, it exists in $obj, $def otherwise
 *  if $def not set, return first key from $obj
 *  use $return_null if wish to get null as $def value
 *
 * @param      $obj
 * @param      $key
 * @param null $def
 * @param bool $return_null
 *
 * @return int|string|null
 */
function isset_key($obj, $key, $def=null, $return_null=false){
    return SH::isset_key($obj, $key, $def, $return_null);
}

/**
 * Return $val, it exists in $list, $def otherwise
 *  if $def not set, return first val from $list
 *  use $return_null if wish to get null as $def value
 *
 * @param array $list
 * @param       $val
 * @param null  $def
 * @param bool  $return_null
 *
 * @return mixed|null
 */
function isset_in_list($list, $val, $def=null, $return_null=false){
    return SH::isset_in_list($list, $val, $def, $return_null);
}

/**
 * Returns list of users enrolled into course.
 *
 * @param $courseid
 * @param \context $context
 * @param string $withcapability
 * @param int $groupid 0 means ignore groups, USERSWITHOUTGROUP without any group and any other value limits the result by group id
 * @param string $userfields requested user record fields
 * @param string $orderby
 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
 * @return array of user records
 */
function get_enrolled_users($courseid, \context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = null,
    $limitfrom = 0, $limitnum = 0) {

    $onlyactive = !get_cache_show_inactive($courseid);
    return \get_enrolled_users($context, $withcapability, $groupid, $userfields, $orderby, $limitfrom, $limitnum, $onlyactive);
}

/**
 * @return bool
 */
function is_kica_exists(){
    return SH::is_kica_exists();
}

/**
 * @param $courseid
 *
 * @return \stdClass|bool|null
 */
function get_kica_if_exists($courseid){
    return SH::get_kica_enabled($courseid);
}

/**
 * Return sql with FROM, JOIN and WHERE for getting user_enrolments
 *  if sent $params, necessary data will be saved here
 *
 * @param string $select
 * @param array  $where
 * @param array  $params
 * @param string $rolename
 * @param int    $courseid
 * @param int    $groupid
 * @param int    $cmid
 * @param bool   $is_active
 * @param string $other_sql
 * @param string $add_join
 * @param string $prefix
 *
 * @return string
 */
function get_sql_user_enrolments($select='ue.id', $where=[], &$params=[], $rolename=ROLE_STUDENT,
    $courseid=0, $groupid=0, $cmid=0, $is_active=true, $other_sql='', $add_join='', $prefix='ue_'){
    if (!isset($params[$prefix.'rolename'])){
        $params[$prefix.'rolename'] = $rolename;
    }

    $limit_cm = '';
    if ($cmid){
        if (!isset($params[$prefix.'cmid'])){
            $params[$prefix.'cmid'] = $cmid;
        }
        $limit_cm = "AND cm.id = :{$prefix}cmid";
    }
    if ($courseid){
        $params[$prefix.'courseid'] = $courseid;
        $where[] = "e.courseid = :{$prefix}courseid";
    }
    if ($groupid > 0){
        $params[$prefix.'groupid'] = $groupid;
        $where[] = "gr.id = :{$prefix}groupid";
    }
    $params[$prefix.'ctx_course'] = CONTEXT_COURSE;
    $params[$prefix.'ctx_module'] = CONTEXT_MODULE;
    $where[] = "(r.id = role_course.id OR r.id = role_cm.id)";

    if ($is_active){
        $where[] = "(
                ue.status = :{$prefix}active AND e.status = :{$prefix}enabled AND 
                ue.timestart <= :{$prefix}now1 AND (ue.timeend = 0 OR ue.timeend > :{$prefix}now2) AND
                u.suspended = 0 AND u.deleted = 0
            )";
        $t =  time();
        $params[$prefix.'now1'] = $t;
        $params[$prefix.'now2'] = $t;
        $params[$prefix.'enabled'] = ENROL_INSTANCE_ENABLED;
        $params[$prefix.'active'] = ENROL_USER_ACTIVE;
    }

    if (is_array($add_join)){
        $add_join = join("\n", $add_join);
    }

    $sql = "SELECT $select
            FROM {user_enrolments} ue
            JOIN {user} u
                ON u.id = ue.userid
            JOIN {enrol} e
                ON e.id = ue.enrolid
            JOIN {role} r 
                ON r.shortname = :{$prefix}rolename
                
            LEFT JOIN {context} ctx_course
                ON ctx_course.contextlevel = :{$prefix}ctx_course
                AND ctx_course.instanceid = e.courseid
            LEFT JOIN {role_assignments} ra_course
                ON ra_course.contextid = ctx_course.id
                AND ra_course.userid = ue.userid
            LEFT JOIN {role} role_course
                ON role_course.id = ra_course.roleid
                
            LEFT JOIN {course_modules} cm
                ON cm.course = e.courseid
                $limit_cm
            LEFT JOIN {context} ctx_cm
                ON ctx_cm.contextlevel = :{$prefix}ctx_module
                AND ctx_cm.instanceid = cm.id
            LEFT JOIN {role_assignments} ra_cm
                ON ra_cm.contextid = ctx_cm.id
                AND ra_cm.userid = ue.userid
            LEFT JOIN {role} role_cm
                ON role_cm.id = ra_cm.roleid
                
            LEFT JOIN (
                    SELECT grp.id, g_m.userid, grp.courseid
                    FROM {groups} grp
                    JOIN {groups_members} g_m
                       ON g_m.groupid = grp.id
            ) gr 
                ON gr.courseid = e.courseid
                AND gr.userid = ue.userid
                
            $add_join
        ";
    $where = !empty($where) ? ("\nWHERE (" . join(') AND (', $where) . ')') : '';
    return $sql.$where.$other_sql;
}

/**
 * Return course_modules_completion by WHERE conditions from options
 * if $return_first - return only one record or null
 * if only 'courseid' in $options, return array by cmid and userid of records (or array of true, if $check_complete is true)
 *
 * @param array $options
 * @param bool  $return_first
 * @param bool  $check_complete - get only records with complete status
 *
 * @return array|\stdClass|bool|null
 */
function get_course_modules_completion($options=[], $return_first=false, $check_complete=false){
    global $DB;
    $params_options = ['courseid' => 'cm.course', 'cmid' => 'cm.id', 'userid' => 'cmc.userid'];
    $params = [];
    $where = [];
    foreach ($params_options as $param => $wh){
        if (isset($options[$param])){
            $where[] = "$wh = :$param";
            $params[$param] = $options[$param];
        }
    }

    if (empty($where)){
        SH::print_error('error', 'error', '', "You can't call get_course_modules_completion functions without options!");
    }

    if ($check_complete){
        $where[] = "(cmc.completionstate = :completionpass OR cmc.completionstate = :completioncomplete)";
        $params['completionpass'] = COMPLETION_COMPLETE_PASS;
        $params['completioncomplete'] = COMPLETION_COMPLETE;
    }

    $where = empty($where) ? '' : ("\nWHERE (" . join(') AND (', $where) . ')');
    $sql = "SELECT cmc.*
        FROM {course_modules} cm
        JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
        $where
    ";
    $records = $DB->get_records_sql($sql, $params);
    if ($return_first){
        $records = empty($records) ? null : reset($records);
        if ($check_complete){
            $records = (bool)$records;
        }
        return $records;
    }

    if (isset($options['courseid']) && !(isset($options['cmid']) || isset($options['userid']))){
        $res = [];
        foreach ($records as $record){
            $res[$record->coursemoduleid][$record->userid] = $check_complete ? true : $record;
        }
        return $res;
    } elseif ((isset($options['cmid']) && !isset($options['userid'])) || (isset($options['userid']) && !isset($options['cmid']))){
        $res = [];
        $key = isset($options['cmid']) ? 'userid' : 'coursemoduleid';
        foreach ($records as $record){
            $res[$record->$key] = $check_complete ? true : $record;
        }
        return $res;
    }

    return $records;
}

/**
 * Print error in the moodle style
 *
 * @param      $message
 * @param      $moreinfourl
 * @param      $link
 * @param      $backtrace
 * @param null $debuginfo
 * @param bool $forcibly
 *
 * @return string
 */
function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $forcibly=false) {
    global $CFG, $OUTPUT;

    $output = '';
    $obbuffer = '';

    if (is_object($message)){
        $info = $message;
        [$message, $moreinfourl, $link, $backtrace, $debuginfo, $errorcode] =
            [$info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo, $info->errorcode];
    }

    $msgs = explode("\n", $message);
    $message = '';
    foreach ($msgs as $msg){
        $message .= '<p class="errormessage">' . s($msg) . '</p>';
    }
    $message .= '<p class="errorcode"><a href="' . s($moreinfourl) . '">' . get_string('moreinformation') . '</a></p>';
    if (empty($CFG->rolesactive)) {
        $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
        //It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix for new installation.
    }
    $output .= $OUTPUT->box($message, 'errorbox alert alert-danger', null, array('data-rel' => 'fatalerror'));

    if ($CFG->debugdeveloper || $forcibly) {
        $labelsep = get_string('labelsep', 'langconfig');
        if (!empty($debuginfo)) {
            $debuginfo = s($debuginfo); // removes all nasty JS
            $debuginfo = str_replace("\n", '<br />', $debuginfo); // keep newlines
            $label = get_string('debuginfo', 'debug') . $labelsep;
            $output .= $OUTPUT->notification("<strong>$label</strong> " . $debuginfo, 'notifytiny');
        }
        if (!empty($backtrace)) {
            $label = get_string('stacktrace', 'debug') . $labelsep;
            $output .= $OUTPUT->notification("<strong>$label</strong> " . format_backtrace($backtrace), 'notifytiny');
        }
        if ($obbuffer !== '' ) {
            $label = get_string('outputbuffer', 'debug') . $labelsep;
            $output .= $OUTPUT->notification("<strong>$label</strong> " . s($obbuffer), 'notifytiny');
        }
    }

    /** @noinspection PhpStatementHasEmptyBodyInspection */
    if (empty($CFG->rolesactive)) {
        // continue does not make much sense if moodle is not installed yet because error is most probably not recoverable
    } else if (!empty($link)) {
        $output .= $OUTPUT->continue_button($link);
    }

    return $output;
}

/**
 * Print error in the moodle style from an exception
 *
 * @param \Exception|\Throwable $ex
 * @param array|string          $messages
 * @param bool                  $forcibly
 * @param null                  $a
 * @param null                  $debuginfo
 *
 * @return string
 *
 * @throws \moodle_exception
 */
function print_error($ex, $messages=[], $forcibly=true, $a=null, $debuginfo=null){
    if (is_string($ex)){
        $msg = SH::arr2str($messages, $ex, '; ');
        SH::print_error($msg, $a, $forcibly, $debuginfo);
    }

    $output = '';
    if (is_string($messages)){
        $messages = explode("\n", $messages);
    }
    foreach ($messages as $msg){
        $output .= '<p class="errormessage">' . s($msg) . '</p>';
    }
    $info = get_exception_info($ex);
    $output .= fatal_error($info->message, $info->moreinfourl, null, $info->backtrace, $info->debuginfo, $forcibly);
    return $output;
}

/**
 * Print hidden (if debugdeveloper is off) error in the moodle style from an exception
 *
 * @param \Exception|\Throwable   $ex
 * @param array|string  $messages
 * @param string        $not_debug_text
 *
 * @return string
 */
function print_hidden_error($ex, $messages=[], $not_debug_text=''){
    global $CFG;
    $error_msg = print_error($ex, $messages, true);
    if (!($CFG->debugdeveloper ?? 0)){
        $error_msg = $not_debug_text . \html_writer::div($error_msg, '', ['style' => 'display: none;']);
    }
    return $error_msg;
}

/**
 * Print error in the moodle style
 *
 * @param \Exception | \Throwable $exception
 *
 */
function cron_print_error($exception) {
    global $CFG;

    $obbuffer = '';
    $info = get_exception_info($exception);
    [$message, $moreinfourl, $link, $backtrace, $debuginfo, $errorcode] =
        [$info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo, $info->errorcode];

    $msgs = explode("\n", $message);
    foreach ($msgs as $msg){
        mtrace(s($msg));
    }
    if (empty($CFG->rolesactive)) {
        mtrace(get_string('installproblem', 'error'));
        //It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix for new installation.
    }

    $labelsep = get_string('labelsep', 'langconfig');
    if (!empty($debuginfo)) {
        $debuginfo = s($debuginfo); // removes all nasty JS
        $label = get_string('debuginfo', 'debug') . $labelsep;
        mtrace($label.': '.$debuginfo);
    }
    if (!empty($backtrace)) {
        $label = get_string('stacktrace', 'debug') . $labelsep;
        mtrace($label.': '.format_backtrace($backtrace, true));
    }
    if ($obbuffer !== '' ) {
        $label = get_string('outputbuffer', 'debug') . $labelsep;
        mtrace($label.': '.s($obbuffer));
    }
}

/**
 * Check, does course categories (array of ids) has such course id
 * If none $courseid, return array of all course id from the current course categories
 *
 * @param array|string $course_cats - if string, id values should be delimiter by ','
 * @param              $courseid
 *
 * @return array|bool
 */
function course_cats_has_courseid($course_cats, $courseid=0){
    return SH::course_cats_has_courseid($course_cats, $courseid);
}
