import { BooleanFlag, Constant, Dimension, Event } from "@clarity-types/data";
import { ScrollState, Setting } from "@clarity-types/interaction";
import { bind } from "@src/core/event";
import { schedule } from "@src/core/task";
import { time } from "@src/core/time";
import { clearTimeout, setTimeout } from "@src/core/timeout";
import throttle from "@src/core/throttle";
import { iframe } from "@src/layout/dom";
import { target, metadata } from "@src/layout/target";
import encode from "./encode";
import * as dimension from "@src/data/dimension";

export let state: ScrollState[] = [];
let initialTop: Node = null;
let initialBottom: Node = null;
let timeout: number = null;

export function start(): void {
    state = [];
    recompute();
}

export function observe(root: Node): void {
    let frame = iframe(root);
    let node = frame ? frame.contentWindow : (root === document ? window : root);
    bind(node, "scroll", throttledRecompute, true);
}

function recompute(event: UIEvent = null): void {
    let w = window as Window;
    let de = document.documentElement;
    let element = event ? target(event) : de;

    // In some edge cases, it's possible for target to be null.
    // In those cases, we cannot proceed with scroll event instrumentation.
    if (!element) { return; }

    // If the target is a Document node, then identify corresponding documentElement and window for this document
    if (element && element.nodeType === Node.DOCUMENT_NODE) {
        let frame = iframe(element);
        w = frame ? frame.contentWindow : w;
        element = de = (element as Document).documentElement;
    }

    // Edge doesn't support scrollTop position on document.documentElement.
    // For cross browser compatibility, looking up pageYOffset on window if the scroll is on document.
    // And, if for some reason that is not available, fall back to looking up scrollTop on document.documentElement.
    let x = element === de && "pageXOffset" in w ? Math.round(w.pageXOffset) : Math.round((element as HTMLElement).scrollLeft);
    let y = element === de && "pageYOffset" in w ? Math.round(w.pageYOffset) : Math.round((element as HTMLElement).scrollTop);
    const width = window.innerWidth;
    const height = window.innerHeight;
    const xPosition = width / 3;
    const yOffset = width > height ? height * 0.15 : height * 0.2;
    const startYPosition = yOffset;
    const endYPosition = height - yOffset;
    const top = getPositionNode(xPosition, startYPosition);
    const bottom = getPositionNode(xPosition, endYPosition);
    const trust = event && event.isTrusted ? BooleanFlag.True : BooleanFlag.False;

    let current: ScrollState = { time: time(event), event: Event.Scroll, data: {target: element, x, y, top, bottom, trust} };

    // We don't send any scroll events if this is the first event and the current position is top (0,0)
    if ((event === null && x === 0 && y === 0) || (x === null || y === null)) {
        initialTop = top;
        initialBottom = bottom;
        return;
    }

    let length = state.length;
    let last = length > 1 ? state[length - 2] : null;
    if (last && similar(last, current)) { state.pop(); }
    state.push(current);

    clearTimeout(timeout);
    timeout = setTimeout(process, Setting.LookAhead, Event.Scroll);
}

const throttledRecompute = throttle(recompute, Setting.Throttle);

function getPositionNode(x: number, y: number): Node {
    let node: Node;
    if ("caretPositionFromPoint" in document) {
        node = (document as any).caretPositionFromPoint(x, y)?.offsetNode;
    } else if ("caretRangeFromPoint" in document) {
        node = (document as any).caretRangeFromPoint(x, y)?.startContainer;
    }
    if (!node) {
        node = document.elementFromPoint(x, y) as Node;
    }
    if (node && node.nodeType === Node.TEXT_NODE) {
        node = node.parentNode;
    }

    return node;
}

export function reset(): void {
    state = [];
    initialTop = null;
    initialBottom = null;
}

function process(event: Event): void {
    schedule(encode.bind(this, event));
}

function similar(last: ScrollState, current: ScrollState): boolean {
    let dx = last.data.x - current.data.x;
    let dy = last.data.y - current.data.y;
    return (dx * dx + dy * dy < Setting.Distance * Setting.Distance) && (current.time - last.time < Setting.ScrollInterval);
}

export function compute(): void {
    if (initialTop) {
        const top = metadata(initialTop, null);
        dimension.log(Dimension.InitialScrollTop, top?.hash?.join(Constant.Dot));
    }
    if (initialBottom) {
        const bottom = metadata(initialBottom, null);
        dimension.log(Dimension.InitialScrollBottom, bottom?.hash?.join(Constant.Dot));
    }
}

export function stop(): void {
    clearTimeout(timeout);
    throttledRecompute.cleanup();
    state = [];
    initialTop = null;
    initialBottom = null;
}
