import { defaultRenderer, renderPorts, renderResource, renderers, type NullRenderer, type Renderer, type ViewConfig } from "./renderer";
import { ALL_DIRECTIONS, Direction, exp, trunc } from "./utils";
import type World from "./world";
import { type Tile } from "./world";

const initialTileSize = 32;

export default class Graphics {
    private context: CanvasRenderingContext2D;
    private tileSize = initialTileSize;
    private offset: Point = [0, 0];
    private highlighted: Point = [0, 0];
    private tooltip: [Point, string] | null = null;

    constructor(private canvas: HTMLCanvasElement) {
        this.context = this.canvas.getContext('2d')!;
    }

    get width() {
        return this.canvas.width;
    }

    get height() {
        return this.canvas.height;
    }

    get size(): Point {
        return [this.canvas.width, this.canvas.height];
    }

    applyScale(scale: number, point: Point) {
        const newTileSize = Math.min(Math.max(this.tileSize * scale, 16), this.width / 2, this.height / 2);
        const realScale = newTileSize / this.tileSize;
        this.tileSize = newTileSize;

        this.offset = exp`((${this.offset} - ${point}) * ${realScale}) + ${point}`;
    }

    pan(amount: Point) {
        this.offset = exp`${this.offset} + ${amount}`;
    }

    resetStyle() {
        this.context = this.canvas.getContext('2d')!;
        this.context.imageSmoothingEnabled = false;
        this.context.font = '0.4px "IBM VGA 8x16"';
        this.context.textRendering = 'optimizeSpeed';
        this.context.textAlign = 'center';
        this.context.textBaseline = 'middle';
    }

    resetView() {
        this.tileSize = initialTileSize;
        this.offset = exp`(${this.size} - ${this.tileSize}) / ${2}`;
    }

    highlight(screenPoint: Point) {
        this.highlighted = this.screenToWorld(screenPoint);
    }

    clear() {
        this.context.clearRect(0, 0, this.width, this.height);
    }

    debug() {

    }

    showTooltip(pos: Point, text: string) {
        if (text) {
            this.tooltip = [pos, text];
        } else {
            this.tooltip = null;
        }
    }

    drawTooltip() {
        if (this.tooltip) {
            this.context.save();
            // this.context.reset();
            this.context.font = '16px sans-serif';
            this.context.textAlign = 'left';

            const [pos, text] = this.tooltip;
            const lines = text.split('\n');

            let maxWidth = 0;
            let maxHeight = 10;
            for (const line of lines) {
                const measure = this.context.measureText(line);
                maxWidth = Math.max(maxWidth, measure.width);
                maxHeight += 18;
            }

            this.context.fillStyle = 'white';
            this.context.fillRect(...pos, maxWidth + 10, maxHeight + 3);
            this.context.strokeStyle = 'black';
            this.context.strokeRect(...pos, maxWidth + 10, maxHeight + 3);
            this.context.font = '16px';
            this.context.fillStyle = 'black';

            let y = pos[1] + 18;
            for (const line of lines) {
                const measure = this.context.measureText(line);
                this.context.fillText(line, pos[0] + 5, y);
                y += 18;
            }
            this.context.restore();
        }
    }

    drawGrid() {
        this.context.beginPath();
        this.context.strokeStyle = 'gray';
        let [x0, y0, x1, y1] = this.visibleWorld;
        [x0, y0] = this.worldToScreen([x0, y0]);
        [x1, y1] = this.worldToScreen([x1, y1]);

        for (let x = x0; x < x1; x += this.tileSize) {
            this.context.moveTo(x, 0);
            this.context.lineTo(x, this.height);
        }
        for (let y = y0; y < y1; y += this.tileSize) {
            this.context.moveTo(0, y);
            this.context.lineTo(this.width, y);
        }
        this.context.stroke();
    }

    drawHighlight() {
        this.drawTile(this.highlighted, (ctx) => {
            ctx.fillStyle = getComputedStyle(this.canvas).getPropertyValue("--color-bg-select");
            ctx.fillRect(0, 0, 1, 1);
        });
    }

    drawTile<T extends Tile>(position: Point, renderer: Renderer<T>, tile: T, drawPorts?: boolean): void;
    drawTile<T extends null>(position: Point, renderer: NullRenderer): void;
    drawTile<T extends Tile>(position: Point, renderer: Renderer<T> | NullRenderer, tile?: T, drawPorts: boolean = true): void {
        this.context.save();
        const viewConfig: ViewConfig = {
            tileSize: this.tileSize,
        }

        // TODO skip drawing if outside screen
        const screenPosition = this.worldToScreen(trunc(position));
        this.context.translate(screenPosition[0], screenPosition[1]);
        this.context.scale(this.tileSize, this.tileSize);

        if (tile) {
            if (drawPorts) {
                renderPorts(this.context, viewConfig, tile);
            }
            renderer(this.context, viewConfig, tile);
        } else {
            (renderer as NullRenderer)(this.context, viewConfig);
        }

        this.context.restore();
    }

    drawWorld(world: World) {
        const [x0, y0, x1, y1] = this.visibleWorld;
        const resourceTiles: [Point, Tile][] = [];
        for (let y = y0; y <= y1; y++) {
            for (let x = x0; x <= x1; x++) {
                const pos: Point = [x, y];
                const tile = world.getTile(pos);
                if (tile) {
                    let renderer = renderers[tile.type] as Renderer<Tile>;
                    if (!renderer) {
                        renderer = defaultRenderer;
                    }
                    this.drawTile(pos, renderer, tile);
                    if ([...ALL_DIRECTIONS, Direction.NONE].some(d => tile.inv[d] != null)) {
                        resourceTiles.push([pos, tile]);
                    }
                }
            }
        }

        this.context.fillStyle = 'blue';
        for (const [pos, tile] of resourceTiles) {
            this.drawTile(pos, renderResource, tile, false);
        }
    }

    get visibleWorld(): Rect {
        const [x0, y0] = this.screenToWorld([0, 0]);
        const [x1, y1] = this.screenToWorld([this.width, this.height]);

        return [Math.floor(x0), Math.floor(y0), Math.ceil(x1), Math.ceil(y1)];
    }

    worldToScreen(point: Point): Point {
        return [
            point[0] * this.tileSize + this.offset[0],
            point[1] * this.tileSize + this.offset[1],
        ];
    }

    screenToWorld(point: Point): Point {
        return exp`(${point} - ${this.offset}) / ${this.tileSize}`;
    }
}