// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Injectable } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { makeSingleton } from '@singletons';
import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { CoreEvents } from '@singletons/events';
import { CoreWSError } from '@classes/errors/wserror';

const ROOT_CACHE_KEY = 'mmCourses:';

/**
 * Service that provides some features regarding lists of courses and categories.
 */
@Injectable({ providedIn: 'root' })
export class CoreCoursesProvider {

    static readonly SEARCH_PER_PAGE = 20;
    static readonly ENROL_INVALID_KEY = 'CoreCoursesEnrolInvalidKey';
    static readonly EVENT_MY_COURSES_CHANGED = 'courses_my_courses_changed'; // User course list changed while app is running.
    // A course was hidden/favourite, or user enroled in a course.
    static readonly EVENT_MY_COURSES_UPDATED = 'courses_my_courses_updated';
    static readonly EVENT_MY_COURSES_REFRESHED = 'courses_my_courses_refreshed';
    static readonly EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED = 'dashboard_download_enabled_changed';

    // Actions for event EVENT_MY_COURSES_UPDATED.
    static readonly ACTION_ENROL = 'enrol'; // User enrolled in a course.
    static readonly ACTION_STATE_CHANGED = 'state_changed'; // Course state changed (hidden, favourite).
    static readonly ACTION_VIEW = 'view'; // Course viewed.

    // Possible states changed.
    static readonly STATE_HIDDEN = 'hidden';
    static readonly STATE_FAVOURITE = 'favourite';

    protected logger: CoreLogger;
    protected userCoursesIds: { [id: number]: boolean } = {}; // Use an object to make it faster to search.

    constructor() {
        this.logger = CoreLogger.getInstance('CoreCoursesProvider');
    }

    /**
     * Whether current site supports getting course options.
     *
     * @return Whether current site supports getting course options.
     */
    canGetAdminAndNavOptions(): boolean {
        return CoreSites.instance.wsAvailableInCurrentSite('core_course_get_user_navigation_options') &&
                CoreSites.instance.wsAvailableInCurrentSite('core_course_get_user_administration_options');
    }

    /**
     * Get categories. They can be filtered by id.
     *
     * @param categoryId Category ID to get.
     * @param addSubcategories If it should add subcategories to the list.
     * @param siteId Site to get the courses from. If not defined, use current site.
     * @return Promise resolved with the categories.
     */
    async getCategories(
        categoryId: number,
        addSubcategories: boolean = false,
        siteId?: string,
    ): Promise<CoreCourseGetCategoriesWSResponse> {
        const site = await CoreSites.instance.getSite(siteId);

        // Get parent when id is the root category.
        const criteriaKey = categoryId == 0 ? 'parent' : 'id';
        const params: CoreCourseGetCategoriesWSParams = {
            criteria: [
                {
                    key: criteriaKey,
                    value: categoryId,
                },
            ],
            addsubcategories: addSubcategories,
        };

        const preSets = {
            cacheKey: this.getCategoriesCacheKey(categoryId, addSubcategories),
            updateFrequency: CoreSite.FREQUENCY_RARELY,
        };

        return site.read('core_course_get_categories', params, preSets);
    }

    /**
     * Get cache key for get categories methods WS call.
     *
     * @param categoryId Category ID to get.
     * @param addSubcategories If add subcategories to the list.
     * @return Cache key.
     */
    protected getCategoriesCacheKey(categoryId: number, addSubcategories?: boolean): string {
        return ROOT_CACHE_KEY + 'categories:' + categoryId + ':' + !!addSubcategories;
    }

    /**
     * Given a list of course IDs to get course admin and nav options, return the list of courseIds to use.
     *
     * @param courseIds Course IDs.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved with the list of course IDs.
     */
    protected async getCourseIdsForAdminAndNavOptions(courseIds: number[], siteId?: string): Promise<number[]> {
        const site = await CoreSites.instance.getSite(siteId);

        const siteHomeId = site.getSiteHomeId();
        if (courseIds.length == 1) {
            // Only 1 course, check if it belongs to the user courses. If so, use all user courses.
            return this.getCourseIdsIfEnrolled(courseIds[0], siteId);
        } else {
            if (courseIds.length > 1 && courseIds.indexOf(siteHomeId) == -1) {
                courseIds.push(siteHomeId);
            }

            // Sort the course IDs.
            courseIds.sort((a, b) => b - a);

            return courseIds;
        }
    }

    /**
     * Given a course ID, if user is enrolled in the course it will return the IDs of all enrolled courses and site home.
     * Return only the course ID otherwise.
     *
     * @param courseIds Course IDs.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved with the list of course IDs.
     */
    async getCourseIdsIfEnrolled(courseId: number, siteId?: string): Promise<number[]> {
        const site = await CoreSites.instance.getSite(siteId);
        const siteHomeId = site.getSiteHomeId();

        try {
            // Check if user is enrolled in the course.
            const courses = await this.getUserCourses(true, siteId);
            let useAllCourses = false;

            if (courseId == siteHomeId) {
                // It's site home, use all courses.
                useAllCourses = true;
            } else {
                useAllCourses = !!courses.find((course) => course.id == courseId);
            }

            if (useAllCourses) {
                // User is enrolled, return all the courses.
                const courseIds = courses.map((course) => course.id);

                // Always add the site home ID.
                courseIds.push(siteHomeId);

                // Sort the course IDs.
                courseIds.sort((a, b) => b - a);

                return courseIds;
            }
        } catch {
            // Ignore errors.
        }

        return [courseId];
    }

    /**
     * Check if download a whole course is disabled in a certain site.
     *
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
     */
    async isDownloadCourseDisabled(siteId?: string): Promise<boolean> {
        const site = await CoreSites.instance.getSite(siteId);

        return this.isDownloadCoursesDisabledInSite(site);
    }

    /**
     * Check if download a whole course is disabled in a certain site.
     *
     * @param site Site. If not defined, use current site.
     * @return Whether it's disabled.
     */
    isDownloadCourseDisabledInSite(site?: CoreSite): boolean {
        site = site || CoreSites.instance.getCurrentSite();

        return !site || site.isOfflineDisabled() || site.isFeatureDisabled('NoDelegate_CoreCourseDownload');
    }

    /**
     * Check if download all courses is disabled in a certain site.
     *
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
     */
    async isDownloadCoursesDisabled(siteId?: string): Promise<boolean> {
        const site = await CoreSites.instance.getSite(siteId);

        return this.isDownloadCoursesDisabledInSite(site);
    }

    /**
     * Check if download all courses is disabled in a certain site.
     *
     * @param site Site. If not defined, use current site.
     * @return Whether it's disabled.
     */
    isDownloadCoursesDisabledInSite(site?: CoreSite): boolean {
        site = site || CoreSites.instance.getCurrentSite();

        return !site || site.isOfflineDisabled() || site.isFeatureDisabled('NoDelegate_CoreCoursesDownload');
    }

    /**
     * Check if My Courses is disabled in a certain site.
     *
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
     */
    async isMyCoursesDisabled(siteId?: string): Promise<boolean> {
        const site = await CoreSites.instance.getSite(siteId);

        return this.isMyCoursesDisabledInSite(site);
    }

    /**
     * Check if My Courses is disabled in a certain site.
     *
     * @param site Site. If not defined, use current site.
     * @return Whether it's disabled.
     */
    isMyCoursesDisabledInSite(site?: CoreSite): boolean {
        site = site || CoreSites.instance.getCurrentSite();

        return !site || site.isFeatureDisabled('CoreMainMenuDelegate_CoreCourses');
    }

    /**
     * Check if Search Courses is disabled in a certain site.
     *
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
     */
    async isSearchCoursesDisabled(siteId?: string): Promise<boolean> {
        const site = await CoreSites.instance.getSite(siteId);

        return this.isSearchCoursesDisabledInSite(site);
    }

    /**
     * Check if Search Courses is disabled in a certain site.
     *
     * @param site Site. If not defined, use current site.
     * @return Whether it's disabled.
     */
    isSearchCoursesDisabledInSite(site?: CoreSite): boolean {
        site = site || CoreSites.instance.getCurrentSite();

        return !site || site.isFeatureDisabled('CoreCourseOptionsDelegate_search');
    }

    /**
     * Get course.
     *
     * @param id ID of the course to get.
     * @param siteId Site to get the courses from. If not defined, use current site.
     * @return Promise resolved with the course.
     */
    async getCourse(id: number, siteId?: string): Promise<CoreCourseGetCoursesData> {
        const courses = await this.getCourses([id], siteId);

        if (courses && courses.length > 0) {
            return courses[0];
        }

        throw Error('Course not found on core_course_get_courses');
    }

    /**
     * Get the enrolment methods from a course.
     *
     * @param id ID of the course.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved with the methods.
     */
    async getCourseEnrolmentMethods(id: number, siteId?: string): Promise<CoreCourseEnrolmentMethod[]> {
        const site = await CoreSites.instance.getSite(siteId);

        const params: CoreEnrolGetCourseEnrolmentMethodsWSParams = {
            courseid: id,
        };
        const preSets = {
            cacheKey: this.getCourseEnrolmentMethodsCacheKey(id),
            updateFrequency: CoreSite.FREQUENCY_RARELY,
        };

        return site.read('core_enrol_get_course_enrolment_methods', params, preSets);
    }

    /**
     * Get cache key for get course enrolment methods WS call.
     *
     * @param id Course ID.
     * @return Cache key.
     */
    protected getCourseEnrolmentMethodsCacheKey(id: number): string {
        return ROOT_CACHE_KEY + 'enrolmentmethods:' + id;
    }

    /**
     * Get info from a course guest enrolment method.
     *
     * @param instanceId Guest instance ID.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved when the info is retrieved.
     */
    async getCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise<CoreCourseEnrolmentGuestMethod> {
        const site = await CoreSites.instance.getSite(siteId);
        const params: EnrolGuestGetInstanceInfoWSParams = {
            instanceid: instanceId,
        };
        const preSets: CoreSiteWSPreSets = {
            cacheKey: this.getCourseGuestEnrolmentInfoCacheKey(instanceId),
            updateFrequency: CoreSite.FREQUENCY_RARELY,
        };
        const response = await site.read<EnrolGuestGetInstanceInfoWSResponse>('enrol_guest_get_instance_info', params, preSets);

        return response.instanceinfo;
    }

    /**
     * Get cache key for get course guest enrolment methods WS call.
     *
     * @param instanceId Guest instance ID.
     * @return Cache key.
     */
    protected getCourseGuestEnrolmentInfoCacheKey(instanceId: number): string {
        return ROOT_CACHE_KEY + 'guestinfo:' + instanceId;
    }

    /**
     * Get courses.
     * Warning: if the user doesn't have permissions to view some of the courses passed the WS call will fail.
     * The user must be able to view ALL the courses passed.
     *
     * @param ids List of IDs of the courses to get.
     * @param siteId Site to get the courses from. If not defined, use current site.
     * @return Promise resolved with the courses.
     */
    async getCourses(ids: number[], siteId?: string): Promise<CoreCourseGetCoursesWSResponse> {
        if (!Array.isArray(ids)) {
            throw Error('ids parameter should be an array');
        }

        if (ids.length === 0) {
            return [];
        }

        const site = await CoreSites.instance.getSite(siteId);

        const params: CoreCourseGetCoursesWSParams = {
            options: {
                ids: ids,
            },
        };
        const preSets: CoreSiteWSPreSets = {
            cacheKey: this.getCoursesCacheKey(ids),
            updateFrequency: CoreSite.FREQUENCY_RARELY,
        };

        return site.read('core_course_get_courses', params, preSets);
    }

    /**
     * Get cache key for get courses WS call.
     *
     * @param ids Courses IDs.
     * @return Cache key.
     */
    protected getCoursesCacheKey(ids: number[]): string {
        return ROOT_CACHE_KEY + 'course:' + JSON.stringify(ids);
    }

    /**
     * This function is meant to decrease WS calls.
     * When requesting a single course that belongs to enrolled courses, request all enrolled courses because
     * the WS call is probably cached.
     *
     * @param field The field to search.
     * @param value The value to match.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved with the field and value to use.
     */
    protected async fixCoursesByFieldParams(
        field: string = '',
        value: number | string = '',
        siteId?: string,
    ): Promise<{ field: string; value: number | string }> {

        if (field == 'id' || field == 'ids') {
            let courseIds: number[];
            if (typeof value == 'string') {
                courseIds = value.split(',').map((id) => parseInt(id, 10));
            } else {
                courseIds = [value];
            }

            // Use the same optimization as in get admin and nav options. This will return the course IDs to use.
            courseIds = await this.getCourseIdsForAdminAndNavOptions(courseIds, siteId);

            if (courseIds.length > 1) {
                return { field: 'ids', value: courseIds.join(',') };
            } else {
                return { field: 'id', value: Number(courseIds[0]) };
            }
        } else {
            // Nothing to do.
            return { field: field, value: value };
        }
    }

    /**
     * Get the first course returned by getCoursesByField.
     *
     * @param field The field to search. Can be left empty for all courses or:
     *              id: course id.
     *              ids: comma separated course ids.
     *              shortname: course short name.
     *              idnumber: course id number.
     *              category: category id the course belongs to.
     * @param value The value to match.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved with the first course.
     * @since 3.2
     */
    async getCourseByField(field?: string, value?: string | number, siteId?: string): Promise<CoreCourseSearchedData> {
        const courses = await this.getCoursesByField(field, value, siteId);

        if (courses && courses.length > 0) {
            return courses[0];
        }

        throw Error('Course not found on core_course_get_courses_by_field');
    }

    /**
     * Get courses. They can be filtered by field.
     *
     * @param field The field to search. Can be left empty for all courses or:
     *              id: course id.
     *              ids: comma separated course ids.
     *              shortname: course short name.
     *              idnumber: course id number.
     *              category: category id the course belongs to.
     * @param value The value to match.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved with the courses.
     * @since 3.2
     */
    async getCoursesByField(
        field: string = '',
        value: string | number = '',
        siteId?: string,
    ): Promise<CoreCourseSearchedData[]> {
        siteId = siteId || CoreSites.instance.getCurrentSiteId();

        const originalValue = value;

        const site = await CoreSites.instance.getSite(siteId);

        const fieldParams = await this.fixCoursesByFieldParams(field, value, siteId);

        const hasChanged = fieldParams.field != field || fieldParams.value != value;
        field = fieldParams.field;
        value = fieldParams.value;
        const data: CoreCourseGetCoursesByFieldWSParams = {
            field: field,
            value: field ? value : '',
        };
        const preSets: CoreSiteWSPreSets = {
            cacheKey: this.getCoursesByFieldCacheKey(field, value),
            updateFrequency: CoreSite.FREQUENCY_RARELY,
        };

        const response = await site.read<CoreCourseGetCoursesByFieldWSResponse>('core_course_get_courses_by_field', data, preSets);
        if (!response.courses) {
            throw Error('WS core_course_get_courses_by_field failed');
        }

        if (field == 'ids' && hasChanged) {
            // The list of courses requestes was changed to optimize it.
            // Return only the ones that were being requested.
            const courseIds = String(originalValue).split(',').map((id) => parseInt(id, 10));

            // Only courses from the original selection.
            response.courses = response.courses.filter((course) => courseIds.indexOf(course.id) >= 0);
        }

        // Courses will be sorted using sortorder if avalaible.
        return response.courses.sort((a, b) => {
            if (typeof a.sortorder == 'undefined' && typeof b.sortorder == 'undefined') {
                return b.id - a.id;
            }

            if (typeof a.sortorder == 'undefined') {
                return 1;
            }

            if (typeof b.sortorder == 'undefined') {
                return -1;
            }

            return a.sortorder - b.sortorder;
        });
    }

    /**
     * Get cache key for get courses WS call.
     *
     * @param field The field to search.
     * @param value The value to match.
     * @return Cache key.
     */
    protected getCoursesByFieldCacheKey(field: string = '', value: string | number = ''): string {
        return ROOT_CACHE_KEY + 'coursesbyfield:' + field + ':' + value;
    }

    /**
     * Get courses matching the given custom field. Only works in online.
     *
     * @param  customFieldName Custom field name.
     * @param  customFieldValue Custom field value.
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved with the list of courses.
     * @since 3.8
     */
    async getEnrolledCoursesByCustomField(
        customFieldName: string,
        customFieldValue: string,
        siteId?: string,
    ): Promise<CoreCourseGetEnrolledCoursesByTimelineClassification[]> {
        const site = await CoreSites.instance.getSite(siteId);
        const params: CoreCourseGetEnrolledCoursesByTimelineClassificationWSParams = {
            classification: 'customfield',
            customfieldname: customFieldName,
            customfieldvalue: customFieldValue,
        };
        const preSets: CoreSiteWSPreSets = {
            getFromCache: false,
        };
        const courses = await site.read<CoreCourseGetEnrolledCoursesByTimelineClassificationWSResponse>(
            'core_course_get_enrolled_courses_by_timeline_classification',
            params,
            preSets,
        );
        if (courses.courses) {
            return courses.courses;
        }

        throw Error('WS core_course_get_enrolled_courses_by_timeline_classification failed');
    }

    /**
     * Check if get courses by field WS is available in a certain site.
     *
     * @param site Site to check.
     * @return Whether get courses by field is available.
     * @since 3.2
     */
    isGetCoursesByFieldAvailable(site?: CoreSite): boolean {
        site = site || CoreSites.instance.getCurrentSite();

        return !!site && site.wsAvailable('core_course_get_courses_by_field');
    }

    /**
     * Check if get courses by field WS is available in a certain site, by site ID.
     *
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved with boolean: whether get courses by field is available.
     * @since 3.2
     */
    async isGetCoursesByFieldAvailableInSite(siteId?: string): Promise<boolean> {
        const site = await CoreSites.instance.getSite(siteId);

        return this.isGetCoursesByFieldAvailable(site);
    }

    /**
     * Get the navigation and administration options for the given courses.
     *
     * @param courseIds IDs of courses to get.
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved with the options for each course.
     */
    async getCoursesAdminAndNavOptions(
        courseIds: number[],
        siteId?: string,
    ): Promise<{
            navOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
            admOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
        }> {
        siteId = siteId || CoreSites.instance.getCurrentSiteId();

        // Get the list of courseIds to use based on the param.
        courseIds = await this.getCourseIdsForAdminAndNavOptions(courseIds, siteId);

        let navOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
        let admOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;

        // Get user navigation and administration options.
        try {
            navOptions = await this.getUserNavigationOptions(courseIds, siteId);
        } catch {
            // Couldn't get it, return empty options.
            navOptions = {};
        }

        try {
            admOptions = await this.getUserAdministrationOptions(courseIds, siteId);
        } catch {
            // Couldn't get it, return empty options.
            admOptions = {};
        }

        return ({ navOptions: navOptions, admOptions: admOptions });
    }

    /**
     * Get the common part of the cache keys for user administration options WS calls.
     *
     * @return Cache key.
     */
    protected getUserAdministrationOptionsCommonCacheKey(): string {
        return ROOT_CACHE_KEY + 'administrationOptions:';
    }

    /**
     * Get cache key for get user administration options WS call.
     *
     * @param courseIds IDs of courses to get.
     * @return Cache key.
     */
    protected getUserAdministrationOptionsCacheKey(courseIds: number[]): string {
        return this.getUserAdministrationOptionsCommonCacheKey() + courseIds.join(',');
    }

    /**
     * Get user administration options for a set of courses.
     *
     * @param courseIds IDs of courses to get.
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved with administration options for each course.
     */
    async getUserAdministrationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
        if (!courseIds || courseIds.length == 0) {
            return {};
        }

        const site = await CoreSites.instance.getSite(siteId);

        const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
            courseids: courseIds,
        };
        const preSets: CoreSiteWSPreSets = {
            cacheKey: this.getUserAdministrationOptionsCacheKey(courseIds),
            updateFrequency: CoreSite.FREQUENCY_RARELY,
        };

        const response: CoreCourseGetUserAdminOrNavOptionsWSResponse =
            await site.read('core_course_get_user_administration_options', params, preSets);

        // Format returned data.
        return this.formatUserAdminOrNavOptions(response.courses);
    }

    /**
     * Get the common part of the cache keys for user navigation options WS calls.
     *
     * @param courseIds IDs of courses to get.
     * @return Cache key.
     */
    protected getUserNavigationOptionsCommonCacheKey(): string {
        return ROOT_CACHE_KEY + 'navigationOptions:';
    }

    /**
     * Get cache key for get user navigation options WS call.
     *
     * @return Cache key.
     */
    protected getUserNavigationOptionsCacheKey(courseIds: number[]): string {
        return this.getUserNavigationOptionsCommonCacheKey() + courseIds.join(',');
    }

    /**
     * Get user navigation options for a set of courses.
     *
     * @param courseIds IDs of courses to get.
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved with navigation options for each course.
     */
    async getUserNavigationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
        if (!courseIds || courseIds.length == 0) {
            return {};
        }

        const site = await CoreSites.instance.getSite(siteId);

        const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
            courseids: courseIds,
        };
        const preSets: CoreSiteWSPreSets = {
            cacheKey: this.getUserNavigationOptionsCacheKey(courseIds),
            updateFrequency: CoreSite.FREQUENCY_RARELY,
        };

        const response: CoreCourseGetUserAdminOrNavOptionsWSResponse =
            await site.read('core_course_get_user_navigation_options', params, preSets);

        // Format returned data.
        return this.formatUserAdminOrNavOptions(response.courses);
    }

    /**
     * Format user navigation or administration options.
     *
     * @param courses Navigation or administration options for each course.
     * @return Formatted options.
     */
    protected formatUserAdminOrNavOptions(courses: CoreCourseUserAdminOrNavOption[]): CoreCourseUserAdminOrNavOptionCourseIndexed {
        const result = {};

        courses.forEach((course) => {
            const options = {};

            if (course.options) {
                course.options.forEach((option) => {
                    options[option.name] = option.available;
                });
            }

            result[course.id] = options;
        });

        return result;
    }

    /**
     * Get a course the user is enrolled in. This function relies on getUserCourses.
     * preferCache=true will try to speed up the response, but the data returned might not be updated.
     *
     * @param id ID of the course to get.
     * @param preferCache True if shouldn't call WS if data is cached, false otherwise.
     * @param siteId Site to get the courses from. If not defined, use current site.
     * @return Promise resolved with the course.
     */
    async getUserCourse(id: number, preferCache?: boolean, siteId?: string): Promise<CoreEnrolledCourseData> {
        if (!id) {
            throw Error('Invalid id parameter on getUserCourse');
        }

        const courses = await this.getUserCourses(preferCache, siteId);

        const course = courses.find((course) => course.id == id);

        if (course) {
            return course;
        }

        throw Error('Course not found on core_enrol_get_users_courses');
    }

    /**
     * Get user courses.
     *
     * @param preferCache True if shouldn't call WS if data is cached, false otherwise.
     * @param siteId Site to get the courses from. If not defined, use current site.
     * @return Promise resolved with the courses.
     */
    async getUserCourses(
        preferCache: boolean = false,
        siteId?: string,
        strategy?: CoreSitesReadingStrategy,
    ): Promise<CoreEnrolledCourseData[]> {
        const site = await CoreSites.instance.getSite(siteId);

        const userId = site.getUserId();
        const wsParams: CoreEnrolGetUsersCoursesWSParams = {
            userid: userId,
        };
        const strategyPreSets = strategy
            ? CoreSites.instance.getReadingStrategyPreSets(strategy)
            : { omitExpires: !!preferCache };

        const preSets = {
            cacheKey: this.getUserCoursesCacheKey(),
            getCacheUsingCacheKey: true,
            updateFrequency: CoreSite.FREQUENCY_RARELY,
            ...strategyPreSets,
        };

        if (site.isVersionGreaterEqualThan('3.7')) {
            wsParams.returnusercount = false;
        }

        const courses = await site.read<CoreEnrolGetUsersCoursesWSResponse>('core_enrol_get_users_courses', wsParams, preSets);

        if (this.userCoursesIds) {
            // Check if the list of courses has changed.
            const added: number[] = [];
            const removed: number[] = [];
            const previousIds = Object.keys(this.userCoursesIds);
            const currentIds = {}; // Use an object to make it faster to search.

            courses.forEach((course) => {
                // Move category field to categoryid on a course.
                course.categoryid = course.category;
                delete course.category;

                currentIds[course.id] = true;

                if (!this.userCoursesIds[course.id]) {
                    // Course added.
                    added.push(course.id);
                }
            });

            if (courses.length - added.length != previousIds.length) {
                // A course was removed, check which one.
                previousIds.forEach((id) => {
                    if (!currentIds[id]) {
                        // Course removed.
                        removed.push(Number(id));
                    }
                });
            }

            if (added.length || removed.length) {
                // At least 1 course was added or removed, trigger the event.
                CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, {
                    added: added,
                    removed: removed,
                }, site.getId());
            }

            this.userCoursesIds = currentIds;
        } else {
            this.userCoursesIds = {};

            // Store the list of courses.
            courses.forEach((course) => {
                // Move category field to categoryid on a course.
                course.categoryid = course.category;
                delete course.category;

                this.userCoursesIds[course.id] = true;
            });
        }

        return courses;
    }

    /**
     * Get cache key for get user courses WS call.
     *
     * @return Cache key.
     */
    protected getUserCoursesCacheKey(): string {
        return ROOT_CACHE_KEY + 'usercourses';
    }

    /**
     * Invalidates get categories WS call.
     *
     * @param categoryId Category ID to get.
     * @param addSubcategories If it should add subcategories to the list.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateCategories(categoryId: number, addSubcategories?: boolean, siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKey(this.getCategoriesCacheKey(categoryId, addSubcategories));
    }

    /**
     * Invalidates get course WS call.
     *
     * @param id Course ID.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    invalidateCourse(id: number, siteId?: string): Promise<void> {
        return this.invalidateCourses([id], siteId);
    }

    /**
     * Invalidates get course enrolment methods WS call.
     *
     * @param id Course ID.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateCourseEnrolmentMethods(id: number, siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKey(this.getCourseEnrolmentMethodsCacheKey(id));
    }

    /**
     * Invalidates get course guest enrolment info WS call.
     *
     * @param instanceId Guest instance ID.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKey(this.getCourseGuestEnrolmentInfoCacheKey(instanceId));
    }

    /**
     * Invalidates the navigation and administration options for the given courses.
     *
     * @param courseIds IDs of courses to get.
     * @param siteId Site ID to invalidate. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateCoursesAdminAndNavOptions(courseIds: number[], siteId?: string): Promise<void> {
        siteId = siteId || CoreSites.instance.getCurrentSiteId();

        const ids = await this.getCourseIdsForAdminAndNavOptions(courseIds, siteId);

        const promises: Promise<void>[] = [];
        promises.push(this.invalidateUserAdministrationOptionsForCourses(ids, siteId));
        promises.push(this.invalidateUserNavigationOptionsForCourses(ids, siteId));

        await Promise.all(promises);
    }

    /**
     * Invalidates get courses WS call.
     *
     * @param ids Courses IDs.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateCourses(ids: number[], siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKey(this.getCoursesCacheKey(ids));
    }

    /**
     * Invalidates get courses by field WS call.
     *
     * @param field See getCoursesByField for info.
     * @param value The value to match.
     * @param siteId Site Id. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateCoursesByField(field: string = '', value: number | string = '', siteId?: string): Promise<void> {
        siteId = siteId || CoreSites.instance.getCurrentSiteId();

        const result = await this.fixCoursesByFieldParams(field, value, siteId);
        field = result.field;
        value = result.value;

        const site = await CoreSites.instance.getSite(siteId);

        return site.invalidateWsCacheForKey(this.getCoursesByFieldCacheKey(field, value));
    }

    /**
     * Invalidates all user administration options.
     *
     * @param siteId Site ID to invalidate. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateUserAdministrationOptions(siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKeyStartingWith(this.getUserAdministrationOptionsCommonCacheKey());
    }

    /**
     * Invalidates user administration options for certain courses.
     *
     * @param courseIds IDs of courses.
     * @param siteId Site ID to invalidate. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateUserAdministrationOptionsForCourses(courseIds: number[], siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKey(this.getUserAdministrationOptionsCacheKey(courseIds));
    }

    /**
     * Invalidates get user courses WS call.
     *
     * @param siteId Site ID to invalidate. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateUserCourses(siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKey(this.getUserCoursesCacheKey());
    }

    /**
     * Invalidates all user navigation options.
     *
     * @param siteId Site ID to invalidate. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateUserNavigationOptions(siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKeyStartingWith(this.getUserNavigationOptionsCommonCacheKey());
    }

    /**
     * Invalidates user navigation options for certain courses.
     *
     * @param courseIds IDs of courses.
     * @param siteId Site ID to invalidate. If not defined, use current site.
     * @return Promise resolved when the data is invalidated.
     */
    async invalidateUserNavigationOptionsForCourses(courseIds: number[], siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        await site.invalidateWsCacheForKey(this.getUserNavigationOptionsCacheKey(courseIds));
    }

    /**
     * Check if WS to retrieve guest enrolment data is available.
     *
     * @return Whether guest WS is available.
     * @since 3.1
     * @deprecated Will always return true since it's available since 3.1.
     */
    isGuestWSAvailable(): boolean {
        return true;
    }

    /**
     * Search courses.
     *
     * @param text Text to search.
     * @param page Page to get.
     * @param perPage Number of courses per page. Defaults to CoreCoursesProvider.SEARCH_PER_PAGE.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved with the courses and the total of matches.
     */
    async search(
        text: string,
        page: number = 0,
        perPage: number = CoreCoursesProvider.SEARCH_PER_PAGE,
        siteId?: string,
    ): Promise<{ total: number; courses: CoreCourseBasicSearchedData[] }> {
        const site = await CoreSites.instance.getSite(siteId);
        const params: CoreCourseSearchCoursesWSParams = {
            criterianame: 'search',
            criteriavalue: text,
            page: page,
            perpage: perPage,
        };
        const preSets: CoreSiteWSPreSets = {
            getFromCache: false,
        };

        const response = await site.read<CoreCourseSearchCoursesWSResponse>('core_course_search_courses', params, preSets);

        return ({ total: response.total, courses: response.courses });
    }

    /**
     * Self enrol current user in a certain course.
     *
     * @param courseId Course ID.
     * @param password Password to use.
     * @param instanceId Enrol instance ID.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved if the user is enrolled. If the password is invalid, the promise is rejected
     *         with an object with errorcode = CoreCoursesProvider.ENROL_INVALID_KEY.
     */
    async selfEnrol(courseId: number, password: string = '', instanceId?: number, siteId?: string): Promise<boolean> {

        const site = await CoreSites.instance.getSite(siteId);

        const params: EnrolSelfEnrolUserWSParams = {
            courseid: courseId,
            password: password,
        };
        if (instanceId) {
            params.instanceid = instanceId;
        }

        const response = await site.write<CoreStatusWithWarningsWSResponse>('enrol_self_enrol_user', params);

        if (!response) {
            throw Error('WS enrol_self_enrol_user failed');
        }

        if (response.status) {
            return true;
        }

        if (response.warnings && response.warnings.length) {
            // Invalid password warnings.
            const warning = response.warnings.find((warning) =>
                warning.warningcode == '2' || warning.warningcode == '3' || warning.warningcode == '4');

            if (warning) {
                throw new CoreWSError({ errorcode: CoreCoursesProvider.ENROL_INVALID_KEY, message: warning.message });
            } else {
                throw new CoreWSError(response.warnings[0]);
            }
        }

        throw Error('WS enrol_self_enrol_user failed without warnings');
    }

    /**
     * Set favourite property on a course.
     *
     * @param courseId Course ID.
     * @param favourite If favourite or unfavourite.
     * @param siteId Site ID. If not defined, use current site.
     * @return Promise resolved when done.
     */
    async setFavouriteCourse(courseId: number, favourite: boolean, siteId?: string): Promise<CoreWarningsWSResponse> {
        const site = await CoreSites.instance.getSite(siteId);

        const params: CoreCourseSetFavouriteCoursesWSParams = {
            courses: [
                {
                    id: courseId,
                    favourite: favourite,
                },
            ],
        };

        return site.write('core_course_set_favourite_courses', params);
    }

}

export class CoreCourses extends makeSingleton(CoreCoursesProvider) {}

/**
 * Data sent to the EVENT_MY_COURSES_UPDATED.
 *
 * @todo course type.
 */
export type CoreCoursesMyCoursesUpdatedEventData = {
    action: string; // Action performed.
    courseId?: number; // Course ID affected (if any).
    course?: CoreCourseAnyCourseData; // Course affected (if any).
    state?: string; // Only for ACTION_STATE_CHANGED. The state that changed (hidden, favourite).
    value?: boolean; // The new value for the state changed.
};

/**
 * Params of core_enrol_get_users_courses WS.
 */
type CoreEnrolGetUsersCoursesWSParams = {
    userid: number; // User id.
    returnusercount?: boolean; // Include count of enrolled users for each course? This can add several seconds to the response
    // time if a user is on several large courses, so set this to false if the value will not be used to improve performance.
};

/**
 * Data returned by core_enrol_get_users_courses WS.
 */
type CoreEnrolGetUsersCoursesWSResponse = (CoreEnrolledCourseData & {
    category?: number; // Course category id.
})[];

/**
 * Basic data obtained form any course.
 */
export type CoreCourseBasicData = {
    id: number; // Course id.
    fullname: string; // Course full name.
    displayname?: string; // Course display name.
    shortname: string; // Course short name.
    summary: string; // Summary.
    summaryformat: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
    categoryid?: number; // Course category id.
};

/**
 * Basic data obtained from a course when the user is enrolled.
 */
export type CoreEnrolledCourseBasicData = CoreCourseBasicData & {
    idnumber?: string; // Id number of course.
    visible?: number; // 1 means visible, 0 means not yet visible course.
    format?: string; // Course format: weeks, topics, social, site.
    showgrades?: boolean; // True if grades are shown, otherwise false.
    lang?: string; // Forced course language.
    enablecompletion?: boolean; // True if completion is enabled, otherwise false.
    startdate?: number; // Timestamp when the course start.
    enddate?: number; // Timestamp when the course end.
};

/**
 * Course Data model received when the user is enrolled.
 */
export type CoreEnrolledCourseData = CoreEnrolledCourseBasicData & {
    enrolledusercount?: number; // Number of enrolled users in this course.
    completionhascriteria?: boolean; // If completion criteria is set.
    completionusertracked?: boolean; // If the user is completion tracked.
    progress?: number; // Progress percentage.
    completed?: boolean; // Whether the course is completed.
    marker?: number; // Course section marker.
    lastaccess?: number; // Last access to the course (timestamp).
    isfavourite?: boolean; // If the user marked this course a favourite.
    hidden?: boolean; // If the user hide the course from the dashboard.
    overviewfiles?: CoreWSExternalFile[];
};

/**
 * Basic course data received on search.
 */
export type CoreCourseBasicSearchedData = CoreCourseBasicData & {
    categoryid: number; // Category id.
    categoryname: string; // Category name.
    sortorder?: number; // Sort order in the category.
    summaryfiles?: CoreWSExternalFile[];
    overviewfiles: CoreWSExternalFile[];
    contacts: { // Contact users.
        id: number; // Contact user id.
        fullname: string; // Contact user fullname.
    }[];
    enrollmentmethods: string[]; // Enrollment methods list.
    customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
};

export type CoreCourseSearchedData = CoreCourseBasicSearchedData & {
    idnumber?: string; // Id number.
    format?: string; // Course format: weeks, topics, social, site,..
    showgrades?: number; // 1 if grades are shown, otherwise 0.
    newsitems?: number; // Number of recent items appearing on the course page.
    startdate?: number; // Timestamp when the course start.
    enddate?: number; // Timestamp when the course end.
    maxbytes?: number; // Largest size of file that can be uploaded into.
    showreports?: number; // Are activity report shown (yes = 1, no =0).
    visible?: number; // 1: available to student, 0:not available.
    groupmode?: number; // No group, separate, visible.
    groupmodeforce?: number; // 1: yes, 0: no.
    defaultgroupingid?: number; // Default grouping id.
    enablecompletion?: number; // Completion enabled? 1: yes 0: no.
    completionnotify?: number; // 1: yes 0: no.
    lang?: string; // Forced course language.
    theme?: string; // Fame of the forced theme.
    marker?: number; // Current course marker.
    legacyfiles?: number; // If legacy files are enabled.
    calendartype?: string; // Calendar type.
    timecreated?: number; // Time when the course was created.
    timemodified?: number; // Last time  the course was updated.
    requested?: number; // If is a requested course.
    cacherev?: number; // Cache revision number.
    filters?: { // Course filters.
        filter: string; // Filter plugin name.
        localstate: number; // Filter state: 1 for on, -1 for off, 0 if inherit.
        inheritedstate: number; // 1 or 0 to use when localstate is set to inherit.
    }[];
    courseformatoptions?: CoreCourseFormatOption[]; // Additional options for particular course format.
};

export type CoreCourseGetCoursesData = CoreEnrolledCourseBasicData & {
    categoryid: number; // Category id.
    categorysortorder?: number; // Sort order into the category.
    newsitems?: number; // Number of recent items appearing on the course page.
    /**
     * Number of weeks/topics.
     *
     * @deprecated use courseformatoptions.
     */
    numsections?: number;
    maxbytes?: number; // Largest size of file that can be uploaded into the course.
    showreports?: number; // Are activity report shown (yes = 1, no =0).
    /**
     * How the hidden sections in the course are displayed to students.
     *
     * @deprecated use courseformatoptions.
     */
    hiddensections?: number;
    groupmode?: number; // No group, separate, visible.
    groupmodeforce?: number; // 1: yes, 0: no.
    defaultgroupingid?: number; // Default grouping id.
    timecreated?: number; // Timestamp when the course have been created.
    timemodified?: number; // Timestamp when the course have been modified.
    completionnotify?: number; // 1: yes 0: no.
    forcetheme?: string; // Name of the force theme.
    courseformatoptions?: CoreCourseFormatOption[]; // Additional options for particular course format.
    customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
};

/**
 * Course custom fields and associated values.
 */
export type CoreCourseCustomField = {
    name: string; // The name of the custom field.
    shortname: string; // The shortname of the custom field.
    type: string; // The type of the custom field - text, checkbox...
    valueraw: string; // The raw value of the custom field.
    value: string; // The value of the custom field.
};

/**
 * Additional options for particular course format.
 */
export type CoreCourseFormatOption =  {
    name: string; // Course format option name.
    value: string; // Course format option value.
};

/**
 * Indexed course format options.
 */
export type CoreCourseFormatOptionsIndexed = {
    [name: string]: string;
};

/**
 * Params of core_course_get_courses_by_field WS.
 */
type CoreCourseGetCoursesByFieldWSParams = {
    /**
     * The field to search can be left empty for all courses or:
     * id: course id
     * ids: comma separated course ids
     * shortname: course short name
     * idnumber: course id number
     * category: category id the course belongs to.
     */
    field?: string;
    value?: string | number; // The value to match.
};

/**
 * Data returned by core_course_get_courses_by_field WS.
 */
export type CoreCourseGetCoursesByFieldWSResponse = {
    courses: CoreCourseSearchedData[];
    warnings?: CoreWSExternalWarning[];
};

/**
 * Params of core_course_search_courses WS.
 */
type CoreCourseSearchCoursesWSParams = {
    criterianame: string; // Criteria name (search, modulelist (only admins), blocklist (only admins), tagid).
    criteriavalue: string; // Criteria value.
    page?: number; // Page number (0 based).
    perpage?: number; // Items per page.
    requiredcapabilities?: string[]; // Optional list of required capabilities (used to filter the list).
    limittoenrolled?: boolean; // Limit to enrolled courses.
    onlywithcompletion?: boolean; // Limit to courses where completion is enabled.
};

/**
 * Data returned by core_course_search_courses WS.
 */
export type CoreCourseSearchCoursesWSResponse = {
    total: number; // Total course count.
    courses: CoreCourseBasicSearchedData[];
    warnings?: CoreWSExternalWarning[];
};

/**
 * Params of core_course_get_courses WS.
 */
type CoreCourseGetCoursesWSParams = {
    options?: {
        ids?: number[]; // List of course id. If empty return all courses except front page course.
    }; // Options - operator OR is used.
};

/**
 * Data returned by core_course_get_courses WS.
 */
export type CoreCourseGetCoursesWSResponse = CoreCourseGetCoursesData[];

/**
 * Course type exported in CoreCourseGetEnrolledCoursesByTimelineClassificationWSResponse;
 */
export type CoreCourseGetEnrolledCoursesByTimelineClassification = CoreCourseBasicData & { // Course.
    idnumber: string; // Idnumber.
    startdate: number; // Startdate.
    enddate: number; // Enddate.
    visible: boolean; // Visible.
    fullnamedisplay: string; // Fullnamedisplay.
    viewurl: string; // Viewurl.
    courseimage: string; // Courseimage.
    progress?: number; // Progress.
    hasprogress: boolean; // Hasprogress.
    isfavourite: boolean; // Isfavourite.
    hidden: boolean; // Hidden.
    timeaccess?: number; // Timeaccess.
    showshortname: boolean; // Showshortname.
    coursecategory: string; // Coursecategory.
};

/**
 * Params of core_course_get_enrolled_courses_by_timeline_classification WS.
 */
type CoreCourseGetEnrolledCoursesByTimelineClassificationWSParams = {
    classification: string; // Future, inprogress, or past.
    limit?: number; // Result set limit.
    offset?: number; // Result set offset.
    sort?: string; // Sort string.
    customfieldname?: string; // Used when classification = customfield.
    customfieldvalue?: string; // Used when classification = customfield.
};

/**
 * Data returned by core_course_get_enrolled_courses_by_timeline_classification WS.
 */
export type CoreCourseGetEnrolledCoursesByTimelineClassificationWSResponse = {
    courses: CoreCourseGetEnrolledCoursesByTimelineClassification[];
    nextoffset: number; // Offset for the next request.
};

/**
 * Params of core_course_get_categories WS.
 */
type CoreCourseGetCategoriesWSParams = {
    criteria?: { // Criteria.
        /**
         * The category column to search, expected keys (value format) are:
         * "id" (int) the category id,
         * "ids" (string) category ids separated by commas,
         * "name" (string) the category name,
         * "parent" (int) the parent category id,
         * "idnumber" (string) category idnumber - user must have 'moodle/category:manage' to search on idnumber,
         * "visible" (int) whether the returned categories must be visible or hidden.
         * If the key is not passed, then the function return all categories that the user can see..
         */
        key: string;
        value: string | number; // The value to match.
    }[];
    addsubcategories?: boolean; // Return the sub categories infos (1 - default) otherwise only the category info (0).
};

/**
 * Data returned by core_course_get_categories WS.
 */
export type CoreCourseGetCategoriesWSResponse = CoreCategoryData[];

/**
 * Category data model.
 */
export type CoreCategoryData = {
    id: number; // Category id.
    name: string; // Category name.
    idnumber?: string; // Category id number.
    description: string; // Category description.
    descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
    parent: number; // Parent category id.
    sortorder: number; // Category sorting order.
    coursecount: number; // Number of courses in this category.
    visible?: number; // 1: available, 0:not available.
    visibleold?: number; // 1: available, 0:not available.
    timemodified?: number; // Timestamp.
    depth: number; // Category depth.
    path: string; // Category path.
    theme?: string; // Category theme.
};

/**
 * Params of core_course_get_user_navigation_options and core_course_get_user_administration_options WS.
 */
type CoreCourseGetUserAdminOrNavOptionsWSParams = {
    courseids: number[];
};

/**
 * Data returned by core_course_get_user_navigation_options and core_course_get_user_administration_options WS.
 */
export type CoreCourseGetUserAdminOrNavOptionsWSResponse = {
    courses: CoreCourseUserAdminOrNavOption[];
    warnings?: CoreWSExternalWarning[];
};

/**
 * Admin or navigation option data.
 */
export type CoreCourseUserAdminOrNavOption = {
    id: number; // Course id.
    options: {
        name: string; // Option name.
        available: boolean; // Whether the option is available or not.
    }[];
};

/**
 * Indexed administration or navigation course options.
 */
export type CoreCourseUserAdminOrNavOptionCourseIndexed = {
    [id: number]: CoreCourseUserAdminOrNavOptionIndexed;
};

/**
 * Indexed administration or navigation options.
 */
export type CoreCourseUserAdminOrNavOptionIndexed = {
    [name: string]: // Option name.
    boolean; // Whether the option is available or not.
};

/**
 * Params of core_enrol_get_course_enrolment_methods WS.
 */
type CoreEnrolGetCourseEnrolmentMethodsWSParams = {
    courseid: number; // Course id.
};

/**
 * Course enrolment method.
 */
export type CoreCourseEnrolmentMethod = {
    id: number; // Id of course enrolment instance.
    courseid: number; // Id of course.
    type: string; // Type of enrolment plugin.
    name: string; // Name of enrolment plugin.
    status: string; // Status of enrolment plugin.
    wsfunction?: string; // Webservice function to get more information.
};

/**
 * Params of enrol_guest_get_instance_info WS.
 */
type EnrolGuestGetInstanceInfoWSParams = {
    instanceid: number; // Instance id of guest enrolment plugin.
};

/**
 * Data returned by enrol_guest_get_instance_info WS.
 */
export type EnrolGuestGetInstanceInfoWSResponse = {
    instanceinfo: CoreCourseEnrolmentGuestMethod;
    warnings?: CoreWSExternalWarning[];
};

/**
 * Course guest enrolment method.
 */
export type CoreCourseEnrolmentGuestMethod = CoreCourseEnrolmentMethod & {
    passwordrequired: boolean; // Is a password required?.
};

/**
 * Params of enrol_self_enrol_user WS.
 */
type EnrolSelfEnrolUserWSParams = {
    courseid: number; // Id of the course.
    password?: string; // Enrolment key.
    instanceid?: number; // Instance id of self enrolment plugin.
};

/**
 * Params of core_course_set_favourite_courses WS.
 */
type CoreCourseSetFavouriteCoursesWSParams = {
    courses: {
        id: number; // Course ID.
        favourite: boolean; // Favourite status.
    }[];
};

export type CoreCourseAnyCourseData = CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData;