export function* shadowCast(
    x: number,
    y: number,
    r: number,
    breaker: (x: number, y: number) => boolean
): Generator<{ x: number; y: number }> {
    if (r < 0) return;

    const radiusSq = r * r;
    const seen = new Set<string>();
    const keyOf = (px: number, py: number) => `${px},${py}`;

    seen.add(keyOf(x, y));
    yield { x, y };
    if (breaker(x, y)) return;

    type Frame = {
        octant: number;
        row: number;
        start: number;
        end: number;
    };

    // 8 octants around the origin.
    const OCTANTS: ReadonlyArray<readonly [number, number, number, number]> = [
        [1, 0, 0, 1],
        [0, 1, 1, 0],
        [0, 1, -1, 0],
        [1, 0, 0, -1],
        [-1, 0, 0, -1],
        [0, -1, -1, 0],
        [0, -1, 1, 0],
        [-1, 0, 0, 1],
    ];

    const stack: Frame[] = [];
    for (let octant = 7; octant >= 0; octant--) {
        stack.push({ octant, row: 1, start: 1.0, end: 0.0 });
    }

    while (stack.length > 0) {
        const frame = stack.pop()!;
        if (frame.start < frame.end) continue;

        const [xx, xy, yx, yy] = OCTANTS[frame.octant];

        let start = frame.start;
        let blocked = false;
        let newStart = start;

        for (let row = frame.row; row <= r; row++) {
            let dx = -row - 1;
            const dy = -row;

            while (dx <= 0) {
                dx++;

                const mapX = x + dx * xx + dy * xy;
                const mapY = y + dx * yx + dy * yy;

                const leftSlope = (dx - 0.5) / (dy + 0.5);
                const rightSlope = (dx + 0.5) / (dy - 0.5);

                if (start < rightSlope) continue;
                if (frame.end > leftSlope) break;

                const opaque = breaker(mapX, mapY);

                if (dx * dx + dy * dy <= radiusSq) {
                    const key = keyOf(mapX, mapY);
                    if (!seen.has(key)) {
                        seen.add(key);
                        yield { x: mapX, y: mapY };
                    }
                }

                if (blocked) {
                    if (opaque) {
                        newStart = rightSlope;
                        continue;
                    }

                    blocked = false;
                    start = newStart;
                } else if (opaque && row < r) {
                    blocked = true;
                    stack.push({
                        octant: frame.octant,
                        row: row + 1,
                        start,
                        end: leftSlope,
                    });
                    newStart = rightSlope;
                }
            }

            if (blocked) break;
        }
    }
}
