import { DATA_URL_CLICK_AUDIO } from "./dataUrlClickAudio.js";
import {
    CUSTOM_UNICODE_MARKER,
    RECORD_ITEM_TYPE_CLICK,
    RECORD_ITEM_TYPE_FOCUS,
    RECORD_ITEM_TYPE_RESIZE,
    RECORD_ITEM_TYPE_SCROLL,
    RECORD_ITEM_TYPE_START,
} from "../recorder/records.js";
import { resolveFullSelector } from "../utils/calcFullSelector.js";

import type { Recorder } from "../recorder/recorder.js";
import type { Records, ReplayItem } from "../recorder/records.js";

const asyncSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

const readableMilliseconds = (ms: number) => new Date(ms).toISOString().slice(11, 19);

const DELAY_CLICK_AUDIO_PLAY = 200;
const DELAY_CLICK = 250;

const STYLESHEET_ID = "web-html-element-interaction-recorder";

class Player {
    private element: HTMLElement;

    private clickWrapper: HTMLElement;

    private selectors: Records["selectors"];

    private items: ReplayItem[] = [];

    private clickAudio: HTMLAudioElement;

    private running: boolean;

    private timerElement: HTMLDivElement;

    public constructor(replay: ReturnType<Recorder["createReplay"]>, element?: HTMLElement) {
        this.element = element;
        this.selectors = replay.selectors.map((selector) => `${replay.selectorsPrefix}${selector}`);
        this.parseItems(replay.items);
        this.clickAudio = new Audio(DATA_URL_CLICK_AUDIO);
    }

    /**
     * Modify the first items until the first real interaction (click, ...). This is helpful for
     * recorders which got started directly with the page request.
     */
    public reflowFirstRealInteraction(throttle = 1000) {
        for (const item of this.items) {
            const [type, duration] = item;
            if ([RECORD_ITEM_TYPE_START, RECORD_ITEM_TYPE_RESIZE].indexOf(type) === -1) {
                if (duration >= throttle) {
                    item[1] = throttle;
                }
                break;
            }
        }
    }

    public start(
        opts?: Parameters<Player["startAsync"]>[0] & {
            onFinish?: () => void;
            onDispatch?: () => void;
        },
    ): [Promise<void>, () => void] {
        this.running = true;
        const fullDurationMs = this.ensureTimerElement(true);
        const playbackTime = this.timerElement.childNodes[0] as HTMLSpanElement;

        let seconds = 0;
        const secondsInterval = setInterval(() => {
            seconds++;
            const ms = seconds * 1000;
            playbackTime.innerHTML = readableMilliseconds(ms > fullDurationMs ? fullDurationMs : ms);
        }, 1000);

        const promise = this.startAsync(opts);
        const dispatch = () => {
            clearInterval(secondsInterval);
            this.running = false;
            opts.onDispatch?.();
        };

        promise.then(dispatch).then(() => {
            // Always ensure the "last" second is shown
            playbackTime.innerHTML = readableMilliseconds(fullDurationMs);
            this.timerElement.classList.add("player-finish");
            opts.onFinish?.();
        });

        return [
            promise,
            () => {
                dispatch();
                this.ensureTimerElement(false);
            },
        ];
    }

    private async startAsync({
        afterReplayItem,
        setElement,
        clickWrapper,
    }: {
        afterReplayItem?: (item: ReplayItem) => Promise<void>;
        setElement?: () => Promise<Player["element"]>;
        /**
         * Put the HTML elements for a click simulation into this wrapper. This could be useful if you are using
         * e.g. the `<dialog` element as no outside-elements are visible (even with `z-index` set).
         */
        clickWrapper?: (element: Player["element"]) => Promise<Player["clickWrapper"]>;
    } = {}) {
        this.createCss();

        if (setElement) {
            this.element = await setElement();
        }
        if (clickWrapper) {
            this.clickWrapper = await clickWrapper(this.element);
        }

        const { items } = this;
        for (const item of items) {
            if (!this.running) {
                break;
            }

            const [type, delay] = item;
            await asyncSleep(delay);

            if (!this.running) {
                break;
            }

            if (type === RECORD_ITEM_TYPE_SCROLL) {
                const [, , selectorIndex, top] = item;
                this.resolveSelector(selectorIndex)?.scrollTo({
                    top,
                    behavior: "smooth",
                });
            } else if (type === RECORD_ITEM_TYPE_CLICK) {
                const [, , selectorIndex] = item;
                let elementToClick = this.resolveSelector(selectorIndex);
                if (elementToClick) {
                    this.clickAudio.play();
                    await asyncSleep(DELAY_CLICK_AUDIO_PLAY);
                    this.showClickEffect(elementToClick);
                    await asyncSleep(DELAY_CLICK);

                    if (elementToClick.getAttribute("target") !== "_blank") {
                        // Find clickable element
                        // A SVG does not have a `.click` handler, so we dispatch the event in another way
                        while (!elementToClick.click) {
                            elementToClick = elementToClick.parentElement;
                        }
                        elementToClick?.click();
                    }
                }
            } else if (type === RECORD_ITEM_TYPE_FOCUS) {
                const [, , selectorIndex] = item;
                const elementToFocus = this.resolveSelector(selectorIndex);
                elementToFocus?.focus({ preventScroll: true });

                // Visually, we also add a class which can be reused by the element to show visual outline
                // Unfortunately, browsers do not provide something like `.focusVisual()`.
                const focusVisibleClassName = "wheir-focus-visible";
                document.querySelector(`.${focusVisibleClassName}`)?.classList.remove(focusVisibleClassName);
                elementToFocus.classList.add(focusVisibleClassName);
            }

            await afterReplayItem?.(item);
        }
    }

    private resolveSelector(selectorIndex: number) {
        return resolveFullSelector(this.element, this.selectors[selectorIndex]);
    }

    private ensureTimerElement(visible: boolean) {
        if (visible) {
            let durationMs = this.items.reduce((p, [type, duration]) => {
                return p + duration + (type === RECORD_ITEM_TYPE_CLICK ? DELAY_CLICK_AUDIO_PLAY + DELAY_CLICK : 0);
            }, 0);
            durationMs = Math.ceil(durationMs / 1000) * 1000;

            this.timerElement = document.createElement("div");
            this.timerElement.className = "wheir-timer";
            this.timerElement.innerHTML = `<span>00:00:00</span> / <span>${readableMilliseconds(durationMs)}</span>`;
            document.body.append(this.timerElement);
            return durationMs;
        } else if (this.timerElement) {
            this.timerElement.remove();
            this.timerElement = undefined;
        }

        return 0;
    }

    private showClickEffect(element: HTMLElement) {
        const rect = element.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top + rect.height / 2;
        const circle = document.createElement("div");
        circle.className = "wheir-pulse-red";
        (this.clickWrapper || document.body).appendChild(circle);
        circle.style.left = `${x - 10}px`;
        circle.style.top = `${y - 10}px`;
        circle.addEventListener("animationend", () => {
            circle.remove();
        });
    }

    private createCss() {
        if (!document.getElementById(STYLESHEET_ID)) {
            const element = document.createElement("style");
            (element.style as any).type = "text/css";
            element.id = STYLESHEET_ID;
            document.getElementsByTagName("head")[0].appendChild(element);

            element.innerHTML = `
.wheir-timer {
    position: fixed;
    top: 10px;
    left: 50%;
    width: 170px;
    background: white;
    z-index: 999999999999999;
    text-align: center;
    padding: 15px;
    margin-left: -100px;
    border-radius: 99px;
    font-size: 16px;
    line-height: 25px;
    color: black;
    border: 1px solid #0000001f;
    box-shadow: 0px 3px 3px 2px #0000000a;
}
.wheir-timer.player-finish > span:first-of-type {
    color: green;
}

.wheir-pulse-red {
    position: absolute;
    width: 20px;
    height: 20px;
    background-color: red;
    border-radius: 50%;
    opacity: 0;
    animation: wheir-pulse-red ${DELAY_CLICK}ms ease-out;
    pointer-events: none;
    z-index: 999999999;
}
@keyframes wheir-pulse-red {
    0% {
      opacity: 1;
      transform: scale(0);
    }
    70% {
      opacity: 0.5;
      transform: scale(2);
    }
    100% {
      opacity: 0;
      transform: scale(3);
    }
}`;
        }
    }

    private parseItems(items: string) {
        this.items = items.split(" ").map((item) => {
            const customDataMap: any[] = [];
            return item
                .replace(new RegExp(`${CUSTOM_UNICODE_MARKER}(.*)${CUSTOM_UNICODE_MARKER}`, "gm"), (m) => {
                    let dataString = m.substring(CUSTOM_UNICODE_MARKER.length);
                    dataString = dataString.substring(0, dataString.length - CUSTOM_UNICODE_MARKER.length);
                    const idx = customDataMap.push(JSON.parse(dataString)) - 1;
                    return `<${idx}`;
                })
                .split(",")
                .map((i) => (i.startsWith("<") ? customDataMap[+i.substring(1)] : Number(i))) as ReplayItem;
        });
    }
}

export { Player };
