import { Event } from "@clarity-types/data";
import { type PointerState, Setting } from "@clarity-types/interaction";
import { FunctionNames } from "@clarity-types/performance";
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 { iframe } from "@src/layout/dom";
import { offset } from "@src/layout/offset";
import { target } from "@src/layout/target";
import encode from "./encode";

export let state: PointerState[] = [];
let timeout: number = null;
let hasPrimaryTouch = false;
let primaryTouchId = 0;
const activeTouchPointIds = new Set<number>();

export function start(): void {
    reset();
}

export function observe(root: Node): void {
    bind(root, "mousedown", mouse.bind(this, Event.MouseDown, root), true);
    bind(root, "mouseup", mouse.bind(this, Event.MouseUp, root), true);
    bind(root, "mousemove", mouse.bind(this, Event.MouseMove, root), true);
    bind(root, "wheel", mouse.bind(this, Event.MouseWheel, root), true);
    bind(root, "dblclick", mouse.bind(this, Event.DoubleClick, root), true);
    bind(root, "touchstart", touch.bind(this, Event.TouchStart, root), true);
    bind(root, "touchend", touch.bind(this, Event.TouchEnd, root), true);
    bind(root, "touchmove", touch.bind(this, Event.TouchMove, root), true);
    bind(root, "touchcancel", touch.bind(this, Event.TouchCancel, root), true);
}

function mouse(event: Event, root: Node, evt: MouseEvent): void {
    mouse.dn = FunctionNames.PointerMouse;
    const frame = iframe(root);
    const d = frame ? frame.contentDocument.documentElement : document.documentElement;
    let x = "pageX" in evt ? Math.round(evt.pageX) : "clientX" in evt ? Math.round((evt as MouseEvent).clientX + d.scrollLeft) : null;
    let y = "pageY" in evt ? Math.round(evt.pageY) : "clientY" in evt ? Math.round((evt as MouseEvent).clientY + d.scrollTop) : null;
    // In case of iframe, we adjust (x,y) to be relative to top parent's origin
    if (frame) {
        const distance = offset(frame);
        x = x ? x + Math.round(distance.x) : x;
        y = y ? y + Math.round(distance.y) : y;
    }

    // Check for null values before processing this event
    if (x !== null && y !== null) {
        handler({ time: time(evt), event, data: { target: target(evt), x, y } });
    }
}

function touch(event: Event, root: Node, evt: TouchEvent): void {
    touch.dn = FunctionNames.PointerTouch;
    const frame = iframe(root);
    const d = frame ? frame.contentDocument.documentElement : document.documentElement;
    const touches = evt.changedTouches;

    const t = time(evt);
    if (touches) {
        for (let i = 0; i < touches.length; i++) {
            const entry = touches[i];
            let x = "clientX" in entry ? Math.round(entry.clientX + d.scrollLeft) : null;
            let y = "clientY" in entry ? Math.round(entry.clientY + d.scrollTop) : null;
            x = x && frame ? x + Math.round(frame.offsetLeft) : x;
            y = y && frame ? y + Math.round(frame.offsetTop) : y;

            // We cannot rely on identifier to determine primary touch as its value doesn't always start with 0.
            // Safari/Webkit uses the address of the UITouch object as the identifier value for each touch point.
            const id = "identifier" in entry ? entry.identifier : undefined;

            switch (event) {
                case Event.TouchStart:
                    if (activeTouchPointIds.size === 0) {
                        // Track presence of primary touch separately to handle scenarios when same id is repeated
                        hasPrimaryTouch = true;
                        primaryTouchId = id;
                    }
                    activeTouchPointIds.add(id);
                    break;
                case Event.TouchEnd:
                case Event.TouchCancel:
                    activeTouchPointIds.delete(id);
                    break;
            }
            const isPrimary = hasPrimaryTouch && primaryTouchId === id;

            // Check for null values before processing this event
            if (x !== null && y !== null) {
                handler({ time: t, event, data: { target: target(evt), x, y, id, isPrimary } });
            }

            // Reset primary touch point id once touch event ends
            if (event === Event.TouchCancel || event === Event.TouchEnd) {
                if (primaryTouchId === id) {
                    hasPrimaryTouch = false;
                }
            }
        }
    }
}

function handler(current: PointerState): void {
    switch (current.event) {
        case Event.MouseMove:
        case Event.MouseWheel:
        case Event.TouchMove: {
            const length = state.length;
            const last = length > 1 ? state[length - 2] : null;
            if (last && similar(last, current)) {
                state.pop();
            }
            state.push(current);

            clearTimeout(timeout);
            timeout = setTimeout(process, Setting.LookAhead, current.event);
            break;
        }
        default:
            state.push(current);
            process(current.event);
            break;
    }
}

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

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

function similar(last: PointerState, current: PointerState): boolean {
    const dx = last.data.x - current.data.x;
    const dy = last.data.y - current.data.y;
    const distance = Math.sqrt(dx * dx + dy * dy);
    const gap = current.time - last.time;
    const match = current.data.target === last.data.target;
    const sameId = current.data.id !== undefined ? current.data.id === last.data.id : true;
    return current.event === last.event && match && distance < Setting.Distance && gap < Setting.PointerInterval && sameId;
}

export function stop(): void {
    clearTimeout(timeout);
    // Send out any pending pointer events in the pipeline
    if (state.length > 0) {
        process(state[state.length - 1].event);
    }
}
