import '@common/assets/fonts/vga.font.css';
import { randInt } from "@common/utils";
import { createCanvas } from './canvas';

export type IColorLike = string | number | Color;
export type IChar = [string, IColorLike?, IColorLike?] | string;
export type IDefinedChar = [string, IColorLike, IColorLike];

export const randChar = (min = ' ', max = '~') =>
    String.fromCharCode(randInt(
        min.charCodeAt(0),
        max.charCodeAt(0) + 1,
    ));

const generateColors = () => {
    const colors: string[] = [];
    for (let i = 0; i < 16; i++) {
        const h = (i & 0b1000) ? 'ff' : '7f';
        
        const r = (i & 0b0100) ? h : '00';
        const g = (i & 0b0010) ? h : '00';
        const b = (i & 0b0001) ? h : '00';

        const color = `#${r}${g}${b}`;
        colors.push(color);
    }

    return colors;
};

const COLORS = generateColors();

export const GAME_WIDTH = 80;
export const GAME_HEIGHT = 25;

export enum Color {
    BLACK = 0b0000,
    DARK_BLUE = 0b0001,
    DARK_CYAN = 0b0011,
    DARK_GREEN = 0b0010,
    DARK_YELLOW = 0b0110,
    DARK_RED = 0b0100,
    DARK_MAGENTA = 0b0101,
    GRAY = 0b0111,
    BLUE = 0b1001,
    CYAN = 0b1011,
    GREEN = 0b1010,
    YELLOW = 0b1110,
    RED = 0b1100,
    MAGENTA = 0b1101,
    WHITE = 0b1111,
}

export const DEFAULT_FG = Color.WHITE;
export const DEFAULT_BG = Color.BLACK;

export enum Direction {
    NORTH = 0,
    EAST = 1,
    SOUTH = 2,
    WEST = 3,
    UP = 4,
    DOWN = 5,
}

export const isVertical = (char: string) => char === '│';
export const isHorizontal = (char: string) => char === '─';
export const isCorner = (char: string) => '┌┐└┘'.includes(char);

interface IBoxOptions {
    vertical?: string;
    horizontal?: string;
    topLeft?: string;
    topRight?: string;
    bottomLeft?: string;
    bottomRight?: string;
    fill?: IChar;
    fg?: IColorLike;
    bg?: IColorLike;
    title?: string;
}

const CHAR_W = 8;
const CHAR_H = 16;
const NATIVE_FONT = `${CHAR_H}px "IBM VGA 8x16"`;
const FALLBACK_FONT = `${CHAR_H}px monospace`;
const REGION_DATA = Symbol('TextRegion.data');

const colorToCSS = (c: IColorLike): string =>
    typeof c === 'number' ? COLORS[c] : c as string;

const parseChar = (char: IChar): IDefinedChar => (
    typeof char === 'string'
        ? [char, DEFAULT_FG, DEFAULT_BG]
        : [
            char[0],
            char[1] ?? DEFAULT_FG,
            char[2] ?? DEFAULT_BG,
        ]
);

export class TextDisplay {
    private chars: string[];
    private fgs: IColorLike[];
    private bgs: IColorLike[];
    private canvas: HTMLCanvasElement;
    private ctx: CanvasRenderingContext2D;
    private font = FALLBACK_FONT;
    private letterboxColor: string;

    constructor(
        public width = GAME_WIDTH,
        public height = GAME_HEIGHT,
        parent?: HTMLCanvasElement,
        letterboxColor: IColorLike = DEFAULT_BG,
    ) {
        this.letterboxColor = colorToCSS(letterboxColor);
        const canvas = parent ?? createCanvas(width * CHAR_W, height * CHAR_H);
        canvas.width = width * CHAR_W;
        canvas.height = height * CHAR_H;
        canvas.style.imageRendering = 'pixelated';
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d')!;
        this.ctx.textBaseline = 'top';

        const size = width * height;
        this.chars = new Array(size).fill(' ');
        this.fgs = new Array(size).fill(DEFAULT_FG);
        this.bgs = new Array(size).fill(DEFAULT_BG);

        window.addEventListener('resize', () => this.updateScale());
        this.updateScale();

        Promise.race([
            document.fonts.load(NATIVE_FONT).then(() => true).catch(() => false),
            new Promise<boolean>(resolve => setTimeout(() => resolve(false), 1000)),
        ]).then(loaded => {
            this.font = loaded ? NATIVE_FONT : FALLBACK_FONT;
            this.redraw();
        });
    }

    private updateScale() {
        const scale = Math.max(1, Math.min(
            Math.floor(window.innerWidth / (this.width * CHAR_W)),
            Math.floor(window.innerHeight / (this.height * CHAR_H)),
        ));
        this.canvas.style.width = `${this.width * CHAR_W * scale}px`;
        this.canvas.style.height = `${this.height * CHAR_H * scale}px`;
        document.body.style.background = this.letterboxColor;
    }

    private redraw() {
        for (let y = 0; y < this.height; y++) {
            for (let x = 0; x < this.width; x++) {
                this.drawCell(x, y);
            }
        }
    }

    private drawCell(x: number, y: number) {
        const i = y * this.width + x;
        const px = x * CHAR_W;
        const py = y * CHAR_H;

        this.ctx.fillStyle = colorToCSS(this.bgs[i]);
        this.ctx.fillRect(px, py, CHAR_W, CHAR_H);

        const char = this.chars[i];
        if (char !== ' ') {
            this.ctx.save();
            this.ctx.beginPath();
            this.ctx.rect(px, py, CHAR_W, CHAR_H);
            this.ctx.clip();
            this.ctx.font = this.font;
            this.ctx.fillStyle = colorToCSS(this.fgs[i]);
            this.ctx.fillText(char, px, py);
            this.ctx.restore();
        }
    }

    private setCharRaw(x: number, y: number, char: string, fg: IColorLike, bg: IColorLike) {
        if (x < 0 || y < 0 || y >= this.height || x >= this.width || !char) return;
        const i = (y | 0) * this.width + (x | 0);
        let dirty = false;
        if (this.chars[i] !== char) {
            this.chars[i] = char;
            dirty = true;
        }
        if (this.fgs[i] !== fg) {
            this.fgs[i] = fg;
            dirty = true;
        }
        if (this.bgs[i] !== bg) {
            this.bgs[i] = bg;
            dirty = true;
        }
        if (dirty) {
            this.drawCell(x | 0, y | 0);
        }
    }

    setChar(x: number, y: number, char: IChar = '█') {
        this.setCharRaw(x | 0, y | 0, ...parseChar(char));
    }

    getChar(x: number, y: number): IDefinedChar {
        x = x | 0; y = y | 0;
        if (x < 0 || y < 0 || y >= this.height || x >= this.width) {
            return [' ', DEFAULT_FG, DEFAULT_BG];
        }
        return [this.chars[y * this.width + x], this.fgs[y * this.width + x], this.bgs[y * this.width + x]];
    }

    setRegion(x: number, y: number, region: TextRegion) {
        x = x | 0;
        y = y | 0;
        const { chars, fgs, bgs } = region[REGION_DATA];
        const rw = region.width;
        const x0 = Math.max(0, x);
        const y0 = Math.max(0, y);
        const x1 = Math.min(this.width, x + rw);
        const y1 = Math.min(this.height, y + region.height);
        const copyW = x1 - x0;
        if (copyW <= 0) return;

        const regColOffset = x0 - x;
        for (let row = y0; row < y1; row++) {
            const regRow = row - y;
            const regBase = regRow * rw + regColOffset;
            const dispBase = row * this.width + x0;
            const regRowStr = chars[regRow];
            for (let i = 0; i < copyW; i++) {
                this.chars[dispBase + i] = regRowStr[regColOffset + i];
                this.fgs[dispBase + i] = fgs[regBase + i];
                this.bgs[dispBase + i] = bgs[regBase + i];
                this.drawCell(x0 + i, row);
            }
        }
    }

    getRegion(x: number, y: number, w: number, h: number): TextRegion {
        x = x | 0; y = y | 0; w = w | 0; h = h | 0;
        const rows: string[] = [];
        const fgs: IColorLike[][] = [];
        const bgs: IColorLike[][] = [];

        for (let row = 0; row < h; row++) {
            const dispRow = y + row;
            let rowStr = '';
            const fgRow: IColorLike[] = [];
            const bgRow: IColorLike[] = [];
            for (let col = 0; col < w; col++) {
                const dispCol = x + col;
                if (dispRow < 0 || dispRow >= this.height || dispCol < 0 || dispCol >= this.width) {
                    rowStr += ' ';
                    fgRow.push(DEFAULT_FG);
                    bgRow.push(DEFAULT_BG);
                } else {
                    const i = dispRow * this.width + dispCol;
                    rowStr += this.chars[i];
                    fgRow.push(this.fgs[i]);
                    bgRow.push(this.bgs[i]);
                }
            }
            rows.push(rowStr);
            fgs.push(fgRow);
            bgs.push(bgRow);
        }

        return new TextRegion(rows, fgs, bgs);
    }

    drawText(x: number, y: number, text: string, fg: IColorLike = DEFAULT_FG, bg: IColorLike = DEFAULT_BG) {
        x = x | 0; y = y | 0;
        const lines = text.split('\n');
        for (let row = 0; row < lines.length; row++) {
            const line = lines[row];
            const ry = y + row;
            if (ry < 0 || ry >= this.height) continue;
            for (let col = 0; col < line.length; col++) {
                this.setCharRaw(x + col, ry, line[col], fg, bg);
            }
        }
    }

    drawVLine(x: number, y1: number, y2: number, char: IChar = '│') {
        x = x | 0; y1 = y1 | 0; y2 = y2 | 0;
        if (y2 < y1) { const t = y2; y2 = y1; y1 = t; }

        y1 = Math.max(0, y1);
        y2 = Math.min(this.height - 1, y2);
        if (x < 0 || x >= this.width) return;

        const ch = parseChar(char);
        for (let y = y1; y <= y2; y++) {
            this.setCharRaw(x, y, ...ch);
        }
    }

    drawHLine(x1: number, x2: number, y: number, char: IChar = '─') {
        x1 = x1 | 0; x2 = x2 | 0; y = y | 0;
        if (x2 < x1) { const t = x2; x2 = x1; x1 = t; }
        x1 = Math.max(0, x1);
        x2 = Math.min(this.width - 1, x2);
        if (y < 0 || y >= this.height) return;

        const ch = parseChar(char);
        for (let x = x1; x <= x2; x++) {
            this.setCharRaw(x, y, ...ch);
        }
    }

    drawBox(x: number, y: number, width: number, height: number, options: IBoxOptions = {}) {
        x = x | 0; y = y | 0;
        const {
            vertical = '│',
            horizontal = '─',
            topLeft = '┌',
            topRight = '┐',
            bottomLeft = '└',
            bottomRight = '┘',
            fg = DEFAULT_FG,
            bg = DEFAULT_BG,
            fill,
            title,
        } = options;

        this.setCharRaw(x, y, topLeft, fg, bg);
        this.setCharRaw(x + width + 1, y, topRight, fg, bg);
        this.setCharRaw(x, y + height + 1, bottomLeft, fg, bg);
        this.setCharRaw(x + width + 1, y + height + 1, bottomRight, fg, bg);

        if (fill) {
            this.fillBox(x + 1, y + 1, width, height, fill);
        }

        this.drawHLine(x + 1, x + width, y, [horizontal, fg, bg]);
        this.drawHLine(x + 1, x + width, y + height + 1, [horizontal, fg, bg]);

        this.drawVLine(x, y + 1, y + height, [vertical, fg, bg]);
        this.drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]);

        if (title) {
            this.drawText(x + 1, y, title, fg, bg);
        }
    }

    drawTextInBox(x: number, y: number, text: string, options: IBoxOptions = {}) {
        x = x | 0; y = y | 0;
        const {
            fg = DEFAULT_FG,
            bg = DEFAULT_BG,
        } = options;
        let width = 0;
        const lines = text.split('\n');
        const height = lines.length;

        for (const line of lines) {
            if (line.length > width) width = line.length;
        }

        this.drawBox(x, y, width, height, { ...options, fill: [' '] });
        this.drawText(x + 1, y + 1, text, fg, bg);
    }

    fillBox(x: number, y: number, width: number, height: number, char: IChar = '█') {
        x = x | 0; y = y | 0;
        for (let i = y; i < y + height; i++) {
            this.drawHLine(x, x + width - 1, i, char);
        }
    }
}

export class TextRegion {
    readonly #chars: string[];  // one string per row, all same length
    readonly #fgs: IColorLike[];
    readonly #bgs: IColorLike[];

    get width(): number { return this.#chars[0]?.length ?? 0; }
    get height(): number { return this.#chars.length; }

    constructor(chars: IChar[][]);
    constructor(chars: string, fg?: IColorLike | IColorLike[], bg?: IColorLike | IColorLike[]);
    constructor(chars: string[], fg?: IColorLike | IColorLike[][], bg?: IColorLike | IColorLike[][]);

    constructor(
        chars: IChar[][] | string | string[],
        fg?: IColorLike | IColorLike[] | IColorLike[][],
        bg?: IColorLike | IColorLike[] | IColorLike[][],
    ) {
        if (typeof chars === 'string') {
            chars = chars.split('\n');
        }
        if (chars.length === 0 || typeof chars[0] === 'string') {
            const rows = chars as string[];
            const w = rows.reduce((m, r) => Math.max(m, r.length), 0);
            const h = rows.length;
            this.#chars = rows.map(r => r.padEnd(w, ' '));
            this.#fgs = Array(w * h).fill(DEFAULT_FG);
            this.#bgs = Array(w * h).fill(DEFAULT_BG);
            if (fg != null && !Array.isArray(fg)) this.#fgs.fill(fg);
            if (bg != null && !Array.isArray(bg)) this.#bgs.fill(bg);
            if (Array.isArray(fg) || Array.isArray(bg)) {
                for (let y = 0; y < h; y++) {
                    for (let x = 0; x < w; x++) {
                        const i = y * w + x;
                        if (Array.isArray(fg)) this.#fgs[i] = (fg as IColorLike[][])[y]?.[x] ?? DEFAULT_FG;
                        if (Array.isArray(bg)) this.#bgs[i] = (bg as IColorLike[][])[y]?.[x] ?? DEFAULT_BG;
                    }
                }
            }
        } else {
            const ichars = chars as IChar[][];
            const h = ichars.length;
            const w = ichars.reduce((m, r) => Math.max(m, r.length), 0);
            this.#chars = ichars.map(row => row.map(ch => ch[0]).join('').padEnd(w, ' '));
            this.#fgs = Array(w * h).fill(DEFAULT_FG);
            this.#bgs = Array(w * h).fill(DEFAULT_BG);
            for (let y = 0; y < h; y++) {
                for (let x = 0; x < ichars[y].length; x++) {
                    const ch = ichars[y][x];
                    const i = y * w + x;
                    if (ch[1] != null) this.#fgs[i] = ch[1];
                    if (ch[2] != null) this.#bgs[i] = ch[2];
                }
            }
        }
    }

    get [REGION_DATA]() {
        return {
            chars: this.#chars,
            fgs: this.#fgs,
            bgs: this.#bgs,
        };
    }
}