type PartInfo = {
    partNumber: number;
    begin: number;
    end: number;
    y: number;
}

function nextPartNumber(line: string, pos: number, y: number): PartInfo | null {
    const numbers: string[] = [];
    // eat characters until a number is found
    while (pos < line.length) {
        const ch = line[pos];
        if (/\d/.test(ch)) {
            break;
        }
        pos++;
    }
    // collect numbers
    while (pos < line.length) {
        const ch = line[pos];
        if (!/\d/.test(ch)) {
            break;
        }
        numbers.push(ch);
        pos++;
    }
    if (numbers.length === 0) {
        return null;
    }
    const partNumber = parseInt(numbers.join(""))
    return {
        partNumber,
        begin: pos - numbers.length,
        end: pos - 1, // inclusive
        y,
    }
}

function* queryNeighbors(x: number, y: number, boxWidth: number, width: number, height: number) {
    boxWidth = Math.min(boxWidth, width - x);
    if (x > 0) {
        if (y > 0) {
            yield [x - 1, y - 1];
        }
        yield [x - 1, y];
        if (y < height - 1) {
            yield [x - 1, y + 1];
        }
    }
    if (x + boxWidth < width) {
        if (y > 0) {
            yield [x + boxWidth, y - 1];
        }
        yield [x + boxWidth, y];
        if (y < height - 1) {
            yield [x + boxWidth, y + 1];
        }
    }
    if (y > 0) {
        for (let i = x; i < x + boxWidth; i++) {
            yield [i, y - 1];
        }
    }
    if (y < height - 1) {
        for (let i = x; i < x + boxWidth; i++) {
            yield [i, y + 1];
        }
    }
}

function checkValidPartNumber(lines: string[], info: PartInfo) {
    const neighbors = queryNeighbors(info.begin, info.y, info.end - info.begin + 1, lines[0].length, lines.length);
    for (const [x, y] of neighbors) {
        const line = lines[y];
        const ch = line[x];
        if ((!/\d/.test(ch)) && ch !== ".") {
            return true;
        }
    }
    return false;
}

function* iterPartNumbers(lines: string[]) {
    let pos = 0;
    let y = 0;
    while (y < lines.length) {
        const line = lines[y];
        while (pos < line.length) {
            const part = nextPartNumber(line, pos, y);
            if (!part) {
                break;
            }
            pos = part.end + 1;
            // check valid part number
            if (!checkValidPartNumber(lines, part)) {
                continue;
            }
            yield part;
        }
        pos = 0;
        y++;
    }
}

// const example = `467..114..
// ...*......
// ..35..633.
// ......#...
// 617*......
// .....+.58.
// ..592.....
// ......755.
// ...$.*....
// .664.598..`;
const example = await Deno.readTextFile("input.txt");
const lines = example.split("\n").map(x => x.trim()).filter(x => x.length > 0);
const width = lines[0].length;
const height = lines.length;

console.log(width, height);

type GearCandidate = {
    x: number;
    y: number;
    id: number;
    neighbors: PartInfo[];
}

const gearCandidates: GearCandidate[] = [];
const coordinateToGearIndex = new Map<string, number>();
for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    for (let j = 0; j < line.length; j++) {
        if (line[j] === "*") {
            const index = gearCandidates.length;
            coordinateToGearIndex.set(`${j},${i}`, index);
            gearCandidates.push({
                x: j,
                y: i,
                id: index,
                neighbors: [],
            });
        }
    }
}

for (const part of iterPartNumbers(lines)) {
    const { begin, end, y } = part;
    const neighbors = queryNeighbors(begin, y, end - begin + 1, width, height);
    for (const [x, y] of neighbors) {
        const id = coordinateToGearIndex.get(`${x},${y}`);
        if (id !== undefined) {
            gearCandidates[id].neighbors.push(part);
        }
    }
}

console.log(gearCandidates);
console.log(gearCandidates.filter(c => c.neighbors.length == 2).map(c=> c.neighbors[0].partNumber * c.neighbors[1].partNumber).reduce((a, b) => a + b, 0));
// console.log([...queryNeighbors(7, 1, 3, width, height)]
// .map(n => lines[n[1]][n[0]])
// )