import { commonStringPrefix } from "../utils/commonStringPrefix.js";

/**
 * Milliseconds to cut off scroll area to a new record. E.g. user scrolls down,
 * pauses 2 seconds and scrolls again.
 */
const SCROLL_THRESHOLD_NEW_RECORD_MS = 50;
const RESIZE_THRESHOLD_NEW_RECORD_MS = 400;

const RECORD_ITEM_TYPE_START = 1;
const RECORD_ITEM_TYPE_SCROLL = 2;
const RECORD_ITEM_TYPE_CLICK = 3;
const RECORD_ITEM_TYPE_RESIZE = 4;
const RECORD_ITEM_TYPE_FOCUS = 5;
const RECORD_ITEM_TYPE_PAUSE = 9;
const RECORD_ITEM_TYPE_CUSTOM = 10;

/**
 * Due to serialization and data economy we hold as array instead of object with key names to
 * not blow up the memory and network transfer.
 *
 * Record:
 *
 * ```
 * [Start,  Current time in milliseconds, Milliseconds to previous record]
 * [Pause,  Current time in milliseconds, Milliseconds to previous record]
 * [Scroll, Current time in milliseconds, Milliseconds to previous record, Selector Index, Scroll top in px]
 * [Click,  Current time in milliseconds, Milliseconds to previous record, Selector Index]
 * [Resize, Current time in milliseconds, Milliseconds to previous record, Viewport width, Viewport height]
 * [Focus,  Current time in milliseconds, Milliseconds to previous record, Selector Index]
 * [Custom, Current time in milliseconds, Milliseconds to previous record, Type identifier, JSON encoded string]
 * ```
 *
 * Replay:
 *
 * ```
 * [Start,  Milliseconds to previous record]
 * [Pause,  Milliseconds to previous record]
 * [Scroll, Milliseconds to previous record, Selector Index, Scroll top in px]
 * [Click,  Milliseconds to previous record, Selector Index]
 * [Resize, Milliseconds to previous record, Viewport width, Viewport height]
 * [Focus,  Milliseconds to previous record, Focus]
 * [Custom, Milliseconds to previous record, Type identifier, JS object]
 * ```
 */
type RecordItem =
    | [typeof RECORD_ITEM_TYPE_START, number, number]
    | [typeof RECORD_ITEM_TYPE_PAUSE, number, number]
    | [typeof RECORD_ITEM_TYPE_SCROLL, number, number, number, number]
    | [typeof RECORD_ITEM_TYPE_CLICK, number, number, number]
    | [typeof RECORD_ITEM_TYPE_RESIZE, number, number, number, number]
    | [typeof RECORD_ITEM_TYPE_FOCUS, number, number, number]
    | [typeof RECORD_ITEM_TYPE_CUSTOM, number, number, number, any];

type ReplayItem =
    | [typeof RECORD_ITEM_TYPE_START, number]
    | [typeof RECORD_ITEM_TYPE_PAUSE, number]
    | [typeof RECORD_ITEM_TYPE_SCROLL, number, number, number]
    | [typeof RECORD_ITEM_TYPE_CLICK, number, number]
    | [typeof RECORD_ITEM_TYPE_RESIZE, number, number, number]
    | [typeof RECORD_ITEM_TYPE_FOCUS, number, number]
    | [typeof RECORD_ITEM_TYPE_CUSTOM, number, number, any];

/**
 * Use for `RECORD_ITEM_TYPE_CUSTOM` to allow strings in interactions.
 */
const CUSTOM_UNICODE_MARKER = `<äßæ>`;

/**
 * Records represents all interactions but without any DOM state / element and
 * is fully serializable.
 */
class Records {
    private items: RecordItem[] = [];

    private selectors: string[] = [];

    public constructor() {
        // Silence is golden.
    }

    /**
     * Get the record items garbage collected and ready for transfer via e.g. network
     * and to make it replayable.
     */
    public createReplay() {
        this.garbageCollect();

        const { items } = this;
        const replayItems: ReplayItem[] = [];

        // Remove all timestamps as no longer needed
        for (let i = 0; i < items.length; i++) {
            const [type, , ...rest] = items[i];
            replayItems.push([type, ...rest] as any);
        }

        // To save bandwidth find common string in selectors
        const commonPrefix = commonStringPrefix(this.selectors);

        const result = {
            v: 1,
            selectorsPrefix: commonPrefix,
            selectors: this.selectors.map((selector) => selector.substr(commonPrefix.length)),
            items: replayItems
                .map((item) =>
                    item
                        .map((v: any) =>
                            typeof v === "number"
                                ? v
                                : `${CUSTOM_UNICODE_MARKER}${JSON.stringify(v)}${CUSTOM_UNICODE_MARKER}`,
                        )
                        .join(","),
                )
                .join(" "),
        };

        this.items = [];
        this.selectors = [];

        return result;
    }

    /**
     * Add custom interaction to the records. The custom interaction is the only interaction which allows
     * you to add mixed (not only numbers) data.
     */
    public addCustom(type: number, data: any) {
        this.items.push([
            RECORD_ITEM_TYPE_CUSTOM,
            new Date().getTime(),
            this.calculateMillisecondsToPrevious(),
            type,
            data,
        ]);
    }

    public toggle(state: boolean) {
        this.items.push([
            state ? RECORD_ITEM_TYPE_START : RECORD_ITEM_TYPE_PAUSE,
            new Date().getTime(),
            this.calculateMillisecondsToPrevious(),
        ]);
    }

    public addClick(selector: string) {
        const calculated = this.calculateMillisecondsToPrevious(
            ([typePrevious]) =>
                [RECORD_ITEM_TYPE_CLICK, RECORD_ITEM_TYPE_FOCUS].indexOf(typePrevious) > -1
                    ? // No one can click so fast, the click is surely programmatically added and we do not want to record that
                      2
                    : false,
            () => {
                // Silence is golden.
            },
        );

        if (calculated > -1) {
            this.items.push([RECORD_ITEM_TYPE_CLICK, new Date().getTime(), calculated, this.ensureSelector(selector)]);
        }

        this.garbageCollect();
    }

    public addFocusIn(selector: string) {
        this.items.push([
            RECORD_ITEM_TYPE_FOCUS,
            new Date().getTime(),
            this.calculateMillisecondsToPrevious(),
            this.ensureSelector(selector),
        ]);

        this.garbageCollect();
    }

    public addScroll(selector: string, scrollTop: number) {
        const selectorIdx = this.ensureSelector(selector);
        const calculated = this.calculateMillisecondsToPrevious(
            ([typePrevious, , , selectorIdxPrevious, scrollTopPrevious], secondLast) => {
                const previousIsSame = typePrevious === RECORD_ITEM_TYPE_SCROLL && selectorIdxPrevious === selectorIdx;

                if (!previousIsSame) {
                    return false;
                } else if (!secondLast) {
                    return SCROLL_THRESHOLD_NEW_RECORD_MS;
                }

                const [typeSecondLast, , , selectorIdxSecondLast, scrollTopSecondLast] = secondLast;
                if (typeSecondLast === RECORD_ITEM_TYPE_SCROLL && selectorIdxPrevious === selectorIdxSecondLast) {
                    // It is definitely the same scroll container, check if direction is the same
                    const directionPrevious = scrollTop > scrollTopPrevious;
                    const directionSecondLast = scrollTopPrevious > scrollTopSecondLast;
                    return directionPrevious === directionSecondLast ? SCROLL_THRESHOLD_NEW_RECORD_MS : false;
                }
                return false;
            },
            (item) => (item[4] = scrollTop),
        );
        if (calculated > -1) {
            this.items.push([RECORD_ITEM_TYPE_SCROLL, new Date().getTime(), calculated, selectorIdx, scrollTop]);
        }

        this.garbageCollect();
    }

    public addResize(width: number, height: number) {
        const calculated = this.calculateMillisecondsToPrevious(
            ([typePrevious, , , widthPrevious, heightPrevious]) =>
                typePrevious === RECORD_ITEM_TYPE_RESIZE
                    ? widthPrevious === width && heightPrevious === height
                        ? true
                        : RESIZE_THRESHOLD_NEW_RECORD_MS
                    : false,
            (item) => {
                item[3] = width;
                item[4] = height;
            },
        );
        if (calculated > -1) {
            this.items.push([RECORD_ITEM_TYPE_RESIZE, new Date().getTime(), calculated, width, height]);
        }

        this.garbageCollect();
    }

    public ensureSelector(selector: string) {
        const { selectors } = this;
        const idx = selectors.indexOf(selector);
        return idx === -1 ? selectors.push(selector) - 1 : idx;
    }

    /**
     * Calculate the milliseconds to the previous recorded interaction.
     *
     * Returns `-1` when the previous one was modified instead of a new record was created.
     */
    public calculateMillisecondsToPrevious(
        considerReplaceWhen?: (previous: RecordItem, secondLast: RecordItem | undefined) => boolean | number,
        replaceData?: (previous: RecordItem) => void,
    ) {
        const { items } = this;
        if (items.length === 0) {
            return 0;
        }

        const lastItem = items[items.length - 1];
        const secondLast = items[items.length - 2];
        const [, previousCurrentTime] = lastItem;
        const delay = new Date().getTime() - previousCurrentTime;

        const expectedDelay = considerReplaceWhen?.(lastItem, secondLast);
        if (expectedDelay && (typeof expectedDelay === "number" ? delay < expectedDelay : true)) {
            lastItem[1] += delay;
            replaceData(lastItem);
            return -1;
        }

        return delay;
    }

    /**
     * Remove unnecessary data which is no longer needed to calculate new items.
     */
    private garbageCollect() {
        const { items } = this;

        // Remove all timestamps as no longer processed
        for (let i = 0; i < items.length; i++) {
            if (i < items.length - 2) {
                items[i][1] = 0;
            }
        }
    }
}

export {
    Records,
    type ReplayItem,
    type RecordItem,
    CUSTOM_UNICODE_MARKER,
    RECORD_ITEM_TYPE_START,
    RECORD_ITEM_TYPE_SCROLL,
    RECORD_ITEM_TYPE_CLICK,
    RECORD_ITEM_TYPE_PAUSE,
    RECORD_ITEM_TYPE_CUSTOM,
    RECORD_ITEM_TYPE_RESIZE,
    RECORD_ITEM_TYPE_FOCUS,
};
