// 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 { createRuntimeSnapshot, snapshotManager } from './definition.js';
import { DynamicPartType } from './dynamicPartType.js';
import { reconstructInstanceTree } from './reconstructInstanceTree.js';
import { applyRef, clearQueuedRefs, getRefFromValue, queueRefAttrUpdate } from './ref.js';
import { snapshotCreatorMap } from './snapshot.js';
import { hydrationMap } from './snapshotInstanceHydrationMap.js';
import { transformSpread } from './spread.js';
import { isCompiledSnapshot, traverseSnapshotInstance } from './utils.js';
import { profileEnd, profileStart } from '../../shared/profile.js';
import { isDirectOrDeepEqual } from '../../utils.js';
import { clearSnapshotVNodeSource, getSnapshotVNodeSource, moveSnapshotVNodeSource } from '../debug/vnodeSource.js';
import { prepareGestureForCommit } from '../gesture/processGestureBagkround.js';
import { globalBackgroundSnapshotInstancesToRemove } from '../lifecycle/patch/globalState.js';
import { SnapshotOperation, __globalSnapshotPatch, initGlobalSnapshotPatch, takeGlobalSnapshotPatch, } from '../lifecycle/patch/snapshotPatch.js';
import { globalPipelineOptions } from '../lynx/performance.js';
import { clearPendingPortalInsertBefore } from '../lynx/portalsPending.js';
import { diffArrayAction, diffArrayLepus } from '../renderToOpcodes/hydrate.js';
import { onPostWorkletCtx } from '../worklet/ctx.js';
/**
 * Background snapshot instance manager that manages all background snapshot instances.
 */
export const backgroundSnapshotInstanceManager = {
    nextId: 0,
    values: /* @__PURE__ */ new Map(),
    clear() {
        // not resetting `nextId` to prevent id collision
        this.values.clear();
        if (__DEV__) {
            clearSnapshotVNodeSource();
        }
    },
    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__ && __BACKGROUND__) {
            lynx.getJSModule('GlobalEventEmitter').emit('onBackgroundSnapshotInstanceUpdateId', [
                {
                    backgroundSnapshotInstance: si,
                    oldId: id,
                    newId,
                },
            ]);
        }
        values.delete(id);
        values.set(newId, si);
        si.__id = newId;
        if (__DEV__) {
            moveSnapshotVNodeSource(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;
        }
        /**
         * 1. normal event
         *  `${ctx.__id}:${expIndex}:${spreadKey}`
         * 2. defer list event
         *   ${ctx.__id}:__extraProps:onRecycleComponent`
         *   ${ctx.__id}:__extraProps:onComponentAtIndex`
         */
        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];
            }
        }
    },
};
function prepareWorkletForCommit(worklet) {
    // Copy-on-commit: do not mutate the background-side worklet ctx.
    // `_execId` is injected into the payload object that will be sent to the main thread.
    return onPostWorkletCtx({ ...worklet });
}
function prepareSpreadForCommit(spread, oldSpread) {
    let committed;
    for (const key in spread) {
        const v = spread[key];
        if (key === '__lynx_timing_flag' && oldSpread?.[key] != v && globalPipelineOptions) {
            globalPipelineOptions.needTimestamps = true;
        }
        if (!v || typeof v !== 'object') {
            continue;
        }
        const valueRecord = v;
        let committedValue;
        if ('_wkltId' in valueRecord) {
            committedValue = prepareWorkletForCommit(v);
        }
        else if ('__isGesture' in valueRecord) {
            committedValue = prepareGestureForCommit(v);
        }
        else {
            continue;
        }
        committed ??= { ...spread };
        committed[key] = committedValue;
    }
    return committed ?? spread;
}
export class BackgroundSnapshotInstance {
    type;
    constructor(type) {
        this.type = type;
        // Suspense uses 'div'
        if (!snapshotManager.values.has(type) && type !== 'div') {
            if (snapshotCreatorMap[type]) {
                snapshotCreatorMap[type](type);
            }
            else if (isCompiledSnapshot(type)) {
                throw new Error('BackgroundSnapshot not found: ' + type);
            }
            else {
                // add runtime snapshot to backgroundSnapshotInstanceManager
                createRuntimeSnapshot(type);
            }
        }
        this.__snapshot_def = snapshotManager.values.get(type);
        const id = this.__id = backgroundSnapshotInstanceManager.nextId += 1;
        backgroundSnapshotInstanceManager.values.set(id, this);
        __globalSnapshotPatch?.push(SnapshotOperation.CreateElement, type, id);
    }
    __id;
    __values;
    __snapshot_def;
    __extraProps;
    __slotIndex = 0;
    __parent = null;
    __firstChild = null;
    __lastChild = null;
    __previousSibling = null;
    __nextSibling = null;
    __removed_from_tree;
    get parentNode() {
        return this.__parent;
    }
    get nextSibling() {
        return this.__nextSibling;
    }
    // get isConnected() {
    //   return !!this.__parent;
    // }
    // contains(child: BackgroundSnapshotInstance): boolean {
    //   return child.parentNode === this;
    // }
    // This will be called in `lazy`/`Suspense`.
    appendChild(child) {
        return this.insertBefore(child);
    }
    insertBefore(node, beforeNode) {
        if (node.__removed_from_tree) {
            node.__removed_from_tree = false;
            // This is only called by `lazy`/`Suspense` through `appendChild` so beforeNode is always undefined.
            /* v8 ignore next */
            reconstructInstanceTree([node], this.__id, beforeNode?.__id);
        }
        else {
            __globalSnapshotPatch?.push(SnapshotOperation.InsertBefore, this.__id, node.__id, beforeNode?.__id, node.__slotIndex);
        }
        // If the node already has a parent, remove it from its current parent
        const p = node.__parent;
        if (p) {
            if (node.__previousSibling) {
                node.__previousSibling.__nextSibling = node.__nextSibling;
            }
            else {
                p.__firstChild = node.__nextSibling;
            }
            if (node.__nextSibling) {
                node.__nextSibling.__previousSibling = node.__previousSibling;
            }
            else {
                p.__lastChild = node.__previousSibling;
            }
        }
        // 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) {
        __globalSnapshotPatch?.push(SnapshotOperation.RemoveChild, this.__id, node.__id);
        node.__removed_from_tree = true;
        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;
        queueRefAttrUpdate(() => {
            traverseSnapshotInstance(node, v => {
                if (v.__values) {
                    v.__snapshot_def.refAndSpreadIndexes?.forEach((i) => {
                        const value = v.__values[i];
                        if (value && (typeof value === 'object' || typeof value === 'function')) {
                            if ('__spread' in value && 'ref' in value && value.ref) {
                                applyRef(value.ref, null);
                            }
                            else if ('__ref' in value) {
                                applyRef(value, null);
                            }
                        }
                    });
                }
            });
        }, null, 0, 0);
        globalBackgroundSnapshotInstancesToRemove.push(node.__id);
    }
    tearDown() {
        traverseSnapshotInstance(this, v => {
            v.__parent = null;
            v.__previousSibling = null;
            v.__nextSibling = null;
            backgroundSnapshotInstanceManager.values.delete(v.__id);
        });
    }
    get childNodes() {
        const nodes = [];
        let node = this.__firstChild;
        while (node) {
            nodes.push(node);
            node = node.__nextSibling;
        }
        return nodes;
    }
    setAttribute(key, value) {
        if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
            profileStart('ReactLynx::BSI::setAttribute');
        }
        if (key === 'values') {
            if (__globalSnapshotPatch) {
                const oldValues = this.__values;
                if (oldValues) {
                    for (let index = 0; index < value.length; index++) {
                        const { needUpdate, valueToCommit } = this.setAttributeImpl(value[index], oldValues[index], index);
                        if (needUpdate) {
                            __globalSnapshotPatch.push(SnapshotOperation.SetAttribute, this.__id, index, valueToCommit);
                        }
                    }
                }
                else {
                    const patch = [];
                    const length = value.length;
                    for (let index = 0; index < length; ++index) {
                        const { valueToCommit } = this.setAttributeImpl(value[index], null, index);
                        patch[index] = valueToCommit;
                    }
                    __globalSnapshotPatch.push(SnapshotOperation.SetAttributes, this.__id, patch);
                }
            }
            else {
                this.__snapshot_def.refAndSpreadIndexes?.forEach((index) => {
                    // In first render, this.__values is undefined.
                    // In next rerenders before hydration, this.__values is not undefined.
                    const oldValue = this.__values?.[index];
                    const v = value[index];
                    queueRefAttrUpdate(getRefFromValue(oldValue), getRefFromValue(v), this.__id, index);
                });
            }
            this.__values = value;
            if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
                profileEnd();
            }
            return;
        }
        if (typeof key === 'string') {
            (this.__extraProps ??= {})[key] = value;
        }
        else {
            // old path (`this.setAttribute(0, xxx)`)
            // is reserved as slow path
            (this.__values ??= [])[key] = value;
        }
        __globalSnapshotPatch?.push(SnapshotOperation.SetAttribute, this.__id, key, value);
        if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
            profileEnd();
        }
    }
    setAttributeImpl(newValue, oldValue, index) {
        if (!newValue) {
            // `oldValue` can't be a spread.
            if (oldValue && typeof oldValue === 'object' && '__ref' in oldValue) {
                queueRefAttrUpdate(oldValue, null, this.__id, index);
            }
            return { needUpdate: oldValue !== newValue, valueToCommit: newValue };
        }
        const newType = typeof newValue;
        if (newType === 'object') {
            const newValueObj = newValue;
            if ('__spread' in newValueObj) {
                const oldSpread = oldValue?.__spread;
                const newSpread = transformSpread(this, index, newValueObj);
                const needUpdate = !isDirectOrDeepEqual(oldSpread, newSpread);
                // use __spread to cache the transform result for next diff
                newValueObj['__spread'] = newSpread;
                queueRefAttrUpdate(oldSpread && (oldValue.ref), newValueObj['ref'], this.__id, index);
                return {
                    needUpdate,
                    valueToCommit: needUpdate ? prepareSpreadForCommit(newSpread, oldSpread) : newSpread,
                };
            }
            if ('__ref' in newValueObj) {
                queueRefAttrUpdate(oldValue, newValueObj, this.__id, index);
                return { needUpdate: false, valueToCommit: 1 };
            }
            if ('_wkltId' in newValueObj) {
                // Worklet ctx can be stable across rerenders (e.g. memoized by the user).
                // In that case we should NOT re-register / re-send it, otherwise `_execId` churn
                // will cause unnecessary patches.
                const needUpdate = oldValue !== newValue;
                return {
                    needUpdate,
                    valueToCommit: needUpdate ? prepareWorkletForCommit(newValueObj) : newValue,
                };
            }
            if ('__isGesture' in newValueObj) {
                // Gestures are large objects; if the reference is stable, avoid reprocessing and patching.
                const needUpdate = oldValue !== newValue;
                return {
                    needUpdate,
                    valueToCommit: needUpdate
                        ? prepareGestureForCommit(newValueObj)
                        : newValue,
                };
            }
            if ('__ltf' in newValueObj) {
                // __lynx_timing_flag
                if (globalPipelineOptions && oldValue?.__ltf != newValueObj['__ltf']) {
                    globalPipelineOptions.needTimestamps = true;
                    return { needUpdate: true, valueToCommit: newValue };
                }
                return { needUpdate: false, valueToCommit: newValue };
            }
            return { needUpdate: !isDirectOrDeepEqual(oldValue, newValue), valueToCommit: newValue };
        }
        if (newType === 'function') {
            if (newValue.__ref) {
                queueRefAttrUpdate(oldValue, newValue, this.__id, index);
                return { needUpdate: false, valueToCommit: 1 };
            }
            /* event */
            return { needUpdate: !oldValue, valueToCommit: 1 };
        }
        return { needUpdate: oldValue !== newValue, valueToCommit: newValue };
    }
}
export function hydrate(before, after) {
    const shouldProfile = typeof __PROFILE__ !== 'undefined' && __PROFILE__;
    if (shouldProfile) {
        profileStart('ReactLynx::BSI::hydrate');
    }
    try {
        initGlobalSnapshotPatch();
        const helper = (before, after) => {
            hydrationMap.set(after.__id, before.id);
            backgroundSnapshotInstanceManager.updateId(after.__id, before.id);
            // handle value by index
            after.__values?.forEach((value, index) => {
                // render with different root would cause different values length
                const old = before.values?.[index];
                if (value) {
                    if (typeof value === 'object') {
                        if ('__spread' in value) {
                            // `value.__spread` my contain event ids using snapshot ids before hydration. Remove it.
                            delete value.__spread;
                            const __spread = transformSpread(after, index, value);
                            // Cache a clean spread for future diffs. For the patch payload, create a committed copy
                            // with runtime fields (e.g. `_execId`) injected.
                            after.__values[index]['__spread'] = __spread;
                            value = prepareSpreadForCommit(__spread, old);
                        }
                        else if ('__ref' in value) {
                            // skip patch
                            value = old;
                        }
                        else if ('_wkltId' in value) {
                            value = prepareWorkletForCommit(value);
                        }
                        else if ('__isGesture' in value) {
                            value = prepareGestureForCommit(value);
                        }
                    }
                    else if (typeof value === 'function') {
                        if ('__ref' in value) {
                            // skip patch
                            value = old;
                        }
                        else {
                            value = `${after.__id}:${index}:`;
                        }
                    }
                }
                if (!isDirectOrDeepEqual(value, old)) {
                    if (value === undefined && old === null) {
                        // This is a workaround for the case where we set an attribute to `undefined` in the main thread,
                        // but the old value becomes `null` during JSON serialization.
                        // In this case, we should not patch the value.
                    }
                    else {
                        if (shouldProfile) {
                            profileStart('ReactLynx::hydrate::setAttribute', {
                                args: {
                                    id: String(after.__id),
                                    snapshotType: String(after.type),
                                    source: getSnapshotVNodeSource(after.__id) ?? '',
                                    dynamicPartIndex: String(index),
                                    valueType: value === null ? 'null' : typeof value,
                                },
                            });
                            try {
                                __globalSnapshotPatch.push(SnapshotOperation.SetAttribute, after.__id, index, value);
                            }
                            finally {
                                profileEnd();
                            }
                        }
                        else {
                            __globalSnapshotPatch.push(SnapshotOperation.SetAttribute, after.__id, index, value);
                        }
                    }
                }
            });
            // handle extraProps as attributes and set by key
            if (after.__extraProps) {
                for (const key in after.__extraProps) {
                    const value = after.__extraProps[key];
                    const old = before.extraProps?.[key];
                    if (!isDirectOrDeepEqual(value, old)) {
                        if (shouldProfile) {
                            profileStart('ReactLynx::hydrate::setAttribute', {
                                args: {
                                    id: String(after.__id),
                                    snapshotType: String(after.type),
                                    source: getSnapshotVNodeSource(after.__id) ?? '',
                                    dynamicPartIndex: key,
                                    valueType: value === null ? 'null' : typeof value,
                                },
                            });
                            try {
                                __globalSnapshotPatch.push(SnapshotOperation.SetAttribute, after.__id, key, value);
                            }
                            finally {
                                profileEnd();
                            }
                        }
                        else {
                            __globalSnapshotPatch.push(SnapshotOperation.SetAttribute, after.__id, key, value);
                        }
                    }
                }
            }
            const { slot } = after.__snapshot_def;
            const beforeChildNodes = before.children ?? [];
            const afterChildNodes = after.childNodes;
            if (!slot) {
                return;
            }
            slot.forEach(([type], index) => {
                switch (type) {
                    case DynamicPartType.Slot:
                    case DynamicPartType.MultiChildren: {
                        // TODO: the following null assertions are not 100% safe
                        const v1 = beforeChildNodes[index];
                        const v2 = afterChildNodes[index];
                        helper(v1, v2);
                        break;
                    }
                    case DynamicPartType.SlotV2:
                    case DynamicPartType.ListSlotV2:
                    case DynamicPartType.Children:
                    case DynamicPartType.ListChildren: {
                        let filteredBeforeChildNodes = beforeChildNodes;
                        let filteredAfterChildNodes = afterChildNodes;
                        if (type === DynamicPartType.SlotV2 || type === DynamicPartType.ListSlotV2) {
                            filteredBeforeChildNodes = beforeChildNodes.filter(v => (v.slotIndex ?? 0) === index);
                            filteredAfterChildNodes = afterChildNodes.filter(v => v.__slotIndex === index);
                        }
                        const diffResult = diffArrayLepus(filteredBeforeChildNodes, filteredAfterChildNodes, (a, b) => a.type === b.type, (a, b) => {
                            helper(a, b);
                        }, 
                        // Should be `false` in hydrate as SerializedSnapshotInstance has no item-key
                        false);
                        diffArrayAction(filteredBeforeChildNodes, diffResult, (node, target) => {
                            if (shouldProfile) {
                                profileStart('ReactLynx::BSI::reconstructInstanceTree', {
                                    args: {
                                        id: String(node.__id),
                                        snapshotType: String(node.type),
                                        source: getSnapshotVNodeSource(node.__id) ?? '',
                                    },
                                });
                            }
                            try {
                                reconstructInstanceTree([node], before.id, target?.id);
                            }
                            finally {
                                if (shouldProfile) {
                                    profileEnd();
                                }
                            }
                            return undefined;
                        }, node => {
                            if (shouldProfile) {
                                profileStart('ReactLynx::hydrate::removeChild', {
                                    args: {
                                        id: String(node.id),
                                        snapshotType: String(node.type),
                                        source: getSnapshotVNodeSource(node.id) ?? '',
                                        parentId: String(before.id),
                                    },
                                });
                                try {
                                    __globalSnapshotPatch.push(SnapshotOperation.RemoveChild, before.id, node.id);
                                }
                                finally {
                                    profileEnd();
                                }
                            }
                            else {
                                __globalSnapshotPatch.push(SnapshotOperation.RemoveChild, before.id, node.id);
                            }
                        }, (node, target) => {
                            // changedList.push([SnapshotOperation.RemoveChild, before.id, node.id]);
                            if (shouldProfile) {
                                profileStart('ReactLynx::hydrate::insertBefore', {
                                    args: {
                                        id: String(node.id),
                                        snapshotType: String(node.type),
                                        source: getSnapshotVNodeSource(node.id) ?? '',
                                        parentId: String(before.id),
                                        targetId: String(target?.id ?? ''),
                                    },
                                });
                                try {
                                    __globalSnapshotPatch.push(SnapshotOperation.InsertBefore, before.id, node.id, target?.id, node.slotIndex ?? 0);
                                }
                                finally {
                                    profileEnd();
                                }
                            }
                            else {
                                __globalSnapshotPatch.push(SnapshotOperation.InsertBefore, before.id, node.id, target?.id, node.slotIndex ?? 0);
                            }
                        });
                        break;
                    }
                    default:
                        throw new Error('Unexpected slot type: ' + type);
                }
            });
        };
        helper(before, after);
        // Hydration should not trigger ref updates. They were incorrectly triggered when using `setAttribute` to add values to the patch list.
        clearQueuedRefs();
        clearPendingPortalInsertBefore();
        return takeGlobalSnapshotPatch();
    }
    finally {
        if (shouldProfile) {
            profileEnd();
        }
    }
}
//# sourceMappingURL=backgroundSnapshot.js.map