// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NavController } from 'ionic-angular';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreGradesProvider } from './grades';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';

/**
 * Service that provides some features regarding grades information.
 */
@Injectable()
export class CoreGradesHelperProvider {
    protected logger;

    constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider,
            private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider,
            private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider,
            private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider,
            private linkHelper: CoreContentLinksHelperProvider, private courseHelper: CoreCourseHelperProvider) {
        this.logger = logger.getInstance('CoreGradesHelperProvider');
    }

    /**
     * Formats a row from the grades table te be rendered in a page.
     *
     * @param  {any}  tableRow JSON object representing row of grades table data.
     * @return {any}           Formatted row object.
     */
    protected formatGradeRow(tableRow: any): any {
        const row = {};
        for (const name in tableRow) {
            if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
                let content = String(tableRow[name].content);

                if (name == 'itemname') {
                    this.setRowIcon(row, content);
                    row['link'] = this.getModuleLink(content);
                    row['rowclass'] += tableRow[name].class.indexOf('hidden') >= 0 ? ' hidden' : '';
                    row['rowclass'] += tableRow[name].class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';

                    content = content.replace(/<\/span>/gi, '\n');
                    content = this.textUtils.cleanTags(content);
                } else {
                    content = this.textUtils.replaceNewLines(content, '<br>');
                }

                if (content == '&nbsp;') {
                    content = '';
                }

                row[name] = content.trim();
            }
        }

        return row;
    }

    /**
     * Formats a row from the grades table to be rendered in one table.
     *
     * @param  {any}  tableRow JSON object representing row of grades table data.
     * @return {any}           Formatted row object.
     */
    protected formatGradeRowForTable(tableRow: any): any {
        const row = {};
        for (let name in tableRow) {
            if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
                let content = String(tableRow[name].content);

                if (name == 'itemname') {
                    row['id'] = parseInt(tableRow[name].id.split('_')[1], 10);
                    row['colspan'] = tableRow[name].colspan;
                    row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1;

                    this.setRowIcon(row, content);
                    row['rowclass'] = tableRow[name].class.indexOf('leveleven') < 0 ? 'odd' : 'even';
                    row['rowclass'] += tableRow[name].class.indexOf('hidden') >= 0 ? ' hidden' : '';
                    row['rowclass'] += tableRow[name].class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';

                    content = content.replace(/<\/span>/gi, '\n');
                    content = this.textUtils.cleanTags(content);
                    name = 'gradeitem';
                } else {
                    content = this.textUtils.replaceNewLines(content, '<br>');
                }

                if (content == '&nbsp;') {
                    content = '';
                }

                row[name] = content.trim();
            }
        }

        return row;
    }

    /**
     * Removes suffix formatted to compatibilize data from table and items.
     *
     * @param  {any} item Grade item to format.
     * @return {any}      Grade item formatted.
     */
    protected formatGradeItem(item: any): any {
        for (const name in item) {
            const index = name.indexOf('formatted');
            if (index > 0) {
                item[name.substr(0, index)] = item[name];
            }
        }

        return item;
    }

    /**
     * Formats the response of gradereport_user_get_grades_table to be rendered.
     *
     * @param  {any}  table          JSON object representing a table with data.
     * @return {any}             Formatted HTML table.
     */
    formatGradesTable(table: any): any {
        const maxDepth = table.maxdepth,
            formatted = {
                columns: [],
                rows: []
            },
            // Columns, in order.
            columns = {
                gradeitem: true,
                weight: false,
                grade: false,
                range: false,
                percentage: false,
                lettergrade: false,
                rank: false,
                average: false,
                feedback: false,
                contributiontocoursetotal: false
            };
        formatted.rows = table.tabledata.map((row: any) => {
            return this.formatGradeRowForTable(row);
        });

        // Get a row with some info.
        let normalRow = formatted.rows.find((e) => {
            return e.itemtype != 'leader' && (typeof e.grade != 'undefined' || typeof e.percentage != 'undefined');
        });

        // Decide if grades or percentage is being shown on phones.
        if (normalRow && typeof normalRow.grade != 'undefined') {
            columns.grade = true;
        } else if (normalRow && typeof normalRow.percentage != 'undefined') {
            columns.percentage = true;
        } else {
            normalRow = formatted.rows.find((e) => {
                return e.itemtype != 'leader';
            });
            columns.grade = true;
        }

        for (const colName in columns) {
            if (typeof normalRow[colName] != 'undefined') {
                formatted.columns.push({
                    name: colName,
                    colspan: colName == 'gradeitem' ? maxDepth : 1,
                    hiddenPhone: !columns[colName]
                });
            }
        }

        return formatted;
    }

    /**
     * Get course data for grades since they only have courseid.
     *
     * @param  {any} grades    Grades to get the data for.
     * @return {Promise<any>}  Promise always resolved. Resolve param is the formatted grades.
     */
    getGradesCourseData(grades: any): Promise<any> {
        // Using cache for performance reasons.
        return this.coursesProvider.getUserCourses(true).then((courses) => {
            const indexedCourses = {};
            courses.forEach((course) => {
                indexedCourses[course.id] = course;
            });

            grades.forEach((grade) => {
                if (typeof indexedCourses[grade.courseid] != 'undefined') {
                    grade.courseFullName = indexedCourses[grade.courseid].fullname;
                }
            });

            return grades;
        });
    }

    /**
     * Get an specific grade item.
     *
     * @param  {number}  courseId             ID of the course to get the grades from.
     * @param  {number}  gradeId              Grade ID.
     * @param  {number}  [userId]             ID of the user to get the grades from. If not defined use site's current user.
     * @param  {string}  [siteId]             Site ID. If not defined, current site.
     * @param  {boolean} [ignoreCache=false]  True if it should ignore cached data (it will always fail in offline or server down).
     * @return {Promise<any>}                Promise to be resolved when the grades are retrieved.
     */
    getGradeItem(courseId: number, gradeId: number, userId?: number, siteId?: string, ignoreCache: boolean = false): Promise<any> {

        return this.gradesProvider.getCourseGradesTable(courseId, userId, siteId, ignoreCache).then((grades) => {
            if (grades) {
                return this.getGradesTableRow(grades, gradeId);
            }

            return Promise.reject(null);
        });
    }

    /**
     * Returns the label of the selected grade.
     *
     * @param {any[]} grades Array with objects with value and label.
     * @param {number} selectedGrade Selected grade value.
     * @return {string} Selected grade label.
     */
    getGradeLabelFromValue(grades: any[], selectedGrade: number): string {
        selectedGrade = Number(selectedGrade);

        if (!grades || !selectedGrade || selectedGrade <= 0) {
            return '';
        }

        for (const x in grades) {
            if (grades[x].value == selectedGrade) {
                return grades[x].label;
            }
        }

        return '';
    }

    /**
     * Get the grade items for a certain module. Keep in mind that may have more than one item to include outcomes and scales.
     *
     * @param  {number}  courseId             ID of the course to get the grades from.
     * @param  {number}  moduleId             Module ID.
     * @param  {number}  [userId]             ID of the user to get the grades from. If not defined use site's current user.
     * @param  {number}  [groupId]            ID of the group to get the grades from. Not used for old gradebook table.
     * @param  {string}  [siteId]             Site ID. If not defined, current site.
     * @param  {boolean} [ignoreCache=false]  True if it should ignore cached data (it will always fail in offline or server down).
     * @return {Promise<any>}                Promise to be resolved when the grades are retrieved.
     */
    getGradeModuleItems(courseId: number, moduleId: number, userId?: number, groupId?: number, siteId?: string,
            ignoreCache: boolean = false): Promise<any> {

        return this.gradesProvider.getGradeItems(courseId, userId, groupId, siteId, ignoreCache).then((grades) => {
            if (grades) {
                if (typeof grades.tabledata != 'undefined') {
                    // Table format.
                    return this.getModuleGradesTableRows(grades, moduleId);
                } else {
                    return grades.filter((item) => {
                        return item.cmid == moduleId;
                    }).map((item) => {
                        return this.formatGradeItem(item);
                    });
                }
            }

            return Promise.reject(null);
        });
    }

    /**
     * Returns the value of the selected grade.
     *
     * @param {any[]} grades Array with objects with value and label.
     * @param {string} selectedGrade Selected grade label.
     * @return {number} Selected grade value.
     */
    getGradeValueFromLabel(grades: any[], selectedGrade: string): number {
        if (!grades || !selectedGrade) {
            return 0;
        }

        for (const x in grades) {
            if (grades[x].label == selectedGrade) {
                return grades[x].value < 0 ? 0 : grades[x].value;
            }
        }

        return 0;
    }

    /**
     * Gets the link to the module for the selected grade.
     *
     * @param  {string} text HTML where the link is present.
     * @return {string | false}      URL linking to the module.
     */
    protected getModuleLink(text: string): string | false {
        const el = this.domUtils.toDom(text)[0],
            link = el.attributes['href'] ? el.attributes['href'].value : false;

        if (!link || link.indexOf('/mod/') < 0) {
            return false;
        }

        return link;
    }

    /**
     * Get a row from the grades table.
     *
     * @param  {any}   table    JSON object representing a table with data.
     * @param  {number} gradeId Grade Object identifier.
     * @return {any}            Formatted HTML table.
     */
    getGradesTableRow(table: any, gradeId: number): any {
        if (table.tabledata) {
            const selectedRow = table.tabledata.find((row) => {
                return row.itemname && row.itemname.id && row.itemname.id.substr(0, 3) == 'row' &&
                    parseInt(row.itemname.id.split('_')[1], 10) == gradeId;
            });

            if (selectedRow) {
                return this.formatGradeRow(selectedRow);
            }
        }

        return '';
    }

    /**
     * Get the rows related to a module from the grades table.
     *
     * @param  {any}   table     JSON object representing a table with data.
     * @param  {number} moduleId Grade Object identifier.
     * @return {any}             Formatted HTML table.
     */
    getModuleGradesTableRows(table: any, moduleId: number): any {

        if (table.tabledata) {
            // Find href containing "/mod/xxx/xxx.php".
            const regex = /href="([^"]*\/mod\/[^"|^\/]*\/[^"|^\.]*\.php[^"]*)/;

            return table.tabledata.filter((row) => {
                if (row.itemname && row.itemname.content) {
                    const matches = row.itemname.content.match(regex);

                    if (matches && matches.length) {
                        const hrefParams = this.urlUtils.extractUrlParams(matches[1]);

                        return hrefParams && hrefParams.id == moduleId;
                    }
                }

                return false;
            }).map((row) => {
                return this.formatGradeRow(row);
            });
        }

        return [];
    }

    /**
     * Go to view grades.
     *
     * @param {number} courseId Course ID t oview.
     * @param {number} [userId] User to view. If not defined, current user.
     * @param {number} [moduleId] Module to view. If not defined, view all course grades.
     * @param {NavController} [navCtrl] NavController to use.
     * @param {string} [siteId] Site ID. If not defined, current site.
     * @return {Promise<any>} Promise resolved when done.
     */
    goToGrades(courseId: number, userId?: number, moduleId?: number, navCtrl?: NavController, siteId?: string): Promise<any> {

        const modal = this.domUtils.showModalLoading();
        let currentUserId;

        return this.sitesProvider.getSite(siteId).then((site) => {
            siteId = site.id;
            currentUserId = site.getUserId();

            if (moduleId) {
                // Try to open the module grade directly. Check if it's possible.
                return this.gradesProvider.isGradeItemsAvalaible(siteId).then((getGrades) => {
                    if (!getGrades) {
                        return Promise.reject(null);
                    }
                });
            } else {
                return Promise.reject(null);
            }

        }).then(() => {

            // Can get grades. Do it.
            return this.gradesProvider.getGradeItems(courseId, userId, undefined, siteId).then((items) => {
                // Find the item of the module.
                const item = items.find((item) => {
                    return moduleId == item.cmid;
                });

                if (item) {
                    // Open the item directly.
                    const pageParams: any = {
                        courseId: courseId,
                        userId: userId,
                        gradeId: item.id
                    };

                    return this.linkHelper.goInSite(navCtrl, 'CoreGradesGradePage', pageParams, siteId).catch(() => {
                        // Ignore errors.
                    });
                }

                return Promise.reject(null);
            });

        }).catch(() => {

            // Cannot get grade items or there's no need to.
            if (userId && userId != currentUserId) {
                // View another user grades. Open the grades page directly.
                const pageParams = {
                    course: {id: courseId},
                    userId: userId
                };

                return this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursePage', pageParams, siteId).catch(() => {
                    // Ignore errors.
                });
            }

            // View own grades. Check if we already are in the course index page.
            if (this.courseProvider.currentViewIsCourse(navCtrl, courseId)) {
                // Current view is this course, just select the grades tab.
                this.courseProvider.selectCourseTab('CoreGrades');

                return;
            }

            // Open the course with the grades tab selected.
            return this.courseHelper.getCourse(courseId, siteId).then((result) => {
                const pageParams: any = {
                    course: result.course,
                    selectedTab: 'CoreGrades'
                };

                return this.linkHelper.goInSite(navCtrl, 'CoreCourseSectionPage', pageParams, siteId).catch(() => {
                    // Ignore errors.
                });
            });
        }).catch(() => {
            // Cannot get course for some reason, just open the grades page.
            return this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursePage', {course: {id: courseId}}, siteId);
        }).finally(() => {
            modal.dismiss();
        });
    }

    /**
     * Invalidate the grade items for a certain module.
     *
     * @param  {number}  courseId     ID of the course to invalidate the grades.
     * @param  {number}  [userId]     ID of the user to invalidate. If not defined use site's current user.
     * @param  {number}  [groupId]    ID of the group to invalidate. Not used for old gradebook table.
     * @param  {string}  [siteId]     Site ID. If not defined, current site.
     * @return {Promise}              Promise to be resolved when the grades are invalidated.
     */
    invalidateGradeModuleItems(courseId: number, userId?: number, groupId?: number, siteId?: string): Promise<any> {
        siteId = siteId || this.sitesProvider.getCurrentSiteId();

        return this.sitesProvider.getSite(siteId).then((site) => {
            userId = userId || site.getUserId();

            return this.gradesProvider.isGradeItemsAvalaible(siteId).then((enabled) => {
                if (enabled) {
                    return this.gradesProvider.invalidateCourseGradesItemsData(courseId, userId, groupId, siteId);
                } else {
                    return this.gradesProvider.invalidateCourseGradesData(courseId, userId, siteId);
                }
            });
        });
    }

    /**
     * Parses the image and sets it to the row.
     *
     * @param  {any} row  Formatted grade row object.
     * @param  {string} text HTML where the image will be rendered.
     * @return {any}      Row object with the image.
     */
    protected setRowIcon(row: any, text: string): any {
        text = text.replace('%2F', '/').replace('%2f', '/');

        if (text.indexOf('/agg_mean') > -1) {
            row['itemtype'] = 'agg_mean';
            row['image'] = 'assets/img/grades/agg_mean.png';
        } else if (text.indexOf('/agg_sum') > -1) {
            row['itemtype'] = 'agg_sum';
            row['image'] = 'assets/img/grades/agg_sum.png';
        } else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks')  > -1) {
            row['itemtype'] = 'outcome';
            row['icon'] = 'fa-tasks';
        } else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder')  > -1) {
            row['itemtype'] = 'category';
            row['icon'] = 'fa-folder';
        } else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o')  > -1) {
            row['itemtype'] = 'manual';
            row['icon'] = 'fa-square-o';
        } else if (text.indexOf('/mod/') > -1) {
            const module = text.match(/mod\/([^\/]*)\//);
            if (typeof module[1] != 'undefined') {
                row['itemtype'] = 'mod';
                row['itemmodule'] = module[1];
                row['image'] = this.courseProvider.getModuleIconSrc(module[1],
                    this.domUtils.convertToElement(text).querySelector('img').getAttribute('src'));
            }
        } else {
            if (row['rowspan'] && row['rowspan'] > 1) {
                row['itemtype'] = 'category';
                row['icon'] = 'fa-folder';
            } else if (text.indexOf('src=') > -1) {
                row['itemtype'] = 'unknown';
                const src = text.match(/src="([^"]*)"/);
                row['image'] = src[1];
            } else if (text.indexOf('<i ') > -1) {
                row['itemtype'] = 'unknown';
                const src = text.match(/<i class="(?:[^"]*?\s)?(fa-[a-z0-9-]+)/);
                row['icon'] = src ? src[1] : '';
            }
        }

        return row;
    }

    /**
     * Creates an array that represents all the current grades that can be chosen using the given grading type.
     * Negative numbers are scales, zero is no grade, and positive numbers are maximum grades.
     *
     * Taken from make_grades_menu on moodlelib.php
     *
     * @param  {number} gradingType     If positive, max grade you can provide. If negative, scale Id.
     * @param  {number} [moduleId]      Module ID. Used to retrieve the scale items when they are not passed as parameter.
     *                                  If the user does not have permision to manage the activity an empty list is returned.
     * @param  {string} [defaultLabel]  Element that will become default option, if not defined, it won't be added.
     * @param  {any}    [defaultValue]  Element that will become default option value. Default ''.
     * @param  {string} [scale]         Scale csv list String. If not provided, it will take it from the module grade info.
     * @return {Promise<any[]>}         Array with objects with value and label to create a propper HTML select.
     */
    makeGradesMenu(gradingType: number, moduleId?: number, defaultLabel: string = '', defaultValue: any = '', scale?: string):
            Promise<any[]> {
        if (gradingType < 0) {
            if (scale) {
                return Promise.resolve(this.utils.makeMenuFromList(scale, defaultLabel, undefined, defaultValue));
            } else if (moduleId) {
                return this.courseProvider.getModuleBasicGradeInfo(moduleId).then((gradeInfo) => {
                    if (gradeInfo.scale) {
                        return this.utils.makeMenuFromList(gradeInfo.scale, defaultLabel, undefined,  defaultValue);
                    }

                    return [];
                });
            } else {
                return Promise.resolve([]);
            }
        }

        if (gradingType > 0) {
            const grades = [];
            if (defaultLabel) {
                // Key as string to avoid resorting of the object.
                grades.push({
                    label: defaultLabel,
                    value: defaultValue
                });
            }
            for (let i = gradingType; i >= 0; i--) {
                 grades.push({
                    label: i + ' / ' + gradingType,
                    value: i
                });
            }

            return Promise.resolve(grades);
        }

        return Promise.resolve([]);
    }
}
