import React, { useEffect, useRef } from "react";

const CHAR_WIDTH = 10; // canvas pixels
const CHAR_HEIGHT = 12; // canvas pixels

const ROWS = 50;
const COLS = 50;

const TRAIL_COUNT = 50;

const ALPHABET = (() => {
    let alphabet = [];

    // 0 - 9
    for (let charCode = "0".charCodeAt(0); charCode <= "9".charCodeAt(0); ++charCode) {
        alphabet.push(String.fromCharCode(charCode));
    }

    // A - Z
    for (let charCode = "A".charCodeAt(0); charCode <= "Z".charCodeAt(0); ++charCode) {
        alphabet.push(String.fromCharCode(charCode));
    }

    // https://en.wikipedia.org/wiki/Half-width_kana
    for (let codePoint = 0xFF71; codePoint <= 0xFF9D; ++codePoint) {
        alphabet.push(String.fromCodePoint(codePoint));
    }

    return alphabet;
})();

// TODO add more
const MEMES = [
    "ABFAHRT",
    "BALLERN",
    "NO SLEEP",
    "ZOCKEN ALTER",
    "MEGA KRASSE LAN",
    "ｱﾔﾔｱﾔﾔ", // Ayaya Ayaya
    "ｵﾏｴﾊﾓｳｼﾝﾃｲﾙ", // Omae wa mou shindeiru
]

const NORMAL_STYLE = "#020";
const MEME_STYLE = "#050";
const HEAD_STYLE = "#555";

/**
 * Likelihood of a meme to appear after each character.
 * 
 * 0.1 means there is one meme approximately every 10 chars.
 */
const MEME_PROBABILITY = 0.02;

type Trail = {
    /** Column in which the trail falls. Should be between 0 and `COLS` */
    col: number,
    /** Row where the head (lowest character) of the trail is. May be negative or above `ROWS`. */
    head: number,
    /** Number of characters to display above, including the head */
    length: number,

    /**
     * Vertical segments of text to show in the trail body
     * 
     * The sum of each `chars.length` should be at least `ROWS`, but may be greater.
     */
    content: {
        chars: string,
        fillStyle: string, // TODO is there a better type for this?
    }[],
}

function randomTrail(): Trail {
    let content = []
    let current = "";
    let totalLength = 0;

    while (totalLength < ROWS) {
        if (Math.random() < MEME_PROBABILITY) {
            // Commit the current non-meme chars
            if (current.length > 0) {
                content.push({
                    chars: current,
                    fillStyle: NORMAL_STYLE,
                });
                current = "";
            }

            // Pick and add a random meme
            const meme = choice(MEMES);

            // If the meme has spaces, replace them with random characters
            // This also places a random character after the meme, ensuring
            // that two memes don't come immediately after each other
            for (let chunk of meme.split(" ")) {
                content.push({
                    chars: chunk,
                    fillStyle: MEME_STYLE,
                });
                content.push({
                    chars: choice(ALPHABET),
                    fillStyle: NORMAL_STYLE,
                });

            }
            totalLength += meme.length + 1;
        } else {
            // No meme, just add one random character
            current += choice(ALPHABET);
            totalLength += 1;
        }
    }
    if (current.length > 0) {
        content.push({
            chars: current,
            fillStyle: NORMAL_STYLE,
        });
    }

    return {
        col: randRange(0, COLS),
        head: randRange(-1 * COLS, 0), // spawn above the screen initially
        length: randRange(5, 20),

        content: content
    };
}

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

function choice<T>(candidates: T[]): T {
    return candidates[randRange(0, candidates.length - 1)];
}

const MatrixBackground = () => {
    const canvasRef: React.MutableRefObject<HTMLCanvasElement | null> = useRef(null);

    useEffect(() => {
        const canvas = canvasRef.current
        if (canvas === null) return;

        const ctx = canvas.getContext("2d");
        if (ctx === null) throw new Error('getContext("2d") returned null!');

        ctx.font = '10pt monospace';

        const width = canvas.width;
        const height = canvas.height;

        const trails: Trail[] = new Array(TRAIL_COUNT).fill(0).map((_) => randomTrail());

        const matrixEffect = () => {
            ctx.fillStyle = "#000";
            ctx.fillRect(0, 0, width, height);

            // draw all trails
            for (let trail of trails) {
                const x = trail.col * CHAR_WIDTH;
                let idx = 0;

                // TODO this can probably be optimized
                outer:
                for (let item of trail.content) {
                    for (let char of item.chars) {
                        // stop drawing characters after reaching the head
                        if (idx > trail.head) break outer;

                        // skip characters behind the trail end
                        if (idx < trail.head - trail.length) {
                            idx += 1;
                            continue;
                        }

                        // draw the character, overwriting any that are already there
                        const y = idx * CHAR_HEIGHT;
                        ctx.fillStyle = "#000";
                        ctx.fillRect(x, y - CHAR_HEIGHT, CHAR_WIDTH, CHAR_HEIGHT);
                        ctx.fillStyle = (idx === trail.head ? HEAD_STYLE : item.fillStyle);
                        ctx.fillText(char, x, y);

                        idx += 1;
                    }
                }
            }

            // update all trails
            for (let i in trails) {
                trails[i].head += 1;

                if (trails[i].head - trails[i].length > ROWS) {
                    // trail is completely off-screen, generate a new one
                    trails[i] = randomTrail();
                    trails[i].head = 0;
                }
            }
        }

        const interval = setInterval(matrixEffect, 100);
        return () => {
            clearInterval(interval);
        }
    }, [canvasRef]);

    return (
        <canvas
            ref={canvasRef}
            style={{
                background: '#000000',
                position: 'fixed',
                width: '100%',
                height: '100%',
                zIndex: -1,
                top: '0',
                left: '0',
                objectFit: 'cover',
                imageRendering: 'crisp-edges',
            }}
            width={COLS * CHAR_WIDTH}
            height={ROWS * CHAR_HEIGHT}
        />
    )

};

export default MatrixBackground;