import {anyCharOf, newline, rest, uniLetter, whitespace} from "parjs";
import {then, many, manyTill, exactly, between, manySepBy, stringify} from "parjs/combinators";
import fs from "fs";
import {CamelCards} from "./day_seven";

const patternParser = anyCharOf("LR").pipe(manyTill(newline().pipe(exactly(2))));

const nodeNameParser = uniLetter().pipe(exactly(3), stringify());
const childParser = nodeNameParser.pipe(manySepBy(", "), exactly(2), between("(", ")"));
const nodeParser = nodeNameParser.pipe(then(childParser.pipe(between(" = ", whitespace()))))

const parser = patternParser.pipe(then(nodeParser.pipe(manySepBy(whitespace()))));

class Node {
	constructor(
		public label: string,
		public _left?: Node,
		public _right?: Node
	) {
		if (this._left?.label === label) this._left = undefined;
		if (this._right?.label === label) this._right = undefined;
	}

	public set left(node: Node) {
		if (node.label !== this.label) {
			this._left = node;
		}
	}

	public set right(node: Node) {
		if (node.label !== this.label) {
			this._right = node;
		}
	}

	public get left(): Node | undefined {
		return this._left;
	}

	public get right(): Node | undefined {
		return this._right;
	}

	public get isBranch(): boolean {
		return Boolean(this.left || this.right);
	}

	/**
	 * Find a node by label using basic Depth-first search
	 */
	public find(label: string, searchedNodes: string[] = []): Node | undefined {
		if (this.label === label) return this;
		if (searchedNodes.includes(this.label)) return;

		if (this.left) {
			const leftSearch = this.left.find(label, [...searchedNodes, this.label]);
			if (leftSearch) return leftSearch;
		}

		if (this.right) {
			const rightSearch = this.right.find(label, [...searchedNodes, this.label]);
			if (rightSearch) return rightSearch;
		}
	}
}

type Instruction = "L" | "R";

export class DesertMap {
	private readonly pattern: Instruction[];
	private tree?: Node;

	constructor(input: string) {
		const [pattern, nodes] = parser.parse(input).value;

		this.pattern = pattern as Instruction[];

		for (const [name, [[leftNode, rightNode]]] of nodes) {
			if (!this.tree) {
				this.tree = new Node(name);
			}

			const parent = this.tree.find(name);

			if (!parent) {
				console.log(`No parent found for label ${name}`)
			} else {

				const foundLeft = this.tree.find(leftNode);
				parent.left = foundLeft || new Node(leftNode);

				const foundRight = this.tree.find(rightNode);
				parent.right = foundRight || new Node(rightNode);
			}
		}
	}

	public stepsTo(node: string): number {
		if (!this.tree) return 0;

		let step = 0;
		let curr = this.tree;

		while (curr.label !== node) {
			const instruction = this.pattern[step % this.pattern.length];

			if (instruction === "L" && curr.left) {
				curr = curr.left;
			} else if (instruction === "R" && curr.right) {
				curr = curr.right;
			}

			if (!curr) return 0;

			step++;
		}
		return step;
	}
}

export const runDayEight = () => {
	const input = fs.readFileSync('./inputs/day_eight_input.txt', 'utf-8').trimEnd();

	const map = new DesertMap(input);
	console.log(map.stepsTo('ZZZ'));
}