<?php
// This file is part of mod_datalynx for Moodle - http://moodle.org/
//
// It is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// It is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 *
 * @package mod_datalynx
 * @copyright based on the work by 2012 Itamar Tzadok
 * @copyright 2015 onwards edulabs.org
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace mod_datalynx;
use coding_exception;
use comment;
use completion_info;
use context_module;
use core_course_category;
use datalynx_preset_manager;
use datalynxfield_status\field as datalynxfield_status;
use html_writer;
use mod_datalynx\local\filter\datalynx_filter_manager;
use mod_datalynx\local\rule\manager;
use mod_datalynx\local\view\base;
use moodle_exception;
use moodle_page;
use moodle_url;
use stdClass;
use user_picture;

/**
 * Class datalynx
 *
 * @package mod_datalynx
 */
class datalynx {
    /** @var int Count all entries. */
    const COUNT_ALL = 0;

    /** @var int Count approved entries. */
    const COUNT_APPROVED = 1;

    /** @var int Count unapproved entries. */
    const COUNT_UNAPPROVED = 2;

    /** @var int Count entries left (within limit). */
    const COUNT_LEFT = 3;

    /** @var int No approval required. */
    const APPROVAL_NONE = 0;

    /** @var int Approval required for new entries and updates. */
    const APPROVAL_ON_UPDATE = 1;

    /** @var int Approval required only for new entries. */
    const APPROVAL_ON_NEW = 2;

    /** @var string Internal-only email template view type. */
    const INTERNAL_VIEW_EMAIL = 'email';

    /**
     * @var ?stdClass course module
     */
    public $cm = null;

    /**
     * @var ?stdClass fieldset object of the course
     */
    public $course = null;

    /**
     * @var ?stdClass fieldset record of datalynx instance
     */
    public $data = null;

    /**
     * @var ?context_module
     */
    public $context = null;

    /**
     * @var int
     */
    public $groupmode = 0;

    /**
     * @var int|mixed
     */
    public $currentgroup = 0;

    /**
     * @var array
     */
    public $notifications = ['bad' => [], 'good' => []];

    /** @var string Page file name for URL construction. */
    protected $pagefile = 'view';

    /** @var array Cached field objects indexed by field id. */
    protected $fields = [];

    /** @var array Cached view objects indexed by view id. */
    protected $views = [];

    /** @var ?object Filter manager instance. */
    protected $filtermanager = null;

    /** @var ?object Custom filter manager instance. */
    protected $customfiltermanager = null;

    /** @var ?object Rule manager instance. */
    protected $rulemanager = null;

    /** @var ?object Preset manager instance. */
    protected $presetmanager = null;

    /** @var ?object Currently active view. */
    protected $currentview = null;

    /** @var array Internal (system) fields. */
    protected $internalfields = [];

    /** @var array Fields available for custom filters. */
    protected $customfilterfields = [];

    /** @var array Internal group modes map (name => id). */
    protected $internalgroupmodes = ['separateparticipants' => -1];

    /**
     * datalynx constructor
     *
     * @param int|object $d (id of datalynx instance fetched from db table)
     * @param int $id (course module id)
     */
    public function __construct($d = 0, int $id = 0) {
        global $DB;

        // Initialize from datalynx id or object.
        if ($d) {
            if (is_object($d)) { // Try object first.
                $this->data = $d;
            } else {
                if (!$this->data = $DB->get_record('datalynx', ['id' => $d])) {
                    throw new moodle_exception(
                        'invaliddatalynx',
                        'datalynx',
                        null,
                        null,
                        "Datalynx id: $d"
                    );
                }
            }
            if (!$this->course = $DB->get_record('course', ['id' => $this->data->course])) {
                throw new moodle_exception(
                    'invalidcourse',
                    'datalynx',
                    null,
                    null,
                    "Course id: {$this->data->course}"
                );
            }
            if (
                !$this->cm = get_coursemodule_from_instance(
                    'datalynx',
                    $this->id(),
                    $this->course->id
                )
            ) {
                throw new moodle_exception(
                    'invalidcoursemodule',
                    'datalynx',
                    null,
                    null,
                    "Cm id: {$this->id()}"
                );
            }
            // Initialize from course module id.
        } else {
            if ($id) {
                if (!$this->cm = get_coursemodule_from_id('datalynx', $id)) {
                    throw new moodle_exception(
                        'invalidcoursemodule ' . $id,
                        'datalynx',
                        null,
                        null,
                        "Cm id: $id"
                    );
                }
                if (!$this->course = $DB->get_record('course', ['id' => $this->cm->course])) {
                    throw new moodle_exception(
                        'invalidcourse',
                        'datalynx',
                        null,
                        null,
                        "Course id: {$this->cm->course}"
                    );
                }
                if (!$this->data = $DB->get_record('datalynx', ['id' => $this->cm->instance])) {
                    throw new moodle_exception(
                        'invaliddatalynx',
                        'datalynx',
                        null,
                        null,
                        "Datalynx id: {$this->cm->instance}"
                    );
                }
            }
        }

        // Get context.
        $this->context = context_module::instance($this->cm->id);

        // Set groups.
        if ($this->cm->groupmode && in_array($this->cm->groupmode, $this->internalgroupmodes)) {
            $this->groupmode = $this->cm->groupmode;
        } else {
            $this->groupmode = groups_get_activity_groupmode($this->cm);
            $this->currentgroup = groups_get_activity_group($this->cm, true);
        }
    }

    /**
     * Get datalynx object by instanceid (id of datalynx table)
     *
     * @param int $instanceid The instance id of the datalynx activity.
     * @return datalynx
     */
    public static function get_datalynx_by_instance($instanceid): datalynx {
        $cm = get_coursemodule_from_instance('datalynx', $instanceid);
        return new datalynx($instanceid, $cm->id);
    }

    /**
     * Get datalynx table id. Not cmid.
     *
     * @return mixed
     */
    public function id() {
        return $this->data->id;
    }

    /**
     * Get name of datalynx instance.
     *
     * @return mixed
     */
    public function name() {
        return $this->data->name;
    }

    /**
     * Returns the page file name used for URL construction.
     *
     * @return string
     */
    public function pagefile() {
        return $this->pagefile;
    }

    /**
     * Returns the canonical page file name for generated user-facing URLs.
     *
     * Internal rendering contexts such as "external" must not leak into action
     * links, menus, or other URLs that users can follow.
     *
     * @return string
     */
    public function pagefile_for_urls(): string {
        if ($this->pagefile === 'external') {
            return 'view';
        }

        return $this->pagefile;
    }

    /**
     * Get internal group modes.
     *
     * @return array
     */
    public function internal_group_modes() {
        return $this->internalgroupmodes;
    }

    /**
     * Get current view.
     *
     * @return ?object view object
     */
    public function get_current_view() {
        return $this->currentview;
    }

    /**
     * Set current view explicitly.
     *
     * Used by internal rendering paths that do not go through the regular
     * request-based current view resolution.
     *
     * @param ?base $view
     * @return void
     */
    public function set_current_view(?base $view): void {
        $this->currentview = $view;
    }

    /**
     * Get filter manager.
     *
     * @return ?datalynx_filter_manager
     */
    public function get_filter_manager() {
        // Set filters manager.
        if (!$this->filtermanager) {
            $this->filtermanager = new datalynx_filter_manager($this);
        }
        return $this->filtermanager;
    }

    /**
     * Get custom filter manager.
     *
     * @return ?\mod_datalynx\local\filter\datalynx_customfilter_manager
     */
    public function get_customfilter_manager() {
        if (!$this->customfiltermanager) {
            $this->customfiltermanager = new local\filter\datalynx_customfilter_manager($this);
        }
        return $this->customfiltermanager;
    }

    /**
     * Get rule manager.
     *
     * @return ?manager
     */
    public function get_rule_manager() {
        // Set rules manager.
        if (!$this->rulemanager) {
            $this->rulemanager = new manager($this);
        }
        return $this->rulemanager;
    }

    /**
     * Get preset manager.
     *
     * @return ?datalynx_preset_manager
     */
    public function get_preset_manager() {
        global $CFG;
        // Set preset manager.
        if (!$this->presetmanager) {
            require_once($CFG->dirroot . '/mod/datalynx/preset/preset_manager.php');
            $this->presetmanager = new datalynx_preset_manager($this);
        }
        return $this->presetmanager;
    }

    /**
     * Get number of entries.
     *
     * @param string $type
     * @param int $user
     * @return int|string
     * @throws \dml_exception
     */
    public function get_entriescount($type, $user = 0) {
        global $DB;

        switch ($type) {
            case self::COUNT_ALL:
                $count = $DB->count_records_sql(
                    'SELECT COUNT(e.id) FROM {datalynx_entries} e WHERE e.dataid = ?',
                    [$this->id()]
                );
                break;

            case self::COUNT_LEFT:
            case self::COUNT_APPROVED:
                $count = '---';
                break;

            case self::COUNT_UNAPPROVED:
                if ($this->data->approval == 0) {
                    return '---';
                }
                $count = $DB->count_records_sql(
                    'SELECT COUNT(e.id) FROM {datalynx_entries} e WHERE e.approved = 0 AND e.dataid = ?',
                    [$this->id()]
                );
                break;

            default:
                $count = '---';
        }

        return $count;
    }

    /**
     * Update datalynx settings
     *
     * @param array $params Parameters to update.
     * @param mixed $notify
     * @return bool
     * @throws \coding_exception
     * @throws \dml_exception
     */
    public function update($params, $notify = '') {
        global $DB;

        if ($params) {
            $updatedf = false;
            foreach ($params as $key => $value) {
                $oldvalue = !empty($this->data->{$key}) ? $this->data->{$key} : null;
                $newvalue = !empty($value) ? $value : null;
                if ($newvalue != $oldvalue) {
                    $this->data->{$key} = $value;
                    $updatedf = true;
                }
            }
            if ($updatedf) {
                if (!$DB->update_record('datalynx', $this->data)) {
                    if ($notify === true) {
                        $this->notifications['bad'][] = get_string('dfupdatefailed', 'datalynx');
                    } else {
                        if ($notify) {
                            $this->notifications['bad'][] = $notify;
                        }
                    }
                    return false;
                } else {
                    if (!$notify === true) {
                        if ($notify) {
                            $this->notifications['good'][] = $notify;
                        }
                    }
                }
            }
        }
        return true;
    }

    /**
     * Sets the datalynx page
     *
     * @param string $page current page
     * @param ?array $params
     * @param bool $skiplogincheck If true, skip the login check.
     * @throws moodle_exception
     * @return string output
     */
    public function set_page($page = 'view', ?array $params = null, $skiplogincheck = false) {
        global $CFG, $PAGE, $USER;

        $this->pagefile = $page;
        $thisid = $this->id();

        $params = (object) $params;
        $urlparams = [];
        if (!empty($params->urlparams)) {
            foreach ($params->urlparams as $param => $value) {
                if ($value != 0 && $value != '') {
                    $urlparams[$param] = $value;
                }
            }
        }
        if (!$skiplogincheck) {
            require_login($this->course->id, true, $this->cm);
        }

        // Make sure there is at least datalynx id param.
        $urlparams['d'] = $thisid;

        $manager = has_capability('mod/datalynx:managetemplates', $this->context);

        // If datalynx activity closed don't let students in.
        if (!$manager) {
            $timenow = time();
            if (!empty($this->data->timeavailable) && $this->data->timeavailable > $timenow) {
                throw new moodle_exception(
                    'notopenyet',
                    'datalynx',
                    '',
                    userdate($this->data->timeavailable)
                );
            }
        }

        // RSS.
        if (
            !empty($params->rss) && !empty($CFG->enablerssfeeds) &&
                !empty($CFG->datalynx_enablerssfeeds) && $this->data->rssarticles > 0
        ) {
            require_once("$CFG->libdir/rsslib.php");
            $rsstitle = format_string($this->course->shortname) . ': %fullname%';
            rss_add_http_header($this->context, 'mod_datalynx', $this->data, $rsstitle);
        }

        // COMMENTS.
        if (!empty($params->comments)) {
            require_once("$CFG->dirroot/comment/lib.php");
            comment::init();
        }

        $fs = get_file_storage();

        // PAGE setup for activity pages only.

        if ($page != 'external') {
            // Is user editing.
            $urlparams['edit'] = optional_param('edit', -1, PARAM_BOOL);
            $PAGE->set_url("/mod/datalynx/$page.php", $urlparams);
            $PAGE->set_context($this->context);
            // Blocks editing button (omit in embedded datalynxs).
            if ($page != 'embed' && $PAGE->user_allowed_editing()) {
                // Teacher editing mode.
                if ($urlparams['edit'] != -1) {
                    $USER->editing = $urlparams['edit'];
                }

                $buttons = '<table><tr><td><form method="get" action="' . $PAGE->url . '"><div>' .
                        '<input type="hidden" name="d" value="' . $thisid . '" />' .
                        '<input type="hidden" name="edit" value="' . ($PAGE->user_is_editing() ? 0 : 1) .
                        '" />' . '<input type="submit" value="' .
                        get_string($PAGE->user_is_editing() ? 'blockseditoff' : 'blocksediton') .
                        '" /></div></form></td></tr></table>';
                $PAGE->set_button($buttons);
            }

            // Auto refresh.
            if (!empty($urlparams['refresh'])) {
                $PAGE->set_periodic_refresh_delay($urlparams['refresh']);
            }

            // Page layout.
            if (!empty($params->pagelayout)) {
                $PAGE->set_pagelayout($params->pagelayout);
            }

            // Set css body class to add top level category of the dl instance.
            $category = core_course_category::get($PAGE->course->category, MUST_EXIST, true);
            $categories = explode("/", $category->path);
            if (isset($categories[1])) {
                $bodyclass = "top-cat-{$categories[1]}";
            } else {
                $bodyclass = "top-cat";
            }
            $PAGE->add_body_class($bodyclass);

            $PAGE->requires->css(
                new moodle_url(
                    $CFG->wwwroot . '/mod/datalynx/field/picture/zoomable/zoomable.css'
                )
            );

            // If completion is on: Mark activity as viewed.
            if (!empty($params->completion)) {
                require_once($CFG->libdir . '/completionlib.php');
                $completion = new completion_info($this->course);
                $completion->set_module_viewed($this->cm);
            }

            $modulename = $this->name();
            $viewid = (!empty($urlparams['view']) ? $urlparams['view'] : (!empty(
                $this->data->defaultview
            ) ? $this->data->defaultview : 0));
            if ($page == 'view' && $viewid) {
                global $DB;
                $viewname = $DB->get_field('datalynx_views', 'name', ['id' => $viewid]);
                $pagestring = get_string('page');
                $pageparam = optional_param('page', 0, PARAM_INT);
                $pagenum = !empty($pageparam) ? $pageparam + 1 : 1;
                $editmode = optional_param('editentries', 0, PARAM_SEQUENCE);
                $editstring = get_string('editmode', 'datalynx');
                $edit = $editmode ? " ({$editstring})" : "";
                $editbodyclass = $editmode ? 'datalynx-editentry' : 'datalynx-displayentry';
                $pagename = "{$modulename}: {$viewname}: {$pagestring} {$pagenum}{$edit}";
                $PAGE->set_title($pagename);
                $PAGE->add_body_classes([$editbodyclass]);
            } else {
                $manage = get_string('managemode', 'datalynx');
                $what = strpos($page, 'view') !== false ? get_string('views', 'datalynx') : '???';
                $what = strpos($page, 'field') !== false ? get_string('fields', 'datalynx') : $what;
                $what = strpos($page, 'filter') !== false ? get_string('filters', 'datalynx') : $what;
                $what = strpos($page, 'customfilter') !== false ? get_string('customfilters', 'datalynx') : $what;
                $what = strpos($page, 'rule') !== false ? get_string('rules', 'datalynx') : $what;
                $what = strpos($page, 'tool') !== false ? get_string('tools', 'datalynx') : $what;
                $what = strpos($page, 'js') !== false ? get_string('jsinclude', 'datalynx') : $what;
                $what = strpos($page, 'css') !== false ? get_string('cssinclude', 'datalynx') : $what;
                $what = strpos($page, 'preset') !== false ? get_string('presets', 'datalynx') : $what;
                $what = strpos($page, 'import') !== false ? get_string('import', 'datalynx') : $what;
                $what = strpos($page, 'statistics') !== false ? get_string('statistics', 'datalynx') : $what;
                $what = strpos($page, 'behavior') !== false ? get_string('behaviors', 'datalynx') : $what;
                $what = strpos($page, 'renderer') !== false ? get_string('renderers', 'datalynx') : $what;
                $pagename = "{$modulename}: {$what} ({$manage})";
                $PAGE->set_title($pagename);
            }
            $PAGE->set_heading($this->course->fullname);
        }

        // PAGE setup for datalynx content anywhere.

        // Use this to return css if this df page is set after header.
        $output = '';

        // CSS (cannot be required after head).
        $cssurls = [];
        if (!empty($params->css)) {
            // Js includes from the js template.
            if ($this->data->cssincludes) {
                foreach (explode("\n", $this->data->cssincludes) as $cssinclude) {
                    $cssinclude = trim($cssinclude);
                    if ($cssinclude) {
                        $cssurls[] = new moodle_url($cssinclude);
                    }
                }
            }
            // Uploaded css files.
            if (
                $files = $fs->get_area_files(
                    $this->context->id,
                    'mod_datalynx',
                    'css',
                    0,
                    'sortorder',
                    false
                )
            ) {
                $path = "/{$this->context->id}/mod_datalynx/css/0";
                foreach ($files as $file) {
                    $filename = $file->get_filename();
                    $cssurls[] = moodle_url::make_file_url('/pluginfile.php', "$path/$filename");
                }
            }
            // Css code from the css template.
            if ($this->data->css) {
                $cssurls[] = new moodle_url('/mod/datalynx/css.php', ['d' => $thisid]);
            }
        }
        if ($PAGE->state == moodle_page::STATE_BEFORE_HEADER) {
            foreach ($cssurls as $cssurl) {
                $PAGE->requires->css($cssurl);
            }
        } else {
            $attrs = ['rel' => 'stylesheet', 'type' => 'text/css'];
            foreach ($cssurls as $cssurl) {
                $attrs['href'] = $cssurl;
                $output .= html_writer::empty_tag('link', $attrs) . "\n";
                unset($attrs['id']);
            }
        }

        // JS.
        $jsurls = [];
        if (!empty($params->js)) {
            // Js includes from the js template.
            if ($this->data->jsincludes) {
                foreach (explode("\n", $this->data->jsincludes) as $jsinclude) {
                    $jsinclude = trim($jsinclude);
                    if ($jsinclude) {
                        $jsurls[] = new moodle_url($jsinclude);
                    }
                }
            }
            // Uploaded js files.
            if (
                $files = $fs->get_area_files(
                    $this->context->id,
                    'mod_datalynx',
                    'js',
                    0,
                    'sortorder',
                    false
                )
            ) {
                $path = "/{$this->context->id}/mod_datalynx/js/0";
                foreach ($files as $file) {
                    $filename = $file->get_filename();
                    $jsurls[] = moodle_url::make_file_url('/pluginfile.php', "$path/$filename");
                }
            }
            // Js code from the js template.
            if ($this->data->js) {
                $jsurls[] = new moodle_url('/mod/datalynx/js.php', ['d' => $thisid]);
            }
        }
        foreach ($jsurls as $jsurl) {
            $PAGE->requires->js($jsurl);
        }

        // Set current view and view's page requirements.
        $currentview = !empty($urlparams['view']) ? $urlparams['view'] : 0;
        $this->currentview = $this->get_current_view_from_id($currentview);

        // If a new datalynx or incomplete design, direct manager to manage area.
        if ($manager) {
            $viewrecords = $this->get_view_records();
            if (empty($viewrecords)) {
                if ($page == 'view' || $page == 'embed') {
                    $getstarted = new stdClass();
                    $getstarted->presets = html_writer::link(
                        new moodle_url('/mod/datalynx/preset/index.php', ['d' => $thisid]),
                        get_string('presets', 'datalynx')
                    );
                    $getstarted->fields = html_writer::link(
                        new moodle_url('/mod/datalynx/field/index.php', ['d' => $thisid]),
                        get_string('fields', 'datalynx')
                    );
                    $getstarted->views = html_writer::link(
                        new moodle_url('/mod/datalynx/view/index.php', ['d' => $thisid]),
                        get_string('views', 'datalynx')
                    );

                    $this->notifications['bad']['getstarted'] = html_writer::tag(
                        'div',
                        get_string('getstarted', 'datalynx', $getstarted),
                        ['class' => 'mdl-left']
                    );
                }
            } else {
                if (!$this->data->defaultview) {
                    $linktoviews = html_writer::link(
                        new moodle_url('/mod/datalynx/view/index.php', ['d' => $thisid]),
                        get_string('views', 'datalynx')
                    );
                    $this->notifications['bad']['defaultview'] = get_string('viewnodefault', 'datalynx', $linktoviews);
                }
            }
        }

        return $output;
    }

    /**
     * prints the header of the current datalynx page
     *
     * @param ?array $params
     */
    public function print_header(?array $params = null) {
        global $OUTPUT, $CFG;

        $params = (object) $params;

        echo $OUTPUT->header();

        // Print intro.
        if (!empty($params->heading)) {
            echo $OUTPUT->heading(format_string($this->name()));
        }

        // Print intro.
        if (!empty($params->intro) && $params->intro) {
            $this->print_intro();
        }

        // Print the tabs.
        if (!empty($params->tab)) {
            $currenttab = $params->tab;
            include($CFG->dirroot . '/mod/datalynx/tabs.php');
        }

        // Print groups menu if needed.
        if (!empty($params->groups)) {
            $this->print_groups_menu($params->urlparams->view, $params->urlparams->filter);
        }

        // TODO: MDL-00000 Explore letting view decide whether to print rsslink and intro.

        // Print any notices.
        if (empty($params->nonotifications)) {
            foreach ($this->notifications['good'] as $notification) {
                if (!empty($notification)) {
                    echo $OUTPUT->notification($notification, 'notifysuccess'); // Good (usually.
                    // Green).
                }
            }
            foreach ($this->notifications['bad'] as $notification) {
                if (!empty($notification)) {
                    echo $OUTPUT->notification($notification); // Bad (usually red).
                }
            }
        }
    }

    /**
     * prints the footer of the current datalynx page
     *
     * @param ?array $params
     */
    public function print_footer(?array $params = null) {
        global $OUTPUT;

        echo $OUTPUT->footer();
    }

    /**
     * TODO: consider moving into the view
     *
     * @param mixed $view The view id.
     * @param mixed $filter The filter id.
     */
    public function print_groups_menu($view, $filter) {
        if ($this->groupmode && !in_array($this->groupmode, $this->internalgroupmodes)) {
            $returnurl = new moodle_url(
                "/mod/datalynx/{$this->pagefile}.php",
                ['d' => $this->id(), 'view' => $view, 'filter' => $filter]
            );
            groups_print_activity_menu($this->cm, $returnurl . '&amp;');
        }
    }

    /**
     * TODO: consider moving into the view
     */
    public function print_rsslink() {
        global $USER, $CFG;
        // Link to the RSS feed.
        if (
            !empty($CFG->enablerssfeeds) && !empty($CFG->datalynx_enablerssfeeds) &&
                $this->data->rssarticles > 0
        ) {
            echo '<div style="float:right;">';
            rss_print_link($this->course->id, $USER->id, 'datalynx', $this->id(), get_string('rsstype'));
            echo '</div>';
            echo '<div style="clear:both;"></div>';
        }
    }

    /**
     * TODO: consider moving into the view
     */
    public function print_intro() {
        global $OUTPUT;
        // TODO: MDL-00000 Make intro stickily closable. Display the intro only when there are no pages.
        if ($this->data->intro) {
            $options = new stdClass();
            $options->noclean = true;
            echo $OUTPUT->box(format_module_intro('datalynx', $this->data, $this->cm->id), 'generalbox', 'intro');
        }
    }

    /**
     * Set view content.
     */
    public function set_content() {
        if (!empty($this->currentview)) {
            $this->currentview->process_data();
            $this->currentview->set_content();
        }
    }

    /**
     * Output the view
     *
     * @throws \coding_exception
     */
    public function display(): void {
        global $PAGE;
        if (!empty($this->currentview)) {
            $event = event\course_module_viewed::create(
                ['objectid' => $PAGE->cm->instance, 'context' => $PAGE->context]
            );
            $event->add_record_snapshot('course', $PAGE->course);
            $event->trigger();
            $this->currentview->display();
        }
    }

    /**
     * Returns datalynx content for inline display.
     * Used in mod_datalynxcoursepage only.
     *
     * @param int $datalynxid The id of the datalynx whose content should be displayed.
     * @param int $viewid The id of the datalynx's view whose content should be displayed.
     * @param ?int $eids The id of the datalynx entrie that should be displayed.
     * @param array $options bool skiplogincheck, bool tohtml, string pagelayout.
     * @return string
     */
    public static function get_content_inline(
        int $datalynxid,
        int $viewid = 0,
        ?int $eids = null,
        array $options = ['tohtml' => true]
    ) {
        $urlparams = new stdClass();
        $datalynx = new datalynx($datalynxid);
        $datalynx->views = $datalynx->get_all_views();
        $urlparams->d = $datalynxid;
        $urlparams->view = $viewid;
        $skiplogincheck = $options['skiplogincheck'] ?? false;

        // It's used nowhere but keeping for backwards compatibility.
        $urlparams->pagelayout = 'external';
        $filteroptions = true;

        // In case we come from the app, do stuff.
        if (isset($options['pagelayout']) && $options['pagelayout'] == 'mobile') {
            $urlparams->pagelayout = 'mobile';
            $filteroptions = ['eids' => $eids];
        }

        // NOTE: The app ignores urlparams, added filter to view constructor.
        if ($eids) {
            $urlparams->eids = $eids;
        }

        $pageparams = ['js' => true, 'css' => true, 'rss' => true, 'modjs' => true,
                'completion' => true, 'comments' => true, 'urlparams' => $urlparams];
        $datalynx->set_page('external', $pageparams, $skiplogincheck);
        $type = $datalynx->views[$viewid]->type;
        $view = $datalynx->get_view($type, $viewid, $filteroptions);
        $datalynx->currentview = $view;
        $view->set_content();

        $viewcontent = $view->display($options);
        return "$viewcontent";
    }

    /**
     * ********************************************************************************
     * FIELDS
     * *******************************************************************************
     */

    /**
     * Initialize if needed and return the internal fields
     *
     * @return array
     */
    public function get_internal_fields(): array {
        if (!$this->internalfields) {
            $fieldplugins = get_list_of_plugins('mod/datalynx/field/');
            foreach ($fieldplugins as $fieldname) {
                $fieldclass = "datalynxfield_{$fieldname}\\field";
                if (!$fieldclass::is_internal()) {
                    continue;
                }
                // By this time, $this->fields should be filled up.
                $internalfields = $fieldclass::get_field_objects($this->data->id, $this->fields);
                foreach ($internalfields as $fid => $field) {
                    $this->internalfields[$fid] = $this->get_field($field);
                }
            }
        }

        return $this->internalfields;
    }

    /**
     * Return the names of the internal fields
     * @return array
     */
    public function get_internal_fields_names(): array {
        $fieldplugins = get_list_of_plugins('mod/datalynx/field/');
        $internalfieldsnames = [];
        foreach ($fieldplugins as $fieldname) {
            $fieldclass = "datalynxfield_{$fieldname}\\field";
            if (!$fieldclass::is_internal()) {
                continue;
            }
            $internalfields = $fieldclass::get_field_objects($this->data->id);
            foreach ($internalfields as $fid => $field) {
                $internalfieldsnames[$fid] = $field->name;
            }
        }

        return $internalfieldsnames;
    }

    /**
     * Returns an array of fields, suitable for use in customfilter form.
     *
     * @return array of strings
     */
    public function get_customfilterfieldtypes() {
        if (!$this->customfilterfields) {
            $this->customfilterfields = [];
            // Collate customfilter fields.
            $fieldplugins = get_list_of_plugins('mod/datalynx/field/');
            foreach ($fieldplugins as $fieldname) {
                $fieldclass = "datalynxfield_{$fieldname}\\field";
                if ($fieldclass::is_internal()) {
                    continue;
                }
                if ($fieldclass::is_customfilterfield()) {
                    $this->customfilterfields[] = $fieldname;
                }
            }
        }

        return $this->customfilterfields;
    }

    /**
     * Given a field id return the field object from get_fields
     * Initializes get_fields if necessary
     *
     * @param int $fieldid
     * @param bool $forceget
     * @return \datalynxfield_base|bool
     */
    public function get_field_from_id($fieldid, $forceget = false) {
        $fields = $this->get_fields(null, false, $forceget);

        if (empty($fields[$fieldid])) {
            return false;
        } else {
            return $fields[$fieldid];
        }
    }

    /**
     * Given a field type returns the field object from get_fields
     * Initializes get_fields if necessary
     *
     * @param string $type
     * @param bool $menu
     * @return \datalynxfield_base[]
     */
    public function get_fields_by_type($type, $menu = false): array {
        $typefields = [];
        foreach ($this->get_fields() as $fieldid => $field) {
            if ($field->type() === $type) {
                if ($menu) {
                    $typefields[$fieldid] = $field->name();
                } else {
                    $typefields[$fieldid] = $field;
                }
            }
        }
        return $typefields;
    }

    /**
     * Given a field name returns the field object from get_fields
     *
     * @param string $name
     * @return \datalynxfield_base|bool
     */
    public function get_field_by_name($name) {
        foreach ($this->get_fields() as $field) {
            if ($field->name() === $name) {
                return $field;
            }
        }
        return false;
    }

    /**
     * returns a subclass field object given a record of the field
     * used to invoke plugin methods
     * input: $param $field record from db, or field type
     *
     * @param mixed $key
     * @return bool|\datalynxfield_base
     */
    public function get_field($key) {
        if ($key) {
            if (is_object($key)) {
                $type = $key->type;
            } else {
                $type = $key;
                $key = 0;
            }
            $fieldclass = 'datalynxfield_' . $type . '\field';
            $field = new $fieldclass($this, $key);
            return $field;
        } else {
            return false;
        }
    }

    /**
     * Returns a subclass field object given a record of the field
     * used to invoke plugin methods
     *
     * @param mixed $key
     * @return bool|\datalynxfield_base
     */
    public function get_fieldname($key) {
        if ($key) {
            if (is_object($key)) {
                $type = $key->type;
            } else {
                $type = $key;
                $key = 0;
            }
            $fieldclass = 'datalynxfield_' . $type . '\field';
            $field = new $fieldclass($this, $key);
            return $field;
        } else {
            return false;
        }
    }

    /**
     * Get fields of datalynx instance
     *
     * @param ?array $exclude
     * @param bool $menu
     * @param bool $forceget
     * @param string $sort
     * @return \datalynxfield_base[]
     */
    public function get_fields(?array $exclude = null, $menu = false, $forceget = false, $sort = ''): array {
        global $DB;

        if (!$this->fields || $forceget) {
            $this->fields = [];
            // Collate user fields.
            if ($fields = $DB->get_records('datalynx_fields', ['dataid' => $this->id()], $sort)) {
                foreach ($fields as $fieldid => $field) {
                    $this->fields[$fieldid] = $this->get_field($field);
                }
            }
        }

        // Collate all fields.
        $fields = $this->fields + $this->get_internal_fields();

        if (empty($exclude) && !$menu) {
            return $fields;
        } else {
            $retfields = [];
            foreach ($fields as $fieldid => $field) {
                if (!empty($exclude) && in_array($fieldid, $exclude)) {
                    continue;
                }
                if ($menu) {
                    $retfields[$fieldid] = $field->name();
                } else {
                    $retfields[$fieldid] = $field;
                }
            }
            return $retfields;
        }
    }

    /**
     * Returns an array of field names for this datalynx instance.
     *
     * @param string $sort
     * @return array
     * @throws \dml_exception
     */
    public function get_fieldnames($sort = '') {
        global $DB;

        $fieldnames = [];
        // Collate user fields.
        if ($fields = $DB->get_records('datalynx_fields', ['dataid' => $this->id()], $sort)) {
            foreach ($fields as $fieldid => $field) {
                $fieldnames[$fieldid] = $field->name;
            }
        }

        // Collate all fields.
        $retfieldnames = $fieldnames + $this->get_internal_fields_names();

        return $retfieldnames;
    }

    /**
     * Find filters via fields
     *
     * @param array $fields
     * @return array
     * @throws \dml_exception
     */
    private function find_filters_using_fields($fields) {
        global $DB;
        $filters = $DB->get_records('datalynx_filters', ['dataid' => $this->id()]);
        $usedfilters = [];
        $fieldids = array_keys($fields);
        foreach ($filters as $filter) {
            $custormsort = unserialize($filter->customsort);
            $custormsort = $custormsort ? array_keys($custormsort) : [];

            $customsearch = unserialize($filter->customsearch);
            $customsearch = $customsearch ? array_keys($customsearch) : [];

            if (array_intersect($fieldids, $customsearch) || array_intersect($fieldids, $custormsort)) {
                $usedfilters[] = $filter;
            }
        }
        return $usedfilters;
    }

    /**
     * Checks if string contains html tags
     *
     * @param string $string
     * @return boolean
     */
    private function is_html($string) {
        return preg_match("/<[^<]+>/", $string) != 0;
    }

    /**
     * Process the action triggered by user for a specific field
     *
     * @param string $action
     * @param string $fids (comma separated numbers of field ids)
     * @param boolean $confirmed
     * @return array|bool
     */
    public function process_fields($action, $fids, $confirmed = false) {
        global $OUTPUT, $DB;

        if (!has_capability('mod/datalynx:managetemplates', $this->context)) {
            // TODO: MDL-00000 Throw exception when capability check fails.
            return false;
        }

        $dffields = $this->get_fields();
        $fields = [];
        // Collate the fields for processing.
        if ($fieldids = explode(',', $fids)) {
            foreach ($fieldids as $fieldid) {
                if (is_numeric($fieldid) && $fieldid > 0 && isset($dffields[$fieldid])) {
                    $fields[$fieldid] = $dffields[$fieldid];
                }
            }
        }

        $processedfids = [];
        $strnotify = '';

        if (empty($fields) && $action != 'add') {
            $this->notifications['bad'][] = get_string("fieldnoneforaction", 'datalynx');
            return false;
        } else {
            if (!$confirmed) {
                // Print header.
                $this->print_header('fields');

                $msg = get_string("fieldsconfirm$action", 'datalynx', count($fields));
                if ($action === 'delete') {
                    $fieldlist = array_reduce(
                        $fields,
                        function ($list, $field) {
                            return $list . "<li>{$field->field->name}</li>";
                        },
                        ''
                    );
                    $fieldlist = "<ul>$fieldlist</ul>";
                    $filters = $this->find_filters_using_fields($fields);
                    $filterlist = array_reduce(
                        $filters,
                        function ($list, $filter) {
                            return $list . "<li>{$filter->name}</li>";
                        },
                        ''
                    );
                    $filterlist = "<ul>$filterlist</ul>";
                    if ($filters) {
                        echo "<div class=\"alert alert-warning\">" .
                                get_string(
                                    'deletefieldfilterwarning',
                                    'datalynx',
                                    ['fieldlist' => $fieldlist, 'filterlist' => $filterlist,
                                    ]
                                ) . "</div>";
                        echo $OUTPUT->continue_button(
                            new moodle_url(
                                '/mod/datalynx/field/index.php',
                                ['d' => $this->id()]
                            )
                        );

                        echo $OUTPUT->footer();
                        exit();
                    }
                }

                echo $OUTPUT->confirm(
                    $msg,
                    new moodle_url(
                        '/mod/datalynx/field/index.php',
                        ['d' => $this->id(),
                                        $action => implode(',', array_keys($fields)),
                                        'sesskey' => sesskey(), 'confirmed' => 1]
                    ),
                    new moodle_url('/mod/datalynx/field/index.php', ['d' => $this->id()])
                );

                echo $OUTPUT->footer();
                exit();
            } else {
                // Go ahead and perform the requested action.
                switch ($action) {
                    case 'add':
                        if ($forminput = data_submitted()) {
                            // Check for arrays and convert to a comma-delimited string.
                            $this->convert_arrays_to_strings($forminput);

                            // Create a field object to collect and store the data safely.
                            $field = $this->get_field($forminput->type);
                            $field->insert_field($forminput);

                            $other = ['dataid' => $this->id()];
                            $event = event\field_created::create(
                                ['context' => $this->context,
                                            'objectid' => $field->field->id, 'other' => $other,
                                    ]
                            );
                            $event->trigger();
                        }
                        $strnotify = 'fieldsadded';
                        break;

                    case 'update': // Update existing.
                        if ($forminput = data_submitted()) {
                            // Check for arrays and convert to a comma-delimited string.
                            $this->convert_arrays_to_strings($forminput);

                            // Create a field object to collect and store the data safely.
                            $field = reset($fields);
                            $oldfieldname = $field->field->name;
                            $field->update_field($forminput);

                            $other = ['dataid' => $this->id()];
                            $event = event\field_updated::create(
                                ['context' => $this->context,
                                            'objectid' => $field->field->id, 'other' => $other,
                                    ]
                            );
                            $event->trigger();

                            // Update the views.
                            if ($oldfieldname != $field->field->name) {
                                $this->replace_field_in_views($oldfieldname, $field->field->name);
                            }
                        }
                        $strnotify = 'fieldsupdated';
                        break;

                    case 'editable':
                        foreach ($fields as $fid => $field) {
                            // Lock = 0; unlock = -1;.
                            $editable = $field->field->edits ? 0 : -1;
                            $DB->set_field(
                                'datalynx_fields',
                                'edits',
                                $editable,
                                ['id' => $fid]
                            );
                            $processedfids[] = $fid;
                            $other = ['dataid' => $this->id()];
                            $event = event\field_updated::create(
                                ['context' => $this->context, 'objectid' => $fid,
                                            'other' => $other,
                                    ]
                            );
                            $event->trigger();
                        }

                        $strnotify = '';
                        break;

                    case 'duplicate':
                        foreach ($fields as $field) {
                            // Set new name.
                            while ($this->name_exists('fields', $field->name())) {
                                $field->field->name .= '_1';
                            }
                            $fieldid = $DB->insert_record('datalynx_fields', $field->field);
                            $processedfids[] = $fieldid;

                            $other = ['dataid' => $this->id()];
                            $event = event\field_created::create(
                                ['context' => $this->context, 'objectid' => $fieldid,
                                            'other' => $other,
                                    ]
                            );
                            $event->trigger();
                        }

                        $strnotify = 'fieldsadded';
                        break;

                    case 'delete':
                        foreach ($fields as $field) {
                            $field->delete_field();
                            $processedfids[] = $field->field->id;
                            // Update views.
                            $this->replace_field_in_views($field->field->name, '');

                            $other = ['dataid' => $this->id()];
                            $event = event\field_deleted::create(
                                ['context' => $this->context,
                                            'objectid' => $field->field->id, 'other' => $other,
                                    ]
                            );
                            $event->trigger();
                        }
                        $strnotify = 'fieldsdeleted';
                        break;

                    case 'convert':
                        foreach ($fields as $fid => $field) {
                            $processedfids[] = $field->field->id;

                            // Convert field content to HTML.
                            $contents = $DB->get_records(
                                'datalynx_contents',
                                ['fieldid' => $fid],
                                null,
                                'id,content'
                            );
                            $htmlcontent = new stdClass();
                            if ($contents) {
                                foreach ($contents as $contentid => $content) {
                                    if (!$this->is_html($content->content)) {
                                        $htmlcontent->id = $contentid;
                                        $htmlcontent->content = format_text($content->content, FORMAT_PLAIN);
                                        $htmlcontent->content1 = FORMAT_HTML;
                                        $DB->update_record('datalynx_contents', $htmlcontent);
                                    }
                                }
                            }
                            // Convert field type to editor.
                            $DB->set_field('datalynx_fields', 'type', 'editor', ['id' => $fid]);

                            $other = ['dataid' => $this->id()];
                            $event = event\field_updated::create(
                                ['context' => $this->context, 'objectid' => $fid, 'other' => $other]
                            );
                            $event->trigger();
                        }
                        $strnotify = 'fieldsupdated';
                        break;

                    default:
                        break;
                }

                if ($strnotify) {
                    $fieldsprocessed = $processedfids ? count($processedfids) : 'No';
                    $this->notifications['good'][] = get_string($strnotify, 'datalynx', $fieldsprocessed);
                }
                return $processedfids;
            }
        }
    }

    /**
     * ********************************************************************************
     * VIEWS
     * *******************************************************************************
     */

    /**
     * Retrieves the views associated with the current
     * datalynx instance visible to the current user
     * Updates $this->views
     *
     * @param bool $forceget if true, the entries will be reread from the database
     * @param string $sort SQL ORDER BY clause
     * @return array an array of datalynx_views entry objects
     */
    public function get_view_records(bool $forceget = false, string $sort = ''): array {
        global $DB;
        if (empty($this->views) || $forceget) {
            $views = $DB->get_records('datalynx_views', ['dataid' => $this->id()], $sort);
            // Reinitialise the views array.
            $this->views = [];
            if (empty($views)) {
                return $views;
            }
            foreach ($views as $viewid => $view) {
                if ($this->is_visible_to_user($view) && $this->is_browsable_view($view)) {
                    $this->views[$viewid] = $view;
                }
            }
        }
        return $this->views;
    }

    /**
     * Get view subclass objects editable by the user
     *
     * @param string $sort SQL ORDER BY clause
     * @return array an array of datalynx_views subclass objects
     */
    public function get_views_editable_by_user(string $sort = ''): array {
        global $DB;
        if (has_capability('mod/datalynx:managetemplates', $this->context)) {
            $views = $DB->get_records('datalynx_views', ['dataid' => $this->id()], $sort);
            $returnviews = [];
            if (!empty($views)) {
                foreach ($views as $viewid => $view) {
                    $returnviews[$viewid] = $this->get_view($view->type, $view);
                }
            }
            return $returnviews;
        }
        return [];
    }

    /**
     * Get view by name. Return the view record as stdClass or an empty stdClass if view was not found.
     *
     * @param string $viewname
     * @return stdClass
     */
    public function get_view_by_name(string $viewname): stdClass {
        $views = $this->get_view_records(true);
        foreach ($views as $view) {
            if ($view->name === $viewname) {
                return $view;
            }
        }
        return new stdClass();
    }

    /**
     * Get all views of a datalynx instance
     *
     * @return array of view objects indexed by view id, empty array if no views are found
     */
    public function get_all_views() {
        global $DB;
        $views = [];
        if (!$views = $DB->get_records('datalynx_views', ['dataid' => $this->id()])) {
            return [];
        } else {
            return $views;
        }
    }

    /**
     * Verifies whether the current user has the needed
     * permission to access a particular datalynx view.
     *
     * @param stdClass $view datalynx_views entry
     * @return bool true if user can see the view, false otherwise
     */
    public function is_visible_to_user(stdClass $view): bool {
        $isadmin = has_capability('mod/datalynx:viewprivilegeadmin', $this->context, null, true);
        $mask = has_capability('mod/datalynx:viewprivilegemanager', $this->context, null, false) ? 1 : 0;
        $mask |= has_capability('mod/datalynx:viewprivilegeteacher', $this->context, null, false) ? 2 : 0;
        $mask |= has_capability('mod/datalynx:viewprivilegestudent', $this->context, null, false) ? 4 : 0;
        $mask |= has_capability('mod/datalynx:viewprivilegeguest', $this->context, null, false) ? 8 : 0;
        return $isadmin || ($view->visible & $mask);
    }

    /**
     * Whether a view type is internal-only and excluded from the browse experience.
     *
     * @param string $type
     * @return bool
     */
    public function is_internal_view_type(string $type): bool {
        return $type === self::INTERNAL_VIEW_EMAIL;
    }

    /**
     * Whether a view record should participate in the browse experience.
     *
     * @param stdClass $view
     * @return bool
     */
    public function is_browsable_view(stdClass $view): bool {
        return !$this->is_internal_view_type($view->type);
    }

    /**
     * TODO there is no need to instantiate all views!!!
     * this function creates an instance of the particular subtemplate class
     *
     * @param int $viewid
     * @return \mod_datalynx\local\view\base|bool
     */
    public function get_current_view_from_id(int $viewid = 0) {
        if ($views = $this->get_view_records()) {
            if ($viewid && isset($views[$viewid])) {
                $view = $views[$viewid];
                // If can't find the requested, try the default.
            } else {
                if ($this->data->defaultview && isset($views[$this->data->defaultview])) {
                    $view = $views[$this->data->defaultview];
                } else {
                    return false;
                }
            }
            return $this->get_view($view->type, $view);
        }
        return false;
    }

    /**
     * TODO there is no need to instantiate all views!!!
     * this function creates an instance of the particular subtemplate class *
     *
     * @param int $viewid
     * @return \mod_datalynx\local\view\base|bool
     */
    public function get_view_from_id(int $viewid = 0) {
        // Get views visible to user.
        $views = $this->get_view_records();
        if (!empty($views)) {
            if ($viewid && isset($views[$viewid])) {
                $view = $views[$viewid];
                // If can't find the requested, try the default.
            } else {
                if ($viewid = $this->data->defaultview && isset($views[$viewid])) {
                    $view = $views[$viewid];
                } else {
                    return false;
                }
            }
            return $this->get_view($view->type, $view);
        }
        return false;
    }

    /**
     * Resolve the concrete class name for a datalynx view type.
     *
     * @param string $type
     * @return string
     */
    private function get_view_classname(string $type): string {
        global $CFG;

        $type = trim($type);
        if ($type === '') {
            throw new coding_exception('View type must be provided.');
        }

        $viewpath = $CFG->dirroot . '/mod/datalynx/view/' . $type . '/classes/view.php';
        if (!is_readable($viewpath)) {
            throw new coding_exception('Invalid view type: ' . $type);
        }

        require_once($viewpath);
        $viewclass = 'datalynxview_' . $type . '\\view';
        if (!class_exists($viewclass)) {
            throw new coding_exception('Missing view class for type: ' . $type);
        }

        return $viewclass;
    }

    /**
     * Instantiate a datalynx view by type.
     *
     * @param string $type View type.
     * @param stdClass|int $view Existing view record or id, or 0 for a new unsaved view.
     * @param bool $filteroptions Whether to apply filter options.
     * @return base
     */
    public function get_view(string $type, stdClass|int $view = 0, bool $filteroptions = true): base {
        $viewclass = $this->get_view_classname($type);
        return new $viewclass($this, $view, $filteroptions);
    }

    /**
     * given a view type returns the view object from $this->views
     * Initializes $this->views if necessary
     *
     * @param string $type
     * @param bool $forceget
     * @return array|bool
     */
    public function get_views_by_type($type, $forceget = false) {
        $views = $this->get_view_records($forceget);
        if (empty($views)) {
            return false;
        }

        $typeviews = [];
        foreach ($views as $viewid => $view) {
            if ($view->type === $type) {
                $typeviews[$viewid] = $this->get_view($view->type, $view);
            }
        }
        return $typeviews;
    }

    /**
     * Get the view objects visible to the user as an array indexed by the view id
     *
     * @param array $exclude array of viewids to exclude
     * @param bool $forceget true to get from db directly
     * @param string $sort SQL ORDER BY clause
     * @return \mod_datalynx\local\view\base[] array of view objects indexed by view id or empty array if no views are found
     */
    public function get_views(array $exclude = [], bool $forceget = false, string $sort = ''): array {
        if (empty($this->get_view_records($forceget, $sort))) {
            return [];
        }

        static $views = null;
        if ($views === null || $forceget) {
            $views = [];
            foreach ($this->views as $viewid => $view) {
                if (!empty($exclude) && in_array($viewid, $exclude)) {
                    continue;
                }
                $views[$viewid] = $this->get_view($view->type, $view);
            }
        }
        return $views;
    }

    /**
     * Get all viewnames visible to the user of a datalynx instance as an array indexed by viewid
     *
     * @param ?array $exclude
     * @param boolean $forceget
     * @param string $sort
     * @return array $viewids[viewid]
     */
    public function get_views_menu(?array $exclude = null, $forceget = false, $sort = '') {
        $views = [];

        if (!empty($this->get_view_records($forceget, $sort))) {
            foreach ($this->views as $viewid => $view) {
                if (!empty($exclude) && in_array($viewid, $exclude)) {
                    continue;
                }
                $views[$viewid] = $view->name;
            }
        }
        return $views;
    }

    /**
     * Set default view
     *
     * @param int $viewid
     */
    public function set_default_view(int $viewid = 0): void {
        global $DB;

        if ($viewid && $this->is_internal_view_id($viewid)) {
            throw new moodle_exception('invalidview', 'datalynx', '', $viewid);
        }

        $rec = new stdClass();
        $rec->id = $this->id();
        $rec->defaultview = $viewid;
        if (!$DB->update_record('datalynx', $rec)) {
            throw new moodle_exception('Failed to update the database');
        }
        $this->data->defaultview = $viewid;
    }

    /**
     * Set default filter
     *
     * @param int $filterid
     */
    public function set_default_filter(int $filterid = 0): void {
        global $DB;

        $rec = new stdClass();
        $rec->id = $this->id();
        $rec->defaultfilter = $filterid;
        if (!$DB->update_record('datalynx', $rec)) {
            throw new moodle_exception('Failed to update the database');
        }
        $this->data->defaultfilter = $filterid;
    }

    /**
     * Set the default edit view
     *
     * @param int $viewid
     */
    public function set_single_edit_view(int $viewid = 0): void {
        global $DB;

        if ($viewid && $this->is_internal_view_id($viewid)) {
            throw new moodle_exception('invalidview', 'datalynx', '', $viewid);
        }

        $rec = new stdClass();
        $rec->id = $this->id();
        $rec->singleedit = $viewid;
        if (!$DB->update_record('datalynx', $rec)) {
            throw new moodle_exception('Failed to update the database');
        }
        $this->data->singleedit = $viewid;
    }

    /**
     * Set view that is linked with the more-view patterns
     *
     * @param int $viewid
     */
    public function set_single_more_view(int $viewid = 0): void {
        global $DB;

        if ($viewid && $this->is_internal_view_id($viewid)) {
            throw new moodle_exception('invalidview', 'datalynx', '', $viewid);
        }

        $rec = new stdClass();
        $rec->id = $this->id();
        $rec->singleview = $viewid;
        if (!$DB->update_record('datalynx', $rec)) {
            throw new moodle_exception('Failed to update the database');
        }
        $this->data->singleview = $viewid;
    }

    /**
     * Whether the given stored view id belongs to an internal-only view.
     *
     * @param int $viewid
     * @return bool
     */
    protected function is_internal_view_id(int $viewid): bool {
        global $DB;

        $type = $DB->get_field('datalynx_views', 'type', ['id' => $viewid]);
        return !empty($type) && $this->is_internal_view_type($type);
    }

    /**
     * Returns the default view id for this datalynx instance.
     *
     * @return int
     */
    public function get_default_view_id() {
        return $this->data->defaultview;
    }

    /**
     * Search for a field name and replaces it with another one in all the
     * form templates.
     * Set $newfieldname as '' if you want to delete the field from the form.
     *
     * @param string $searchfieldname
     * @param string $newfieldname
     */
    public function replace_field_in_views(string $searchfieldname, string $newfieldname): void {
        $views  = $this->get_views();
        if (!empty($views)) {
            foreach ($views as $view) {
                $view->replace_field_in_view($searchfieldname, $newfieldname);
            }
        }
    }

    /**
     * Apply actions to a view
     *
     * @param string $action
     * @param string $vids viewids comma separated
     * @param bool $confirmed
     * @return array|bool
     * @throws \coding_exception
     * @throws \dml_exception
     * @throws \file_exception
     * @throws \stored_file_creation_exception
     * @throws moodle_exception
     */
    public function process_views($action, $vids, $confirmed = false) {
        global $DB, $OUTPUT;

        if (!has_capability('mod/datalynx:managetemplates', $this->context)) {
            // TODO: MDL-00000 Throw exception when capability check fails.
            return false;
        }

        if ($vids) { // Some views are specified for action.
            $views = [];
            $viewobjs = $this->get_views([], true);
            foreach (explode(',', $vids) as $vid) {
                if (!empty($viewobjs[$vid])) {
                    $views[$vid] = $viewobjs[$vid];
                }
            }
        }

        $processedvids = [];
        $strnotify = '';

        if (empty($views)) {
            $this->notifications['bad'][] = get_string("viewnoneforaction", 'datalynx');
            return false;
        } else {
            if (!$confirmed) {
                // Print header.
                $this->print_header('views');

                // Print a confirmation page.
                echo $OUTPUT->confirm(
                    get_string("viewsconfirm$action", 'datalynx', count($views)),
                    new moodle_url(
                        '/mod/datalynx/view/index.php',
                        ['d' => $this->id(),
                                        $action => implode(',', array_keys($views)),
                        'sesskey' => sesskey(),
                            'confirmed' => 1]
                    ),
                    new moodle_url('/mod/datalynx/view/index.php', ['d' => $this->id()])
                );

                echo $OUTPUT->footer();
                exit();
            } else {
                // Go ahead and perform the requested action.
                switch ($action) {
                    case 'visible':
                        $updateview = new stdClass();
                        foreach ($views as $vid => $view) {
                            if ($vid == $this->data->defaultview) {
                                // TODO: MDL-00000 Notify when default view cannot be hidden.
                                continue;
                            } else {
                                $updateview->id = $vid;
                                $DB->update_record('datalynx_views', $updateview);

                                $other = ['dataid' => $this->id()];
                                $event = event\view_updated::create(
                                    ['context' => $this->context, 'objectid' => $vid,
                                    'other' => $other]
                                );
                                $event->trigger();

                                $processedvids[] = $vid;
                            }
                        }

                        $strnotify = '';
                        break;

                    case 'filter':
                        $updateview = new stdClass();
                        $filterid = optional_param('fid', 0, PARAM_INT);
                        foreach ($views as $vid => $view) {
                            if ($filterid != $view->view->filter) {
                                $updateview->id = $vid;
                                if ($filterid == -1) {
                                    $updateview->filter = 0;
                                } else {
                                    $updateview->filter = $filterid;
                                }
                                $DB->update_record('datalynx_views', $updateview);

                                $other = ['dataid' => $this->id()];
                                $event = event\view_updated::create(
                                    ['context' => $this->context, 'objectid' => $vid,
                                    'other' => $other]
                                );
                                $event->trigger();

                                $processedvids[] = $vid;
                            }
                        }

                        $strnotify = 'viewsupdated';
                        break;

                    case 'reset':
                        foreach ($views as $vid => $view) {
                            // Generate default view and update.
                            $view->generate_default_view();

                            // Update view.
                            $view->update($view->view);

                            $other = ['dataid' => $this->id()];
                            $event = event\view_updated::create(
                                ['context' => $this->context, 'objectid' => $vid,
                                'other' => $other]
                            );
                            $event->trigger();

                            $processedvids[] = $vid;
                        }

                        $strnotify = 'viewsupdated';
                        break;

                    case 'duplicate':
                        foreach ($views as $vid => $view) {
                            // TODO: MDL-00000 Check for view count limit before duplicating.

                            // Set name.
                            if ($this->name_exists('views', $view->name())) {
                                $copyname = $view->view->name = 'Copy of ' . $view->name();
                            }
                            $i = 2;
                            while ($this->name_exists('views', $view->name())) {
                                $view->view->name = $copyname . " ($i)";
                                $i++;
                            }
                            // Reset id.
                            $oldviewid = $view->view->id;
                            $view->view->id = 0;
                            $viewid = $view->add($view->view);

                            $newviewid = $viewid;
                            $contextid = $this->context->id;
                            $component = 'mod_datalynx';
                            $fs = get_file_storage();
                            foreach (
                                ['viewsection', 'viewparam2',
                                ] as $filearea
                            ) {
                                $files = $fs->get_area_files(
                                    $contextid,
                                    $component,
                                    $filearea,
                                    $oldviewid
                                );
                                foreach ($files as $file) {
                                    if ($file->is_directory() && $file->get_filepath() === '/') {
                                        continue;
                                    }
                                    $filerecord = ['contextid' => $contextid,
                                            'component' => $component, 'filearea' => $filearea,
                                            'itemid' => $newviewid,
                                    ];
                                    $fs->create_file_from_storedfile($filerecord, $file);
                                }
                            }

                            $other = ['dataid' => $this->id()];
                            $event = event\view_created::create(
                                ['context' => $this->context, 'objectid' => $newviewid,
                                'other' => $other]
                            );
                            $event->trigger();

                            $processedvids[] = $viewid;
                        }

                        $strnotify = 'viewsadded';
                        break;

                    case 'delete':
                        foreach ($views as $vid => $view) {
                            $view->delete();
                            $processedvids[] = $vid;

                            // Reset default view if needed.
                            if ($view->id() == $this->data->defaultview) {
                                $this->set_default_view();
                            }

                            $other = ['dataid' => $this->id()];
                            $event = event\view_deleted::create(
                                ['context' => $this->context, 'objectid' => $vid,
                                'other' => $other]
                            );
                            $event->trigger();
                        }

                        $strnotify = 'viewsdeleted';
                        break;

                    case 'default':
                        foreach ($views as $vid => $view) { // There should be only one.
                            $this->set_default_view($vid);
                            $processedvids[] = $vid;
                            break;
                        }
                        $strnotify = '';
                        break;

                    default:
                        break;
                }

                if ($strnotify) {
                    $viewsprocessed = $processedvids ? count($processedvids) : 'No';
                    $this->notifications['good'][] = get_string($strnotify, 'datalynx', $viewsprocessed);
                }
                return $processedvids;
            }
        }
    }

    /**
     * ********************************************************************************
     * USER
     * *******************************************************************************
     */

    /**
     * Returns users that appear in the gradebook for this datalynx instance.
     *
     * @param ?array $userids
     * @return ?array
     * @throws \coding_exception
     * @throws \dml_exception
     */
    public function get_gradebook_users(?array $userids = null) {
        global $DB, $CFG;

        // Get the list of users by gradebook roles.
        if (!empty($CFG->gradebookroles)) {
            $gradebookroles = explode(",", $CFG->gradebookroles);
        } else {
            $gradebookroles = '';
        }

        if (!empty($CFG->enablegroupings) && $this->cm->groupmembersonly) {
            $groupingsusers = groups_get_grouping_members($this->cm->groupingid, 'u.id', 'u.id');
            $gusers = $groupingsusers ? array_keys($groupingsusers) : null;
        }

        if (!empty($userids)) {
            if (!empty($gusers)) {
                $gusers = array_intersect($userids, $gusers);
            } else {
                $gusers = $userids;
            }
        }

        if (isset($gusers)) {
            if (!empty($gusers)) {
                [$inuids, $params] = $DB->get_in_or_equal($gusers);
                return get_role_users(
                    $gradebookroles,
                    $this->context,
                    true,
                    user_picture::fields('u'),
                    'u.lastname ASC',
                    true,
                    $this->currentgroup,
                    '',
                    '',
                    "u.id $inuids",
                    $params
                );
            } else {
                return null;
            }
        } else {
            return get_role_users(
                $gradebookroles,
                $this->context,
                true,
                'u.id, u.lastname, u.firstname',
                'u.lastname ASC',
                true,
                $this->currentgroup
            );
        }
    }

    /**
     * Has a user reached the max number of entries?
     * If interval is set then required entries, max entries etc. are relative to the current interval
     *
     * @param bool $perinterval
     * @return bool
     * @throws \coding_exception
     * @throws \dml_exception
     */
    public function user_at_max_entries($perinterval = false) {
        if (
            $this->data->maxentries < 0 ||
                has_capability('mod/datalynx:manageentries', $this->context)
        ) {
            return false;
        } else {
            if ($this->data->maxentries == 0) {
                return true;
            } else {
                return ($this->user_num_entries($perinterval) >= $this->data->maxentries);
            }
        }
    }

    /**
     * Check if user has permission to view all entries.
     *
     * @param ?array $options
     * @return bool
     * @throws \dml_exception
     * @throws coding_exception
     */
    public function user_can_view_all_entries(?array $options = null) {
        global $OUTPUT;
        if (has_capability('mod/datalynx:manageentries', $this->context)) {
            return true;
        } else {
            // Check the number of entries required against the number of entries already made.
            $numentries = $this->user_num_entries();
            if ($this->data->entriesrequired && $numentries < $this->data->entriesrequired) {
                $entriesleft = $this->data->entriesrequired - $numentries;
                if (!empty($options['notify'])) {
                    echo $OUTPUT->notification(
                        get_string('entrieslefttoadd', 'datalynx', $entriesleft)
                    );
                }
            }

            // Check separate participants group.
            if ($this->groupmode == $this->internalgroupmodes['separateparticipants']) {
                return false;
            } else {
                // Check the number of entries required before to view other participant's entries.
                // Against the number of entries already made (doesn't apply to teachers).
                if ($this->data->entriestoview && $numentries < $this->data->entriestoview) {
                    $entrieslefttoview = $this->data->entriestoview - $numentries;
                    if (!empty($options['notify'])) {
                        echo $OUTPUT->notification(
                            get_string('entrieslefttoaddtoview', 'datalynx', $entrieslefttoview)
                        );
                    }
                    return false;
                } else {
                    return true;
                }
            }
        }
    }

    /**
     * Check if user is allowed to export an entry.
     *
     * @param ?stdClass $entry
     * @return bool
     * @throws \coding_exception
     */
    public function user_can_export_entry(?stdClass $entry = null) {
        global $CFG, $USER;
        // We need portfolios for export.
        if (!empty($CFG->enableportfolios)) {
            // Can export all entries.
            if (has_capability('mod/datalynx:exportallentries', $this->context)) {
                return true;
            }

            // For others, it depends on the entry.
            if (isset($entry->id) && $entry->id > 0) {
                if (has_capability('mod/datalynx:exportownentry', $this->context)) {
                    if (!$this->data->grouped && $USER->id == $entry->userid) {
                        return true;
                    } else {
                        if ($this->data->grouped && groups_is_member($entry->groupid)) {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }

    /**
     * Has the actual user the right to edit any entries or the optional single entry parameter?
     *
     * @param ?object $entry The entry object to check, or null for any entry.
     * @return boolean
     * @throws \coding_exception
     */
    public function user_can_manage_entry(?object $entry = null) {
        global $USER, $CFG;

        // Teachers can always manage entries.
        if (has_capability('mod/datalynx:manageentries', $this->context)) {
            return true;
        }

        // Anonymous/guest can only add entries if enabled.
        if (
            (!isloggedin() || isguestuser()) && empty($entry->id) && $CFG->datalynx_anonymous &&
                $this->data->anonymous
        ) {
            return true;
        }

        // For others, it depends ...
        if (has_capability('mod/datalynx:writeentry', $this->context)) {
            $timeavailable = $this->data->timeavailable;
            $timedue = $this->data->timedue;
            $allowlate = $this->data->allowlate;
            $now = time();

            // If there is an activity timeframe, we must be inside the timeframe right now.
            if (
                $timeavailable && !($now >= $timeavailable) ||
                    ($timedue && !($now < $timedue) && !$allowlate)
            ) {
                return false;
            }

            // If group mode is enabled user has to be in the right group.
            if (
                $this->groupmode && !in_array($this->groupmode, $this->internalgroupmodes) &&
                    !has_capability('moodle/site:accessallgroups', $this->context) && (($this->currentgroup &&
                                    !groups_is_member($this->currentgroup)) ||
                            (!$this->currentgroup && $this->groupmode == VISIBLEGROUPS))
            ) {
                return false; // For members only.
            }

            // Managing a certain entry.
            if (!empty($entry->id)) {
                // Entry owner.
                // TODO: MDL-00000 groups_is_member queries the DB for each entry; consider caching.
                if (
                    empty($USER->id) ||
                        (!$this->data->grouped && $USER->id != $entry->userid && !$this->teammember_can_edit($entry)) ||
                        ($this->data->grouped && !groups_is_member($entry->groupid))
                ) {
                    return false; // Who are you anyway???
                }

                // If nor status 'draft' neither status 'not set' user is not allowed to manage this entry.
                if (
                    !($entry->status == datalynxfield_status::STATUS_DRAFT ||
                        $entry->status == datalynxfield_status::STATUS_NOT_SET)
                ) {
                    return false;
                }
                // Ok owner, what's the time (limit)?
                if ($this->data->timelimit != -1) {
                    $timelimitsec = ($this->data->timelimit * 60);
                    $elapsed = $now - $entry->timecreated;
                    if ($elapsed > $timelimitsec) {
                        return false; // Too late ...
                    }
                }

                // Phew, within time limit, but wait, are we still in the same interval?
                if ($timeinterval = $this->data->timeinterval) {
                    $elapsed = $now - $timeavailable;
                    $currentintervalstarted = (floor($elapsed / $timeinterval) * $timeinterval) + $timeavailable;
                    if ($entry->timecreated < $currentintervalstarted) {
                        return false; // Nop ...
                    }
                }
            }// If you got this far you probably deserve to do something ... go ahead.
            return true;
        }

        return false;
    }

    /**
     * Checks if team member can edit the entry.
     *
     * @param ?stdClass $entry
     * @return bool
     */
    public function teammember_can_edit(?stdClass $entry = null) {
        global $USER;
        // Get all teammemberselect fields, that allow editing of entry.
        $teammemberfields = $this->get_fields_by_type('teammemberselect');
        $fieldids = [];
        foreach ($teammemberfields as $fieldid => $field) {
            if ($field->field->param9 == "1") {
                $fieldids[] = $fieldid;
            }
        }
        if (empty($fieldids)) {
            return false;
        }
        foreach ($fieldids as $fieldid) {
            // Extract all userids of teammemberselect field.
            $userids = isset($entry->{"c{$fieldid}_content"}) ? json_decode(
                $entry->{"c{$fieldid}_content"},
                true
            ) : [];
            // Check if $USER is part of the teammembers who are allowed to edit.
            if (in_array($USER->id, $userids)) {
                return true;
            }
        }
        return false;
    }

    /**
     * returns the number of entries already made by this user; defaults to all entries
     *
     * @param bool $perinterval output int
     * @return int
     * @throws \dml_exception
     */
    public function user_num_entries($perinterval = false) {
        global $USER, $DB;

        static $numentries = null;
        static $numentriesintervaled = null;

        if (!$perinterval && !is_null($numentries)) {
            return $numentries;
        }

        if ($perinterval && !is_null($numentriesintervaled)) {
            return $numentriesintervaled;
        }

        $params = [];
        $params['dataid'] = $this->id();

        $andwhereuserorgroup = '';
        $andwhereinterval = '';

        // Go by user.
        if (!$this->data->grouped) {
            $andwhereuserorgroup = " AND userid = :userid ";
            $params['userid'] = $USER->id;
            // Go by group.
        } else {
            $andwhereuserorgroup = " AND groupid = :groupid ";
            // If user is trying add an entry and got this far.
            // The user should belong to the current group.
            $params['groupid'] = $this->currentgroup;
        }

        // Time interval.
        if ($timeinterval = $this->data->timeinterval && $perinterval) {
            $timeavailable = $this->data->timeavailable;
            $elapsed = time() - $timeavailable;
            $intervalstarttime = (floor($elapsed / $timeinterval) * $timeinterval) + $timeavailable;
            $intervalendtime = $intervalstarttime + $timeinterval;
            $andwhereinterval = " AND timecreated >= :starttime AND timecreated < :endtime ";
            $params['starttime'] = $intervalstarttime;
            $params['endtime'] = $intervalendtime;
        }

        $sql = "SELECT COUNT(*)
                FROM {datalynx_entries}
                WHERE dataid = :dataid $andwhereuserorgroup $andwhereinterval";
        $entriescount = $DB->count_records_sql($sql, $params);

        if (!$perinterval) {
            $numentries = $entriescount;
        } else {
            $numentriesintervaled = $entriescount;
        }

        return $entriescount;
    }

    /**
     * ********************************************************************************
     * UTILITY
     * *******************************************************************************
     */
    const PERMISSION_MANAGER = 1;

    /** @var int Permission level: teacher. */
    const PERMISSION_TEACHER = 2;

    /** @var int Permission level: student. */
    const PERMISSION_STUDENT = 4;

    /** @var int Permission level: guest. */
    const PERMISSION_GUEST = 8;

    /** @var int Permission level: entry author. */
    const PERMISSION_AUTHOR = 16;

    /** @var int Permission level: mentor. */
    const PERMISSION_MENTOR = 32;

    /** @var int Permission level: admin. */
    const PERMISSION_ADMIN = 64;

    /**
     * Get permission localised permission names
     * TODO: Rename mentor, this is team member?
     *
     * @param bool $absoluteonly
     * @param bool $includeadmin
     * @return array
     * @throws \coding_exception
     */
    public function get_datalynx_permission_names($absoluteonly = false, $includeadmin = false) {
        $permissions = [];

        if ($includeadmin) {
            $permissions[self::PERMISSION_ADMIN] = get_string('admin', 'datalynx');
        }

        $permissions[self::PERMISSION_MANAGER] = get_string('visible1', 'datalynx');
        $permissions[self::PERMISSION_TEACHER] = get_string('visible2', 'datalynx');
        $permissions[self::PERMISSION_STUDENT] = get_string('visible4', 'datalynx');
        $permissions[self::PERMISSION_GUEST] = get_string('visible8', 'datalynx');

        if (!$absoluteonly) {
            $permissions[self::PERMISSION_AUTHOR] = get_string('author', 'datalynx');
            $permissions[self::PERMISSION_MENTOR] = get_string('mentor', 'datalynx');
        }

        return $permissions;
    }

    /**
     * Note: this function doesn't return contextual permissions (entry author/mentor).
     *
     * @param int $userid ID of the user. If not given the current user is presumed.
     * @param string $type 'any', 'both', 'view', or 'edit'. Defaults to 'any'.
     * @return int[] Array of IDs of permissions the user has in this instance.
     * @throws coding_exception
     */
    public function get_user_datalynx_permissions(int $userid = 0, string $type = 'any'): array {
        global $USER, $DB;

        if (!$userid) {
            $user = $USER;
        } else {
            $user = $DB->get_record('user', ['id' => $userid]);
        }

        $edit = $type === 'edit' || $type === 'both';
        $view = $type === 'view' || $type === 'both';

        $permissions = [];

        if ($type === 'any') {
            if (
                has_capability('mod/datalynx:viewprivilegeadmin', $this->context, $user, true) ||
                    has_capability('mod/datalynx:editprivilegeadmin', $this->context, $user, true)
            ) {
                $permissions[] = self::PERMISSION_ADMIN;
            }
            if (
                has_capability('mod/datalynx:viewprivilegemanager', $this->context, $user, false) ||
                    has_capability(
                        'mod/datalynx:editprivilegemanager',
                        $this->context,
                        $user,
                        false
                    )
            ) {
                $permissions[] = self::PERMISSION_MANAGER;
            }
            if (
                has_capability('mod/datalynx:viewprivilegeteacher', $this->context, $user, false) ||
                    has_capability(
                        'mod/datalynx:editprivilegeteacher',
                        $this->context,
                        $user,
                        false
                    )
            ) {
                $permissions[] = self::PERMISSION_TEACHER;
            }
            if (
                has_capability('mod/datalynx:viewprivilegestudent', $this->context, $user, false) ||
                    has_capability(
                        'mod/datalynx:editprivilegestudent',
                        $this->context,
                        $user,
                        false
                    )
            ) {
                $permissions[] = self::PERMISSION_STUDENT;
            }
            if (
                has_capability('mod/datalynx:viewprivilegeguest', $this->context, $user, false) ||
                    has_capability('mod/datalynx:editprivilegeguest', $this->context, $user, false)
            ) {
                $permissions[] = self::PERMISSION_GUEST;
            }
        } else {
            if ($edit || $view) {
                if (
                    (!$view ||
                                has_capability('mod/datalynx:viewprivilegeadmin', $this->context, $user, true)) &&
                        (!$edit ||
                                has_capability('mod/datalynx:editprivilegeadmin', $this->context, $user, true))
                ) {
                    $permissions[] = self::PERMISSION_ADMIN;
                    $permissions[] = self::PERMISSION_MANAGER; // Bug#876 Admin has manager permission by default.
                }
                if (
                    (!$view ||
                                has_capability(
                                    'mod/datalynx:viewprivilegemanager',
                                    $this->context,
                                    $user,
                                    false
                                )) &&
                        (!$edit ||
                                has_capability(
                                    'mod/datalynx:editprivilegemanager',
                                    $this->context,
                                    $user,
                                    false
                                ))
                ) {
                    $permissions[] = self::PERMISSION_MANAGER;
                }
                if (
                    (!$view ||
                                has_capability(
                                    'mod/datalynx:viewprivilegeteacher',
                                    $this->context,
                                    $user,
                                    false
                                )) &&
                        (!$edit ||
                                has_capability(
                                    'mod/datalynx:editprivilegeteacher',
                                    $this->context,
                                    $user,
                                    false
                                ))
                ) {
                    $permissions[] = self::PERMISSION_TEACHER;
                }
                if (
                    (!$view ||
                                has_capability(
                                    'mod/datalynx:viewprivilegestudent',
                                    $this->context,
                                    $user,
                                    false
                                )) &&
                        (!$edit ||
                                has_capability(
                                    'mod/datalynx:editprivilegestudent',
                                    $this->context,
                                    $user,
                                    false
                                ))
                ) {
                    $permissions[] = self::PERMISSION_STUDENT;
                }
                if (
                    (!$view ||
                                has_capability('mod/datalynx:viewprivilegeguest', $this->context, $user, false)) &&
                        (!$edit ||
                                has_capability('mod/datalynx:editprivilegeguest', $this->context, $user, false))
                ) {
                    $permissions[] = self::PERMISSION_GUEST;
                }
            } else {
                debug("Invalid \$type parameter: $type");
            }
        }

        return $permissions;
    }

    /**
     * Checks if a name exists in the given table.
     * Use $id to exclude known entry when editing.
     *
     * @param string $table Table name suffix (without datalynx_ prefix).
     * @param string $name Name to check for existence.
     * @param int $id Entry id to exclude (0 for new entries).
     * @return bool True if the name exists.
     */
    public function name_exists($table, $name, $id = 0) {
        global $DB;

        $params = [$this->id(), $name, $id];

        $where = " dataid = ? AND name = ? AND id <> ? ";
        return $DB->record_exists_select("datalynx_{$table}", $where, $params);
    }

    /**
     * // TODO.
     */
    public function settings_navigation() {
    }

    /**
     * Converts array values in field input to comma-delimited strings.
     *
     * @param object $fieldinput Field input object passed by reference.
     */
    public function convert_arrays_to_strings(&$fieldinput) {
        foreach ($fieldinput as $key => $val) {
            if (is_array($val)) {
                $str = '';
                foreach ($val as $inner) {
                    $str .= $inner . ',';
                }
                $str = substr($str, 0, -1);
                $fieldinput->$key = $str;
            }
        }
    }

    /**
     * Method triggering entry-based events
     *
     * @param string $event Event name.
     * @param object $data Event data object.
     */
    public function events_trigger($event, $data) {
        $data->df = $this;
        $data->coursename = $this->course->shortname;
        $data->datalynxname = $this->name();
        if (isset($data->view)) {
            $data->datalynxbaselink = html_writer::link(
                $data->view->get_dl()->get_baseurl(),
                $data->datalynxname
            );
            $data->datalynxlink = html_writer::link($data->view->get_baseurl(), $data->datalynxname);
        }
        $data->context = $this->context->id;
        $data->event = $event;
        $data->notification = 1;
        $data->notificationformat = 1;

        $other = ['dataid' => $this->id()];

        foreach (array_keys($data->items) as $id) {
            switch ($event) {
                case 'entryadded':
                    $eventobj = event\entry_created::create(
                        ['context' => $this->context, 'objectid' => $id, 'other' => $other]
                    );
                    $eventobj->trigger();
                    break;
                case 'entryupdated':
                    $eventobj = event\entry_updated::create(
                        ['context' => $this->context, 'objectid' => $id, 'other' => $other]
                    );
                    $eventobj->trigger();
                    break;
                case 'entrydeleted':
                    $eventobj = event\entry_deleted::create(
                        ['context' => $this->context, 'objectid' => $id, 'other' => $other]
                    );
                    $eventobj->trigger();
                    break;
                case 'entryapproved':
                    $eventobj = event\entry_approved::create(
                        ['context' => $this->context, 'objectid' => $id, 'other' => $other]
                    );
                    $eventobj->trigger();
                    break;
                case 'entrydisapproved':
                    $eventobj = event\entry_disapproved::create(
                        ['context' => $this->context, 'objectid' => $id, 'other' => $other]
                    );
                    $eventobj->trigger();
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * Returns the base URL for this datalynx instance.
     *
     * @return moodle_url
     */
    public function get_baseurl() {
        // Base url params.
        $baseurlparams = [];
        $baseurlparams['d'] = $this->id();
        return new moodle_url("/mod/datalynx/{$this->pagefile_for_urls()}.php", $baseurlparams);
    }

    /**
     * Given the ID of a textfield for this instance, it returns all used values as array
     *
     * @param int $id datalynx-instance-ID
     * @return string[] array with used text-fields-values!
     */
    public function get_distinct_textfieldvalues_by_id($id) {
        global $DB;

        $sql = "SELECT DISTINCT c.content FROM {datalynx_contents} c WHERE c.fieldid = :fieldid ORDER BY c.content";
        $sqlparams['fieldid'] = $id;

        $textfieldvalues = $DB->get_fieldset_sql($sql, $sqlparams);

        return array_combine($textfieldvalues, $textfieldvalues);
    }
}
