// 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 { DEFAULT_CSS_ID, DEFAULT_ENTRY_NAME } from './constants.js';
import { createRuntimeSnapshot, snapshotManager } from './definition.js';
import { DynamicPartType, __DynamicPartChildren_0 } from './dynamicPartType.js';
import { snapshotDestroyList } from './list.js';
import { unref } from './ref.js';
import { isCompiledSnapshot, traverseSnapshotInstance } from './utils.js';
import { isDirectOrDeepEqual } from '../../utils.js';
import { clearSnapshotVNodeSource } from '../debug/vnodeSource.js';
import { SnapshotOperation, __globalSnapshotPatch } from '../lifecycle/patch/snapshotPatch.js';
import { ListUpdateInfoRecording } from '../list/listUpdateInfo.js';
import { __pendingListUpdates } from '../list/pendingListUpdates.js';
export const snapshotInstanceManager = {
    nextId: 0,
    values: /* @__PURE__ */ new Map(),
    clear() {
        // not resetting `nextId` to prevent id collision
        this.values.clear();
        if (__DEV__) {
            clearSnapshotVNodeSource();
        }
    },
};
function isRemovedSnapshot(value, removedSnapshots) {
    return (typeof value === 'object' || typeof value === 'function') && value !== null
        && removedSnapshots.has(value);
}
function clearRemovedSnapshotsFromArray(values, removedSnapshots, seen = new WeakSet()) {
    if (seen.has(values)) {
        return false;
    }
    seen.add(values);
    let changed = false;
    values.forEach((value, index) => {
        if (isRemovedSnapshot(value, removedSnapshots)) {
            values[index] = undefined;
            changed = true;
        }
        else if (Array.isArray(value) && clearRemovedSnapshotsFromArray(value, removedSnapshots, seen)) {
            changed = true;
        }
    });
    return changed;
}
function collectRemovedSnapshots(removedChild) {
    const removedSnapshots = new WeakSet();
    traverseSnapshotInstance(removedChild, snapshot => {
        removedSnapshots.add(snapshot);
    });
    return removedSnapshots;
}
function clearTransientChildPropRefs(owner, removedSnapshots) {
    const props = owner.props;
    if (!props) {
        return;
    }
    for (const key of Object.keys(props)) {
        // Named child props are transient staging refs. Once a child subtree is
        // removed, they must not keep the old snapshots alive.
        if (!key.startsWith('$')) {
            continue;
        }
        const value = props[key];
        if (isRemovedSnapshot(value, removedSnapshots)) {
            delete props[key];
        }
        else if (Array.isArray(value)) {
            clearRemovedSnapshotsFromArray(value, removedSnapshots);
        }
    }
}
export let snapshotCreatorMap = {};
if (__DEV__ && __JS__) {
    snapshotCreatorMap = new Proxy(snapshotCreatorMap, {
        set(target, prop, value) {
            if (
            // `__globalSnapshotPatch` does not exist before hydration,
            // so the snapshot of the first screen will not be sent to the main thread.
            __globalSnapshotPatch
                // `prop` will be `https://example.com/main.lynx.bundle:__snapshot_835da_eff1e_1` when loading a standalone lazy bundle after hydration.
                && !prop.includes(':')) {
                __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;
        },
    });
}
/**
 * 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 for dynamic parts
    // only increment when inserting dynamic parts
    // when removing dynamic parts, the slot index will not change
    // cause there would be a wrapper to keep the slot index stable
    __current_slot_index = 0;
    __worklet_ref_set;
    __listItemPlatformInfo;
    __extraProps;
    __slotIndex = 0;
    constructor(type, id) {
        this.type = type;
        // Suspense uses 'div'
        if (!snapshotManager.values.has(type) && type !== 'div') {
            if (snapshotCreatorMap[type]) {
                snapshotCreatorMap[type](type);
            }
            else if (isCompiledSnapshot(type)) {
                let message = 'Snapshot not found: ' + type;
                if (__DEV__) {
                    message +=
                        '. You can set environment variable `REACT_ALOG=true` and restart your dev server for troubleshooting.';
                }
                throw new Error(message);
            }
            else {
                createRuntimeSnapshot(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);
            }
        }
        if (entryName !== DEFAULT_ENTRY_NAME && entryName !== undefined && this.parentNode
            && entryName !== this.parentNode.__snapshot_def.entryName) {
            __SetAttribute(this.__element_root, 'lazy-bundle-url', this.__snapshot_def.entryName);
        }
        __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[this.__snapshot_def.isSlotV2 ? child.__slotIndex : 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:
                    case DynamicPartType.SlotV2:
                    case DynamicPartType.ListSlotV2: {
                        __AppendElement(elements[elementIndex], child.__element_root);
                        break;
                    }
                    default:
                        throw new Error('Unexpected slot type: ' + type);
                }
                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;
        a.__slotIndex = this.__slotIndex;
        // 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 || __snapshot_def.isSlotV2) {
            const [, elementIndex] = __snapshot_def.slot[typeof newNode.__slotIndex === 'number' ? newNode.__slotIndex : 0];
            const parent = __elements[elementIndex];
            if (shouldRemove) {
                __RemoveElement(parent, newNode.__element_root);
            }
            if (existingNode) {
                // SlotV2: each slot has its own wrapper. `existingNode` may live in a
                // different wrapper — `insertBefore(node, ref)` across wrappers throws,
                // so fall back to `append` (DOM auto-detaches the node from old parent).
                if (__snapshot_def.isSlotV2
                    && newNode.__slotIndex !== existingNode.__slotIndex) {
                    __AppendElement(parent, newNode.__element_root);
                }
                else {
                    __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;
        const removedSnapshots = collectRemovedSnapshots(child);
        clearTransientChildPropRefs(this, removedSnapshots);
        if (__snapshot_def.isListHolder) {
            if (__pendingListUpdates.values) {
                (__pendingListUpdates.values[this.__id] ??= new ListUpdateInfoRecording(this)).onRemoveChild(child);
            }
            this.__removeChild(child);
            traverseSnapshotInstance(child, v => {
                clearTransientChildPropRefs(v, removedSnapshots);
                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[typeof child.__slotIndex === 'number' ? child.__slotIndex : 0];
            __RemoveElement(this.__elements[elementIndex], child.__element_root);
        }
        this.__removeChild(child);
        traverseSnapshotInstance(child, v => {
            if (v.__snapshot_def.isListHolder) {
                snapshotDestroyList(v);
            }
            clearTransientChildPropRefs(v, removedSnapshots);
            v.__parent = null;
            v.__previousSibling = null;
            v.__nextSibling = null;
            delete v.__elements;
            delete v.__element_root;
            snapshotInstanceManager.values.delete(v.__id);
        });
    }
    // remove all children from start or this.__firstChild
    removeChildren(start = this.__firstChild) {
        let nodeToRemove = start;
        while (nodeToRemove) {
            const next = nodeToRemove.__nextSibling;
            this.removeChild(nodeToRemove);
            nodeToRemove = next;
        }
    }
    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() {
        const json = {
            id: this.__id,
            type: this.type,
            values: this.__values,
            extraProps: this.__extraProps,
            children: this.__firstChild ? this.childNodes : undefined,
        };
        // To save serialize time, we only serialize slotIndex if it is not 0
        if (this.__slotIndex > 0) {
            json.slotIndex = this.__slotIndex;
        }
        return json;
    }
    callUpdateIfNotDirectOrDeepEqual(index, oldValue, newValue) {
        if (isDirectOrDeepEqual(oldValue, newValue)) { }
        else {
            this.__snapshot_def.update[index](this, index, oldValue);
        }
    }
}
//# sourceMappingURL=snapshot.js.map