// Copyright 2025 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 { globalCommitContext, markRemovedSubtreeForCurrentCommit, resetGlobalCommitContext } from './commit-context.js';
import { BUILTIN_RAW_TEXT_TEMPLATE_KEY } from './instance.js';
import { backgroundElementTemplateInstanceManager } from './manager.js';
import { isDirectOrDeepEqual } from '../../utils.js';
import { ElementTemplateUpdateOps } from '../protocol/opcodes.js';
export function hydrate(serialized, instance) {
    resetGlobalCommitContext();
    hydrateIntoContext(serialized, instance);
    return globalCommitContext.ops;
}
export function hydrateIntoContext(serialized, instance) {
    hydrateInstance(serialized, instance);
}
function hydrateMatchingChildrenAndDiffSlot(serializedChildren, backgroundChildren) {
    let lastPlacedIndex = 0;
    const result = {
        hasChanges: false,
        insertions: {},
        insertionCount: 0,
        removals: [],
        moves: {},
    };
    const serializedByTemplateKey = {};
    const serializedCursorByTemplateKey = {};
    for (let i = 0; i < serializedChildren.length; i += 1) {
        const node = serializedChildren[i];
        (serializedByTemplateKey[node.templateKey] ??= []).push([node, i]);
    }
    for (let i = 0; i < backgroundChildren.length; i += 1) {
        const backgroundChild = backgroundChildren[i];
        const serializedCandidates = serializedByTemplateKey[backgroundChild.type];
        const candidateCursor = serializedCursorByTemplateKey[backgroundChild.type] ?? 0;
        const matchedSerialized = serializedCandidates?.[candidateCursor];
        if (matchedSerialized) {
            serializedCursorByTemplateKey[backgroundChild.type] = candidateCursor + 1;
            const oldIndex = matchedSerialized[1];
            hydrateInstance(matchedSerialized[0], backgroundChild);
            if (oldIndex < lastPlacedIndex) {
                result.moves[oldIndex] = { toIndex: i, instance: backgroundChild };
                result.hasChanges = true;
            }
            else {
                lastPlacedIndex = oldIndex;
            }
        }
        else {
            result.insertions[i] = backgroundChild;
            result.insertionCount += 1;
            result.hasChanges = true;
        }
    }
    for (const key in serializedByTemplateKey) {
        const candidates = serializedByTemplateKey[key];
        const candidateCursor = serializedCursorByTemplateKey[key] ?? 0;
        for (let i = candidateCursor; i < candidates.length; i += 1) {
            result.removals.push(candidates[i][1]);
            result.hasChanges = true;
        }
    }
    return result;
}
function hydrateInstance(serialized, instance) {
    if (serialized.templateKey !== instance.type) {
        if (__DEV__) {
            lynx.reportError(new Error(`ElementTemplate hydrate key mismatch: main='${serialized.templateKey}' background='${instance.type}'.`));
        }
        return;
    }
    const handleId = serialized.uid;
    if (!bindHydrationHandleId(instance, handleId, serialized.templateKey)) {
        return;
    }
    instance.prepareAttributeSlotsForNative();
    hydrateAttributeSlots(handleId, serialized.attributeSlots ?? [], instance.attributeSlots);
    if (serialized.templateKey === BUILTIN_RAW_TEXT_TEMPLATE_KEY) {
        return;
    }
    const serializedElementSlots = serialized.elementSlots ?? [];
    // Snapshot hydrates dynamic children through slot-filtered lists. Keeping ET
    // scoped the same way means a cross-slot candidate is a source remove plus a
    // target create/insert, while same-slot reorder can still stay move-like.
    const slotCount = Math.max(serializedElementSlots.length, instance.elementSlots.length);
    for (let slotId = 0; slotId < slotCount; slotId += 1) {
        const serializedSlot = serializedElementSlots[slotId];
        const backgroundSlot = instance.elementSlots[slotId];
        if (!serializedSlot && !backgroundSlot) {
            continue;
        }
        hydrateElementSlot(instance, slotId, serializedSlot ?? []);
    }
}
function hydrateElementSlot(parent, slotId, serializedChildren) {
    const backgroundChildren = parent.elementSlots[slotId] ?? [];
    if (backgroundChildren.length === 0) {
        for (const serialized of serializedChildren) {
            emitSerializedSubtreeRemove(parent, slotId, serialized);
        }
        return;
    }
    const listDiff = hydrateMatchingChildrenAndDiffSlot(serializedChildren, backgroundChildren);
    if (!listDiff.hasChanges) {
        return;
    }
    // Hydrate emits patches directly here. Replaying against serialized order
    // keeps insert targets in the main-thread slot without reshaping background.
    const removalIndexes = new Set(listDiff.removals);
    const { insertions, moves } = listDiff;
    const pendingMoves = new Map();
    let serializedCursor = 0;
    let currentSerializedChild = serializedChildren[serializedCursor];
    let newIndex = 0;
    let oldIndex = 0;
    // Insertions are known before replay starts. Moves are counted only when their
    // old serialized position is reached, so the cursor can keep emitting patches
    // even after all serialized children have been consumed.
    let pendingInsertOrMovePatchCount = listDiff.insertionCount;
    while (currentSerializedChild || pendingInsertOrMovePatchCount > 0) {
        let keepCurrentSerializedChild = false;
        if (currentSerializedChild && removalIndexes.has(oldIndex)) {
            emitSerializedSubtreeRemove(parent, slotId, currentSerializedChild);
        }
        else if (currentSerializedChild && moves[oldIndex] !== undefined) {
            const move = moves[oldIndex];
            pendingMoves.set(move.toIndex, move.instance);
            pendingInsertOrMovePatchCount += 1;
        }
        else {
            const beforeChildId = currentSerializedChild ? currentSerializedChild.uid : 0;
            const movedChild = pendingMoves.get(newIndex);
            if (movedChild) {
                keepCurrentSerializedChild = true;
                globalCommitContext.ops.push(ElementTemplateUpdateOps.insertNode, parent.instanceId, slotId, movedChild.instanceId, beforeChildId);
                pendingInsertOrMovePatchCount -= 1;
            }
            else if (insertions[newIndex] !== undefined) {
                const insertedChild = insertions[newIndex];
                keepCurrentSerializedChild = true;
                emitCreateSubtree(insertedChild);
                globalCommitContext.ops.push(ElementTemplateUpdateOps.insertNode, parent.instanceId, slotId, insertedChild.instanceId, beforeChildId);
                pendingInsertOrMovePatchCount -= 1;
            }
            newIndex += 1;
        }
        if (currentSerializedChild && !keepCurrentSerializedChild) {
            currentSerializedChild = serializedChildren[++serializedCursor];
            oldIndex += 1;
        }
    }
}
function emitSerializedSubtreeRemove(parent, slotId, serialized) {
    const childId = serialized.uid;
    const existing = backgroundElementTemplateInstanceManager.get(childId);
    // The removed child may no longer have a live background instance in this
    // slot, so serialized data is the source of truth for registry cleanup.
    const removedSubtreeHandleIds = [];
    collectSerializedSubtreeHandleIdsInto(serialized, removedSubtreeHandleIds);
    globalCommitContext.ops.push(ElementTemplateUpdateOps.removeNode, parent.instanceId, slotId, childId, removedSubtreeHandleIds);
    if (existing && !existing.parent) {
        markRemovedSubtreeForCurrentCommit(existing);
    }
}
function collectSerializedSubtreeHandleIdsInto(serialized, handles) {
    handles.push(serialized.uid);
    for (const slotChildren of serialized.elementSlots ?? []) {
        if (!slotChildren) {
            continue;
        }
        for (const child of slotChildren) {
            collectSerializedSubtreeHandleIdsInto(child, handles);
        }
    }
}
function emitCreateSubtree(node) {
    for (const slotChildren of node.elementSlots) {
        /* v8 ignore start */
        if (!slotChildren) {
            continue;
        }
        /* v8 ignore stop */
        for (const child of slotChildren) {
            emitCreateSubtree(child);
        }
    }
    node.prepareAttributeSlotsForNative();
    node.emitCreate();
}
function bindHydrationHandleId(instance, handleId, templateKey) {
    try {
        backgroundElementTemplateInstanceManager.updateId(instance.instanceId, handleId);
        instance.markCreateEmittedForHydration();
        return true;
    }
    catch (error) {
        if (__DEV__) {
            const reason = error instanceof Error ? error.message : String(error);
            lynx.reportError(new Error(`ElementTemplate hydrate received invalid uid ${handleId} for '${templateKey}': ${reason}`));
        }
        return false;
    }
}
function hydrateAttributeSlots(handleId, beforeSlots, afterSlots) {
    const slotCount = Math.max(beforeSlots.length, afterSlots.length);
    for (let slotIndex = 0; slotIndex < slotCount; slotIndex += 1) {
        const beforeValue = beforeSlots[slotIndex];
        const afterValue = afterSlots[slotIndex];
        if (isDirectOrDeepEqual(beforeValue, afterValue)) {
            continue;
        }
        if (afterValue === undefined && beforeValue === null) {
            // JSON serialization turns undefined array slots into null on the main-thread payload.
            continue;
        }
        globalCommitContext.ops.push(ElementTemplateUpdateOps.setAttribute, handleId, slotIndex, afterValue ?? null);
    }
}
//# sourceMappingURL=hydrate.js.map