// (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, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren, QueryList } from '@angular/core';
import { CoreEvents, CoreEventObserver } from '@singletons/events';
import { CoreSites } from '@services/sites';
import {
    AddonModAssignProvider,
    AddonModAssignAssign,
    AddonModAssignSubmissionFeedback,
    AddonModAssignSubmissionAttempt,
    AddonModAssignSubmissionPreviousAttempt,
    AddonModAssignPlugin,
    AddonModAssign,
    AddonModAssignGetSubmissionStatusWSResponse,
    AddonModAssignSavePluginData,
} from '../../services/assign';
import {
    AddonModAssignAutoSyncData,
    AddonModAssignManualSyncData,
    AddonModAssignSync,
    AddonModAssignSyncProvider,
} from '../../services/assign-sync';
import { CoreTabsComponent } from '@components/tabs/tabs';
import { CoreTabComponent } from '@components/tabs/tab';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreGradesFormattedItem, CoreGradesHelper } from '@features/grades/services/grades-helper';
import { CoreMenuItem, CoreUtils } from '@services/utils/utils';
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreTextUtils } from '@services/utils/text';
import { CoreCourse, CoreCourseModuleGradeInfo, CoreCourseModuleGradeOutcome } from '@features/course/services/course';
import { AddonModAssignOffline } from '../../services/assign-offline';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreNavigator } from '@services/navigator';
import { CoreApp } from '@services/app';
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
import { CoreLang } from '@services/lang';
import { CoreError } from '@classes/errors/error';
import { CoreGroups } from '@services/groups';
import { CoreSync } from '@services/sync';
import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin';
import { AddonModAssignModuleHandlerService } from '../../services/handlers/module';
import { CanLeave } from '@guards/can-leave';

/**
 * Component that displays an assignment submission.
 */
@Component({
    selector: 'addon-mod-assign-submission',
    templateUrl: 'addon-mod-assign-submission.html',
    styleUrls: ['submission.scss'],
})
export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, CanLeave {

    @ViewChild(CoreTabsComponent) tabs!: CoreTabsComponent;
    @ViewChildren(AddonModAssignSubmissionPluginComponent) submissionComponents!:
        QueryList<AddonModAssignSubmissionPluginComponent>;

    @Input() courseId!: number; // Course ID the submission belongs to.
    @Input() moduleId!: number; // Module ID the submission belongs to.
    @Input() submitId!: number; // User that did the submission.
    @Input() blindId?: number; // Blinded user ID (if it's blinded).

    loaded = false; // Whether data has been loaded.
    selectedTab = 'submission'; // Tab selected on start.
    assign?: AddonModAssignAssign; // The assignment the submission belongs to.
    userSubmission?: AddonModAssignSubmissionFormatted; // The submission object.
    isSubmittedForGrading = false; // Whether the submission has been submitted for grading.
    acceptStatement = false; // Statement accepted (for grading).
    feedback?: AddonModAssignSubmissionFeedbackFormatted; // The feedback.
    hasOffline = false; // Whether there is offline data.
    submittedOffline = false; // Whether it was submitted in offline.
    fromDate?: string; // Readable date when the assign started accepting submissions.
    currentAttempt = 0; // The current attempt number.
    maxAttemptsText: string; // The text for maximum attempts.
    blindMarking = false; // Whether blind marking is enabled.
    user?: CoreUserProfile; // The user.
    lastAttempt?: AddonModAssignSubmissionAttemptFormatted; // The last attempt.
    membersToSubmit: CoreUserProfile[] = []; // Team members that need to submit the assignment.
    membersToSubmitBlind: number[] = []; // Team members that need to submit the assignment (blindmarking).
    canSubmit = false; // Whether the user can submit for grading.
    canEdit = false; // Whether the user can edit the submission.
    submissionStatement?: string; // The submission statement.
    showErrorStatementEdit = false; // Whether to show an error in edit due to submission statement.
    showErrorStatementSubmit = false; // Whether to show an error in submit due to submission statement.
    gradingStatusTranslationId?: string; // Key of the text to display for the grading status.
    gradingColor = ''; // Color to apply to the grading status.
    workflowStatusTranslationId?: string; // Key of the text to display for the workflow status.
    submissionPlugins: AddonModAssignPlugin[] = []; // List of submission plugins.
    timeRemaining = ''; // Message about time remaining.
    timeRemainingClass = ''; // Class to apply to time remaining message.
    statusTranslated?: string; // Status.
    statusColor = ''; // Color to apply to the status.
    unsupportedEditPlugins: string[] = []; // List of submission plugins that don't support edit.
    grade: AddonModAssignSubmissionGrade = {
        method: '',
        modified: 0,
        addAttempt : false,
        applyToAll: false,
        lang: 'en',
        disabled: false,
    }; // Data about the grade.

    grader?: CoreUserProfile; // Profile of the teacher that graded the submission.
    gradeInfo?: AddonModAssignGradeInfo; // Grade data for the assignment, retrieved from the server.
    isGrading = false; // Whether the user is grading.
    canSaveGrades = false; // Whether the user can save the grades.
    allowAddAttempt = false; // Allow adding a new attempt when grading.
    gradeUrl?: string; // URL to grade in browser.
    isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.

    // Some constants.
    statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW;
    statusReopened = AddonModAssignProvider.SUBMISSION_STATUS_REOPENED;
    attemptReopenMethodNone = AddonModAssignProvider.ATTEMPT_REOPEN_METHOD_NONE;
    unlimitedAttempts = AddonModAssignProvider.UNLIMITED_ATTEMPTS;

    protected siteId: string; // Current site ID.
    protected currentUserId: number; // Current user ID.
    protected previousAttempt?: AddonModAssignSubmissionPreviousAttempt; // The previous attempt.
    protected submissionStatusAvailable = false; // Whether we were able to retrieve the submission status.
    protected originalGrades: AddonModAssignSubmissionOriginalGrades = {
        addAttempt: false,
        applyToAll: false,
        outcomes: {},
    }; // Object with the original grade data, to check for changes.

    protected isDestroyed = false; // Whether the component has been destroyed.
    protected syncObserver: CoreEventObserver;
    protected hasOfflineGrade = false;

    constructor(
        @Optional() protected splitviewCtrl: CoreSplitViewComponent,
    ) {
        this.siteId = CoreSites.getCurrentSiteId();
        this.currentUserId = CoreSites.getCurrentSiteUserId();
        this.maxAttemptsText = Translate.instant('addon.mod_assign.unlimitedattempts');

        // Refresh data if this assign is synchronized and it's grading.
        const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED];
        this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
            events,
            async (data) => {
                // Check that user is grading and this grade wasn't blocked when sync was performed.
                if (!this.loaded || !this.isGrading || data.gradesBlocked.indexOf(this.submitId) != -1) {
                    return;
                }

                if ('context' in data && data.context == 'submission' && data.submitId == this.submitId) {
                    // Manual sync triggered by this same submission, ignore it.
                    return;
                }

                // Don't refresh if the user has modified some data.
                const hasDataToSave = await this.hasDataToSave();

                if (!hasDataToSave) {
                    this.invalidateAndRefresh(false);
                }
            },
            this.siteId,
        );
    }

    /**
     * Component being initialized.
     */
    ngOnInit(): void {
        this.isSubmittedForGrading = !!this.submitId;

        this.loadData(true);
    }

    /**
     * Calculate the time remaining message and class.
     *
     * @param response Response of get submission status.
     */
    protected calculateTimeRemaining(response: AddonModAssignGetSubmissionStatusWSResponse): void {
        if (this.assign!.duedate <= 0) {
            this.timeRemaining = '';
            this.timeRemainingClass = '';

            return;
        }

        const time = CoreTimeUtils.timestamp();
        const dueDate = response.lastattempt?.extensionduedate
            ? response.lastattempt.extensionduedate
            : this.assign!.duedate;
        const timeRemaining = dueDate - time;

        if (timeRemaining > 0) {
            this.timeRemaining = CoreTimeUtils.formatDuration(timeRemaining, 3);
            this.timeRemainingClass = '';

            return;
        }

        // Not submitted.
        if (!this.userSubmission || this.userSubmission.status != AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED) {

            if (response.lastattempt?.submissionsenabled || response.gradingsummary?.submissionsenabled) {
                this.timeRemaining = Translate.instant(
                    'addon.mod_assign.overdue',
                    { $a: CoreTimeUtils.formatDuration(-timeRemaining, 3) },
                );
                this.timeRemainingClass = 'overdue';

                return;
            }

            this.timeRemaining = Translate.instant('addon.mod_assign.duedatereached');
            this.timeRemainingClass = '';

            return;
        }

        const timeSubmittedDiff = this.userSubmission.timemodified - dueDate;
        if (timeSubmittedDiff > 0) {
            this.timeRemaining = Translate.instant(
                'addon.mod_assign.submittedlate',
                { $a: CoreTimeUtils.formatDuration(timeSubmittedDiff, 2) },
            );
            this.timeRemainingClass = 'latesubmission';

            return;
        }

        this.timeRemaining = Translate.instant(
            'addon.mod_assign.submittedearly',
            { $a: CoreTimeUtils.formatDuration(-timeSubmittedDiff, 2) },
        );
        this.timeRemainingClass = 'earlysubmission';
    }

    /**
     * Check if the user can leave the view. If there are changes to be saved, it will ask for confirm.
     *
     * @return Promise resolved with true if can leave the view, rejected otherwise.
     */
    async canLeave(): Promise<boolean> {
        // Check if there is data to save.
        const modified = await this.hasDataToSave();

        if (modified) {
            // Modified, confirm user wants to go back.
            await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));

            await this.discardDrafts();
        }

        return true;
    }

    /**
     * Copy a previous attempt and then go to edit.
     */
    async copyPrevious(): Promise<void> {
        if (!CoreApp.isOnline()) {
            CoreDomUtils.showErrorModal('core.networkerrormsg', true);

            return;
        }

        if (!this.previousAttempt?.submission) {
            // Cannot access previous attempts, just go to edit.
            return this.goToEdit();
        }

        const previousSubmission = this.previousAttempt.submission;
        let modal = await CoreDomUtils.showModalLoading();

        const size = await CoreUtils.ignoreErrors(
            AddonModAssignHelper.getSubmissionSizeForCopy(this.assign!, previousSubmission),
            -1,
        ); // Error calculating size, return -1.

        modal.dismiss();

        try {
            // Confirm action.
            await CoreFileUploaderHelper.confirmUploadFile(size, true);
        } catch {
            // Cancelled.
            return;
        }

        // User confirmed, copy the attempt.
        modal = await CoreDomUtils.showModalLoading('core.sending', true);

        try {
            await AddonModAssignHelper.copyPreviousAttempt(this.assign!, previousSubmission);
            // Now go to edit.
            this.goToEdit();

            if (!this.assign!.submissiondrafts) {
                // No drafts allowed, so it was submitted. Trigger event.
                CoreEvents.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, {
                    assignmentId: this.assign!.id,
                    submissionId: this.userSubmission!.id,
                    userId: this.currentUserId,
                }, this.siteId);
            } else {
                // Invalidate and refresh data to update this view.
                await this.invalidateAndRefresh(true);
            }
        } catch (error) {
            CoreDomUtils.showErrorModalDefault(error, 'core.error', true);
        } finally {
            modal.dismiss();
        }
    }

    /**
     * Discard feedback drafts.
     *
     * @return Promise resolved when done.
     */
    protected async discardDrafts(): Promise<void> {
        if (this.feedback && this.feedback.plugins) {
            await AddonModAssignHelper.discardFeedbackPluginData(this.assign!.id, this.submitId, this.feedback);
        }
    }

    /**
     * Go to the page to add or edit submission.
     */
    goToEdit(): void {
        CoreNavigator.navigateToSitePath(
            AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit',
            {
                params: {
                    blindId: this.blindId,
                },
            },
        );
    }

    /**
     * Check if there's data to save (grade).
     *
     * @param isSubmit Whether the user is about to submit the grade.
     * @return Promise resolved with boolean: whether there's data to save.
     */
    protected async hasDataToSave(isSubmit = false): Promise<boolean> {
        if (!this.canSaveGrades || !this.loaded) {
            return false;
        }

        if (isSubmit && this.hasOfflineGrade) {
            // Always allow sending if the grade is saved in offline.
            return true;
        }

        // Check if numeric grade and toggles changed.
        if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt ||
                this.originalGrades.applyToAll != this.grade.applyToAll) {
            return true;
        }

        // Check if outcomes changed.
        if (this.gradeInfo && this.gradeInfo.outcomes) {
            for (const x in this.gradeInfo.outcomes) {
                const outcome = this.gradeInfo.outcomes[x];

                if (this.originalGrades.outcomes[outcome.id] == 'undefined' ||
                        this.originalGrades.outcomes[outcome.id] != outcome.selectedId) {
                    return true;
                }
            }
        }

        if (!this.feedback?.plugins) {
            return false;
        }

        try {
            return AddonModAssignHelper.hasFeedbackDataChanged(
                this.assign!,
                this.userSubmission,
                this.feedback,
                this.submitId,
            );
        } catch (error) {
            // Error ocurred, consider there are no changes.
            return false;
        }
    }

    /**
     * User entered the page that contains the component.
     */
    ionViewDidEnter(): void {
        this.tabs?.ionViewDidEnter();
    }

    /**
     * User left the page that contains the component.
     */
    ionViewDidLeave(): void {
        this.tabs?.ionViewDidLeave();
    }

    /**
     * Invalidate and refresh data.
     *
     * @param sync Whether to try to synchronize data.
     * @return Promise resolved when done.
     */
    async invalidateAndRefresh(sync = false): Promise<void> {
        this.loaded = false;

        const promises: Promise<void>[] = [];

        promises.push(AddonModAssign.invalidateAssignmentData(this.courseId));
        if (this.assign) {
            promises.push(AddonModAssign.invalidateSubmissionStatusData(
                this.assign!.id,
                this.submitId,
                undefined,
                !!this.blindId,
            ));
            promises.push(AddonModAssign.invalidateAssignmentUserMappingsData(this.assign!.id));
            promises.push(AddonModAssign.invalidateListParticipantsData(this.assign!.id));
        }
        promises.push(CoreGradesHelper.invalidateGradeModuleItems(this.courseId, this.submitId));
        promises.push(CoreCourse.invalidateModule(this.moduleId));

        // Invalidate plugins.
        if (this.submissionComponents && this.submissionComponents.length) {
            this.submissionComponents.forEach((component) => {
                promises.push(component.invalidate());
            });
        }

        await CoreUtils.ignoreErrors(Promise.all(promises));

        await this.loadData(sync);
    }

    /**
     * Load the data to render the submission.
     *
     * @param sync Whether to try to synchronize data.
     * @return Promise resolved when done.
     */
    protected async loadData(sync = false): Promise<void> {
        let isBlind = !!this.blindId;

        this.previousAttempt = undefined;
        this.isPreviousAttemptEmpty = true;

        if (!this.submitId) {
            this.submitId = this.currentUserId;
            isBlind = false;
        }

        try {
            // Get the assignment.
            this.assign = await AddonModAssign.getAssignment(this.courseId, this.moduleId);

            if (this.submitId != this.currentUserId && sync) {
                // Teacher viewing a student submission. Try to sync the assign, there could be offline grades stored.
                try {
                    const result = await AddonModAssignSync.syncAssign(this.assign.id);

                    if (result && result.updated) {
                        CoreEvents.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, {
                            assignId: this.assign.id,
                            warnings: result.warnings,
                            gradesBlocked: result.gradesBlocked,
                            context: 'submission',
                            submitId: this.submitId,
                        }, this.siteId);
                    }
                } catch (error) {
                    // Ignore errors, probably user is offline or sync is blocked.
                }
            }

            const time = CoreTimeUtils.timestamp();
            let promises: Promise<void>[] = [];

            if (this.assign.allowsubmissionsfromdate && this.assign.allowsubmissionsfromdate >= time) {
                this.fromDate = CoreTimeUtils.userDate(this.assign.allowsubmissionsfromdate * 1000);
            }

            this.blindMarking = this.isSubmittedForGrading && !!this.assign.blindmarking && !this.assign.revealidentities;

            if (!this.blindMarking && this.submitId != this.currentUserId) {
                promises.push(this.loadSubmissionUserProfile());
            }

            // Check if there's any offline data for this submission.
            promises.push(this.loadSubmissionOfflineData());

            await Promise.all(promises);

            // Get submission status.
            const submissionStatus =
                await AddonModAssign.getSubmissionStatusWithRetry(this.assign, { userId: this.submitId, isBlind });

            this.submissionStatusAvailable = true;
            this.lastAttempt = submissionStatus.lastattempt;
            this.membersToSubmit = [];
            this.membersToSubmitBlind = [];

            // Search the previous attempt.
            if (submissionStatus.previousattempts && submissionStatus.previousattempts.length > 0) {
                const previousAttempts = submissionStatus.previousattempts.sort((a, b) => a.attemptnumber - b.attemptnumber);
                this.previousAttempt = previousAttempts[previousAttempts.length - 1];
                this.isPreviousAttemptEmpty =
                    AddonModAssignHelper.isSubmissionEmpty(this.assign, this.previousAttempt.submission);
            }

            // Treat last attempt.
            promises = this.treatLastAttempt(submissionStatus);

            // Calculate the time remaining.
            this.calculateTimeRemaining(submissionStatus);

            // Load the feedback.
            promises.push(this.loadFeedback(submissionStatus.feedback));

            // Check if there's any unsupported plugin for editing.
            if (!this.userSubmission || !this.userSubmission.plugins) {
                // Submission not created yet, we have to use assign configs to detect the plugins used.
                this.userSubmission = AddonModAssignHelper.createEmptySubmission();
                this.userSubmission.plugins = AddonModAssignHelper.getPluginsEnabled(this.assign, 'assignsubmission');
            }

            // Get the submission plugins that don't support editing.
            promises.push(this.loadUnsupportedPlugins());

            await Promise.all(promises);
        } catch (error) {
            CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.');
        } finally {
            this.loaded = true;
        }
    }

    /**
     * Load profile of submission's user.
     *
     * @return Promise resolved when done.
     */
    protected async loadSubmissionUserProfile(): Promise<void> {
        this.user = await CoreUser.getProfile(this.submitId, this.courseId);
    }

    /**
     * Load offline data for the submission (not the submission grade).
     *
     * @return Promise resolved when done.
     */
    protected async loadSubmissionOfflineData(): Promise<void> {
        try {
            const submission = await AddonModAssignOffline.getSubmission(this.assign!.id, this.submitId);

            this.hasOffline = submission && submission.plugindata && Object.keys(submission.plugindata).length > 0;

            this.submittedOffline = !!submission?.submitted;
        } catch (error) {
            // No offline data found.
            this.hasOffline = false;
            this.submittedOffline = false;
        }
    }

    /**
     * Load the data to render the feedback and grade.
     *
     * @param feedback The feedback data from the submission status.
     * @return Promise resolved when done.
     */
    protected async loadFeedback(feedback?: AddonModAssignSubmissionFeedback): Promise<void> {
        this.grade = {
            method: '',
            modified: 0,
            addAttempt : false,
            applyToAll: false,
            lang: '',
            disabled: false,
        };

        this.originalGrades =  {
            addAttempt: false,
            applyToAll: false,
            outcomes: {},
        };

        if (feedback) {
            this.feedback = feedback;

            // If we have data about the grader, get its profile.
            if (feedback.grade && feedback.grade.grader > 0) {
                try {
                    this.grader =  await CoreUser.getProfile(feedback.grade.grader, this.courseId);
                } catch {
                    // Ignore errors.
                }
            } else {
                delete this.grader;
            }

            // Check if the grade uses advanced grading.
            if (feedback.gradefordisplay) {
                const position = feedback.gradefordisplay.indexOf('class="advancedgrade"');
                if (position > -1) {
                    this.feedback.advancedgrade = true;
                }
            }

            // Do not override already loaded grade.
            if (feedback.grade && feedback.grade.grade && !this.grade.grade) {
                const parsedGrade = parseFloat(feedback.grade.grade);

                this.grade!.grade = parsedGrade >= 0 ? parsedGrade : undefined;
                this.grade.gradebookGrade = CoreUtils.formatFloat(this.grade.grade);
                this.originalGrades.grade = this.grade.grade;
            }
        } else {
            // If no feedback, always show Submission.
            this.selectedTab = 'submission';
            this.tabs.selectTab(this.selectedTab);
        }

        this.grade.gradingStatus = this.lastAttempt?.gradingstatus;

        // Get the grade for the assign.
        this.gradeInfo = await CoreCourse.getModuleBasicGradeInfo(this.moduleId);

        if (!this.gradeInfo) {
            // It won't get gradeinfo on 3.1.
            return;
        }

        // Treat the grade info.
        await this.treatGradeInfo();

        const isManual = this.assign!.attemptreopenmethod == AddonModAssignProvider.ATTEMPT_REOPEN_METHOD_MANUAL;
        const isUnlimited = this.assign!.maxattempts == AddonModAssignProvider.UNLIMITED_ATTEMPTS;
        const isLessThanMaxAttempts = !!this.userSubmission && (this.userSubmission.attemptnumber < (this.assign!.maxattempts - 1));

        this.allowAddAttempt = isManual && (!this.userSubmission || isUnlimited || isLessThanMaxAttempts);

        if (this.assign!.teamsubmission) {
            this.grade.applyToAll = true;
            this.originalGrades.applyToAll = true;
        }
        if (this.assign!.markingworkflow && this.grade.gradingStatus) {
            this.workflowStatusTranslationId =
                AddonModAssign.getSubmissionGradingStatusTranslationId(this.grade.gradingStatus);
        }

        if (this.lastAttempt?.gradingstatus == 'graded' && !this.assign!.markingworkflow) {
            if (this.feedback!.gradeddate < this.lastAttempt!.submission!.timemodified) {
                this.lastAttempt.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT;

                // Get grading text and color.
                this.gradingStatusTranslationId = AddonModAssign.getSubmissionGradingStatusTranslationId(
                    this.lastAttempt.gradingstatus,
                );
                this.gradingColor = AddonModAssign.getSubmissionGradingStatusColor(this.lastAttempt.gradingstatus);

            }
        }

        if (!this.feedback || !this.feedback.plugins) {
            // Feedback plugins not present, we have to use assign configs to detect the plugins used.
            this.feedback = AddonModAssignHelper.createEmptyFeedback();
            this.feedback.plugins = AddonModAssignHelper.getPluginsEnabled(this.assign!, 'assignfeedback');
        }

        // Check if there's any offline data for this submission.
        if (!this.canSaveGrades) {
            // User cannot save grades in the app. Load the URL to grade it in browser.
            const mod = await CoreCourse.getModule(this.moduleId, this.courseId, undefined, true);
            this.gradeUrl = mod.url + '&action=grader&userid=' + this.submitId;

            return;
        }

        // Submission grades aren't identified by attempt number so it can retrieve the feedback for a previous attempt.
        // The app will not treat that as an special case.
        const submissionGrade = await CoreUtils.ignoreErrors(
            AddonModAssignOffline.getSubmissionGrade(this.assign!.id, this.submitId),
        );

        this.hasOfflineGrade = false;

        // Load offline grades.
        if (submissionGrade && (!feedback || !feedback.gradeddate || feedback.gradeddate < submissionGrade.timemodified)) {
            // If grade has been modified from gradebook, do not use offline.
            if ((this.grade.modified || 0) < submissionGrade.timemodified) {
                this.hasOfflineGrade = true;
                this.grade.grade = !this.grade.scale
                    ? CoreUtils.formatFloat(submissionGrade.grade)
                    : submissionGrade.grade;
                this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
                this.gradingColor = '';
                this.originalGrades.grade = this.grade.grade;
            }

            this.grade.applyToAll = !!submissionGrade.applytoall;
            this.grade.addAttempt = !!submissionGrade.addattempt;
            this.originalGrades.applyToAll = !!this.grade.applyToAll;
            this.originalGrades.addAttempt = !!this.grade.addAttempt;

            if (submissionGrade.outcomes && Object.keys(submissionGrade.outcomes).length && this.gradeInfo?.outcomes) {
                this.gradeInfo.outcomes.forEach((outcome) => {
                    if (typeof submissionGrade.outcomes[outcome.itemNumber!] != 'undefined') {
                        // If outcome has been modified from gradebook, do not use offline.
                        if (outcome.modified! < submissionGrade.timemodified) {
                            outcome.selectedId = submissionGrade.outcomes[outcome.itemNumber!];
                            this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
                        }
                    }
                });
            }
        }
    }

    /**
     * Get the submission plugins that don't support editing.
     *
     * @return Promise resolved when done.
     */
    protected async loadUnsupportedPlugins(): Promise<void> {
        this.unsupportedEditPlugins = await AddonModAssign.getUnsupportedEditPlugins(this.userSubmission?.plugins || []);
    }

    /**
     * Set the submission status name and class.
     *
     * @param status Submission status.
     */
    protected setStatusNameAndClass(status: AddonModAssignGetSubmissionStatusWSResponse): void {
        const translateService = Translate.instance;

        if (this.hasOffline || this.submittedOffline) {
            // Offline data.
            this.statusTranslated = translateService.instant('core.notsent');
            this.statusColor = 'warning';
        } else if (!this.assign!.teamsubmission) {

            // Single submission.
            if (this.userSubmission && this.userSubmission.status != this.statusNew) {
                this.statusTranslated = translateService.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
                this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status);
            } else {
                if (!status.lastattempt?.submissionsenabled) {
                    this.statusTranslated = translateService.instant('addon.mod_assign.noonlinesubmissions');
                    this.statusColor = AddonModAssign.getSubmissionStatusColor('noonlinesubmissions');
                } else {
                    this.statusTranslated = translateService.instant('addon.mod_assign.noattempt');
                    this.statusColor = AddonModAssign.getSubmissionStatusColor('noattempt');
                }
            }
        } else {

            // Team submission.
            if (!status.lastattempt?.submissiongroup && this.assign!.preventsubmissionnotingroup) {
                this.statusTranslated = translateService.instant('addon.mod_assign.nosubmission');
                this.statusColor = AddonModAssign.getSubmissionStatusColor('nosubmission');
            } else if (this.userSubmission && this.userSubmission.status != this.statusNew) {
                this.statusTranslated = translateService.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
                this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status);
            } else {
                if (!status.lastattempt?.submissionsenabled) {
                    this.statusTranslated = translateService.instant('addon.mod_assign.noonlinesubmissions');
                    this.statusColor = AddonModAssign.getSubmissionStatusColor('noonlinesubmissions');
                } else {
                    this.statusTranslated = translateService.instant('addon.mod_assign.nosubmission');
                    this.statusColor = AddonModAssign.getSubmissionStatusColor('nosubmission');
                }
            }
        }
    }

    /**
     * Show advanced grade.
     */
    showAdvancedGrade(): void {
        if (this.feedback && this.feedback.advancedgrade) {
            CoreTextUtils.viewText(
                Translate.instant('core.grades.grade'),
                this.feedback.gradefordisplay,
                {
                    component: AddonModAssignProvider.COMPONENT,
                    componentId: this.moduleId,
                },
            );
        }
    }

    /**
     * Submit for grading.
     *
     * @param acceptStatement Whether the statement has been accepted.
     */
    async submitForGrading(acceptStatement: boolean): Promise<void> {
        if (this.assign!.requiresubmissionstatement && !acceptStatement) {
            CoreDomUtils.showErrorModal('addon.mod_assign.acceptsubmissionstatement', true);

            return;
        }

        try {
            // Ask for confirmation. @todo plugin precheck_submission
            await CoreDomUtils.showConfirm(Translate.instant('addon.mod_assign.confirmsubmission'));

            const modal = await CoreDomUtils.showModalLoading('core.sending', true);

            try {
                await AddonModAssign.submitForGrading(
                    this.assign!.id,
                    this.courseId,
                    acceptStatement,
                    this.userSubmission!.timemodified,
                    this.hasOffline,
                );

                // Submitted, trigger event.
                CoreEvents.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, {
                    assignmentId: this.assign!.id,
                    submissionId: this.userSubmission!.id,
                    userId: this.currentUserId,
                }, this.siteId);
            } catch (error) {
                CoreDomUtils.showErrorModalDefault(error, 'core.error', true);
            } finally {
                modal.dismiss();
            }
        } catch {
            // Cancelled.
        }
    }

    /**
     * Submit a grade and feedback.
     *
     * @return Promise resolved when done.
     */
    async submitGrade(): Promise<void> {
        // Check if there's something to be saved.
        const modified = await this.hasDataToSave(true);
        if (!modified) {
            return;
        }

        const attemptNumber = this.userSubmission ? this.userSubmission.attemptnumber : -1;
        const outcomes: Record<number, number> = {};
        // Scale "no grade" uses -1 instead of 0.
        const grade = this.grade.scale && this.grade.grade == 0
            ? -1
            : CoreUtils.unformatFloat(this.grade.grade, true);

        if (grade === false) {
            // Grade is invalid.
            throw new CoreError(Translate.instant('core.grades.badgrade'));
        }

        const modal = await CoreDomUtils.showModalLoading('core.sending', true);

        (this.gradeInfo?.outcomes || []).forEach((outcome) => {
            if (outcome.itemNumber) {
                outcomes[outcome.itemNumber] = outcome.selectedId!;
            }
        });

        let pluginData: AddonModAssignSavePluginData = {};
        try {
            if (this.feedback && this.feedback.plugins) {
                pluginData =
                    await AddonModAssignHelper.prepareFeedbackPluginData(this.assign!.id, this.submitId, this.feedback);
            }

            try {
                // We have all the data, now send it.
                await AddonModAssign.submitGradingForm(
                    this.assign!.id,
                    this.submitId,
                    this.courseId,
                    grade || 0,
                    attemptNumber,
                    this.grade.addAttempt,
                    this.grade.gradingStatus || '',
                    this.grade.applyToAll,
                    outcomes,
                    pluginData,
                );

                // Data sent, discard draft.
                await this.discardDrafts();
            } finally {
                // Invalidate and refresh data.
                this.invalidateAndRefresh(true);

                CoreEvents.trigger(AddonModAssignProvider.GRADED_EVENT, {
                    assignmentId: this.assign!.id,
                    submissionId: this.submitId,
                    userId: this.currentUserId,
                }, this.siteId);
            }
        } finally {
            // Select submission view.
            this.tabs.selectTab('submission');
            modal.dismiss();
        }
    }

    /**
     * Treat the grade info.
     *
     * @return Promise resolved when done.
     */
    protected async treatGradeInfo(): Promise<void> {
        if (!this.gradeInfo) {
            return;
        }

        this.isGrading = true;

        // Make sure outcomes is an array.
        this.gradeInfo.outcomes = this.gradeInfo.outcomes || [];

        // Check if grading method is simple or not.
        if (this.gradeInfo.advancedgrading && this.gradeInfo.advancedgrading[0] &&
                typeof this.gradeInfo.advancedgrading[0].method != 'undefined') {
            this.grade.method = this.gradeInfo.advancedgrading[0].method || 'simple';
        } else {
            this.grade.method = 'simple';
        }

        this.canSaveGrades = this.grade.method == 'simple'; // Grades can be saved if simple grading.

        if (this.gradeInfo.scale) {
            this.grade.scale =
                CoreUtils.makeMenuFromList(this.gradeInfo.scale, Translate.instant('core.nograde'));
        } else {
            // Format the grade.
            this.grade.grade = CoreUtils.formatFloat(this.grade.grade);
            this.originalGrades.grade = this.grade.grade;

            // Get current language to format grade input field.
            this.grade.lang = await CoreLang.getCurrentLanguage();
        }

        // Treat outcomes.
        if (this.gradeInfo.outcomes && AddonModAssign.isOutcomesEditEnabled()) {
            this.gradeInfo.outcomes.forEach((outcome) => {
                if (outcome.scale) {
                    outcome.options =
                        CoreUtils.makeMenuFromList<number>(
                            outcome.scale,
                            Translate.instant('core.grades.nooutcome'),
                        );
                }
                outcome.selectedId = 0;
                this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
            });
        }

        // Get grade items.
        const grades = <CoreGradesFormattedItem[]>
            await CoreGradesHelper.getGradeModuleItems(this.courseId, this.moduleId, this.submitId);

        const outcomes: AddonModAssignGradeOutcome[] = [];

        grades.forEach((grade: CoreGradesFormattedItem) => {
            if (!grade.outcomeid && !grade.scaleid) {

                const gradeFormatted = grade.gradeformatted || '';
                // Not using outcomes or scale, get the numeric grade.
                if (this.grade.scale) {
                    this.grade.gradebookGrade = CoreUtils.formatFloat(
                        CoreGradesHelper.getGradeValueFromLabel(this.grade.scale, gradeFormatted),
                    );
                } else {
                    const parsedGrade = parseFloat(gradeFormatted);
                    this.grade.gradebookGrade = parsedGrade || parsedGrade == 0
                        ? CoreUtils.formatFloat(parsedGrade)
                        : undefined;
                }

                this.grade.disabled = !!grade.gradeislocked || !!grade.gradeisoverridden;
                this.grade.modified = grade.gradedategraded;
            } else if (grade.outcomeid) {

                // Only show outcomes with info on it, outcomeid could be null if outcomes are disabled on site.
                this.gradeInfo!.outcomes && this.gradeInfo!.outcomes.forEach((outcome) => {
                    if (outcome.id == String(grade.outcomeid)) {
                        outcome.selected = grade.gradeformatted;
                        outcome.modified = grade.gradedategraded;
                        if (outcome.options) {
                            outcome.selectedId =
                                CoreGradesHelper.getGradeValueFromLabel(outcome.options, outcome.selected || '');
                            this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
                            outcome.itemNumber = grade.itemnumber;
                        }
                        outcomes.push(outcome);
                    }
                });
                this.gradeInfo!.disabled = grade.gradeislocked || grade.gradeisoverridden;
            }
        });

        this.gradeInfo.outcomes = outcomes;
    }

    /**
     * Treat the last attempt.
     *
     * @param submissionStatus Response of get submission status.
     * @param promises List where to add the promises.
     */
    protected treatLastAttempt(submissionStatus: AddonModAssignGetSubmissionStatusWSResponse): Promise<void>[] {
        const promises: Promise<void>[] =[];

        if (!submissionStatus.lastattempt) {
            return [];
        }

        const submissionStatementMissing = !!this.assign!.requiresubmissionstatement &&
            typeof this.assign!.submissionstatement == 'undefined';

        this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (submissionStatus.lastattempt.cansubmit ||
            (this.hasOffline && AddonModAssign.canSubmitOffline(this.assign!, submissionStatus)));

        this.canEdit = !this.isSubmittedForGrading && submissionStatus.lastattempt.canedit &&
            (!this.submittedOffline || !this.assign!.submissiondrafts);

        // Get submission statement if needed.
        if (this.assign!.requiresubmissionstatement && this.assign!.submissiondrafts && this.submitId == this.currentUserId) {
            this.submissionStatement = this.assign!.submissionstatement;
            this.acceptStatement = false;
        } else {
            this.submissionStatement = undefined;
            this.acceptStatement = true; // No submission statement, so it's accepted.
        }

        // Show error if submission statement should be shown but it couldn't be retrieved.
        this.showErrorStatementEdit = submissionStatementMissing && !this.assign!.submissiondrafts &&
            this.submitId == this.currentUserId;

        this.showErrorStatementSubmit = submissionStatementMissing && !!this.assign!.submissiondrafts;

        this.userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(this.assign!, submissionStatus.lastattempt);

        if (this.assign!.attemptreopenmethod != this.attemptReopenMethodNone && this.userSubmission) {
            this.currentAttempt = this.userSubmission.attemptnumber + 1;
        }

        this.setStatusNameAndClass(submissionStatus);

        if (this.assign!.teamsubmission) {
            if (submissionStatus.lastattempt.submissiongroup) {
                // Get the name of the group.
                promises.push(CoreGroups.getActivityAllowedGroups(this.assign!.cmid).then((result) => {
                    const group = result.groups.find((group) => group.id == submissionStatus.lastattempt!.submissiongroup);
                    if (group) {
                        this.lastAttempt!.submissiongroupname = group.name;
                    }

                    return;
                }));
            }

            // Get the members that need to submit.
            if (this.userSubmission &&
                this.userSubmission.status != this.statusNew &&
                submissionStatus.lastattempt.submissiongroupmemberswhoneedtosubmit
            ) {
                submissionStatus.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => {
                    if (this.blindMarking) {
                        // Users not blinded! (Moodle < 3.1.1, 3.2).
                        promises.push(AddonModAssign.getAssignmentUserMappings(this.assign!.id, member, {
                            cmId: this.moduleId,
                        }).then((blindId) => {
                            this.membersToSubmitBlind.push(blindId);

                            return;
                        }));
                    } else {
                        promises.push(CoreUser.getProfile(member, this.courseId).then((profile) => {
                            this.membersToSubmit.push(profile);

                            return;
                        }));
                    }
                });
            }
        }

        // Get grading text and color.
        this.gradingStatusTranslationId = AddonModAssign.getSubmissionGradingStatusTranslationId(
            submissionStatus.lastattempt.gradingstatus,
        );
        this.gradingColor = AddonModAssign.getSubmissionGradingStatusColor(submissionStatus.lastattempt.gradingstatus);

        // Get the submission plugins.
        if (this.userSubmission) {
            if (!this.assign!.teamsubmission ||
                !submissionStatus.lastattempt.submissiongroup ||
                !this.assign!.preventsubmissionnotingroup
            ) {
                if (this.previousAttempt && this.previousAttempt.submission!.plugins &&
                        this.userSubmission.status == this.statusReopened) {
                    // Get latest attempt if avalaible.
                    this.submissionPlugins = this.previousAttempt.submission!.plugins;
                } else {
                    this.submissionPlugins = this.userSubmission.plugins!;
                }
            }
        }

        return promises;
    }

    /**
     * Block or unblock the automatic sync of the user grade.
     *
     * @param block Whether to block or unblock.
     */
    protected setGradeSyncBlocked(block = false): void {
        if (this.isDestroyed || !this.assign || !this.isGrading) {
            return;
        }

        const syncId = AddonModAssignSync.getGradeSyncId(this.assign!.id, this.submitId);

        if (block) {
            CoreSync.blockOperation(AddonModAssignProvider.COMPONENT, syncId);
        } else {
            CoreSync.unblockOperation(AddonModAssignProvider.COMPONENT, syncId);
        }
    }

    /**
     * A certain tab has been selected, either manually or automatically.
     *
     * @param tab The tab that was selected.
     */
    tabSelected(tab: CoreTabComponent): void {
        // Block sync when selecting grade tab, unblock when leaving it.
        this.setGradeSyncBlocked(tab.id === 'grade');
    }

    /**
     * Component being destroyed.
     */
    ngOnDestroy(): void {
        this.setGradeSyncBlocked(false);
        this.isDestroyed = true;
        this.syncObserver?.off();
    }

}

/**
 * Submission attempt with some calculated data.
 */
type AddonModAssignSubmissionAttemptFormatted = AddonModAssignSubmissionAttempt & {
    submissiongroupname?: string; // Calculated in the app. Group name the attempt belongs to.
};

/**
 * Feedback of an assign submission with some calculated data.
 */
type AddonModAssignSubmissionFeedbackFormatted = AddonModAssignSubmissionFeedback & {
    advancedgrade?: boolean; // Calculated in the app. Whether it uses advanced grading.
};

type AddonModAssignSubmissionGrade = {
    method: string;
    grade?: number | string;
    gradebookGrade?: string;
    modified?: number;
    gradingStatus?: string;
    addAttempt: boolean;
    applyToAll: boolean;
    scale?: CoreMenuItem<number>[];
    lang: string;
    disabled: boolean;
};

type AddonModAssignSubmissionOriginalGrades = {
    grade?: number | string;
    addAttempt: boolean;
    applyToAll: boolean;
    outcomes: Record<number, AddonModAssignGradeOutcome>;
};

type AddonModAssignGradeInfo = Omit<CoreCourseModuleGradeInfo, 'outcomes'> & {
    outcomes?: AddonModAssignGradeOutcome[];
    disabled?: boolean;
};

type AddonModAssignGradeOutcome = CoreCourseModuleGradeOutcome & {
    selectedId?: number;
    selected?: string;
    modified?: number;
    options?: CoreMenuItem<number>[];
    itemNumber?: number;
};
