// Copyright 2024 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { SnapshotOperation, __globalSnapshotPatch } from './lifecycle/patch/snapshotPatch.js';
import { ListUpdateInfoRecording } from './listUpdateInfo.js';
import { __pendingListUpdates } from './pendingListUpdates.js';
import { DynamicPartType } from './snapshot/dynamicPartType.js';
import { snapshotDestroyList } from './snapshot/list.js';
import { unref } from './snapshot/ref.js';
import { isDirectOrDeepEqual } from './utils.js';
export let __page;
export let __pageId = 0;
export function setupPage(page) {
    __page = page;
    __pageId = __GetElementUniqueID(page);
}
export function clearPage() {
    __page = undefined;
    __pageId = 0;
}
export const __DynamicPartChildren_0 = [[DynamicPartType.Children, 0]];
export const snapshotManager = {
    values: /* @__PURE__ */ new Map([
        [
            'root',
            {
                create() {
                    /* v8 ignore start */
                    if (__JS__ && !__DEV__) {
                        return [];
                    }
                    /* v8 ignore stop */
                    return [__page];
                },
                update: [],
                slot: __DynamicPartChildren_0,
                isListHolder: false,
                cssId: 0,
            },
        ],
        [
            'wrapper',
            {
                create() {
                    /* v8 ignore start */
                    if (__JS__ && !__DEV__) {
                        return [];
                    }
                    /* v8 ignore stop */
                    return [__CreateWrapperElement(__pageId)];
                },
                update: [],
                slot: __DynamicPartChildren_0,
                isListHolder: false,
            },
        ],
        [
            null,
            {
                create() {
                    /* v8 ignore start */
                    if (__JS__ && !__DEV__) {
                        return [];
                    }
                    /* v8 ignore stop */
                    return [__CreateRawText('')];
                },
                update: [
                    ctx => {
                        /* v8 ignore start */
                        if (__JS__ && !__DEV__) {
                            return;
                        }
                        /* v8 ignore stop */
                        if (ctx.__elements) {
                            __SetAttribute(ctx.__elements[0], 'text', ctx.__values[0]);
                        }
                    },
                ],
                slot: [],
                isListHolder: false,
            },
        ],
    ]),
};
export const snapshotInstanceManager = {
    nextId: 0,
    values: /* @__PURE__ */ new Map(),
    clear() {
        // not resetting `nextId` to prevent id collision
        this.values.clear();
    },
};
export let snapshotCreatorMap = {};
if (__DEV__ && __JS__) {
    snapshotCreatorMap = new Proxy(snapshotCreatorMap, {
        set(target, prop, value) {
            // `__globalSnapshotPatch` does not exist before hydration,
            // so the snapshot of the first screen will not be sent to the main thread.
            if (__globalSnapshotPatch) {
                __globalSnapshotPatch.push(SnapshotOperation.DEV_ONLY_AddSnapshot, prop, 
                // We use `Function.prototype.toString` to serialize the `() => createSnapshot()` function for main thread.
                // This allows the updates to be applied to main thread.
                value.toString());
            }
            target[prop] = value;
            return true;
        },
    });
}
export const backgroundSnapshotInstanceManager = {
    nextId: 0,
    values: /* @__PURE__ */ new Map(),
    clear() {
        // not resetting `nextId` to prevent id collision
        this.values.clear();
    },
    updateId(id, newId) {
        const values = this.values;
        const si = values.get(id);
        // For PreactDevtools, on first hydration,
        // PreactDevtools can get the real snapshot instance id in main-thread
        if (__DEV__) {
            lynx.getJSModule('GlobalEventEmitter').emit('onBackgroundSnapshotInstanceUpdateId', [
                {
                    backgroundSnapshotInstance: si,
                    oldId: id,
                    newId,
                },
            ]);
        }
        values.delete(id);
        values.set(newId, si);
        si.__id = newId;
    },
    getValueBySign(str) {
        const res = str?.split(':');
        if (!res || (res.length != 2 && res.length != 3)) {
            throw new Error('Invalid ctx format: ' + str);
        }
        const id = Number(res[0]);
        const expIndex = Number(res[1]);
        const ctx = this.values.get(id);
        if (!ctx) {
            return null;
        }
        const spreadKey = res[2];
        if (res[1] === '__extraProps') {
            if (spreadKey) {
                return ctx.__extraProps[spreadKey];
            }
            throw new Error('unreachable');
        }
        else {
            if (spreadKey) {
                return ctx.__values[expIndex][spreadKey];
            }
            else {
                return ctx.__values[expIndex];
            }
        }
    },
};
export function entryUniqID(uniqID, entryName) {
    return entryName ? `${entryName}:${uniqID}` : uniqID;
}
export function createSnapshot(uniqID, create, update, slot, cssId, entryName, refAndSpreadIndexes, isLazySnapshotSupported = false) {
    if (!isLazySnapshotSupported) {
        uniqID = entryUniqID(uniqID, entryName);
    }
    const s = { create, update, slot, cssId, entryName, refAndSpreadIndexes };
    snapshotManager.values.set(uniqID, s);
    if (slot && slot[0] && slot[0][0] === DynamicPartType.ListChildren) {
        s.isListHolder = true;
    }
    return uniqID;
}
export function traverseSnapshotInstance(si, callback) {
    const c = si.childNodes;
    callback(si);
    for (const vv of c) {
        traverseSnapshotInstance(vv, callback);
    }
}
const DEFAULT_ENTRY_NAME = '__Card__';
const DEFAULT_CSS_ID = 0;
/**
 * The runtime instance of a {@link Snapshot} on the main thread that manages
 * the actual elements and handles updates to dynamic parts.
 *
 * This class is designed to be compatible with Preact's {@link ContainerNode}
 * interface for Preact's renderer to operate upon.
 */
export class SnapshotInstance {
    type;
    __id;
    __snapshot_def;
    __elements;
    __element_root;
    __values;
    __current_slot_index = 0;
    __worklet_ref_set;
    __listItemPlatformInfo;
    __extraProps;
    constructor(type, id) {
        this.type = type;
        // Suspense uses 'div'
        if (!snapshotManager.values.has(type) && type !== 'div') {
            if (snapshotCreatorMap[type]) {
                snapshotCreatorMap[type](type);
            }
            else {
                throw new Error('Snapshot not found: ' + type);
            }
        }
        this.__snapshot_def = snapshotManager.values.get(type);
        id ??= snapshotInstanceManager.nextId -= 1;
        this.__id = id;
        snapshotInstanceManager.values.set(id, this);
    }
    ensureElements() {
        const { create, slot, isListHolder, cssId, entryName } = this.__snapshot_def;
        const elements = create(this);
        this.__elements = elements;
        this.__element_root = elements[0];
        if (cssId === undefined) {
            // This means either:
            //   CSS Scope is removed(We only need to call `__SetCSSId` when there is `entryName`)
            //   Or an old bundle(`__SetCSSId` is called in `create`), we skip calling `__SetCSSId`
            if (entryName !== DEFAULT_ENTRY_NAME && entryName !== undefined) {
                __SetCSSId(this.__elements, DEFAULT_CSS_ID, entryName);
            }
        }
        else {
            // cssId !== undefined
            if (entryName !== DEFAULT_ENTRY_NAME && entryName !== undefined) {
                // For lazy bundle, we need add `entryName` to the third params
                __SetCSSId(this.__elements, cssId, entryName);
            }
            else {
                __SetCSSId(this.__elements, cssId);
            }
        }
        __pendingListUpdates.runWithoutUpdates(() => {
            const values = this.__values;
            if (values) {
                this.__values = undefined;
                this.setAttribute('values', values);
            }
        });
        if (isListHolder) {
            // never recurse into list's children
            // In nested list scenarios, there are some `list` that are lazily created.
            // We need to `flush` them during `ensureElements`.
            // Also, `flush` is a safe operation since it checks if the `list` is in `__pendingListUpdates`.
            if (__pendingListUpdates.values && !__pendingListUpdates.values[this.__id] && this.__firstChild !== null) {
                let child = this.__firstChild;
                while (child) {
                    (__pendingListUpdates.values[this.__id] ??= new ListUpdateInfoRecording(this)).onInsertBefore(child);
                    child = child.__nextSibling;
                }
            }
            __pendingListUpdates.flushWithId(this.__id);
        }
        else {
            let index = 0;
            let child = this.__firstChild;
            while (child) {
                child.ensureElements();
                const [type, elementIndex] = slot[index];
                switch (type) {
                    case DynamicPartType.Slot: {
                        __ReplaceElement(child.__element_root, elements[elementIndex]);
                        elements[elementIndex] = child.__element_root;
                        index++;
                        break;
                    }
                    /* v8 ignore start */
                    case DynamicPartType.MultiChildren: {
                        if (__GetTag(elements[elementIndex]) === 'wrapper') {
                            __ReplaceElement(child.__element_root, elements[elementIndex]);
                        }
                        else {
                            __AppendElement(elements[elementIndex], child.__element_root);
                        }
                        index++;
                        break;
                    }
                    /* v8 ignore end */
                    case DynamicPartType.Children:
                    case DynamicPartType.ListChildren: {
                        __AppendElement(elements[elementIndex], child.__element_root);
                        break;
                    }
                }
                child = child.__nextSibling;
            }
        }
    }
    unRenderElements() {
        const { isListHolder } = this.__snapshot_def;
        this.__elements = undefined;
        this.__element_root = undefined;
        if (isListHolder) {
            // never recurse into list's children
        }
        else {
            let child = this.__firstChild;
            while (child) {
                child.unRenderElements();
                child = child.__nextSibling;
            }
        }
    }
    takeElements() {
        const a = Object.create(SnapshotInstance.prototype);
        a.__id = this.__id;
        a.__snapshot_def = this.__snapshot_def;
        a.__values = this.__values;
        // all clear
        a.__parent = null;
        a.__firstChild = null;
        a.__lastChild = null;
        a.__nextSibling = null;
        a.__previousSibling = null;
        this.childNodes.map(c => c.takeElements()).forEach(node => a.__insertBefore(node));
        a.__elements = this.__elements;
        a.__element_root = this.__element_root;
        this.__elements = undefined;
        this.__element_root = undefined;
        return a;
    }
    tearDown() {
        traverseSnapshotInstance(this, v => {
            v.__parent = null;
            v.__previousSibling = null;
            v.__nextSibling = null;
        });
    }
    // onCreate?: () => void;
    // onAttach?: () => void;
    // onDetach?: () => void;
    // onRef?: () => void;
    // onUnref?: () => void;
    __parent = null;
    __firstChild = null;
    __lastChild = null;
    __previousSibling = null;
    __nextSibling = null;
    get parentNode() {
        return this.__parent;
    }
    get nextSibling() {
        return this.__nextSibling;
    }
    // get isConnected() {
    //   return !!this.__parent;
    // }
    contains(child) {
        return child.parentNode === this;
    }
    get childNodes() {
        const nodes = [];
        let node = this.__firstChild;
        while (node) {
            nodes.push(node);
            node = node.__nextSibling;
        }
        return nodes;
    }
    __insertBefore(node, beforeNode) {
        // If the node already has a parent, remove it from its current parent
        if (node.__parent) {
            node.__parent.__removeChild(node);
        }
        // If beforeNode is not provided, add the new node as the last child
        if (beforeNode) {
            // If beforeNode is provided, insert the new node before beforeNode
            if (beforeNode.__previousSibling) {
                beforeNode.__previousSibling.__nextSibling = node;
                node.__previousSibling = beforeNode.__previousSibling;
            }
            else {
                this.__firstChild = node;
                node.__previousSibling = null;
            }
            beforeNode.__previousSibling = node;
            node.__nextSibling = beforeNode;
            node.__parent = this;
        }
        else {
            if (this.__lastChild) {
                this.__lastChild.__nextSibling = node;
                node.__previousSibling = this.__lastChild;
            }
            else {
                this.__firstChild = node;
                node.__previousSibling = null;
            }
            this.__lastChild = node;
            node.__parent = this;
            node.__nextSibling = null;
        }
    }
    __removeChild(node) {
        if (node.__parent !== this) {
            throw new Error('The node to be removed is not a child of this node.');
        }
        if (node.__previousSibling) {
            node.__previousSibling.__nextSibling = node.__nextSibling;
        }
        else {
            this.__firstChild = node.__nextSibling;
        }
        if (node.__nextSibling) {
            node.__nextSibling.__previousSibling = node.__previousSibling;
        }
        else {
            this.__lastChild = node.__previousSibling;
        }
        node.__parent = null;
        node.__previousSibling = null;
        node.__nextSibling = null;
    }
    insertBefore(newNode, existingNode) {
        const __snapshot_def = this.__snapshot_def;
        if (__snapshot_def.isListHolder) {
            if (__pendingListUpdates.values) {
                (__pendingListUpdates.values[this.__id] ??= new ListUpdateInfoRecording(this)).onInsertBefore(newNode, existingNode);
            }
            this.__insertBefore(newNode, existingNode);
            return;
        }
        const shouldRemove = newNode.__parent === this;
        this.__insertBefore(newNode, existingNode);
        const __elements = this.__elements;
        if (__elements) {
            if (!newNode.__elements) {
                newNode.ensureElements();
            }
        }
        else {
            return;
        }
        const count = __snapshot_def.slot.length;
        if (count === 1) {
            const [, elementIndex] = __snapshot_def.slot[0];
            const parent = __elements[elementIndex];
            if (shouldRemove) {
                __RemoveElement(parent, newNode.__element_root);
            }
            if (existingNode) {
                __InsertElementBefore(parent, newNode.__element_root, existingNode.__element_root);
            }
            else {
                __AppendElement(parent, newNode.__element_root);
            }
        }
        else if (count > 1) {
            const index = this.__current_slot_index++;
            const [s, elementIndex] = __snapshot_def.slot[index];
            if (s === DynamicPartType.Slot) {
                __ReplaceElement(newNode.__element_root, __elements[elementIndex]);
                __elements[elementIndex] = newNode.__element_root;
                /* v8 ignore start */
            }
            else if (s === DynamicPartType.MultiChildren) {
                if (__GetTag(__elements[elementIndex]) === 'wrapper') {
                    __ReplaceElement(newNode.__element_root, __elements[elementIndex]);
                }
                else {
                    __AppendElement(__elements[elementIndex], newNode.__element_root);
                }
            }
            /* v8 ignore end */
        }
    }
    removeChild(child) {
        const __snapshot_def = this.__snapshot_def;
        if (__snapshot_def.isListHolder) {
            if (__pendingListUpdates.values) {
                (__pendingListUpdates.values[this.__id] ??= new ListUpdateInfoRecording(this)).onRemoveChild(child);
            }
            this.__removeChild(child);
            traverseSnapshotInstance(child, v => {
                snapshotInstanceManager.values.delete(v.__id);
            });
            // mark this child as deleted
            child.__id = 0;
            return;
        }
        unref(child, true);
        if (this.__elements) {
            const [, elementIndex] = __snapshot_def.slot[0];
            __RemoveElement(this.__elements[elementIndex], child.__element_root);
        }
        if (child.__snapshot_def.isListHolder) {
            snapshotDestroyList(child);
        }
        this.__removeChild(child);
        traverseSnapshotInstance(child, v => {
            v.__parent = null;
            v.__previousSibling = null;
            v.__nextSibling = null;
            delete v.__elements;
            delete v.__element_root;
            snapshotInstanceManager.values.delete(v.__id);
        });
    }
    setAttribute(key, value) {
        if (key === 'values') {
            const oldValues = this.__values;
            const values = value;
            this.__values = values;
            if (oldValues) {
                for (let index = 0; index < values.length; index++) {
                    this.callUpdateIfNotDirectOrDeepEqual(index, oldValues[index], values[index]);
                }
            }
            else {
                for (let index = 0; index < values.length; index++) {
                    this.callUpdateIfNotDirectOrDeepEqual(index, undefined, values[index]);
                }
            }
            return;
        }
        if (typeof key === 'string') {
            // for more flexible usage, we allow setting non-indexed attributes
            (this.__extraProps ??= {})[key] = value;
            return;
        }
        this.__values ??= [];
        this.callUpdateIfNotDirectOrDeepEqual(key, this.__values[key], this.__values[key] = value);
    }
    toJSON() {
        return {
            id: this.__id,
            type: this.type,
            values: this.__values,
            extraProps: this.__extraProps,
            children: this.__firstChild ? this.childNodes : undefined,
        };
    }
    callUpdateIfNotDirectOrDeepEqual(index, oldValue, newValue) {
        if (isDirectOrDeepEqual(oldValue, newValue)) { }
        else {
            this.__snapshot_def.update[index](this, index, oldValue);
        }
    }
}
//# sourceMappingURL=snapshot.js.map