import "@common/assets/fonts/lcd.font.css";
import { clamp, range } from "@common/utils";
import clsx from "clsx";
import { render, type ComponentChildren } from "preact";

import styles from './assets/brick.module.css';

const FIELD_WIDTH = 10;
const FIELD_HEIGHT = 20;

const MINI_FIELD_WIDTH = 4;
const MINI_FIELD_HEIGHT = 4;

export class BrickDisplayImage {
    public image: boolean[];
    public width: number;
    public height: number;

    constructor(image: boolean[] = [], width: number = 0, height: number = 0) {
        this.image = image;
        this.width = width;
        this.height = height;
    }
}

export class BrickDisplay {
    #field: boolean[] = new Array(FIELD_HEIGHT * FIELD_WIDTH);
    #miniField: boolean[] = new Array(MINI_FIELD_HEIGHT * MINI_FIELD_WIDTH);
    #score: number = 0;
    #speed: number = 1;
    #level: number = 1;
    public pause: boolean = false;
    public gameOver: boolean = false;
    public helpText: ComponentChildren = '';

    init() {
        this.update();
    }

    get score() {
        return this.#score;
    }

    set score(value) {
        this.#score = Math.max(0, (value | 0) % 1_000_000_000);
    }

    get speed() {
        return this.#speed;
    }

    set speed(value) {
        this.#speed = clamp(value | 0, 1, 15);
    }

    get level() {
        return this.#level;
    }

    set level(value) {
        this.#level = clamp(value | 0, 1, 15);
    }

    get width() {
        return FIELD_WIDTH;
    }

    get height() {
        return FIELD_HEIGHT;
    }

    update() {
        render(this.#display, document.body);
    }

    setPixel(x: number, y: number, value: boolean, miniDisplay = false) {
        const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH;
        const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT;

        x = x | 0;
        y = y | 0;

        if (x < 0 || x >= w || y < 0 || y >= h) return;

        const field = miniDisplay ? this.#miniField : this.#field;
        field[y * w + x] = value;
    }

    getPixel(x: number, y: number, miniDisplay = false): boolean {
        const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH;
        const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT;

        x = x | 0;
        y = y | 0;

        if (x < 0 || x >= w || y < 0 || y >= h) return false;

        const field = miniDisplay ? this.#miniField : this.#field;
        return field[y * w + x];
    }

    togglePixel(x: number, y: number, miniDisplay = false) {
        const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH;
        const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT;

        x = x | 0;
        y = y | 0;

        if (x < 0 || x >= w || y < 0 || y >= h) return;

        const field = miniDisplay ? this.#miniField : this.#field;
        field[y * w + x] = !field[y * w + x];
    }

    clear(miniDisplay = false) {
        const length = miniDisplay ? this.#miniField.length : this.#field.length;
        const field = miniDisplay ? this.#miniField : this.#field;
        for (let i = 0; i < length; i++) {
            field[i] = false;
        }
    }

    drawVLine(x: number, y1: number, y2: number, value = true, miniDisplay = false) {
        const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH;
        const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT;
        if (x < 0 || x >= w) return;

        x = x | 0;
        y1 = y1 | 0;
        y2 = y2 | 0;

        if (y2 < y1) {
            const t = y2;
            y2 = y1;
            y1 = t;
        }
        if (y2 < 0 || y1 >= h) return;

        for (let y = Math.max(y1, 0); y <= Math.min(y2, h); y++) {
            this.setPixel(x, y, value, miniDisplay);
        }
    }

    drawHLine(x1: number, x2: number, y: number, value = true, miniDisplay = false) {
        const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH;
        const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT;
        if (y < 0 || y >= h) return;

        x1 = x1 | 0;
        x2 = x2 | 0;
        y = y | 0;

        if (x2 < x1) {
            const t = x2;
            x2 = x1;
            x1 = t;
        }
        if (x2 < 0 || x1 >= w) return;

        for (let x = Math.max(x1, 0); x <= Math.min(x2, w); x++) {
            this.setPixel(x, y, value, miniDisplay);
        }
    }

    drawRect(x1: number, y1: number, x2: number, y2: number, value = true, miniDisplay = false) {
        this.drawHLine(x1, x2, y1, value, miniDisplay);
        this.drawHLine(x1, x2, y2, value, miniDisplay);

        this.drawVLine(x1, y1, y2, value, miniDisplay);
        this.drawVLine(x2, y1, y2, value, miniDisplay);
    }

    fillRect(x1: number, y1: number, x2: number, y2: number, value = true, miniDisplay = false) {
        y1 = y1 | 0;
        y2 = y2 | 0;

        if (y2 < y1) {
            const t = y2;
            y2 = y1;
            y1 = t;
        }

        for (let y = Math.max(0, y1); y <= y2; y++) {
            this.drawHLine(x1, x2, y, value, miniDisplay);
        }
    }

    drawImage(image: BrickDisplayImage, x: number, y: number, miniDisplay = false, xor = false) {
        for (let j = 0; j < image.height; j++) {
            for (let i = 0; i < image.width; i++) {
                const px = image.image[j * image.width + i];
                if (px) {
                    if (xor) {
                        this.togglePixel(x + i, y + j, miniDisplay);
                    } else {
                        this.setPixel(x + i, y + j, px, miniDisplay);
                    }
                }
            }
        }
    }

    get #display() {
        return (
            <div class={styles.root}>
                <div className={styles.field}>
                    {range(FIELD_WIDTH * FIELD_HEIGHT).map(i => (
                        <div
                            key={i}
                            class={clsx(styles.pixel, {
                                [styles.active]: this.#field[i],
                            })}
                        />
                    ))}
                </div>
                <div className={styles.sidebar}>
                    <div className={styles.score}>{this.#score}</div>
                    <div className={styles.miniField}>
                        {range(MINI_FIELD_WIDTH * MINI_FIELD_HEIGHT).map(i => (
                            <div
                                key={i}
                                class={clsx(styles.pixel, {
                                    [styles.active]: this.#miniField[i],
                                })}
                            />
                        ))}
                    </div>
                    <div className={styles.speedLevel}>
                        <div className={styles.speedLevelColumn}>
                            <div className={styles.value}>{this.#speed}</div>
                            <div className={styles.text}>Speed</div>
                        </div>
                        <div className={styles.speedLevelColumn}>
                            <div className={styles.value}>{this.#level}</div>
                            <div className={styles.text}>Level</div>
                        </div>
                    </div>
                    <div className={styles.helpText}>
                        {this.helpText}
                    </div>
                    <div className={styles.footer}>
                        <div className={clsx(styles.text, this.pause && styles.active)}>Pause</div>
                        <div className={clsx(styles.text, this.gameOver && styles.active)}>Game over</div>
                    </div>
                </div>
            </div>
        );
    }

    static convertImage(image: HTMLImageElement): BrickDisplayImage {
        const result = new BrickDisplayImage();

        const canvas = document.createElement('canvas');
        result.width = canvas.width = image.naturalWidth;
        result.height = canvas.height = image.naturalHeight;
        const ctx = canvas.getContext('2d');
        if (ctx) {
            ctx.drawImage(image, 0, 0);
            const pxData = ctx.getImageData(0, 0, canvas.width, canvas.height);

            for (let i = 0; i < pxData.data.length; i += 4) {
                result.image[i >> 2] = pxData.data[i] < 128;
            }
        }

        return result;
    }

    static extractSprite(image: BrickDisplayImage, x: number, y: number, w: number, h: number): BrickDisplayImage {
        if (w <= 0 || h <= 0 || x >= image.width || y >= image.height) {
            return new BrickDisplayImage();
        }

        x = clamp(x | 0, 0, image.width);
        y = clamp(y | 0, 0, image.height);

        w = clamp(w | 0, 1, image.width - x);
        h = clamp(h | 0, 1, image.height - y);

        const result = new BrickDisplayImage(new Array(w * h), w, h);

        for (let j = 0; j < h; j++) {
            for (let i = 0; i < w; i++) {
                const px = image.image[(y + j) * image.width + (x + i)];
                result.image[j * w + i] = px;
            }
        }

        return result;
    }

    static autoCrop(image: BrickDisplayImage): BrickDisplayImage {
        let minX = image.width;
        let minY = image.height;
        let maxX = 0;
        let maxY = 0;

        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const i = y * image.width + x;
                if (image.image[i]) {
                    minX = Math.min(minX, x);
                    maxX = Math.max(maxX, x);
                    minY = Math.min(minY, y);
                    maxY = Math.max(maxY, y);
                }
            }
        }

        return this.extractSprite(image, minX, minY, maxX - minX + 1, maxY - minY + 1);
    }

    static copySprite(image: BrickDisplayImage): BrickDisplayImage {
        return this.extractSprite(image, 0, 0, image.width, image.height);
    }

    static rotateSprite(image: BrickDisplayImage, angle: 0 | 90 | 180 | 270): BrickDisplayImage {
        if (angle === 0) return this.copySprite(image);

        const newImage = new BrickDisplayImage(
            new Array(image.width * image.height),
            angle === 180 ? image.width : image.height,
            angle === 180 ? image.height : image.width,
        );

        for (let j = 0; j < image.height; j++) {
            for (let i = 0; i < image.width; i++) {
                const originalPixel = image.image[j * image.width + i];
                let x = i;
                let y = j;

                if (angle === 90) {
                    const tmp = y;
                    y = x;
                    x = image.height - tmp - 1;
                } else if (angle === 180) {
                    x = image.width - x - 1;
                    y = image.height - y - 1;
                } else if (angle === 270) {
                    const tmp = x;
                    x = y;
                    y = image.width - tmp - 1;
                }

                newImage.image[y * newImage.width + x] = originalPixel
            }
        }

        return newImage;
    }
}
