import { Image } from "./gradingschema";


export const GRADE_VALUES = [
    // 0     1      2      3      4      5      6      7      8      9     10  
    "5,0", "4,0", "3,7", "3,3", "3,0", "2,7", "2,3", "2,0", "1,7", "1,3", "1,0"
    //     50%
]


export function gradeFromPoints(points: number, schema: number[]) {
    for (let note = GRADE_VALUES.length - 1; note > 0; note--) {
        if (points >= schema[note]) {
            return GRADE_VALUES[note];
        }
    }
    return GRADE_VALUES[0];
}

function gradingTable(pointsMax: number, requiredPoints?: number): number[] {
    if (!requiredPoints) {
        requiredPoints = Math.floor(pointsMax / 2);
    }

    const schema: number[] = [];
    const stepsBestanden = (pointsMax - (requiredPoints - 1)) / (GRADE_VALUES.length - 1);

    schema[0] = 0;

    // falls schlechter als 4 (4,3, 4,7 etc); nicht an der BHT
    // const stepsDurchgefallen = bestandenAb / 5;
    // for (let i = 0; i < 5; i++) {
    //     schema[i] = Math.floor(stepsDurchgefallen * i);
    // }
    schema[1] = requiredPoints;
    for (let i = 2; i < GRADE_VALUES.length; i++) {
        schema[i] = requiredPoints + Math.floor(stepsBestanden * (i - 1));
    }

    return schema;
}


export class Results {
    readonly studentResults: StudentResult[] = []
    readonly gradingTable: number[];
    readonly dateTime: number;
    appliedCorrections: number = 0;


    constructor(
        public readonly course: string,
        public readonly term: string,
        public readonly examName: string,
        public readonly maxPoints: number,
        public readonly minCoverageStatements: number = 0,
        public readonly penaltiesCoverageMax: number = 0,
        public readonly extraCoverageMax: number = 0,
        /**
         * How many points are necessary to pass the exam.
         */
        public readonly requiredPoints?: number
    ) {
        this.gradingTable = gradingTable(maxPoints, requiredPoints);
        this.dateTime = Date.now();
    }

    /**
     * 
     * @param rawPoints Punkte ohne Coverage-Korrektur
     * @param stmtCoverage  Statement-Coverage (des Studenten)
     * @returns Korrekturfaktor für Coverage; 0, wenn keine Coverage verlangt (minCoverageStatements undefined oder 0)
     */
    coveragePunkte(rawPoints: number, stmtCoverage: number) {
        if (this.minCoverageStatements <= 0 || this.minCoverageStatements > 100 || this.minCoverageStatements == stmtCoverage) {
            return 0;
        }
        if (stmtCoverage < 0) {
            stmtCoverage = 0;
        }
        if (stmtCoverage > 100) {
            stmtCoverage = 100;
        }

        if (stmtCoverage < this.minCoverageStatements) {
            const factor = Math.max(0, stmtCoverage) / this.minCoverageStatements;
            // penaltiesCoverageMax = 0 means: no penalty
            const deductionMax = Math.min(this.penaltiesCoverageMax, this.maxPoints);
            const deduction = (1 - factor) * deductionMax;
            return Math.max(-rawPoints, -Math.floor(deduction));
        } else { // stmtCoverage>this.minCoverageStatements
            if (this.extraCoverageMax > 0) {
                const factor = (stmtCoverage - this.minCoverageStatements) / (100 - this.minCoverageStatements);
                const extra = this.extraCoverageMax * factor;
                return Math.min(rawPoints, Math.ceil(extra));
            }
        }
        return 0;
    }
}

class Skippable {
    skipMessage: string | null = null;

    skipped(): this is { skipMessage: string } {
        return this.skipMessage ? true : false;
    }
}

/**
 * Points may be 0 if only a comment is added.
 */
export class ManualCorrection {
    points: number;
    reason: string|string[];
    absolute: boolean;

    constructor(points: number | undefined, reason: string|string[], absolute: boolean) {
        if (!points) {
            this.points = 0;
        } else {
            this.points = points;
        }
        this.reason = reason;
        this.absolute = absolute;
    }
}

export class StudentResult extends Skippable {
    readonly taskResults: TaskResult[] = [];
    readonly coverageResult = new CoverageResult();
    noSubmission: boolean = false;
    checkReportFound = false;
    studentReportFound = false;
    gradingDone = false;
    readonly penaltyResults: GradingResult[] = []

    manualCorrection: ManualCorrection[] = [];

    errorDetails: string = "";

    /**
     * sum of points of tasks including manual corrections, computed in computeAndSetRawPunkte
     */
    pointsRaw: number = 0;
    /**
     * actual coverage points, can only computed when rawPoints have been set
     */
    pointsCoverage: number = 0;
    /**
     * points raw + point coverage
     */
    points: number = 0;
    /**
     * grade (1-5, no 4,3 and 4,7), can only be computed when points have been set.
     */
    grade: string = "";

    /**
     * Set to true if manual correction changed the points using absolute points.
     */
    absolutePoints = false;

    userTests: boolean = false;
    userTestsSuccess: number = 0;
    userTestsFailureOrError: number = 0;
    userTestsCount: number = 0;

    comment ="";

    patched = false;

    constructor(public readonly submissionId: string, public readonly userName: string) {
        super();
    }

    addComment(comment: string) {
        if (this.comment.length > 0) {
            this.comment += " ";
        }
        this.comment += comment;
    }

    addPenalty(gradingResult: GradingResult) {
        this.penaltyResults.push(gradingResult);
    }

    /**
     * @returns Returns true, if any penalities exist (i.e. taking actual results into account).
     */
    hasPenalties() {
        let penaltyPoints = this.penaltyResults.reduce((sum, bew) => sum + bew.points, 0);
        return penaltyPoints != 0
    }

    computeAndSetRawPunkte() {
        this.pointsRaw = this.taskResults.reduce(
            (sum, taskResult) => {
                // compute pointsRaw for each task even if it skipped as it may contain manual corrections
                taskResult.computeAndSetRawPoints();
                return sum + taskResult.points;
            }, 0)


        if (this.penaltyResults.length > 0) {
            this.penaltyResults.forEach(gradingResult => gradingResult.computeAndSetRawPoints());
            this.pointsRaw += this.penaltyResults.reduce((sum, gradingResult) => sum + gradingResult.points, 0)
        }

        this.manualCorrection.forEach(manualCorrection => this.pointsRaw += manualCorrection.points);
        if (this.pointsRaw < 0) {
            this.pointsRaw = 0;
        }

    }

    get pointsMax() {
        return this.taskResults.reduce(
            (sum, task) => sum + task.pointsMax, 0
        )
    }

}

export class CoverageResult extends Skippable {
    public stmtCoverage: number = 0;
    constructor() {
        super();
    }
}

type ConditionStatus = "none" | "failed" | "success" | "notFound";

export class TaskResult extends Skippable {

    readonly name: string;
    readonly pointsMax: number
    readonly gradingResults: GradingResult[] = []
    readonly penaltyResults: GradingResult[] = []
    points = 0;
    manualCorrections: ManualCorrection[] = [];
    images: Image[] = [];
    message: string | null = null;

    /** for statistics */
    precondition: ConditionStatus = "none";

    constructor(name: string, pointsMax: number) {
        super();
        this.name = name;
        this.pointsMax = pointsMax;
    }

    addGradingResult(gradingResult: GradingResult) {
        this.gradingResults.push(gradingResult);
    }

    addPenalty(gradingResult: GradingResult) {
        this.penaltyResults.push(gradingResult);
    }

    /**
     * @returns Returns true, if any penalities exist (i.e. taking actual results into account).
     */
    hasPenalties() {
        let penaltyPoints = this.penaltyResults.reduce((sum, bew) => sum + bew.points, 0);
        return penaltyPoints != 0
    }

    computeAndSetRawPoints() {
        this.points = 0;
        if (!this.skipped()) {
            if (this.gradingResults.length > 0) {
                this.gradingResults.forEach(gradingResult => gradingResult.computeAndSetRawPoints());
                this.points = this.gradingResults.reduce((sum, gradingResult) => sum + gradingResult.points, 0);
            }
            if (this.penaltyResults.length > 0) {
                this.penaltyResults.forEach(gradingResult => gradingResult.computeAndSetRawPoints());
                this.points += this.penaltyResults.reduce((sum, gradingResult) => sum + gradingResult.points, 0)
            }
        }
        this.manualCorrections.forEach(correction => this.points += correction.points);

        if (this.points < 0) { // minimum 0 points per task
            this.points = 0;
        }
    }
}


export class GradingResult extends Skippable {
    readonly testResults: TestResult[] = []
    points = 0;
    precondition?: string;
    images: Image[] = []
    message: string | null = null;
    
    /**
     * If the grading is negative (i.e., {@link pointsMax} is less 0), the grading is a penalty.
     */
    constructor(public readonly text: string, public readonly pointsMax: number, public isBonus?: boolean) {
        super();
    }

    addTestResult(text: string, testStatus: TestStatus, message: string | undefined) {
        this.testResults.push(new TestResult(text, testStatus, message));
    }

    /**
     * If the grading is negative (i.e., {@link pointsMax} is less 0), the grading is a penalty.
     */
    get isPenalty() {
        return this.pointsMax < 0;
    }

    /**
     * Computes raw points (and sets them internally to field {@link points}).
     * 
     * If the grading is positive, the number of successfully passed tests is used to compute the raw points.
     * If the grading is a penalty (i.e. grading is negative) the  number of failed tests are used instead.
     */
    computeAndSetRawPoints() {
        this.points = 0;
        if (!this.isPenalty) {
            if (this.skipped()) {
                return;
            }
            const successCount = this.testResults.filter(test => test.testStatus === "success").length;
            if (this.testResults.length == 0) {
                console.error("Keine Testergebnisse verzeichnet, setze Punkte für Bewertung " + this.text + " auf 0.")
                this.points = 0;
            } else {
                this.points = Math.ceil(this.pointsMax * successCount / this.testResults.length);
            }
        } else {
            const failureCount = this.testResults.filter(test => test.testStatus !== "success").length;
            if (this.testResults.length == 0) {
                this.points = 0;
            } else {
                this.points = Math.floor(this.pointsMax * failureCount / this.testResults.length);
            }
        }

    }

}

export type TestStatus = "success" | "failure" | "error" | "skipped" | "notFound";

export class TestResult {

    constructor(
        public text: string,
        public testStatus: TestStatus,
        public message: string | undefined) {

    }
}
