import { WorldState } from "./GOAP.js";
export class HTNTask {
    name;
    type;
    preconditions;
    effects;
    methods;
    executeCallback;
    constructor(options) {
        if (options.name.trim().length === 0)
            throw new Error("HTN task name cannot be empty.");
        this.name = options.name;
        this.type = options.type;
        this.preconditions = options.preconditions ?? {};
        this.effects = options.effects ?? {};
        this.methods = [...(options.methods ?? [])].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.name.localeCompare(right.name));
        this.executeCallback = options.execute;
    }
    static primitive(options) {
        return new HTNTask({ ...options, type: "primitive" });
    }
    static compound(options) {
        return new HTNTask({ ...options, type: "compound" });
    }
    getMethods() {
        return this.methods;
    }
    canRun(state) {
        return state.matches(this.preconditions);
    }
    applyEffects(state) {
        return state.apply(this.effects);
    }
    execute(state, context) {
        if (this.type !== "primitive")
            return "failure";
        return this.executeCallback?.(state, context) ?? "success";
    }
}
export class HTNPlanner {
    maxDepth;
    maxIterations;
    constructor(options = {}) {
        this.maxDepth = options.maxDepth ?? 12;
        this.maxIterations = options.maxIterations ?? 128;
        if (!Number.isInteger(this.maxDepth) || this.maxDepth < 1)
            throw new RangeError("HTN maxDepth must be a positive integer.");
        if (!Number.isInteger(this.maxIterations) || this.maxIterations < 1)
            throw new RangeError("HTN maxIterations must be a positive integer.");
    }
    plan(rootTask, initialState, context = {}) {
        const stats = {
            decompositions: 0,
            iterations: 0,
            maxDepthReached: 0,
            methodTrace: []
        };
        const result = this.decompose([rootTask], initialState.clone(), context, 0, stats);
        return {
            valid: result.valid,
            rootTask: rootTask.name,
            tasks: result.tasks.map((task) => task.name),
            methodTrace: stats.methodTrace,
            decompositions: stats.decompositions,
            maxDepthReached: stats.maxDepthReached,
            iterations: stats.iterations,
            finalState: result.state.toObject()
        };
    }
    isPlanValid(plan, rootTask, initialState, context = {}) {
        if (!plan.valid)
            return false;
        const nextPlan = this.plan(rootTask, initialState, context);
        return nextPlan.valid && nextPlan.tasks.join(">") === plan.tasks.join(">");
    }
    decompose(tasks, state, context, depth, stats) {
        stats.maxDepthReached = Math.max(stats.maxDepthReached, depth);
        if (depth > this.maxDepth || stats.iterations >= this.maxIterations) {
            return { valid: false, tasks: [], state };
        }
        if (tasks.length === 0) {
            return { valid: true, tasks: [], state };
        }
        stats.iterations += 1;
        const [currentTask, ...remainingTasks] = tasks;
        if (!currentTask)
            return { valid: true, tasks: [], state };
        if (currentTask.type === "primitive") {
            if (!currentTask.canRun(state))
                return { valid: false, tasks: [], state };
            const executed = currentTask.execute(state, context);
            if (executed === "failure")
                return { valid: false, tasks: [], state };
            const nextState = currentTask.applyEffects(state);
            const remainder = this.decompose(remainingTasks, nextState, context, depth, stats);
            return remainder.valid
                ? { valid: true, tasks: [currentTask, ...remainder.tasks], state: remainder.state }
                : { valid: false, tasks: [], state };
        }
        stats.decompositions += 1;
        for (const method of currentTask.getMethods()) {
            const preconditionsMet = method.check ? method.check(state, context) : state.matches(method.preconditions ?? {});
            if (!preconditionsMet)
                continue;
            const traceLength = stats.methodTrace.length;
            stats.methodTrace.push(`${currentTask.name}:${method.name}`);
            const result = this.decompose([...method.subtasks, ...remainingTasks], state, context, depth + 1, stats);
            if (result.valid)
                return result;
            stats.methodTrace.length = traceLength;
        }
        return { valid: false, tasks: [], state };
    }
}
//# sourceMappingURL=HTN.js.map