import { Event } from "@clarity-types/data";
import { StyleSheetOperation, StyleSheetState } from "@clarity-types/layout";
import { time } from "@src/core/time";
import { shortid } from "@src/data/metadata";
import encode from "@src/layout/encode";
import { getId } from "@src/layout/dom";
import * as core from "@src/core";
import config from "@src/core/config";
import { getCssRules } from "./node";

export let sheetUpdateState: StyleSheetState[] = [];
export let sheetAdoptionState: StyleSheetState[] = [];
const styleSheetId = '__clrSId';
let styleSheetMap = {};
let styleTimeMap: {[key: string]: number} = {};
let documentNodes = [];
let createdSheetIds = [];

function proxyStyleRules(win: any) {
    if ((config.lean && config.lite) || win === null || win === undefined) {
        return;
    }
    
    win.__clr = win.__clr || {};

    if (win['CSSStyleSheet'] && win.CSSStyleSheet.prototype) {
        proxyStyleMethod(win, "replace", StyleSheetOperation.Replace);
        proxyStyleMethod(win, "replaceSync", StyleSheetOperation.ReplaceSync);
    }   
}

// If we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
// and attached the sheet to a document. This way the timestamp of the style sheet creation will align
// to when it is used in the document rather than potentially being misaligned during the traverse process.
function proxyStyleMethod(win: any, method: string, operation: StyleSheetOperation): void {
    if (win.__clr[method] === undefined) {
        win.__clr[method] = win.CSSStyleSheet.prototype[method];
        win.CSSStyleSheet.prototype[method] = function() {
            if (core.active()) {
                if (createdSheetIds.includes(this[styleSheetId])) {
                    trackStyleChange(time(), this[styleSheetId], operation, arguments[0]);
                }
            }
            return win.__clr[method].apply(this, arguments);
        };
    }
}

export function start(): void {
    proxyStyleRules(window);
}

export function checkDocumentStyles(documentNode: Document, timestamp: number): void {
    if (config.lean && config.lite) { return; }

    if (!documentNodes.includes(documentNode)) {
        documentNodes.push(documentNode);
        if (documentNode.defaultView) {
            proxyStyleRules(documentNode.defaultView);
        }
    }
    timestamp = timestamp || time();
    if (!documentNode?.adoptedStyleSheets) {
        // if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit.
        return;
    }
    let currentStyleSheets: string[] = [];
    for (var styleSheet of documentNode.adoptedStyleSheets) {
        // If we haven't seen this style sheet on this page yet, we create a reference to it for the visualizer.
        // For SPA or times in which Clarity restarts on a given page, our visualizer would lose context
        // on the previously created style sheet for page N-1.
        // Then we synthetically call replaceSync with its contents to bootstrap it
        if (!styleSheet[styleSheetId] || !createdSheetIds.includes(styleSheet[styleSheetId])) {
            styleSheet[styleSheetId] = shortid();
            createdSheetIds.push(styleSheet[styleSheetId]);
            trackStyleChange(timestamp, styleSheet[styleSheetId], StyleSheetOperation.Create);
            trackStyleChange(timestamp, styleSheet[styleSheetId], StyleSheetOperation.ReplaceSync, getCssRules(styleSheet));
        }

        currentStyleSheets.push(styleSheet[styleSheetId]);
    }

    let documentId = getId(documentNode, true);
    if (!styleSheetMap[documentId]) {
        styleSheetMap[documentId] = [];
    }
    if (!arraysEqual(currentStyleSheets, styleSheetMap[documentId])) {
        // Using -1 to signify the root document node as we don't track that as part of our nodeMap
        trackStyleAdoption(timestamp, documentNode == document ? -1 : getId(documentNode), StyleSheetOperation.SetAdoptedStyles, currentStyleSheets);
        styleSheetMap[documentId] = currentStyleSheets;
        styleTimeMap[documentId] = timestamp;
    }
}

export function compute(): void {
    for (var documentNode of documentNodes) {
        var docId = documentNode == document ? -1 : getId(documentNode);
        let ts = docId in styleTimeMap ? styleTimeMap[docId] : null;
        checkDocumentStyles(documentNode, ts);
    }
}

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

export function stop(): void {
    styleSheetMap = {};
    styleTimeMap = {};
    documentNodes = [];
    createdSheetIds = [];
    reset();
}

function trackStyleChange(time: number, id: string, operation: StyleSheetOperation, cssRules?: string): void {
    sheetUpdateState.push({
        time,
        event: Event.StyleSheetUpdate,
        data: {
            id,
            operation,
            cssRules
        }
    });

    encode(Event.StyleSheetUpdate);
}

function trackStyleAdoption(time: number, id: number, operation: StyleSheetOperation, newIds: string[]): void {
    sheetAdoptionState.push({
        time,
        event: Event.StyleSheetAdoption,
        data: {
            id,
            operation,
            newIds
        }
    });

    encode(Event.StyleSheetAdoption);
}

function arraysEqual(a: string[], b: string[]): boolean {
    return a.length === b.length && a.every((v, i) => v === b[i]);
}
