import { getParam } from "./engine_utils.js";

const showProgressLogs = getParam("debugprogress");

/** Gets the date formatted as 20240220-161993. When no Date is passed in, the current local date is used. */
export function getFormattedDate(date?: Date) {
    date = date || new Date();

    const month = date.getMonth() + 1;
    const day = date.getDate();
    const hour = date.getHours();
    const min = date.getMinutes();
    const sec = date.getSeconds();

    const s_month = (month < 10 ? "0" : "") + month;
    const s_day = (day < 10 ? "0" : "") + day;
    const s_hour = (hour < 10 ? "0" : "") + hour;
    const s_min = (min < 10 ? "0" : "") + min;
    const s_sec = (sec < 10 ? "0" : "") + sec;

    return date.getFullYear() + s_month + s_day + "-" + s_hour + s_min + s_sec;
}

declare type ProgressOptions = {
    message?: string,
    progress?: number,
    autoStep?: boolean | number;
    currentStep?: number,
    totalSteps?: number
};

declare type ProgressStartOptions = {
    /** This progress scope will be nested below parentScope */
    parentScope?: string,
    /** Callback with progress in 0..1 range. */
    onProgress?: (progress: number) => void,
    /** Log timings using console.time() and console.timeLog(). */
    logTimings?: boolean,
};

/** Progress reporting utility.
 * See `Progress.start` for usage examples.
 */
export class Progress {

    /** Start a new progress reporting scope. Make sure to close it with Progress.end.
     * @param scope The scope to start progress reporting for.
     * @param options Parent scope, onProgress callback and logging. If only a string is provided, it's used as parentScope.
     * @example
     * // Manual usage:
     * Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
     * Progress.report("export-usdz", { message: "Exporting object 1", currentStep: 1, totalSteps: 3 });
     * Progress.report("export-usdz", { message: "Exporting object 2", currentStep: 2, totalSteps: 3 });
     * Progress.report("export-usdz", { message: "Exporting object 3", currentStep: 3, totalSteps: 3 });
     * 
     * // Auto step usage:
     * Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
     * Progress.report("export-usdz", { message: "Exporting objects", autoStep: true, totalSteps: 3 });
     * Progress.report("export-usdz", "Exporting object 1");
     * Progress.report("export-usdz", "Exporting object 2");
     * Progress.report("export-usdz", "Exporting object 3");
     * Progress.end("export-usdz");
     * 
     * // Auto step with weights:
     * Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
     * Progress.report("export-usdz", { message: "Exporting objects", autoStep: true, totalSteps: 10 });
     * Progress.report("export-usdz", { message: "Exporting object 1", autoStep: 8 }); // will advance to 80% progress
     * Progress.report("export-usdz", "Exporting object 2"); // 90%
     * Progress.report("export-usdz", "Exporting object 3"); // 100%
     * 
     * // Child scopes:
     * Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
     * Progress.report("export-usdz", { message: "Overall export", autoStep: true, totalSteps: 2 });
     * Progress.start("export-usdz-objects", "export-usdz");
     * Progress.report("export-usdz-objects", { message: "Exporting objects", autoStep: true, totalSteps: 3 });
     * Progress.report("export-usdz-objects", "Exporting object 1");
     * Progress.report("export-usdz-objects", "Exporting object 2");
     * Progress.report("export-usdz-objects", "Exporting object 3");
     * Progress.end("export-usdz-objects");
     * Progress.report("export-usdz", "Exporting materials");
     * Progress.end("export-usdz");
     * 
     * // Enable console logging:
     * Progress.start("export-usdz", { logTimings: true });
     */
    static start(scope: string, options?: ProgressStartOptions | string) {
        if (typeof options === "string") options = { parentScope: options };
        const p = new ProgressEntry(scope, options);
        progressCache.set(scope, p);
    }

    /** Report progress for a formerly started scope.
     * @param scope The scope to report progress for.
     * @param options Options for the progress report. If a string is passed, it will be used as the message.
     * @example
     * // auto step and show a message
     * Progress.report("export-usdz", "Exporting object 1");
     * // same as above
     * Progress.report("export-usdz", { message: "Exporting object 1", autoStep: true });
     * // show the current step and total steps and implicitly calculate progress as 10%
     * Progress.report("export-usdz", { currentStep: 1, totalSteps: 10 });
     * // enable auto step mode, following calls that have autoStep true will increase currentStep automatically.
     * Progress.report("export-usdz", { totalSteps: 20, autoStep: true });
     * // show the progress as 50%
     * Progress.report("export-usdz", { progress: 0.5 });
     * // give this step a weight of 20, which changes how progress is calculated. Useful for steps that take longer and/or have child scopes.
     * Progress.report("export-usdz", { message. "Long process", autoStep: 20 });
     * // show the current step and total steps and implicitly calculate progress as 10%
     * Progress.report("export-usdz", { currentStep: 1, totalSteps: 10 });
     */
    static report(scope: string, options?: ProgressOptions | string) {
        const p = progressCache.get(scope);
        if (!p) {
            console.warn("Reporting progress for non-existing scope", scope);
            return;
        }
        if (typeof options === "string") options = { message: options, autoStep: true };
        p.report(options);
    }

    /** End a formerly started scope. This will also report the progress as 100%.
     * @remarks Will warn if any child scope is still running (progress < 1).
    */
    static end(scope: string) {
        const p = progressCache.get(scope);
        if (!p) return;
        p.end();
        progressCache.delete(scope);
    }
}

const progressCache: Map<string, ProgressEntry> = new Map<string, ProgressEntry>();

/** Internal class that handles Progress instances and their parent/child relationship. */
class ProgressEntry {
    private scopeLabel: string;
    private parentScope?: ProgressEntry;
    private childScopes: Array<ProgressEntry> = [];
    private parentDepth = 0;
    private lastStep? = 0;
    private lastAutoStepWeight = 1;
    private lastTotalSteps? = 0;
    private onProgress?: (progress: number) => void;
    private showLogs: boolean = false;

    selfProgress: number = 0;
    totalProgress: number = 0;
    selfReports: number = 0;
    totalReports: number = 0;

    constructor(scope: string, options?: ProgressStartOptions) {
        this.parentScope = options?.parentScope ? progressCache.get(options.parentScope) : undefined;
        if (this.parentScope) {
            this.parentScope.childScopes.push(this);
            this.parentDepth = this.parentScope.parentDepth + 1;
        }
        this.scopeLabel = " ".repeat(this.parentDepth * 2) + scope;
        this.showLogs = options?.logTimings ?? !!showProgressLogs;
        if (this.showLogs) console.time(this.scopeLabel);
        this.onProgress = options?.onProgress;
    }

    report(options?: ProgressOptions, indirect: boolean = false) {
        if (options) {
            if (options.totalSteps !== undefined)
                this.lastTotalSteps = options.totalSteps;
            if (options.currentStep !== undefined)
                this.lastStep = options.currentStep;
            if (options.autoStep !== undefined) {
                if (options.currentStep === undefined) {
                    if (this.lastStep === undefined) this.lastStep = 0;
                    const stepIncrease = typeof options.autoStep === "number" ? options.autoStep : 1;
                    this.lastStep += this.lastAutoStepWeight;
                    this.lastAutoStepWeight = stepIncrease; 
                    options.currentStep = this.lastStep;
                }
                options.totalSteps = this.lastTotalSteps;
            }
            if (options.progress !== undefined) 
                this.selfProgress = options.progress;
            else if (options.currentStep !== undefined && options.totalSteps !== undefined) {
                this.selfProgress = options.currentStep / options.totalSteps;
            }
        }

        if (this.childScopes.length > 0) {
            let avgChildProgress = 0;
            let sumChildWeight = 0;
            for (const c of this.childScopes) 
            {
                avgChildProgress += c.selfProgress;
                sumChildWeight += 1;
            }
            if (sumChildWeight > 0)
                avgChildProgress /= sumChildWeight;
            const stepWeight = this.lastAutoStepWeight / (this.lastTotalSteps ?? 1);
            // not entirely sure about this formula – idea is that a step should be weighted by the progress of the children
            this.totalProgress = this.selfProgress + avgChildProgress * stepWeight;
        }
        else {
            this.totalProgress = this.selfProgress;
        }

        // sanitize values
        this.selfProgress = Math.min(1, this.selfProgress);
        this.totalProgress = Math.min(1, this.totalProgress);

        let msg = (this.totalProgress * 100).toFixed(3) + "%"
        if (this.childScopes.length > 0) msg += " (" + (this.selfProgress * 100).toFixed(3) + "% self)";
        if (options?.message) msg = options.message + " – " + msg;
        if (this.lastStep !== undefined && this.lastTotalSteps !== undefined)
            msg = "Step " + (this.lastStep + (this.lastAutoStepWeight != 1 ? "–" + (this.lastStep + this.lastAutoStepWeight) : "") + "/" + this.lastTotalSteps) + " " + msg;

        if (indirect) this.totalReports++;
        else { this.selfReports++; this.totalReports++; }

        if (this.showLogs) console.timeLog(this.scopeLabel, msg);
        if (this.onProgress) this.onProgress(this.totalProgress);
        if (this.parentScope) this.parentScope.report(undefined, true);
    }

    end() {
        this.report({ progress: 1, autoStep: true }, true);
        if (this.showLogs) { 
            console.timeLog(this.scopeLabel, "Total reports: " + this.totalReports, "Self reports: " + this.selfReports);
            console.timeEnd(this.scopeLabel);
        }
        let anyRunningChildProgress = false;
        for (const c of this.childScopes) {
            if (c.selfProgress >= 1) continue;
            anyRunningChildProgress = true;
            break;
        }
        if (anyRunningChildProgress)
            console.warn("Progress end with child scopes that are still running", this);
        this.onProgress = undefined;
    }
}