// (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 { CoreConstants } from '@/core/constants';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import {
    CoreCourseHelper,
    CoreCourseModuleData,
    CoreCourseSectionWithStatus,
    CorePrefetchStatusInfo,
} from '@features/course/services/course-helper';
import {
    CoreCourseModulePrefetchDelegate,
    CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';

/**
 * Page that displays the amount of file storage used by each activity on the course, and allows
 * the user to prefecth and delete this data.
 */
@Component({
    selector: 'page-addon-storagemanager-course-storage',
    templateUrl: 'course-storage.html',
    styleUrls: ['course-storage.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {

    courseId!: number;
    title = '';
    loaded = false;
    sections: AddonStorageManagerCourseSection[] = [];
    totalSize = 0;
    calculatingSize = true;

    downloadEnabled = false;
    downloadCourseEnabled = false;

    prefetchCourseData: CorePrefetchStatusInfo = {
        icon: CoreConstants.ICON_LOADING,
        statusTranslatable: 'core.course.downloadcourse',
        status: '',
        loading: true,
    };

    statusDownloaded = CoreConstants.DOWNLOADED;

    protected initialSectionId?: number;
    protected siteUpdatedObserver?: CoreEventObserver;
    protected courseStatusObserver?: CoreEventObserver;
    protected sectionStatusObserver?: CoreEventObserver;
    protected moduleStatusObserver?: CoreEventObserver;
    protected isDestroyed = false;
    protected isGuest = false;

    constructor(protected elementRef: ElementRef, protected changeDetectorRef: ChangeDetectorRef) {
        // Refresh the enabled flags if site is updated.
        this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
            this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
            this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();

            this.initCoursePrefetch();
            this.initModulePrefetch();
            this.changeDetectorRef.markForCheck();
        }, CoreSites.getCurrentSiteId());
    }

    /**
     * @inheritdoc
     */
    async ngOnInit(): Promise<void> {
        try {
            this.courseId = CoreNavigator.getRequiredRouteParam('courseId');
        } catch (error) {
            CoreDomUtils.showErrorModal(error);

            CoreNavigator.back();

            return;
        }

        this.title = CoreNavigator.getRouteParam<string>('title') || '';
        if (!this.title && this.courseId == CoreSites.getCurrentSiteHomeId()) {
            this.title = Translate.instant('core.sitehome.sitehome');
        }

        this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
        this.initialSectionId = CoreNavigator.getRouteNumberParam('sectionId');

        this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
        this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();

        const sections = (await CoreCourse.getSections(this.courseId, false, true))
            .filter((section) => !CoreCourseHelper.isSectionStealth(section));
        this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
            .map(section => ({
                ...section,
                totalSize: 0,
                calculatingSize: true,
                expanded: section.id === this.initialSectionId,
                modules: section.modules.map(module => ({
                    ...module,
                    calculatingSize: true,
                })),
            }));

        this.loaded = true;

        CoreDom.scrollToElement(
            this.elementRef.nativeElement,
            '.core-course-storage-section-expanded',
            { addYAxis: -10 },
        );

        await Promise.all([
            this.initSizes(),
            this.initCoursePrefetch(),
            this.initModulePrefetch(),
        ]);
        this.changeDetectorRef.markForCheck();
    }

    /**
     * Init course prefetch information.
     *
     * @returns Promise resolved when done.
     */
    protected async initCoursePrefetch(): Promise<void> {
        if (!this.downloadCourseEnabled || this.courseStatusObserver) {
            return;
        }

        // Listen for changes in course status.
        this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
            if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
                this.updateCourseStatus(data.status);
            }
        }, CoreSites.getCurrentSiteId());

        // Determine the course prefetch status.
        await this.determineCoursePrefetchIcon();

        if (this.prefetchCourseData.icon != CoreConstants.ICON_LOADING) {
            return;
        }

        // Course is being downloaded. Get the download promise.
        const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId);
        if (promise) {
            // There is a download promise. Show an error if it fails.
            promise.catch((error) => {
                if (!this.isDestroyed) {
                    CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
                }
            });
        } else {
            // No download, this probably means that the app was closed while downloading. Set previous status.
            const status = await CoreCourse.setCoursePreviousStatus(this.courseId);

            this.updateCourseStatus(status);
        }
    }

    /**
     * Init module prefetch information.
     *
     * @returns Promise resolved when done.
     */
    protected async initModulePrefetch(): Promise<void> {
        if (!this.downloadEnabled || this.sectionStatusObserver) {
            return;
        }

        // Listen for section status changes.
        this.sectionStatusObserver = CoreEvents.on(
            CoreEvents.SECTION_STATUS_CHANGED,
            async (data) => {
                if (!this.downloadEnabled || !this.sections.length || !data.sectionId || data.courseId != this.courseId) {
                    return;
                }

                // Check if the affected section is being downloaded.
                // If so, we don't update section status because it'll already be updated when the download finishes.
                const downloadId = CoreCourseHelper.getSectionDownloadId({ id: data.sectionId });
                if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
                    return;
                }

                // Get the affected section.
                const section = this.sections.find(section => section.id == data.sectionId);
                if (!section) {
                    return;
                }

                // Recalculate the status.
                await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false);

                if (section.isDownloading && !CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
                    // All the modules are now downloading, set a download all promise.
                    this.prefecthSection(section);
                }
            },
            CoreSites.getCurrentSiteId(),
        );

        // The download status of a section might have been changed from within a module page.
        CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);

        this.sections.forEach((section) => {
            section.modules.forEach((module) => {
                if (module.handlerData?.showDownloadButton) {

                    module.spinner = true;
                    // Listen for changes on this module status, even if download isn't enabled.
                    module.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(module.modname);
                    this.calculateModuleStatus(module);
                }
            });
        });

        this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
            let module: AddonStorageManagerModule | undefined;

            this.sections.some((section) => {
                module = section.modules.find((module) =>
                    module.id == data.componentId && module.prefetchHandler && data.component == module.prefetchHandler?.component);

                return !!module;
            });

            if (!module) {
                return;
            }

            // Call determineModuleStatus to get the right status to display.
            const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(module, data.status);

            // Update the status.
            this.updateModuleStatus(module, status);
        }, CoreSites.getCurrentSiteId());
    }

    /**
     * Init section, course and modules sizes.
     */
    protected async initSizes(): Promise<void> {
        await Promise.all(this.sections.map(async (section) => {
            await Promise.all(section.modules.map(async (module) => {
                // Note: This function only gets the size for modules which are downloadable.
                // For other modules it always returns 0, even if they have downloaded some files.
                // However there is no 100% reliable way to actually track the files in this case.
                // You can maybe guess it based on the component and componentid.
                // But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
                // There is nothing enforcing correct values.
                // Most modules which have large files are downloadable, so I think this is sufficient.
                const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);

                // There are some cases where the return from this is not a valid number.
                if (!isNaN(size)) {
                    module.totalSize = Number(size);
                    section.totalSize += size;
                    this.totalSize += size;
                }

                module.calculatingSize = false;
            }));

            section.calculatingSize = false;
        }));

        this.calculatingSize = false;

        // Mark course as not downloaded if course size is 0.
        if (this.totalSize == 0) {
            this.markCourseAsNotDownloaded();
        }
    }

    /**
     * Update the sizes of some modules.
     *
     * @param modules Modules.
     * @param section Section the modules belong to.
     * @returns Promise resolved when done.
     */
    protected async updateModulesSizes(
        modules: AddonStorageManagerModule[],
        section?: AddonStorageManagerCourseSection,
    ): Promise<void> {
        this.calculatingSize = true;

        await Promise.all(modules.map(async (module) => {
            if (module.calculatingSize) {
                return;
            }

            module.calculatingSize = true;
            this.changeDetectorRef.markForCheck();

            if (!section) {
                section = this.sections.find((section) => section.modules.some((mod) => mod.id === module.id));
                if (section) {
                    section.calculatingSize = true;
                    this.changeDetectorRef.markForCheck();
                }
            }

            try {
                const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);

                const diff = (isNaN(size) ? 0 : size) - (module.totalSize ?? 0);

                module.totalSize = Number(size);
                this.totalSize += diff;
                if (section) {
                    section.totalSize += diff;
                }
            } catch {
                // Ignore errors, it shouldn't happen.
            } finally {
                module.calculatingSize = false;
                this.changeDetectorRef.markForCheck();
            }
        }));

        this.calculatingSize = false;
        if (section) {
            section.calculatingSize = false;
        }
        this.changeDetectorRef.markForCheck();
    }

    /**
     * The user has requested a delete for the whole course data.
     *
     * (This works by deleting data for each module on the course that has data.)
     *
     * @param event Event object.
     */
    async deleteForCourse(event: Event): Promise<void> {
        event.stopPropagation();
        event.preventDefault();

        try {
            await CoreDomUtils.showDeleteConfirm(
                'addon.storagemanager.confirmdeletedatafrom',
                { name: this.title },
            );
        } catch (error) {
            if (!CoreDomUtils.isCanceledError(error)) {
                throw error;
            }

            return;
        }

        const modules: AddonStorageManagerModule[] = [];
        this.sections.forEach((section) => {
            section.modules.forEach((module) => {
                if (module.totalSize && module.totalSize > 0) {
                    modules.push(module);
                }
            });
        });

        this.deleteModules(modules);
    }

    /**
     * The user has requested a delete for a section's data.
     *
     * (This works by deleting data for each module in the section that has data.)
     *
     * @param event Event object.
     * @param section Section object with information about section and modules
     */
    async deleteForSection(event: Event, section: AddonStorageManagerCourseSection): Promise<void> {
        event.stopPropagation();
        event.preventDefault();

        try {
            await CoreDomUtils.showDeleteConfirm(
                'addon.storagemanager.confirmdeletedatafrom',
                { name: section.name },
            );
        } catch (error) {
            if (!CoreDomUtils.isCanceledError(error)) {
                throw error;
            }

            return;
        }

        const modules: AddonStorageManagerModule[] = [];
        section.modules.forEach((module) => {
            if (module.totalSize && module.totalSize > 0) {
                modules.push(module);
            }
        });

        this.deleteModules(modules, section);
    }

    /**
     * The user has requested a delete for a module's data
     *
     * @param event Event object.
     * @param module Module details
     * @param section Section the module belongs to.
     */
    async deleteForModule(
        event: Event,
        module: AddonStorageManagerModule,
        section: AddonStorageManagerCourseSection,
    ): Promise<void> {
        event.stopPropagation();
        event.preventDefault();

        if (module.totalSize === 0) {
            return;
        }

        try {
            await CoreDomUtils.showDeleteConfirm(
                'addon.storagemanager.confirmdeletedatafrom',
                { name: module.name },
            );
        } catch (error) {
            if (!CoreDomUtils.isCanceledError(error)) {
                throw error;
            }

            return;
        }

        this.deleteModules([module], section);
    }

    /**
     * Deletes the specified modules, showing the loading overlay while it happens.
     *
     * @param modules Modules to delete
     * @param section Section the modules belong to.
     * @returns Promise<void> Once deleting has finished
     */
    protected async deleteModules(modules: AddonStorageManagerModule[], section?: AddonStorageManagerCourseSection): Promise<void> {
        const modal = await CoreDomUtils.showModalLoading('core.deleting', true);

        const promises: Promise<void>[] = [];
        modules.forEach((module) => {
            // Remove the files.
            const promise = CoreCourseHelper.removeModuleStoredData(module, this.courseId).then(() => {
                const moduleSize = module.totalSize || 0;
                // When the files and cache are removed, update the size.
                if (section) {
                    section.totalSize -= moduleSize;
                }
                this.totalSize -= moduleSize;
                module.totalSize = 0;

                return;
            });

            promises.push(promise);
        });

        try {
            await Promise.all(promises);
        } catch (error) {
            CoreDomUtils.showErrorModalDefault(error, Translate.instant('core.errordeletefile'));
        } finally {
            modal.dismiss();

            await this.updateModulesSizes(modules, section);
            CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);

            // For delete all, reset all section sizes so icons are updated.
            if (this.totalSize === 0) {
                this.sections.map(section => section.totalSize = 0);
            }
            this.changeDetectorRef.markForCheck();
        }
    }

    /**
     * Mark course as not downloaded.
     */
    protected markCourseAsNotDownloaded(): void {
        // @TODO In order to correctly check the status of the course we should check all module statuses.
        // We are currently marking as not downloaded if size is 0 but we should take into account that
        // resources without files can be downloaded and cached.

        CoreCourse.setCourseStatus(this.courseId, CoreConstants.NOT_DOWNLOADED);
    }

    /**
     * Calculate the status of sections.
     *
     * @param refresh If refresh or not.
     */
    protected calculateSectionsStatus(refresh?: boolean): void {
        if (!this.sections) {
            return;
        }

        CoreUtils.ignoreErrors(CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, refresh));
    }

    /**
     * Confirm and prefetch a section. If the section is "all sections", prefetch all the sections.
     *
     * @param section Section to download.
     */
    async prefecthSection(section: AddonStorageManagerCourseSection): Promise<void> {
        section.isCalculating = true;
        this.changeDetectorRef.markForCheck();
        try {
            await CoreCourseHelper.confirmDownloadSizeSection(this.courseId, section, this.sections);

            try {
                await CoreCourseHelper.prefetchSection(section, this.courseId, this.sections);

            } catch (error) {
                if (!this.isDestroyed) {
                    CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
                }
            } finally {
                await this.updateModulesSizes(section.modules, section);
                this.changeDetectorRef.markForCheck();
            }
        } catch (error) {
            // User cancelled or there was an error calculating the size.
            if (!this.isDestroyed && error) {
                CoreDomUtils.showErrorModal(error);
                this.changeDetectorRef.markForCheck();

                return;
            }
        } finally {
            section.isCalculating = false;
            this.changeDetectorRef.markForCheck();
        }
    }

    /**
     * Download the module.
     *
     * @param module Module to prefetch.
     * @param refresh Whether it's refreshing.
     * @returns Promise resolved when done.
     */
    async prefetchModule(
        module: AddonStorageManagerModule,
        refresh = false,
    ): Promise<void> {
        if (!module.prefetchHandler) {
            return;
        }

        // Show spinner since this operation might take a while.
        module.spinner = true;

        try {
            // Get download size to ask for confirm if it's high.

            const size = await module.prefetchHandler.getDownloadSize(module, module.course, true);

            await CoreCourseHelper.prefetchModule(module.prefetchHandler, module, size, module.course, refresh);

            CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
        } catch (error) {
            if (!this.isDestroyed) {
                CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
            }
        } finally {
            module.spinner = false;

            await this.updateModulesSizes([module]);

        }
    }

    /**
     * Show download buttons according to module status.
     *
     * @param module Module to update.
     * @param status Module status.
     */
    protected updateModuleStatus(module: AddonStorageManagerModule, status: string): void {
        if (!status) {
            return;
        }

        module.spinner = false;
        module.downloadStatus = status;

        module.handlerData?.updateStatus?.(status);
        this.changeDetectorRef.markForCheck();
    }

    /**
     * Calculate and show module status.
     *
     * @param module Module to update.
     * @returns Promise resolved when done.
     */
    protected async calculateModuleStatus(module: AddonStorageManagerModule): Promise<void> {
        if (!module) {
            return;
        }

        const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId);

        this.updateModuleStatus(module, status);
    }

    /**
     * Determines the prefetch icon of the course.
     *
     * @returns Promise resolved when done.
     */
    protected async determineCoursePrefetchIcon(): Promise<void> {
        this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId);
    }

    /**
     * Update the course status icon and title.
     *
     * @param status Status to show.
     */
    protected updateCourseStatus(status: string): void {
        const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status);

        this.prefetchCourseData.status = statusData.status;
        this.prefetchCourseData.icon = statusData.icon;
        this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable;
        this.prefetchCourseData.loading = statusData.loading;
        this.changeDetectorRef.markForCheck();
    }

    /**
     * Prefetch the whole course.
     *
     * @param event Event object.
     */
    async prefetchCourse(event: Event): Promise<void> {
        event.stopPropagation();
        event.preventDefault();

        const courses = await CoreCourses.getUserCourses(true);
        let course = courses.find((course) => course.id == this.courseId);
        if (!course) {
            course = await CoreCourses.getCourse(this.courseId);
        }
        if (!course) {
            return;
        }

        try {
            this.changeDetectorRef.markForCheck();
            await CoreCourseHelper.confirmAndPrefetchCourse(
                this.prefetchCourseData,
                course,
                {
                    sections: this.sections,
                    isGuest: this.isGuest,
                },
            );
            await Promise.all(this.sections.map(section => this.updateModulesSizes(section.modules, section)));
            this.changeDetectorRef.markForCheck();
        } catch (error) {
            if (this.isDestroyed) {
                return;
            }

            CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
        }
    }

    /**
     * Toggle expand status.
     *
     * @param event Event object.
     * @param section Section to expand / collapse.
     */
    toggleExpand(event: Event, section: AddonStorageManagerCourseSection): void {
        section.expanded = !section.expanded;
        event.stopPropagation();
        event.preventDefault();
    }

    /**
     * @inheritdoc
     */
    ngOnDestroy(): void {
        this.courseStatusObserver?.off();
        this.sectionStatusObserver?.off();
        this.moduleStatusObserver?.off();
        this.siteUpdatedObserver?.off();

        this.sections.forEach((section) => {
            section.modules.forEach((module) => {
                module.handlerData?.onDestroy?.();
            });
        });
        this.isDestroyed = true;
    }

}

type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
    totalSize: number;
    calculatingSize: boolean;
    expanded: boolean;
    modules: AddonStorageManagerModule[];
};

type AddonStorageManagerModule = CoreCourseModuleData & {
    totalSize?: number;
    calculatingSize: boolean;
    prefetchHandler?: CoreCourseModulePrefetchHandler;
    spinner?: boolean;
    downloadStatus?: string;
};
