import {FormatUtils} from '@/logic/utils/format-utils';
import {CourseUtils} from '@/logic/utils/course-utils';
import Navigation from '@/components/navigation/navigation';
import {GPAUtils} from '@/logic/utils/gpa-utils';
import CacheUtils from '@/logic/utils/cache-utils';
import Constants from '@/constants';
import {Index} from '@/logic/nav-controller';
import App from '@/components/app/app';

/**
 * Objects of this interface represent assignment grades.
 */
export class Assignment
{
    id: number;
    scoreId: number;
    type: string;
    typeId: number;
    description: string;
    time: number;
    complete: string;
    include: boolean;
    display: boolean;

    unread: boolean;

    scoreMax: number;
    score: number;

    gradingPeriod: number;

    // Callbacks when this object updates
    private updateCallbacks: (() => void)[] = [];

    /**
     * Construct assignment with json object
     *
     * @param json Json object
     */
    constructor(json: any)
    {
        this.id =  json.assignment_id;
        this.scoreId =  json.score_id;
        this.type =  json.assignment_type;
        this.typeId =  json.assignment_type_id;
        this.description =  json.assignment_description;
        this.time =  new Date(json._date).getTime();
        this.complete =  json.completion_status;
        this.include =  json.include_in_calculated_grade == 1;
        this.display =  json.display_grade == 1;

        this.unread =  json.is_unread == 1;

        this.scoreMax =  json.maximum_score;
        this.score =  +json.raw_score;

        this.gradingPeriod =  +json.grading_period.replace('Quarter ', '') - 1;
    }

    /**
     * Graded or not
     */
    get graded()
    {
        return this.complete == 'Complete';
    }

    /**
     * Add callback
     *
     * @param callback
     */
    addCallback(callback: () => void)
    {
        this.updateCallbacks.push(callback);
    }

    /**
     * Mark as read
     */
    markAsRead(): Promise<void>
    {
        return new Promise((resolve, reject) => {
            App.http.post('/mark-as-read', {scoreId: this.scoreId})
            .then(response =>
            {
                // Check success
                if (response.success)
                {
                    this.unread = false;
                    this.updateCallbacks.forEach(callback => callback());
                    resolve();
                }
                else reject(response.data);
            })
            .catch(reject)
        })
    }
}

export interface AssignmentType
{
    id: number
    name: string

    weight: number
    scoreMax: number
    score: number
    percent: number
    assignmentCount: number
}

export interface Grading
{
    method: string
    weightingMap: {[index: string]: number}
}

export default class Course
{
    id: number;
    assignmentsId: number;
    name: string;
    teacherName: string;
    status: string;
    rawAssignments: Assignment[];

    rawLetterGrade?: string;
    rawNumericGrade?: number;

    level: string;
    scaleUp: number;

    termGrading: Grading[];
    termAssignments: Assignment[][];

    cache: CacheUtils = new CacheUtils();

    /**
     * Construct a course with a course json object
     *
     * @param courseJson Course json object
     */
    constructor(courseJson: any)
    {
        this.id = courseJson.id;
        this.assignmentsId = courseJson.assignmentsId;
        this.name = FormatUtils.parseText(courseJson.name).trim();
        this.teacherName = courseJson.teacherName;
        this.status = courseJson.status;

        this.rawLetterGrade = courseJson.letterGrade;
        this.rawNumericGrade = courseJson.numericGrade;

        // Other api issue
        if (this.rawLetterGrade == '')
        {
            this.rawNumericGrade = undefined;
            this.rawLetterGrade = undefined;
        }

        // Level and scaleUp
        let level = CourseUtils.detectLevel(this.name);
        if (level != undefined)
        {
            this.level = level.level;
            this.scaleUp = level.scaleUp;
        }
        else this.level = 'Unknown';

        this.termGrading = new Array(4).fill(null);
    }

    /**
     * Load in assignments data
     *
     * @param data Assignments data
     */
    loadAssignments(data: any)
    {
        // Load assignments
        // Parse json and filter it
        this.rawAssignments = data.assignments.map((a: any) => new Assignment(a));

        // Sort by date (Latest is at 0)
        this.rawAssignments.sort((a, b) => b.time - a.time);

        // Filter assignments into terms
        this.termAssignments = [[], [], [], []];

        // Loop through it by time order
        this.rawAssignments.forEach(a => this.termAssignments[a.gradingPeriod].push(a));
    }

    /**
     * Is graded or not
     */
    get isGraded(): boolean
    {
        // Skip future or past courses
        if (this.status != 'active') return false;

        // Skip courses without levels TODO: Ask for user input
        if (this.level == 'None' || this.level == 'Unknown' || this.scaleUp == -1) return false;

        // Skip courses without graded assignments
        if (this.assignments.length == 0) return false;

        // Skip if there are no grading scale
        // if (course.grading.method == 'NOT_GRADED') return;

        // Is graded
        return true;
    }

    /**
     * Get currently selected grading periods
     */
    get gradingPeriods(): number[]
    {
        return this.cache.get('GradingPeriods', () =>
        {
            return (this.rawSelectedTerm == -1 ? [0, 1, 2, 3] : [this.rawSelectedTerm]).filter(term =>
                this.termAssignments[term].filter(a => a.graded).length != 0);
        })
    }

    /**
     * Get currently selected grading periods
     */
    get allGradingPeriods(): number[]
    {
        return this.cache.get('AllGradingPeriods', () =>
        {
            return [0, 1, 2, 3].filter(term => this.termAssignments[term].filter(a => a.graded).length != 0);
        })
    }

    /**
     * Get assignments of the selected grading periods
     */
    get assignments(): Assignment[]
    {
        return this.gradingPeriods
            .flatMap(term => this.termAssignments[term])
            .filter(a => a.graded)
            .sort((a, b) => b.time - a.time);
    }

    /**
     * Get assignments before a certain date
     *
     * @param time
     */
    getAssignmentsBefore(time: number): {term: number, assignments: Assignment[]}
    {
        let term = Constants.getTerm(new Date(time));
        let assignments = this.assignments.filter(a => a.gradingPeriod == term && a.time <= time);

        return {term: term, assignments: assignments}
    }

    /**
     * Get letter grade
     */
    get letterGrade(): string
    {
        return this.cache.get('LetterGrade', () =>
        {
            // Get scale
            let scale = GPAUtils.findScale(this.numericGrade);

            // Scale not found
            return scale == undefined ? '--' : scale.letter;
        })
    }

    /**
     * Get letter grade by term
     *
     * @param term
     */
    letterGradeTerm(term: number): string
    {
        return this.cache.get('LetterGrade' + term, () =>
        {
            // Get scale
            let scale = GPAUtils.findScale(this.numericGradeTerm(term));

            // Scale not found
            return scale == undefined ? '--' : scale.letter;
        })
    }

    /**
     * Get numeric grade
     */
    get numericGrade()
    {
        return this.cache.get('NumericGrade', () =>
        {
            return this.gradingPeriods.map(term => this.numericGradeTerm(term))
                    .reduce((p, v) => p + v) / this.gradingPeriods.length
        })
    }

    /**
     * Get numeric grade by term
     *
     * @param term
     */
    numericGradeTerm(term: number): number
    {
        return this.cache.get('NumericGrade' + term, () =>
        {
            // Calculate
            if (this.termGrading[term].method == 'PERCENT_TYPE')
            {
                return GPAUtils.getPercentTypeAverage(this.termGrading[term], this.termAssignments[term]);
            }
            else if (this.termGrading[term].method == 'TOTAL_MEAN')
            {
                return GPAUtils.getTotalMeanAverage(this.termAssignments[term]);
            }
            else return -1;
        })
    }

    /**
     * Get assignment types
     */
    get assignmentTypes(): AssignmentType[]
    {
        return this.cache.get('AssignmentTypes', () =>
        {
            // Get all types
            let types = this.assignments.map(a => a.type);

            // Remove duplicates
            types = types.filter((type, i, a) => a.indexOf(type) == i);

            // Get total possible score for weight calculation
            let totalScoreMax = this.assignments.reduce((sum, a) => sum + a.scoreMax, 0);

            // For every type...
            return types.map(type =>
            {
                // Get assignments of the type
                let typeAssignments = this.assignments.filter(a => a.type == type);

                // Count scores and max scores
                let score = typeAssignments.reduce((sum, a) => sum + a.score, 0);
                let scoreMax = typeAssignments.reduce((sum, a) => sum + a.scoreMax, 0);

                // Calculate weight
                let weight = this.termGrading[0].method == 'PERCENT_TYPE'
                    ? this.termGrading[0].weightingMap[type] : scoreMax / totalScoreMax;

                // Return
                return {name: type, id: typeAssignments[0].typeId, weight: +(weight * 100).toFixed(2),
                    scoreMax: scoreMax, score: score, percent: +(score / scoreMax * 100).toFixed(2),
                    assignmentCount: typeAssignments.length}
            })
        })
    }

    /**
     * Get url hash code
     */
    get urlHash(): string
    {
        return `course/${this.id}`
    }

    /**
     * Get navigation index
     */
    get urlIndex(): Index
    {
        return {hash: this.urlHash, title: this.name, identifier: 'course', info: {id: this.id}}
    }

    /**
     * Selected term
     */
    get rawSelectedTerm(): number
    {
        return Navigation.instance.getSelectedTerm()
    }
}
