const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')!

interface Grid<T> extends Iterable<[number, number, T]> {
    has(point: [number, number]): boolean
    get(point: [number, number]): T
    set(point: [number, number], value: T): void
}

function createGrid<T>(): Grid<T> {
    const cells: Record<string, T> = {}

    return {
        has([x, y]) {
            return cells[`${x},${y}`] !== undefined
        },
        get([x, y]) {
            return cells[`${x},${y}`]
        },
        set([x, y], value) {
            cells[`${x},${y}`] = value
        },
        *[Symbol.iterator]() {
            for (const [coord, value] of Object.entries(cells)) {
                const [x, y] = coord.split(',').map(s => parseInt(s))
                yield [x, y, value]
            }
        },
    }
}

type WireCell = 'down' | 'down-left' | 'down-right' | 'dot'

type WireDirection = 'down' | 'down-left' | 'down-right'

type WireSteps = { position: Point2; direction: WireCell }[]

type Point2 = [number, number]

type State = {
    grid: Grid<WireCell>
    queuedWire: {
        index: number
        steps: WireSteps
    } | null
    badTries: 0
}

type Renderer = {
    timer: number
}

let renderer: Renderer | null = null

const RENDERER_FPS = 30

function setup() {
    console.log('Setting up circuits art...')

    $canvas.width = $canvas.clientWidth
    $canvas.height = $canvas.clientHeight

    const g = $canvas.getContext('2d')!
    const state: State = {
        grid: createGrid(),
        queuedWire: null,
        badTries: 0,
    }

    const startTime = new Date()

    const handle = window.setInterval(() => {
        const time = new Date().getTime() - startTime.getTime()

        update(state, g.canvas.width, g.canvas.height, time)
        render(g, state, time)
    }, 1000 / RENDERER_FPS)

    renderer = { timer: handle }
}

function update(state: State, width: number, height: number, time: number) {
    const w = (width / CELL_SIZE) | 0
    // const h = (height / CELL_SIZE) | 0

    if (state.badTries > 1000) {
        console.log('finished')
        clearInterval(renderer!.timer)
    }

    if (!state.queuedWire) {
        const sx = randomInt(0, w)
        if (!state.grid.has([sx, 0])) {
            const steps = generateWire(state.grid, [sx, 0])
            if (steps.length < 7) {
                state.badTries++
                return
            }

            state.queuedWire = {
                index: 0,
                steps,
            }

            state.grid.set(state.queuedWire.steps[0].position, 'dot')
            return
        }

        state.badTries++
    } else {
        const wire = state.queuedWire

        const step = wire.steps[wire.index]
        state.grid.set(step.position, step.direction)

        if (wire.index + 1 < wire.steps.length) {
            state.grid.set(wire.steps[wire.index + 1].position, 'dot')
        }

        wire.index++

        if (wire.index >= wire.steps.length) {
            state.queuedWire = null
        }
    }
}

const CELL_SIZE = 27

const BACKGROUND_COLOR = '#ecffe3'
const WIRE_COLOR = '#a6ce94'

const RENDER_CELL: Record<WireCell, (g: CanvasRenderingContext2D) => void> = {
    'down': g => {
        g.strokeStyle = WIRE_COLOR
        g.beginPath()
        g.moveTo(0, 0)
        g.lineTo(0, 1)
        g.stroke()
    },
    'down-left': g => {
        g.strokeStyle = WIRE_COLOR
        g.beginPath()
        g.moveTo(0, 0)
        g.lineTo(-1, 1)
        g.stroke()
    },
    'down-right': g => {
        g.strokeStyle = WIRE_COLOR
        g.beginPath()
        g.moveTo(0, 0)
        g.lineTo(+1, 1)
        g.stroke()
    },
    'dot': g => {
        g.fillStyle = BACKGROUND_COLOR
        g.beginPath()
        g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
        g.fill()

        g.strokeStyle = WIRE_COLOR
        g.beginPath()
        g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
        g.stroke()
    },
}

function render(g: CanvasRenderingContext2D, state: State, time: number) {
    g.clearRect(0, 0, g.canvas.width, g.canvas.height)
    g.resetTransform()
    g.scale(CELL_SIZE, CELL_SIZE)
    g.lineWidth = 3 / CELL_SIZE

    const w = (g.canvas.width / CELL_SIZE) | 0
    const h = (g.canvas.height / CELL_SIZE) | 0
    for (let y = 0; y <= h + 1; y++) {
        for (let x = 0; x <= w + 1; x++) {
            if (!state.grid.has([x, y])) continue

            const cell = state.grid.get([x, y])

            g.save()
            g.translate(x, y)

            // g.fillStyle = '#f008'
            // g.beginPath()
            // g.rect(-0.25, -0.25, 0.5, 0.5)
            // g.fill()

            RENDER_CELL[cell](g)
            g.restore()
        }
    }

    // if (state.queuedWire) {
    //     for (const step of state.queuedWire.steps) {
    //         const [x, y] = step.position

    //         g.fillStyle = '#00f8'
    //         g.save()
    //         g.translate(x, y)

    //         g.beginPath()
    //         g.rect(-0.25, -0.25, 0.5, 0.5)
    //         g.fill()
    //         g.restore()
    //     }
    // }

    // const [mx, my] = state.mouse
    // g.save()
    // g.fillStyle = '#0008'
    // g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE))
    // g.beginPath()
    // g.rect(0, 0, 1, 1)
    // g.fill()
    // g.restore()
}

setup()

window.addEventListener('resize', () => {
    if (renderer) {
        clearInterval(renderer.timer)
    }
    setup()
})

function randomInt(from: number, to: number) {
    return Math.floor(Math.random() * (to - from + 1))
}

function randomChoice<T>(choices: T[]): T {
    return choices[randomInt(0, choices.length - 1)]
}

function randomWeightedChoice<T>(choices: [T, number][]) {
    return
}

// 3 + 4 + 5 + 6
randomWeightedChoice([
    ['a', 3],
    ['b', 4],
    ['c', 5],
    ['d', 6],
])

const DIR_TO_VEC: Record<WireDirection, Point2> = {
    ['down']: [0, 1],
    ['down-left']: [-1, 1],
    ['down-right']: [+1, 1],
}

type ShortCircuitBoolean = boolean | (() => boolean)

const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v())
const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b)

/**
 * Tells whether a given direction is not blocked by some other cell in a given grid and starting position
 */
const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<WireCell>) => boolean> = {
    ['down']: ([x, y], grid) =>
        !grid.has([x, y + 1]) &&
        implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') &&
        implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'),
    ['down-left']: ([x, y], grid) =>
        !grid.has([x - 1, y + 1]) &&
        implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
    ['down-right']: ([x, y], grid) =>
        !grid.has([x + 1, y + 1]) &&
        implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
}

function pruneDirections(
    grid: Grid<WireCell>,
    position: Point2,
    directions: WireDirection[],
): WireDirection[] {
    return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
}

function generateWire(
    grid: Grid<WireCell>,
    startingPoint: Point2,
): { position: Point2; direction: WireCell }[] {
    const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30
    let currentPosition = startingPoint
    let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right'])

    const steps: { position: Point2; direction: WireCell }[] = []

    for (let i = 0; i < segmentLength; i++) {
        const availableDirections = pruneDirections(grid, currentPosition, [
            'down',
            'down-left',
            'down-right',
        ])
        if (availableDirections.length === 0) {
            break
        } else {
            const dir =
                availableDirections.includes(currentDirection) && Math.random() < 0.25
                    ? currentDirection
                    : randomChoice(availableDirections)

            if (
                (currentDirection === 'down-left' && dir === 'down-right') ||
                (currentDirection === 'down-right' && dir === 'down-left')
            ) {
                break
            }

            const [x, y] = currentPosition
            const [dx, dy] = DIR_TO_VEC[dir]

            steps.push({
                position: [x, y],
                direction: dir,
            })

            currentPosition = [x + dx, y + dy]
            currentDirection = dir
        }
    }

    const last = steps.at(-1)
    if (last) {
        last.direction = 'dot'
    }

    return steps
}
