import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators/custom-element.js";
import { queryAll } from "lit/decorators/query-all.js";
import { query } from "lit/decorators/query.js";
import { words } from "./wordlist.js";
import "./fridge-tile.js";
import { property } from "lit/decorators/property.js";
import { FridgeTile } from "./fridge-tile.js";
import { pointerLocation } from "./events.js";

function* pointsIterator(source: FridgeTile[]) {
    for (const tile of source) {
        for (const point of tile.points) {
            yield point;
        }
    }
}

type Point = [number, number];

const isHTMLElement = (v: unknown): v is HTMLElement =>
    typeof v === "object" && v !== null && "nodeType" in v && v.nodeType === Node.ELEMENT_NODE;

@customElement("fridge-board")
export class FridgeBoard extends LitElement {
    static get styles() {
        return css`
            :host {
                display: block;
                position: relative;
            }

            #fridge {
                overflow: hidden;
                position: relative;
                top: 0;
                right: 0;
                width: 100%;
                height: 100%;
                background: transparent;
            }

            fridge-tile {
                position: absolute;
                transition:
                    scale 200ms ease-out,
                    rotate 200ms ease-out;
                will-change: scale rotate translate;
            }

            .ready-to-slide {
                transition:
                    scale 1500ms ease-out,
                    rotate 1500ms ease-out,
                    translate 1500ms cubic-bezier(0.4, 0, 0.2, 1);
                will-change: scale rotate translate;
            }
        `;
    }

    @property({ type: Array })
    words: (typeof words)[] = [];

    @queryAll("fridge-tile")
    tiles!: FridgeTile[];

    @query("#fridge")
    fridge!: HTMLDivElement;

    maxZ: number = 1;

    assignNewPositions() {
        const pointsAlreadyPositioned: Point[] = [];
        const unpositionable: FridgeTile[] = [];
        const { width: boardWidth, height: boardHeight } = this.getBoundingClientRect();

        const setNewPosition = (tile: FridgeTile) => {
            const { height: tileHeight, width: tileWidth } = tile.size;

            for (let i = 0; i < 30; i++) {
                // Where we will put the new tile
                const newYPos = Math.random() * (boardHeight - tileHeight) * 0.96;
                const newXPos = Math.random() * (boardWidth - tileWidth) * 0.96;

                // The box defining the new tile.
                const box = {
                    xl: newXPos,
                    xr: newXPos + tileWidth,
                    yt: newYPos,
                    yb: newYPos + tileHeight,
                };

                const isPointInBox = (point: Point) =>
                    point[0] >= box.xl && point[0] <= box.xr && point[1] >= box.yt && point[1] <= box.yb;

                if (pointsAlreadyPositioned.find(p => isPointInBox(p)) === undefined) {
                    tile.style.top = `${box.yt}px`;
                    tile.style.left = `${box.xl}px`;
                    pointsAlreadyPositioned.push(
                        [box.xl, box.yt],
                        [box.xr, box.yt],
                        [box.xl, box.yb],
                        [box.xr, box.yb]
                    );
                    return true;
                }
            }
            return false;
        };

        for (const tile of this.tiles) {
            if (!setNewPosition(tile)) {
                unpositionable.push(tile);
            }
        }

        for (const tile of unpositionable) {
            tile.remove();
        }
    }

    onPointerDown(ev: PointerEvent) {
        const node = ev.target;
        if (!(isHTMLElement(node) && node.tagName.toLowerCase() === "fridge-tile")) {
            return;
        }

        // The position of the board relative to the viewport;
        const { left: fridgeLeft, top: fridgeTop } = this.getBoundingClientRect();

        // The position of the node relative to the viewport:
        const { left: nodeLeft, top: nodeTop } = node.getBoundingClientRect();

        // The position of the pointer when the event started relative to the viewport;
        const { clientX: pointerStartX, clientY: pointerStartY } = ev;
        let pointerX = pointerStartX;
        let pointerY = pointerStartY;

        // Starting position of the *node* with respect to the board;
        const tileStart = {
            x: nodeLeft - fridgeLeft,
            y: nodeTop - fridgeTop,
        };

        const controller = new AbortController();
        const { signal } = controller;

        let tracking = true;

        const pointer = (ev: PointerEvent) => {
            const { clientX, clientY } = ev;
            pointerX = clientX;
            pointerY = clientY;
        };

        this.maxZ = this.maxZ + 1;
        node.style.setProperty("z-index", `${this.maxZ}`);
        node.style.setProperty("scale", "1.3");
        node.style.setProperty("rotate", `${Math.random() * 30 - 15}deg`);

        const stop = () => {
            tracking = false;
            controller.abort();
            let delX = pointerX - pointerStartX;
            let delY = pointerY - pointerStartY;
            node.style.removeProperty("translate");
            node.style.setProperty("top", `${tileStart.y + delY}px`);
            node.style.setProperty("left", `${tileStart.x + delX}px`);
            requestAnimationFrame(() => {
                node.style.setProperty("scale", "1.0");
                node.style.setProperty("rotate", `${Math.random() * 30 - 15}deg`);
            });
        };

        let animationFrame: number = -1;

        const move = () => {
            if (tracking) {
                let delX = pointerX - pointerStartX;
                let delY = pointerY - pointerStartY;
                node.style.setProperty("translate", `${+delX}px ${+delY}px 0`);
                animationFrame = requestAnimationFrame(move);
                return;
            }

            cancelAnimationFrame(animationFrame);
            return;
        };

        window.addEventListener("pointermove", pointer, { signal });
        window.addEventListener("pointerup", stop, { signal });
        window.addEventListener("pointercancel", stop, { signal });
        animationFrame = requestAnimationFrame(move);
    }

    render() {
        return html`<div id="fridge" @pointerdown=${this.onPointerDown}>
            ${words.map(word => html`<fridge-tile style="opacity: 0.01" word=${word.w}></fridge-tile>`)}
        </div>`;
    }

    updated() {
        const { width: boardWidth, height: boardHeight } = this.getBoundingClientRect();

        const fd = (size: number) => {
            const newDelta = 40 * Math.random();
            return Math.random() < 0.5 ? newDelta + size : newDelta * -1;
        };

        const lastTile = this.tiles[this.tiles.length - 1];

        const slideEnd = (ev: TransitionEvent) => {
            if (ev.propertyName === "translate") {
                this.tiles.forEach(tile => {
                    tile.classList.remove("ready-to-slide");
                });
                lastTile.removeEventListener("transitionend", slideEnd);
            }
        };

        const positionTilesOutsideBoard = () => {
            this.tiles.forEach(tile => {
                const outerX = fd(boardWidth);
                const outerY = fd(boardHeight);
                const [tileX, tileY] = tile.relativePosition;
                tile.style.setProperty("translate", `${outerX - tileX}px ${outerY - tileY}px 0`);
                tile.style.setProperty("scale", "1.5");
                tile.style.setProperty("rotate", `${Math.random() * 720}deg`);
                tile.style.setProperty("opacity", "100%");
            });
        };

        const startTileAnimation = () => {
            this.tiles.forEach(tile => {
                tile.classList.add("ready-to-slide");
                tile.style.setProperty("translate", "0 0 0");
                tile.style.setProperty("rotate", `${Math.random() * 30 - 15}deg`);
                tile.style.setProperty("scale", "1.0");
            });
        };

        requestAnimationFrame(() => {
            this.assignNewPositions();
            requestAnimationFrame(() => {
                positionTilesOutsideBoard();
                lastTile.addEventListener("transitionend", slideEnd);
                requestAnimationFrame(() => startTileAnimation());
            });
        });
    }
}

declare global {
    interface HTMLElementTagNameMap {
        "fridge-board": FridgeBoard;
    }
}
