"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PreBuildContextImpl = void 0;
exports.SleepPromise = SleepPromise;
const tslib_1 = require("tslib");
const semaphore_async_await_1 = tslib_1.__importDefault(require("semaphore-async-await"));
const errors_1 = require("../errors");
const quiet_1 = require("../reporter/quiet");
const interface_1 = require("./interface");
const progress_impl_1 = tslib_1.__importDefault(require("./progress-impl"));
const JOURNAL_VERSION = 2;
function SleepPromise(dt) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(null), dt);
    });
}
class Director {
    constructor() {
        this.reporter = new quiet_1.QuietReporter();
        this.rules = [];
        this.database = new Map();
        this.journal = new Map();
        this.someTargetWrong = false;
        this.buildRev = 0;
        this.lock = null;
    }
    updateBuildRev() {
        let rev = this.buildRev;
        for (let r of this.journal.values()) {
            if (r.revision > rev)
                rev = r.revision;
        }
        this.buildRev = rev + 1;
    }
    createObjective(matchResult, goal) {
        const progress = this.getProgress(goal.id);
        progress.isUser = goal.rule.isUser;
        return {
            matchResult,
            goal,
            progress,
        };
    }
    createProgress(goalID) {
        return new progress_impl_1.default(goalID);
    }
    ruleNotFound(id) {
        return (0, errors_1.createExtError)(new Error(`Building rule for '${id}' is not found.`), {
            system: true,
        });
    }
    dependencyNotFound(id, against) {
        return (0, errors_1.createExtError)(new Error(`Building rule for '${id}' is not found, which is needed by '${against}'.`), { system: true });
    }
    tryToBuildWithDifferentRule(id) {
        return (0, errors_1.createExtError)(new Error(`Trying to build objective '${id}' with a different rule.`), { system: true });
    }
    cancelled(id) {
        return (0, errors_1.createExtError)(new Error(`Build for '${id}' is cancelled.`), {
            system: true,
            hide: true,
        });
    }
    BuildCancelledByUser() {
        return (0, errors_1.createExtError)(new Error(`Build cancelled by user.`), { system: true });
    }
    getProgress(goalID) {
        const existing = this.journal.get(goalID);
        if (existing)
            return existing;
        const p = this.createProgress(goalID);
        this.journal.set(goalID, p);
        return p;
    }
    queryGoal(isName, name, args) {
        const cached = isName ? null : this.database.get(name);
        if (cached)
            return cached;
        for (let j = this.rules.length - 1; j >= 0; j--) {
            const rule = this.rules[j];
            if (!rule)
                continue;
            const match = isName || !args ? rule.matchString(name) : rule.matchGoalID(name, args);
            if (!match)
                continue;
            const goal = { id: match.id, args: match.args, rule };
            const cached = this.database.get(goal.id);
            if (cached && cached.goal.rule === rule) {
                return cached;
            }
            if (cached && cached.goal.rule !== rule) {
                throw this.tryToBuildWithDifferentRule(goal.id);
            }
            const objective = this.createObjective(match.execArgs, goal);
            this.database.set(goal.id, objective);
            return objective;
        }
        return undefined;
    }
    validateGoal(goal) {
        const cached = this.database.get(goal.id);
        if (cached) {
            if (cached.goal.rule === goal.rule) {
                return cached;
            }
            else {
                throw this.tryToBuildWithDifferentRule(goal.id);
            }
        }
        if (!goal.rule)
            return undefined;
        const m = goal.rule.matchGoalID(goal.id, goal.args);
        if (!m)
            return undefined;
        const entry = this.createObjective(m.execArgs, goal);
        this.database.set(goal.id, entry);
        return entry;
    }
    checkNeedRebuild(dep) {
        if (dep.progress.preBuildStatus !== interface_1.PreBuildStatus.UNKNOWN) {
            return dep.progress.modifiedCheckFinishPromise();
        }
        else {
            const proxy = new PreBuildContextImpl(dep, this);
            return dep.progress.startModifiedCheck(() => {
                return dep.goal.rule.preBuild(proxy, ...dep.matchResult);
            });
        }
    }
    async checkTriggerRebuildByDependency(dep, against) {
        const itselfModified = await this.checkNeedRebuild(dep);
        this.reporter.debug("Self modification[", dep.goal.id, "] =", interface_1.PreBuildResult[itselfModified]);
        switch (itselfModified) {
            case interface_1.PreBuildResult.TIME:
                return dep.progress.revision > against.progress.revision;
            case interface_1.PreBuildResult.YES:
                return true;
            case interface_1.PreBuildResult.NO:
                return false;
        }
    }
    async materialBuildNewTarget(target) {
        const proxy = new BuildContextImpl(target, this);
        let r = undefined;
        try {
            r = await target.progress.start(this, () => target.goal.rule.build(proxy, ...target.matchResult));
        }
        catch (e) {
            this.someTargetWrong = true;
            throw e;
        }
        return r;
    }
    async buildNewTarget(target) {
        if (this.someTargetWrong)
            throw this.cancelled(target.goal.id);
        if ((await this.checkNeedRebuild(target)) !== interface_1.PreBuildResult.YES) {
            if (target.goal.rule.isUser)
                this.reporter.targetSkip(target.goal.id);
            return target.progress.result;
        }
        else {
            return this.materialBuildNewTarget(target);
        }
    }
    buildTarget(target) {
        if (target.progress.status !== interface_1.BuildStatus.NOT_STARTED) {
            return target.progress.finishPromise();
        }
        else {
            return this.buildNewTarget(target);
        }
    }
    async want(...args) {
        this.updateBuildRev();
        let deps = new Set();
        getObjectivesOfDepArgs(this, null, args, deps);
        await PromiseAllCatch([...deps].map((dep) => this.buildTarget(dep)));
        return getResultsOfDepArgs(this, null, args);
    }
    userCancelSync() {
        this.someTargetWrong = true;
        return this.BuildCancelledByUser();
    }
    addRule(rule) {
        this.rules.push(rule);
    }
    invalidateJournal() {
        this.database = new Map();
        this.journal = new Map();
        this.reset();
    }
    reset() {
        this.someTargetWrong = false;
        for (const p of this.journal.values()) {
            p.resetBuildStatus();
        }
    }
    fromJson(json) {
        if (json.journalVersion !== JOURNAL_VERSION)
            return null;
        for (const id in json.entries) {
            const p = this.getProgress(id);
            p.fromJson(json.entries[id]);
        }
        return json;
    }
    toJson() {
        const o = {};
        for (const [id, prog] of this.journal) {
            if (prog.status === interface_1.BuildStatus.FINISHED || prog.status === interface_1.BuildStatus.NOT_STARTED) {
                o[id] = prog.toJson();
            }
        }
        return { journalVersion: JOURNAL_VERSION, entries: o };
    }
    setCapacity(cap) {
        this.lock = new semaphore_async_await_1.default(cap);
    }
    async start(p) {
        if (p.isUser)
            this.reporter.targetStart(p.id);
        if (p.isUser)
            this.reporter.targetHalt(p.id);
        if (this.lock && p.isUser)
            await this.lock.acquire();
        if (this.someTargetWrong)
            throw this.cancelled(p.id);
        if (p.isUser)
            this.reporter.targetUnHalt(p.id);
        await SleepPromise(0);
    }
    async end(p, err) {
        if (this.lock && p.isUser)
            this.lock.release();
        if (p.isUser) {
            if (err) {
                this.someTargetWrong = true;
                this.reporter.targetError(p.id, err);
            }
            else {
                this.reporter.targetEnd(p.id);
            }
        }
        await SleepPromise(0);
    }
    async unhalt(p) {
        if (this.lock && p.isUser)
            await this.lock.acquire();
        if (this.someTargetWrong)
            throw this.cancelled(p.id);
        if (p.isUser)
            this.reporter.targetUnHalt(p.id);
        await SleepPromise(0);
    }
    async halt(p) {
        if (this.lock && p.isUser)
            this.lock.release();
        if (p.isUser)
            this.reporter.targetHalt(p.id);
        await SleepPromise(0);
    }
}
exports.default = Director;
class PreBuildContextImpl {
    constructor(obj, resolver) {
        this.objective = obj;
        this.director = resolver;
    }
    get id() {
        return this.objective.progress.id;
    }
    toString() {
        return this.objective.progress.id;
    }
    get dependencies() {
        return this.objective.progress.dependencies;
    }
    get isVolatile() {
        return this.objective.progress.volatile;
    }
    get lastResult() {
        return this.objective.progress.lastResult;
    }
    async dependencyModified() {
        const depIDs = [...this.objective.progress.dependencies];
        for (const group of depIDs) {
            const deps = [...group];
            let triggered = [];
            try {
                triggered = await PromiseAllCatch(deps.map((dep) => {
                    const g = this.director.queryGoal(false, dep.id, dep.args);
                    if (!g)
                        throw this.director.ruleNotFound(dep.id);
                    return this.director.checkTriggerRebuildByDependency(g, this.objective);
                }));
            }
            catch (e) {
                console.log(e);
                return true;
            }
            for (let j = 0; j < deps.length; j++) {
                if (!triggered[j])
                    continue;
                this.director.reporter.debug("Triggered Update:", this.objective.goal.id, "<==", deps[j].id);
                return true;
            }
        }
        return false;
    }
    async cutoffEarly() {
        if (this.director.reporter) {
            this.director.reporter.debug("Started early cutoff execute:", this.objective.goal.id);
        }
        await this.director.materialBuildNewTarget(this.objective);
        return this.objective.progress.preBuildResult === interface_1.PreBuildResult.YES;
    }
}
exports.PreBuildContextImpl = PreBuildContextImpl;
const runningDependencies = new WeakMap();
function getRD(rdMap, ctor, target, resolver) {
    if (rdMap.has(resolver) && rdMap.get(resolver).has(target)) {
        return rdMap.get(resolver).get(target);
    }
    else {
        const proxy = new ctor();
        if (!rdMap.has(resolver))
            rdMap.set(resolver, new WeakMap());
        rdMap.get(resolver).set(target, proxy);
        return proxy;
    }
}
function resolveObjective(dir, rootObjective, t) {
    if (t === null || t === undefined) {
        return t;
    }
    else if (typeof t === "string") {
        const g = dir.queryGoal(true, t);
        if (!g) {
            if (rootObjective)
                throw dir.dependencyNotFound(t, rootObjective.goal.id);
            else
                throw dir.ruleNotFound(t);
        }
        return g;
    }
    else {
        const g = dir.validateGoal(t);
        if (!g) {
            if (rootObjective)
                throw dir.dependencyNotFound(t.id, rootObjective.goal.id);
            else
                throw dir.ruleNotFound(t.id);
        }
        return g;
    }
}
function getObjectivesOfDepArgs(dir, rootObjective, args, out) {
    for (const arg of args) {
        if (Array.isArray(arg)) {
            getObjectivesOfDepArgs(dir, rootObjective, arg, out);
        }
        else {
            const p = resolveObjective(dir, rootObjective, arg);
            if (p)
                out.add(p);
        }
    }
}
function getResultsOfDepArgs(dir, rootObjective, args) {
    const ans = [];
    for (const arg of args) {
        if (Array.isArray(arg)) {
            ans.push(getResultsOfDepArgs(dir, rootObjective, arg));
        }
        else {
            const p = resolveObjective(dir, rootObjective, arg);
            if (p)
                ans.push(p.progress.result);
            else
                ans.push(p);
        }
    }
    return ans;
}
class ProgressIsImpl {
    constructor(progress, flag, director) {
        this.progress = progress;
        this.flag = flag;
        this.director = director;
    }
    get not() {
        return new ProgressIsImpl(this.progress, !this.flag, this.director);
    }
    volatile() {
        this.progress.volatile = this.flag;
    }
    modified() {
        this.progress.preBuildStatus = interface_1.PreBuildStatus.DECIDED;
        this.progress.preBuildResult = this.flag ? interface_1.PreBuildResult.YES : interface_1.PreBuildResult.NO;
        if (this.flag) {
            this.progress.revision = this.director.buildRev;
        }
    }
}
class BuildContextImpl {
    constructor(objective, director) {
        this.objective = objective;
        this.director = director;
        this.is = new ProgressIsImpl(objective.progress, true, director);
    }
    static clear(resolver) {
        runningDependencies.set(resolver, new WeakMap());
    }
    get id() {
        return this.objective.progress.id;
    }
    get lastResult() {
        return this.objective.progress.lastResult;
    }
    get revision() {
        return this.objective.progress.revision;
    }
    set revision(x) {
        this.objective.progress.revision = x;
    }
    get buildRevision() {
        return this.director.buildRev;
    }
    getFlattenProgresses(args, out) {
        return getObjectivesOfDepArgs(this.director, this.objective, args, out);
    }
    collectAllDeps(t, s) {
        if (s.has(t))
            return;
        s.add(t);
        const rd = getRD(runningDependencies, Set, this.objective, this.director);
        for (const target of rd) {
            this.collectAllDeps(target, s);
        }
    }
    checkCircular(deps) {
        let allDeps = new Set();
        for (const t of deps) {
            this.collectAllDeps(t, allDeps);
            if (allDeps.has(this.objective)) {
                throw new Error(`Circular dependency when building ${this.objective.goal.id}, depending on ${t.goal.id}.`);
            }
        }
    }
    async _ordered(deps) {
        const rd = getRD(runningDependencies, Set, this.objective, this.director);
        for (const d of deps)
            rd.add(d);
        this.checkCircular(deps);
        await this.objective.progress.halt(this.director);
        const dependencyPromises = [...deps].map((t) => this.director.buildTarget(t));
        await PromiseAllCatch(dependencyPromises);
        await this.objective.progress.unhalt(this.director);
    }
    async order(...args) {
        let deps = new Set();
        this.getFlattenProgresses(args, deps);
        await this._ordered(deps);
        return getResultsOfDepArgs(this.director, this.objective, args);
    }
    _needed(deps) {
        if (deps.size) {
            this.objective.progress.dependencies.push([...deps].map((d) => ({ id: d.goal.id, args: d.goal.args })));
        }
    }
    needed(...args) {
        let deps = new Set();
        this.getFlattenProgresses(args, deps);
        this._needed(deps);
    }
    async need(...args) {
        let deps = new Set();
        this.getFlattenProgresses(args, deps);
        await this._ordered(deps);
        this._needed(deps);
        return getResultsOfDepArgs(this.director, this.objective, args);
    }
}
async function PromiseAllCatch($) {
    const rawResults = await Promise.all($.map((prom) => prom
        .then((value) => ({ status: "fulfilled", value: value }))
        .catch((reason) => ({ status: "rejected", reason: reason }))));
    let results = [];
    for (const result of rawResults) {
        if (result.status === "rejected") {
            throw result.reason;
        }
        else {
            results.push(result.value);
        }
    }
    return results;
}
