import { error } from '../utils/error'
import { Graph, Node } from './graph'

export enum TaskState {
	Pending,
	Running,
	Done,
	Failed,
}

export interface Task {
	node: Node
	state: TaskState
}

export class Tasks {
	private readonly tasks: Map<string, Task>
	private readonly promises = new Set<Promise<void>>()

	constructor(graph: Graph) {
		this.tasks = new Map(
			graph.nodes.map((node) => [
				node.step.id,
				{ node, state: TaskState.Pending },
			]),
		)
	}

	public wait() {
		return Promise.race(this.promises)
	}

	public run(run: () => Promise<void>) {
		const promise = Promise.resolve()
			.then(run)
			.finally(() => this.promises.delete(promise))

		this.promises.add(promise)
	}

	public done() {
		return (
			!this.promises.size &&
			!Array.from(this.tasks.values()).some((task) => {
				const state = this.state(task)

				return state === TaskState.Pending || state === TaskState.Running
			})
		)
	}

	public state(task?: Task): TaskState {
		if (task) {
			if (task.state === TaskState.Running || task.state === TaskState.Failed) {
				return task.state
			}

			for (const dep of task.node.parents) {
				const depTask = this.get(dep)
				const depState = this.state(depTask)

				if (depState === TaskState.Pending || depState === TaskState.Failed) {
					return depState
				}
			}

			return task.state
		} else {
			for (const task of this.tasks.values()) {
				if (
					task.state === TaskState.Failed ||
					task.state === TaskState.Running
				) {
					return task.state
				}
			}

			for (const task of this.tasks.values()) {
				if (task.state === TaskState.Pending) {
					return task.state
				}
			}

			return TaskState.Done
		}
	}

	public get(node: Node) {
		return (
			this.tasks.get(node.step.id) ?? error(`Step "${node.step.id}" not foun`)
		)
	}

	public ready(task: Task) {
		if (task.state !== TaskState.Pending) {
			return false
		}

		return task.node.parents.every(
			(parent) => this.get(parent).state === TaskState.Done,
		)
	}

	public queue(graph: Graph) {
		return graph.nodes
			.map((node) => this.get(node))
			.filter((task) => this.ready(task))
	}
}
