// (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, Injector, ViewChild } from '@angular/core';
import { Content, PopoverController } from 'ionic-angular';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreRatingProvider } from '@core/rating/providers/rating';
import { CoreRatingOfflineProvider } from '@core/rating/providers/offline';
import { CoreRatingSyncProvider } from '@core/rating/providers/sync';
import { AddonModGlossaryProvider } from '../../providers/glossary';
import { AddonModGlossaryOfflineProvider } from '../../providers/offline';
import { AddonModGlossarySyncProvider } from '../../providers/sync';
import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker';
import { AddonModGlossaryPrefetchHandler } from '../../providers/prefetch-handler';

type FetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';

/**
 * Component that displays a glossary entry page.
 */
@Component({
    selector: 'addon-mod-glossary-index',
    templateUrl: 'addon-mod-glossary-index.html',
})
export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent {
    @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
    @ViewChild(Content) content: Content;

    component = AddonModGlossaryProvider.COMPONENT;
    moduleName = 'glossary';

    isSearch = false;
    entries = [];
    offlineEntries = [];
    canAdd = false;
    canLoadMore = false;
    loadMoreError = false;
    loadingMessage = this.translate.instant('core.loading');
    selectedEntry: number;

    protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
    protected glossary: any;
    protected fetchFunction: Function;
    protected fetchInvalidate: Function;
    protected fetchArguments: any[];
    protected showDivider: (entry: any, previous?: any) => boolean;
    protected getDivider: (entry: any) => string;
    protected addEntryObserver: any;
    protected fetchMode: FetchMode;
    protected viewMode: string;
    protected fetchedEntriesCanLoadMore = false;
    protected fetchedEntries = [];

    hasOfflineRatings: boolean;
    protected ratingOfflineObserver: any;
    protected ratingSyncObserver: any;

    constructor(injector: Injector,
            private popoverCtrl: PopoverController,
            private glossaryProvider: AddonModGlossaryProvider,
            private glossaryOffline: AddonModGlossaryOfflineProvider,
            private prefetchHandler: AddonModGlossaryPrefetchHandler,
            private ratingOffline: CoreRatingOfflineProvider) {
        super(injector);
    }

    /**
     * Component being initialized.
     */
    ngOnInit(): void {
        super.ngOnInit();

        // When an entry is added, we reload the data.
        this.addEntryObserver = this.eventsProvider.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, this.eventReceived.bind(this));

        // Listen for offline ratings saved and synced.
        this.ratingOfflineObserver = this.eventsProvider.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
            if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
                    && data.instanceId == this.glossary.coursemodule) {
                this.hasOfflineRatings = true;
            }
        });
        this.ratingSyncObserver = this.eventsProvider.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
            if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
                    && data.instanceId == this.glossary.coursemodule) {
                this.hasOfflineRatings = false;
            }
        });

        this.loadContent(false, true).then(() => {
            if (!this.glossary) {
                return;
            }

            if (this.splitviewCtrl.isOn()) {
                // Load the first entry.
                if (this.entries.length > 0) {
                    this.openEntry(this.entries[0].id);
                }
            }

            this.glossaryProvider.logView(this.glossary.id, this.viewMode, this.glossary.name).then(() => {
                this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata);
            }).catch((error) => {
                // Ignore errors.
            });
        });
    }

    /**
     * Download the component contents.
     *
     * @param refresh Whether we're refreshing data.
     * @param sync If the refresh needs syncing.
     * @param showErrors Wether to show errors to the user or hide them.
     * @return Promise resolved when done.
     */
    protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
        return this.glossaryProvider.getGlossary(this.courseId, this.module.id).then((glossary) => {
            this.glossary = glossary;

            this.description = glossary.intro || this.description;
            this.canAdd = (this.glossaryProvider.isPluginEnabledForEditing() && glossary.canaddentry) || false;

            this.dataRetrieved.emit(this.glossary);

            if (!this.fetchMode) {
                this.switchMode('letter_all');
            }

            if (sync) {
                // Try to synchronize the glossary.
                return this.syncActivity(showErrors);
            }
        }).then(() => {
            const promises = [];

            promises.push(this.fetchEntries().then(() => {
                // Check if there are responses stored in offline.
                return this.glossaryOffline.getGlossaryNewEntries(this.glossary.id).then((offlineEntries) => {
                    offlineEntries.sort((a, b) => a.concept.localeCompare(b.fullname));
                    this.hasOffline = !!offlineEntries.length;
                    this.offlineEntries = offlineEntries || [];
                });
            }));

            promises.push(this.ratingOffline.hasRatings('mod_glossary', 'entry', 'module', this.glossary.coursemodule)
                    .then((hasRatings) => {
                this.hasOfflineRatings = hasRatings;
            }));

            return Promise.all(promises);
        }).finally(() => {
            this.fillContextMenu(refresh);
        });
    }

    /**
     * Convenience function to fetch entries.
     *
     * @param append True if fetched entries are appended to exsiting ones.
     * @return Promise resolved when done.
     */
    protected fetchEntries(append: boolean = false): Promise<any> {
        this.loadMoreError = false;

        if (!this.fetchFunction || !this.fetchArguments) {
            // This happens in search mode with an empty query.
            return Promise.resolve({entries: [], count: 0});
        }

        const limitFrom = append ? this.entries.length : 0;
        const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES;

        return this.glossaryProvider.fetchEntries(this.fetchFunction, this.fetchArguments, limitFrom, limitNum).then((result) => {
            if (append) {
                Array.prototype.push.apply(this.entries, result.entries);
            } else {
                this.entries = result.entries;

                if (this.splitviewCtrl.isOn()) {
                    // Load the first entry.
                    if (this.entries.length > 0) {
                        const found = this.selectedEntry && this.entries.some((entry) => entry.id == this.selectedEntry);

                        // The current selected entry is not found in the current list, open first item.
                        if (!found) {
                            this.openEntry(this.entries[0].id);
                        }
                    } else {
                        this.selectedEntry = null;
                        this.splitviewCtrl.emptyDetails();
                    }
                }
            }
            this.canLoadMore = this.entries.length < result.count;
        }).catch((error) => {
            return Promise.reject(error);
        });
    }

    /**
     * Perform the invalidate content function.
     *
     * @return Resolved when done.
     */
    protected invalidateContent(): Promise<any> {
        const promises = [];

        if (this.fetchInvalidate && this.fetchArguments) {
            promises.push(this.fetchInvalidate.apply(this.glossaryProvider, this.fetchArguments));
        }

        promises.push(this.glossaryProvider.invalidateCourseGlossaries(this.courseId));

        if (this.glossary && this.glossary.id) {
            promises.push(this.glossaryProvider.invalidateCategories(this.glossary.id));
        }

        return Promise.all(promises);
    }

    /**
     * Performs the sync of the activity.
     *
     * @return Promise resolved when done.
     */
    protected sync(): Promise<boolean> {
        return this.prefetchHandler.sync(this.module, this.courseId);
    }

    /**
     * Checks if sync has succeed from result sync data.
     *
     * @param result Data returned on the sync function.
     * @return Whether it succeed or not.
     */
    protected hasSyncSucceed(result: any): boolean {
        return result.updated;
    }

    /**
     * Compares sync event data with current data to check if refresh content is needed.
     *
     * @param syncEventData Data receiven on sync observer.
     * @return True if refresh is needed, false otherwise.
     */
    protected isRefreshSyncNeeded(syncEventData: any): boolean {
        return this.glossary && syncEventData.glossaryId == this.glossary.id &&
                syncEventData.userId == this.sitesProvider.getCurrentSiteUserId();
    }

    /**
     * Change fetch mode.
     *
     * @param mode New mode.
     */
    protected switchMode(mode: FetchMode): void {
        this.fetchMode = mode;
        this.isSearch = false;

        switch (mode) {
            case 'author_all':
                // Browse by author.
                this.viewMode = 'author';
                this.fetchFunction = this.glossaryProvider.getEntriesByAuthor;
                this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByAuthor;
                this.fetchArguments = [this.glossary.id, 'ALL', 'LASTNAME', 'ASC'];
                this.getDivider = (entry: any): string => entry.userfullname;
                this.showDivider = (entry: any, previous?: any): boolean => {
                    return  typeof previous === 'undefined' || entry.userid != previous.userid;
                };
                break;
            case 'cat_all':
                // Browse by category.
                this.viewMode = 'cat';
                this.fetchFunction = this.glossaryProvider.getEntriesByCategory;
                this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByCategory;
                this.fetchArguments = [this.glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES];
                this.getDivider = (entry: any): string => entry.categoryname;
                this.showDivider = (entry?: any, previous?: any): boolean  => {
                    return !previous || this.getDivider(entry) != this.getDivider(previous);
                };
                break;
            case 'newest_first':
                // Newest first.
                this.viewMode = 'date';
                this.fetchFunction = this.glossaryProvider.getEntriesByDate;
                this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByDate;
                this.fetchArguments = [this.glossary.id, 'CREATION', 'DESC'];
                this.getDivider = null;
                this.showDivider = (): boolean => false;
                break;
            case 'recently_updated':
                // Recently updated.
                this.viewMode = 'date';
                this.fetchFunction = this.glossaryProvider.getEntriesByDate;
                this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByDate;
                this.fetchArguments = [this.glossary.id, 'UPDATE', 'DESC'];
                this.getDivider = null;
                this.showDivider = (): boolean => false;
                break;
            case 'letter_all':
            default:
                // Consider it is 'letter_all'.
                this.viewMode = 'letter';
                this.fetchMode = 'letter_all';
                this.fetchFunction = this.glossaryProvider.getEntriesByLetter;
                this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByLetter;
                this.fetchArguments = [this.glossary.id, 'ALL'];
                this.getDivider = (entry: any): string => {
                    // Try to get the first letter without HTML tags.
                    const noTags = this.textUtils.cleanTags(entry.concept);

                    return (noTags || entry.concept).substr(0, 1).toUpperCase();
                };
                this.showDivider = (entry?: any, previous?: any): boolean  => {
                    return !previous || this.getDivider(entry) != this.getDivider(previous);
                };
                break;
        }
    }

    /**
     * Convenience function to load more forum discussions.
     *
     * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
     * @return Promise resolved when done.
     */
    loadMoreEntries(infiniteComplete?: any): Promise<any> {
        return this.fetchEntries(true).catch((error) => {
            this.loadMoreError = true;
            this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
        }).finally(() => {
            infiniteComplete && infiniteComplete();
        });
    }

    /**
     * Show the mode picker menu.
     *
     * @param event Event.
     */
    openModePicker(event: MouseEvent): void {
        const popover = this.popoverCtrl.create(AddonModGlossaryModePickerPopoverComponent, {
            browsemodes: this.glossary.browsemodes,
            selectedMode: this.isSearch ? '' : this.fetchMode
        });

        popover.onDidDismiss((mode: FetchMode) => {
            if (mode !== this.fetchMode) {
                this.changeFetchMode(mode);
            } else if (this.isSearch) {
                this.toggleSearch();
            }
        });

        popover.present({
            ev: event
        });
    }

    /**
     * Toggles between search and fetch mode.
     */
    toggleSearch(): void {
        if (this.isSearch) {
            this.isSearch = false;
            this.entries = this.fetchedEntries;
            this.canLoadMore = this.fetchedEntriesCanLoadMore;
            this.switchMode(this.fetchMode);
        } else {
            // Search for entries.
            this.fetchFunction = this.glossaryProvider.getEntriesBySearch;
            this.fetchInvalidate = this.glossaryProvider.invalidateEntriesBySearch;
            this.fetchArguments = null; // Dynamically set later.
            this.getDivider = null;
            this.showDivider = (): boolean => false;
            this.isSearch = true;

            this.fetchedEntries = this.entries;
            this.fetchedEntriesCanLoadMore = this.canLoadMore;
            this.canLoadMore = false;
            this.entries = [];
        }
    }

    /**
     * Change fetch mode
     * @param {FetchMode} mode [description]
     */
    changeFetchMode(mode: FetchMode): void {
        this.isSearch = false;
        this.loadingMessage = this.translate.instant('core.loading');
        this.domUtils.scrollToTop(this.content);
        this.switchMode(mode);
        this.loaded = false;
        this.loadContent();
    }

    /**
     * Opens an entry.
     *
     * @param entryId Entry id.
     */
    openEntry(entryId: number): void {
        const params = {
            courseId: this.courseId,
            entryId: entryId
        };
        this.splitviewCtrl.push('AddonModGlossaryEntryPage', params);
        this.selectedEntry = entryId;
    }

    /**
     * Opens new entry editor.
     *
     * @param entry Offline entry to edit.
     */
    openNewEntry(entry?: any): void {
        const params = {
            courseId: this.courseId,
            module: this.module,
            glossary: this.glossary,
            entry: entry,
        };
        this.splitviewCtrl.getMasterNav().push('AddonModGlossaryEditPage', params);
        this.selectedEntry = 0;
    }

    /**
     * Search entries.
     *
     * @param query Text entered on the search box.
     */
    search(query: string): void {
        this.loadingMessage = this.translate.instant('core.searching');
        this.fetchArguments = [this.glossary.id, query, 1, 'CONCEPT', 'ASC'];
        this.loaded = false;
        this.loadContent();
    }

    /**
     * Function called when we receive an event of new entry.
     *
     * @param data Event data.
     */
    protected eventReceived(data: any): void {
        if (this.glossary && this.glossary.id === data.glossaryId) {
            this.showLoadingAndRefresh(false);

            // Check completion since it could be configured to complete once the user adds a new discussion or replies.
            this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata);
        }
    }

    /**
     * Component being destroyed.
     */
    ngOnDestroy(): void {
        super.ngOnDestroy();

        this.addEntryObserver && this.addEntryObserver.off();
        this.ratingOfflineObserver && this.ratingOfflineObserver.off();
        this.ratingSyncObserver && this.ratingSyncObserver.off();
    }
}