// (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 { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, ElementRef } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
import { CoreSyncProvider } from '@providers/sync';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { MoodleMobileApp } from '../../../../../app/app.component';
import { AddonModLessonProvider } from '../../providers/lesson';
import { AddonModLessonOfflineProvider } from '../../providers/lesson-offline';
import { AddonModLessonSyncProvider } from '../../providers/lesson-sync';
import { AddonModLessonHelperProvider } from '../../providers/helper';

/**
 * Page that allows attempting and reviewing a lesson.
 */
@IonicPage({ segment: 'addon-mod-lesson-player' })
@Component({
    selector: 'page-addon-mod-lesson-player',
    templateUrl: 'player.html',
})
export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
    @ViewChild(Content) content: Content;
    @ViewChild('questionFormEl') formElement: ElementRef;

    component = AddonModLessonProvider.COMPONENT;
    LESSON_EOL = AddonModLessonProvider.LESSON_EOL;
    questionForm: FormGroup; // The FormGroup for question pages.
    title: string; // The page title.
    lesson: any; // The lesson object.
    currentPage: number; // Current page being viewed.
    review: boolean; // Whether the user is reviewing.
    messages: any[]; // Messages to display to the user.
    menuModal: Modal; // Modal to navigate through the pages.
    canManage: boolean; // Whether the user can manage the lesson.
    retake: number; // Current retake number.
    showRetake: boolean; // Whether the retake number needs to be displayed.
    lessonWidth: string; // Width of the lesson (if slideshow mode).
    lessonHeight: string; // Height of the lesson (if slideshow mode).
    endTime: number; // End time of the lesson if it's timed.
    pageData: any; // Current page data.
    pageContent: string; // Current page contents.
    pageButtons: any[]; // List of buttons of the current page.
    question: any; // Question of the current page (if it's a question page).
    eolData: any; // Data for EOL page (if current page is EOL).
    processData: any; // Data to display after processing a page.
    loaded: boolean; // Whether data has been loaded.
    displayMenu: boolean; // Whether the lesson menu should be displayed.
    originalData: any; // Original question data. It is used to check if data has changed.

    protected courseId: number; // The course ID the lesson belongs to.
    protected lessonId: number; // Lesson ID.
    protected password: string; // Lesson password (if any).
    protected forceLeave = false; // If true, don't perform any check when leaving the view.
    protected offline: boolean; // Whether we are in offline mode.
    protected accessInfo: any; // Lesson access info.
    protected jumps: any; // All possible jumps.
    protected mediaFile: any; // Media file of the lesson.
    protected firstPageLoaded: boolean; // Whether the first page has been loaded.
    protected loadingMenu: boolean; // Whether the lesson menu is being loaded.
    protected lessonPages: any[]; // Lesson pages (for the lesson menu).

    constructor(protected navParams: NavParams, protected translate: TranslateService,
            protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider,
            protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController,
            protected timeUtils: CoreTimeUtilsProvider, protected lessonProvider: AddonModLessonProvider,
            protected lessonHelper: AddonModLessonHelperProvider, protected lessonSync: AddonModLessonSyncProvider,
            protected lessonOfflineProvider: AddonModLessonOfflineProvider, protected cdr: ChangeDetectorRef,
            modalCtrl: ModalController, protected navCtrl: NavController, protected appProvider: CoreAppProvider,
            protected utils: CoreUtilsProvider, protected urlUtils: CoreUrlUtilsProvider, protected fb: FormBuilder,
            protected mmApp: MoodleMobileApp) {

        this.lessonId = navParams.get('lessonId');
        this.courseId = navParams.get('courseId');
        this.password = navParams.get('password');
        this.review = !!navParams.get('review');
        this.currentPage = navParams.get('pageId');

        // Block the lesson so it cannot be synced.
        this.syncProvider.blockOperation(this.component, this.lessonId);

        // Create the navigation modal.
        this.menuModal = modalCtrl.create('AddonModLessonMenuModalPage', {
            page: this
        }, { cssClass: 'core-modal-lateral',
            showBackdrop: true,
            enableBackdropDismiss: true,
            enterAnimation: 'core-modal-lateral-transition',
            leaveAnimation: 'core-modal-lateral-transition' });
    }

    /**
     * Component being initialized.
     */
    ngOnInit(): void {
        // Fetch the Lesson data.
        this.fetchLessonData().then((success) => {
            if (success) {
                // Review data loaded or new retake started, remove any retake being finished in sync.
                this.lessonSync.deleteRetakeFinishedInSync(this.lessonId);
            }
        }).finally(() => {
            this.loaded = true;
        });
    }

    /**
     * Component being destroyed.
     */
    ngOnDestroy(): void {
        // Unblock the lesson so it can be synced.
        this.syncProvider.unblockOperation(this.component, this.lessonId);
    }

    /**
     * Check if we can leave the page or not.
     *
     * @return Resolved if we can leave it, rejected if not.
     */
    async ionViewCanLeave(): Promise<void> {
        if (this.forceLeave) {
            return;
        }

        if (this.question && !this.eolData && !this.processData && this.originalData) {
            // Question shown. Check if there is any change.
            if (!this.utils.basicLeftCompare(this.questionForm.getRawValue(), this.originalData, 3)) {
                 await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
            }
        }

        this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
    }

    /**
     * Runs when the page is about to leave and no longer be the active page.
     */
    ionViewWillLeave(): void {
        this.mmApp.closeModal();
    }

    /**
     * A button was clicked.
     *
     * @param data Button data.
     */
    buttonClicked(data: any): void {
        this.processPage(data);
    }

    /**
     * Call a function and go offline if allowed and the call fails.
     *
     * @param func Function to call.
     * @param args Arguments to pass to the function.
     * @param options Options passed to the function (also included in args).
     * @return Promise resolved in success, rejected otherwise.
     */
    protected callFunction(func: Function, args: any[], options: any): Promise<any> {
        return func.apply(func, args).catch((error) => {
            if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson) &&
                    !this.utils.isWebServiceError(error)) {
                // If it fails, go offline.
                this.offline = true;

                // Get the possible jumps now.
                return this.lessonProvider.getPagesPossibleJumps(this.lesson.id, {
                    cmId: this.lesson.coursemodule,
                    readingStrategy: CoreSitesReadingStrategy.PreferCache,
                }).then((jumpList) => {
                    this.jumps = jumpList;

                    // Call the function again with offline mode and the new jumps.
                    options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
                    options.jumps = this.jumps;
                    options.offline = true;

                    return func.apply(func, args);
                });
            }

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

    /**
     * Change the page from menu or when continuing from a feedback page.
     *
     * @param pageId Page to load.
     * @param ignoreCurrent If true, allow loading current page.
     */
    changePage(pageId: number, ignoreCurrent?: boolean): void {
        if (!ignoreCurrent && !this.eolData && this.currentPage == pageId) {
            // Page already loaded, stop.
            return;
        }

        this.loaded = true;
        this.messages = [];

        this.loadPage(pageId).catch((error) => {
            this.domUtils.showErrorModalDefault(error, 'Error loading page');
        }).finally(() => {
            this.loaded = true;
        });
    }

    /**
     * Get the lesson data and load the page.
     *
     * @return Promise resolved with true if success, resolved with false otherwise.
     */
    protected fetchLessonData(): Promise<boolean> {
        // Wait for any ongoing sync to finish. We won't sync a lesson while it's being played.
        return this.lessonSync.waitForSync(this.lessonId).then(() => {
            return this.lessonProvider.getLessonById(this.courseId, this.lessonId);
        }).then((lessonData) => {
            this.lesson = lessonData;
            this.title = this.lesson.name; // Temporary title.

            // If lesson has offline data already, use offline mode.
            return this.lessonOfflineProvider.hasOfflineData(this.lessonId);
        }).then((offlineMode) => {
            this.offline = offlineMode;

            if (!offlineMode && !this.appProvider.isOnline() && this.lessonProvider.isLessonOffline(this.lesson) && !this.review) {
                // Lesson doesn't have offline data, but it allows offline and the device is offline. Use offline mode.
                this.offline = true;
            }

            const options = {
                cmId: this.lesson.coursemodule,
                readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
            };

            return this.callFunction(this.lessonProvider.getAccessInformation.bind(this.lessonProvider),
                    [this.lesson.id, options], options);
        }).then((info) => {
            const promises = [];

            this.accessInfo = info;
            this.canManage = info.canmanage;
            this.retake = info.attemptscount;
            this.showRetake = !this.currentPage && this.retake > 0; // Only show it in first page if it isn't the first retake.

            if (info.preventaccessreasons && info.preventaccessreasons.length) {
                // If it's a password protected lesson and we have the password, allow playing it.
                const preventReason = this.lessonProvider.getPreventAccessReason(info, !!this.password, this.review);
                if (preventReason) {
                    // Lesson cannot be played, show message and go back.
                    return Promise.reject(preventReason.message);
                }
            }

            if (this.review && this.navParams.get('retake') != info.attemptscount - 1) {
                // Reviewing a retake that isn't the last one. Error.
                return Promise.reject(this.translate.instant('addon.mod_lesson.errorreviewretakenotlast'));
            }

            if (this.password) {
                // Lesson uses password, get the whole lesson object.
                const options = {
                    password: this.password,
                    cmId: this.lesson.coursemodule,
                    readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
                };
                promises.push(this.callFunction(this.lessonProvider.getLessonWithPassword.bind(this.lessonProvider),
                        [this.lesson.id, options], options).then((lesson) => {
                    this.lesson = lesson;
                }));
            }

            if (this.offline) {
                // Offline mode, get the list of possible jumps to allow navigation.
                promises.push(this.lessonProvider.getPagesPossibleJumps(this.lesson.id, {
                    cmId: this.lesson.coursemodule,
                    readingStrategy: CoreSitesReadingStrategy.PreferCache,
                }).then((jumpList) => {
                    this.jumps = jumpList;
                }));
            }

            return Promise.all(promises);
        }).then(() => {
            this.mediaFile = this.lesson.mediafiles && this.lesson.mediafiles[0];

            this.lessonWidth = this.lesson.slideshow ? this.domUtils.formatPixelsSize(this.lesson.mediawidth) : '';
            this.lessonHeight = this.lesson.slideshow ? this.domUtils.formatPixelsSize(this.lesson.mediaheight) : '';

            return this.launchRetake(this.currentPage);
        }).then(() => {
            return true;
        }).catch((error) => {
            // An error occurred.
            let promise;

            if (this.review && this.navParams.get('retake') && this.utils.isWebServiceError(error)) {
                // The user cannot review the retake. Unmark the retake as being finished in sync.
                promise = this.lessonSync.deleteRetakeFinishedInSync(this.lessonId);
            } else {
                promise = Promise.resolve();
            }

            return promise.then(() => {
                this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
                this.forceLeave = true;
                this.navCtrl.pop();

                return false;
            });
        });
    }

    /**
     * Finish the retake.
     *
     * @param outOfTime Whether the retake is finished because the user ran out of time.
     * @return Promise resolved when done.
     */
    protected finishRetake(outOfTime?: boolean): Promise<any> {
        let promise;

        this.messages = [];

        if (this.offline && this.appProvider.isOnline()) {
            // Offline mode but the app is online. Try to sync the data.
            promise = this.lessonSync.syncLesson(this.lesson.id, true, true).then((result) => {
                if (result.warnings && result.warnings.length) {
                    const error = result.warnings[0];

                    // Some data was deleted. Check if the retake has changed.
                    return this.lessonProvider.getAccessInformation(this.lesson.id, {
                        cmId: this.lesson.coursemodule,
                    }).then((info) => {
                        if (info.attemptscount != this.accessInfo.attemptscount) {
                            // The retake has changed. Leave the view and show the error.
                            this.forceLeave = true;
                            this.navCtrl.pop();

                            return Promise.reject(error);
                        }

                        // Retake hasn't changed, show the warning and finish the retake in offline.
                        this.offline = false;
                        this.domUtils.showErrorModal(error);
                    });
                }

                this.offline = false;
            }, () => {
                // Ignore errors.
            });
        } else {
            promise = Promise.resolve();
        }

        return promise.then(() => {
            // Now finish the retake.
            const options = {
                password: this.password,
                outOfTime,
                review: this.review,
                offline: this.offline,
                accessInfo: this.accessInfo,
            };
            const args = [this.lesson, this.courseId, options];

            return this.callFunction(this.lessonProvider.finishRetake.bind(this.lessonProvider), args, options);
        }).then((data) => {
            this.title = this.lesson.name;
            this.eolData = data.data;
            this.messages = this.messages.concat(data.messages);
            this.processData = undefined;

            this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'lesson' });

            // Format activity link if present.
            if (this.eolData && this.eolData.activitylink) {
                this.eolData.activitylink.value = this.lessonHelper.formatActivityLink(this.eolData.activitylink.value);
            }

            // Format review lesson if present.
            if (this.eolData && this.eolData.reviewlesson) {
                const params = this.urlUtils.extractUrlParams(this.eolData.reviewlesson.value);

                if (!params || !params.pageid) {
                    // No pageid in the URL, the user cannot review (probably didn't answer any question).
                    delete this.eolData.reviewlesson;
                } else {
                    this.eolData.reviewlesson.pageid = params.pageid;
                }
            }
        });
    }

    /**
     * Jump to a certain page after performing an action.
     *
     * @param pageId The page to load.
     * @return Promise resolved when done.
     */
    protected jumpToPage(pageId: number): Promise<any> {
        if (pageId === 0) {
            // Not a valid page, return to entry view.
            // This happens, for example, when the user clicks to go to previous page and there is no previous page.
            this.forceLeave = true;
            this.navCtrl.pop();

            return Promise.resolve();
        } else if (pageId == AddonModLessonProvider.LESSON_EOL) {
            // End of lesson reached.
            return this.finishRetake();
        }

        // Load new page.
        this.messages = [];

        return this.loadPage(pageId);
    }

    /**
     * Start or continue a retake.
     *
     * @param pageId The page to load.
     * @return Promise resolved when done.
     */
    protected launchRetake(pageId: number): Promise<any> {
        let promise;

        if (this.review) {
            // Review mode, no need to launch the retake.
            promise = Promise.resolve({});
        } else if (!this.offline) {
            // Not in offline mode, launch the retake.
            promise = this.lessonProvider.launchRetake(this.lesson.id, this.password, pageId);
        } else {
            // Check if there is a finished offline retake.
            promise = this.lessonOfflineProvider.hasFinishedRetake(this.lesson.id).then((finished) => {
                if (finished) {
                    // Always show EOL page.
                    pageId = AddonModLessonProvider.LESSON_EOL;
                }

                return {};
            });
        }

        return promise.then((data) => {
            this.currentPage = pageId || this.accessInfo.firstpageid;
            this.messages = data.messages || [];

            if (this.lesson.timelimit && !this.accessInfo.canmanage) {
                // Get the last lesson timer.
                return this.lessonProvider.getTimers(this.lesson.id, {
                    cmId: this.lesson.coursemodule,
                    readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
                }).then((timers) => {
                    this.endTime = timers[timers.length - 1].starttime + this.lesson.timelimit;
                });
            }
        }).then(() => {
            return this.loadPage(this.currentPage);
        });
    }

    /**
     * Load the lesson menu.
     *
     * @return Promise resolved when done.
     */
    protected loadMenu(): Promise<any> {
        if (this.loadingMenu) {
            // Already loading.
            return;
        }

        this.loadingMenu = true;

        const options = {
            password: this.password,
            cmId: this.lesson.coursemodule,
            readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
        };
        const args = [this.lessonId, options];

        return this.callFunction(this.lessonProvider.getPages.bind(this.lessonProvider), args, options).then((pages) => {
            this.lessonPages = pages.map((entry) => {
                return entry.page;
            });
        }).catch((error) => {
            this.domUtils.showErrorModalDefault(error, 'Error loading menu.');
        }).finally(() => {
            this.loadingMenu = false;
        });
    }

    /**
     * Load a certain page.
     *
     * @param pageId The page to load.
     * @return Promise resolved when done.
     */
    protected loadPage(pageId: number): Promise<any> {
        if (pageId == AddonModLessonProvider.LESSON_EOL) {
            // End of lesson reached.
            return this.finishRetake();
        }

        const options = {
            password: this.password,
            review: this.review,
            includeContents: true,
            cmId: this.lesson.coursemodule,
            readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
            accessInfo: this.accessInfo,
            jumps: this.jumps,
            includeOfflineData: true,
        };
        const args = [this.lesson, pageId, options];

        return this.callFunction(this.lessonProvider.getPageData.bind(this.lessonProvider), args, options).then((data) => {
            if (data.newpageid == AddonModLessonProvider.LESSON_EOL) {
                // End of lesson reached.
                return this.finishRetake();
            }

            this.pageData = data;
            this.title = data.page.title;
            this.pageContent = this.lessonHelper.getPageContentsFromPageData(data);
            this.loaded = true;
            this.currentPage = pageId;
            this.messages = this.messages.concat(data.messages);

            // Page loaded, hide EOL and feedback data if shown.
            this.eolData = this.processData = undefined;

            if (this.lessonProvider.isQuestionPage(data.page.type)) {
                // Create an empty FormGroup without controls, they will be added in getQuestionFromPageData.
                this.questionForm = this.fb.group({});
                this.pageButtons = [];
                this.question = this.lessonHelper.getQuestionFromPageData(this.questionForm, data);
                this.originalData = this.questionForm.getRawValue(); // Use getRawValue to include disabled values.
            } else {
                this.pageButtons = this.lessonHelper.getPageButtonsFromHtml(data.pagecontent);
                this.question = undefined;
                this.originalData = undefined;
            }

            if (data.displaymenu && !this.displayMenu) {
                // Load the menu.
                this.loadMenu();
            }
            this.displayMenu = !!data.displaymenu;

            if (!this.firstPageLoaded) {
                this.firstPageLoaded = true;
            } else {
                this.showRetake = false;
            }
        });
    }

    /**
     * Process a page, sending some data.
     *
     * @param data The data to send.
     * @param formSubmitted Whether a form was submitted.
     * @return Promise resolved when done.
     */
    protected processPage(data: any, formSubmitted?: boolean): Promise<any> {
        this.loaded = false;

        const options = {
            password: this.password,
            review: this.review,
            offline: this.offline,
            accessInfo: this.accessInfo,
            jumps: this.jumps,
        };
        const args = [this.lesson, this.courseId, this.pageData, data, options];

        return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, options).then((result) => {
            if (formSubmitted) {
                this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.sitesProvider.getCurrentSiteId());
            }

            if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson)) {
                // Lesson allows offline and the user changed some data in server. Update cached data.
                const retake = this.accessInfo.attemptscount;
                const options = {
                    cmId: this.lesson.coursemodule,
                    readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
                };

                if (this.lessonProvider.isQuestionPage(this.pageData.page.type)) {
                    this.lessonProvider.getQuestionsAttemptsOnline(this.lessonId, retake, options);
                } else {
                    this.lessonProvider.getContentPagesViewedOnline(this.lessonId, retake, options);
                }
            }

            if (result.nodefaultresponse || result.inmediatejump) {
                // Don't display feedback or force a redirect to a new page. Load the new page.
                return this.jumpToPage(result.newpageid);
            } else {

                // Not inmediate jump, show the feedback.
                result.feedback = this.lessonHelper.removeQuestionFromFeedback(result.feedback);
                this.messages = result.messages;
                this.processData = result;
                this.processData.buttons = [];

                if (this.lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
                       !result.maxattemptsreached && !result.reviewmode) {
                    // User can try again, show button to do so.
                    this.processData.buttons.push({
                        label: 'addon.mod_lesson.reviewquestionback',
                        pageId: this.currentPage
                    });
                }

                // Button to continue.
                if (this.lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
                       !result.maxattemptsreached) {
                    /* If both the "Yes, I'd like to try again" and "No, I just want to go on to the next question" point to the
                       same page then don't show the "No, I just want to go on to the next question" button. It's confusing. */
                    if (this.pageData.page.id != result.newpageid) {
                        // Button to continue the lesson (the page to go is configured by the teacher).
                        this.processData.buttons.push({
                            label: 'addon.mod_lesson.reviewquestioncontinue',
                            pageId: result.newpageid
                        });
                    }
                } else {
                    this.processData.buttons.push({
                        label: 'addon.mod_lesson.continue',
                        pageId: result.newpageid
                    });
                }
            }
        }).catch((error) => {
            this.domUtils.showErrorModalDefault(error, 'Error processing page');
        }).finally(() => {
            this.loaded = true;
        });
    }

    /**
     * Review the lesson.
     *
     * @param pageId Page to load.
     */
    reviewLesson(pageId: number): void {
        this.loaded = false;
        this.review = true;
        this.offline = false; // Don't allow offline mode in review.

        this.loadPage(pageId).catch((error) => {
            this.domUtils.showErrorModalDefault(error, 'Error loading page');
        }).finally(() => {
            this.loaded = true;
        });
    }

    /**
     * Submit a question.
     *
     * @param e Event.
     */
    submitQuestion(e: Event): void {
        e.preventDefault();
        e.stopPropagation();

        this.loaded = false;

        // Use getRawValue to include disabled values.
        const data = this.lessonHelper.prepareQuestionData(this.question, this.questionForm.getRawValue());

        this.processPage(data, true).finally(() => {
            this.loaded = true;
        });
    }

    /**
     * Time up.
     */
    timeUp(): void {
        // Time up called, hide the timer.
        this.endTime = undefined;
        this.loaded = false;

        this.finishRetake(true).catch((error) => {
            this.domUtils.showErrorModalDefault(error, 'Error finishing attempt');
        }).finally(() => {
            this.loaded = true;
        });
    }
}