// 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 { prepareAttributeSlots as prepareRawAttributeSlots, queueRefAttributeSlotUpdates } from './attr-slots.js';
import { globalCommitContext, markRemovedSubtreeForPostDispatchTeardown } from './commit-context.js';
import { isElementTemplateHydrated } from './commit-hook.js';
import { backgroundElementTemplateInstanceManager } from './manager.js';
import { isDirectOrDeepEqual } from '../../utils.js';
import { ElementTemplateUpdateOps } from '../protocol/opcodes.js';
import { parseElementTemplateType } from '../protocol/template-type.js';
function pushOp(...items) {
    globalCommitContext.ops.push(...items);
}
export const BUILTIN_RAW_TEXT_TEMPLATE_KEY = '_et_builtin_raw_text';
function isBuiltinRawTextTemplateKey(type) {
    return type === BUILTIN_RAW_TEXT_TEMPLATE_KEY;
}
function stringifyRawTextValue(value) {
    if (typeof value === 'string') {
        return value;
    }
    if (typeof value === 'number' || typeof value === 'boolean') {
        return String(value);
    }
    return '';
}
const EMPTY_LIST_ITEM_PLATFORM_INFO = {};
const EMPTY_REMOVED_SUBTREE_HANDLE_IDS = [];
export class BackgroundElementTemplateInstance {
    instanceId = 0; // Assigned by manager
    type;
    parent = null;
    firstChild = null;
    lastChild = null;
    nextSibling = null;
    previousSibling = null;
    __slotIndex = 0;
    // Shadow State for Hydration
    attributeSlots;
    rawAttributeSlots;
    isMaterializedOnMainThread = false;
    listItemPlatformInfo;
    get parentNode() {
        return this.parent;
    }
    get childNodes() {
        const nodes = [];
        let child = this.firstChild;
        while (child) {
            nodes.push(child);
            child = child.nextSibling;
        }
        return nodes;
    }
    get elementSlots() {
        const elementSlots = [];
        let child = this.firstChild;
        while (child) {
            (elementSlots[child.__slotIndex] ??= []).push(child);
            child = child.nextSibling;
        }
        return elementSlots;
    }
    nodeType;
    constructor(type, initialAttributeSlots) {
        this.type = type;
        this.nodeType = isBuiltinRawTextTemplateKey(type) ? 3 : 1;
        backgroundElementTemplateInstanceManager.register(this);
        if (initialAttributeSlots) {
            const preparedSlots = prepareRawAttributeSlots(this.type, this.instanceId, initialAttributeSlots);
            this.attributeSlots = preparedSlots;
            if (preparedSlots !== initialAttributeSlots) {
                this.rawAttributeSlots = initialAttributeSlots;
            }
        }
        else {
            this.attributeSlots = [];
        }
    }
    emitCreate() {
        if (this.isMaterializedOnMainThread) {
            return;
        }
        if (__DEV__ && this.instanceId === 0) {
            lynx.reportError(new Error('ElementTemplate patch has illegal handleId 0.'));
            return;
        }
        this.restoreManagerRegistration();
        // Walk the linked-list children once to build the slot-indexed handle list
        // for the createTemplate op. Going via `this.elementSlots` would allocate
        // the full `Instance[][]` intermediate just to throw it away here.
        const serializedSlots = [];
        let child = this.firstChild;
        while (child) {
            (serializedSlots[child.__slotIndex] ??= []).push(child.instanceId);
            child = child.nextSibling;
        }
        const nativeTemplate = parseElementTemplateType(this.type);
        pushOp(ElementTemplateUpdateOps.createTemplate, this.instanceId, nativeTemplate.templateKey, nativeTemplate.bundleUrl, this.attributeSlots, serializedSlots);
        this.isMaterializedOnMainThread = true;
    }
    needsMainThreadCreate() {
        return this.instanceId !== 0 && !this.isMaterializedOnMainThread;
    }
    markSubtreeDetachedFromMainThread() {
        if (this.instanceId !== 0) {
            this.isMaterializedOnMainThread = false;
        }
        let child = this.firstChild;
        while (child) {
            child.markSubtreeDetachedFromMainThread();
            child = child.nextSibling;
        }
    }
    releaseDetachedSubtreeFromManager() {
        if (this.parent !== null || this.isMaterializedOnMainThread) {
            return;
        }
        this.releaseSubtreeFromManager();
    }
    restoreManagerRegistration() {
        if (this.instanceId === 0) {
            return;
        }
        const instances = backgroundElementTemplateInstanceManager.values;
        const existing = instances.get(this.instanceId);
        if (existing === this) {
            return;
        }
        if (existing) {
            throw new Error(`ElementTemplate handleId ${this.instanceId} is already bound.`);
        }
        instances.set(this.instanceId, this);
    }
    releaseSubtreeFromManager() {
        const instances = backgroundElementTemplateInstanceManager.values;
        if (instances.get(this.instanceId) === this) {
            instances.delete(this.instanceId);
        }
        let child = this.firstChild;
        while (child) {
            child.releaseSubtreeFromManager();
            child = child.nextSibling;
        }
    }
    emitMainThreadCreateIfNeeded() {
        if (!this.needsMainThreadCreate()) {
            return;
        }
        // An unmaterialized subtree may receive attr updates before it is inserted;
        // prepare here so ref attach happens once, at the create boundary.
        this.prepareAttributeSlotsForNative();
        this.emitCreate();
    }
    canEmitUpdatePatch() {
        // Background tree construction is local until hydrate binds it to main-thread
        // instances. Only hydrated and materialized owners can emit update ops.
        return isElementTemplateHydrated() && !this.needsMainThreadCreate();
    }
    cleanupDetachedChildForLifetimeRemoval(child, canEmitUpdatePatch) {
        if (canEmitUpdatePatch) {
            child.markSubtreeDetachedFromMainThread();
            // The removed JS object graph may outlive the detach until GC, so keep
            // it pending and tear it down on the Snapshot-aligned delayed boundary.
            markRemovedSubtreeForPostDispatchTeardown(child);
            child.queueRefCleanupForSubtree();
            return;
        }
        // Mirrors `shouldQueueRefEffects` in `setAttribute`: pre-hydration
        // commits and post-hydration materialized children publish their refs
        // to user effects. Post-hydration unmaterialized children defer attach
        // to `emitCreate`, which never fires for a subtree torn down before
        // insert — so cleaning up there would emit a spurious detach.
        const refAttachWasPublished = !isElementTemplateHydrated()
            || !child.needsMainThreadCreate();
        if (refAttachWasPublished) {
            // Run before any tearDown below: `tearDown` clears `rawAttributeSlots`,
            // which `queueRefCleanupForSubtree` walks to enqueue the detach.
            child.queueRefCleanupForSubtree();
        }
        if (child.needsMainThreadCreate()) {
            // An unmaterialized subtree has no main-thread registry entry, so it
            // can be released from the background manager without delayed cleanup.
            child.tearDown();
        }
    }
    // DOM API for Preact
    appendChild(child) {
        this.insertBefore(child, null);
    }
    insertBefore(child, beforeChild, silent) {
        if (beforeChild === child) {
            throw new Error('Cannot insert a node before itself');
        }
        if (beforeChild && beforeChild.parent !== this) {
            throw new Error('Reference node is not a child of this parent');
        }
        if (child.parent) {
            child.parent.removeChild(child, true);
        }
        child.parent = this;
        if (beforeChild) {
            child.nextSibling = beforeChild;
            child.previousSibling = beforeChild.previousSibling;
            if (beforeChild.previousSibling) {
                beforeChild.previousSibling.nextSibling = child;
            }
            else {
                this.firstChild = child;
            }
            beforeChild.previousSibling = child;
        }
        else {
            if (this.lastChild) {
                this.lastChild.nextSibling = child;
                child.previousSibling = this.lastChild;
            }
            else {
                this.firstChild = child;
            }
            this.lastChild = child;
            child.nextSibling = null;
        }
        if (silent || !this.canEmitUpdatePatch()) {
            return;
        }
        const beforeId = (beforeChild && beforeChild.__slotIndex === child.__slotIndex) ? beforeChild.instanceId : 0;
        emitMainThreadCreateRecursive(child);
        pushOp(ElementTemplateUpdateOps.insertNode, this.instanceId, child.__slotIndex, child.instanceId, beforeId);
    }
    removeChild(child, silent) {
        if (child.parent !== this) {
            throw new Error('Node is not a child of this parent');
        }
        if (child.previousSibling) {
            child.previousSibling.nextSibling = child.nextSibling;
        }
        else {
            this.firstChild = child.nextSibling;
        }
        if (child.nextSibling) {
            child.nextSibling.previousSibling = child.previousSibling;
        }
        else {
            this.lastChild = child.previousSibling;
        }
        child.parent = null;
        child.nextSibling = null;
        child.previousSibling = null;
        const slotId = child.__slotIndex;
        if (silent) {
            return;
        }
        const canEmitUpdatePatch = this.canEmitUpdatePatch();
        if (canEmitUpdatePatch) {
            pushOp(ElementTemplateUpdateOps.removeNode, this.instanceId, slotId, child.instanceId, collectElementTemplateSubtreeHandleIds(child));
        }
        this.cleanupDetachedChildForLifetimeRemoval(child, canEmitUpdatePatch);
    }
    tearDown() {
        // Recursively tear down children first
        let child = this.firstChild;
        while (child) {
            const next = child.nextSibling;
            child.tearDown();
            child = next;
        }
        // Clear references
        this.parent = null;
        this.firstChild = null;
        this.lastChild = null;
        this.previousSibling = null;
        this.nextSibling = null;
        this.attributeSlots = [];
        this.rawAttributeSlots = undefined;
        // Remove from manager
        if (this.instanceId) {
            backgroundElementTemplateInstanceManager.values.delete(this.instanceId);
        }
    }
    queueRefCleanupForSubtree() {
        if (this.rawAttributeSlots) {
            queueRefAttributeSlotUpdates(this.type, this.instanceId, this.rawAttributeSlots);
        }
        let child = this.firstChild;
        while (child) {
            child.queueRefCleanupForSubtree();
            child = child.nextSibling;
        }
    }
    getRawAttributeSlot(attrSlotIndex) {
        return this.rawAttributeSlots?.[attrSlotIndex] ?? this.attributeSlots[attrSlotIndex];
    }
    markMaterializedByHydration() {
        // Hydration binds this object to a template that already exists on the main
        // thread; future updates must treat it as materialized without emitting create.
        this.isMaterializedOnMainThread = true;
        this.restoreManagerRegistration();
    }
    prepareAttributeSlotsForNative(options) {
        if (!this.rawAttributeSlots) {
            return;
        }
        this.attributeSlots = prepareRawAttributeSlots(this.type, this.instanceId, this.rawAttributeSlots, {
            previousPreparedSlots: this.attributeSlots,
            previousRawSlots: this.rawAttributeSlots,
        });
        if (options?.publishRefEffects ?? true) {
            queueRefAttributeSlotUpdates(this.type, this.instanceId, undefined, this.rawAttributeSlots);
        }
    }
    prepareAttributeSlotsForHydration() {
        // Hydrate only rebinds the selector marker to the stable handle. The ref was
        // already made visible to user effects on the pre-hydration commit path.
        this.prepareAttributeSlotsForNative({
            publishRefEffects: false,
        });
    }
    setAttribute(key, value) {
        if (isBuiltinRawTextTemplateKey(this.type) && (key === '0' || key === 'data')) {
            this.text = String(value);
        }
        else if (key === '__listItemPlatformInfo') {
            const previous = this.getListItemPlatformInfo();
            const next = value;
            this.listItemPlatformInfo = next;
            if (!isDirectOrDeepEqual(previous, next)) {
                this.notifyParentListOfLogicalChildUpdate();
            }
        }
        else if (key === 'attributeSlots' && Array.isArray(value)) {
            const previousSlots = this.attributeSlots;
            const previousRawSlots = this.rawAttributeSlots ?? previousSlots;
            const isHydrated = isElementTemplateHydrated();
            const canEmitUpdatePatch = isHydrated && !this.needsMainThreadCreate();
            // Pre-hydration commits must expose refs to effects, while post-hydration
            // unmaterialized nodes defer ref attach to create emission to avoid dupes.
            const shouldQueueRefEffects = !isHydrated || canEmitUpdatePatch;
            const nextSlots = prepareRawAttributeSlots(this.type, this.instanceId, value, {
                previousPreparedSlots: previousSlots,
                previousRawSlots,
            });
            if (shouldQueueRefEffects) {
                queueRefAttributeSlotUpdates(this.type, this.instanceId, previousRawSlots, value);
            }
            this.rawAttributeSlots = nextSlots === value ? undefined : value;
            const maxLength = Math.max(previousSlots.length, nextSlots.length);
            this.attributeSlots = nextSlots;
            for (let slotIndex = 0; slotIndex < maxLength; slotIndex += 1) {
                const previousValue = previousSlots[slotIndex];
                const nextValue = nextSlots[slotIndex];
                if (isDirectOrDeepEqual(previousValue, nextValue)) {
                    continue;
                }
                if (!canEmitUpdatePatch) {
                    continue;
                }
                pushOp(ElementTemplateUpdateOps.setAttribute, this.instanceId, slotIndex, nextValue ?? null);
            }
        }
    }
    get text() {
        return stringifyRawTextValue(this.attributeSlots[0]);
    }
    set text(value) {
        if (!isBuiltinRawTextTemplateKey(this.type)) {
            return;
        }
        const text = String(value);
        if (this.attributeSlots[0] === text) {
            return;
        }
        this.rawAttributeSlots = undefined;
        this.attributeSlots = [text];
        if (!this.canEmitUpdatePatch()) {
            return;
        }
        pushOp(ElementTemplateUpdateOps.setAttribute, this.instanceId, 0, text);
    }
    get data() {
        return this.text;
    }
    set data(value) {
        this.text = value;
    }
    getListItemPlatformInfo() {
        return this.listItemPlatformInfo ?? EMPTY_LIST_ITEM_PLATFORM_INFO;
    }
    notifyParentListOfLogicalChildUpdate() {
        if (this.parent instanceof BackgroundListElementTemplateInstance) {
            this.parent.notifyLogicalChildUpdated(this);
        }
    }
}
function toTypedAttributesCommand(value) {
    if (value == null || typeof value !== 'object' || Array.isArray(value)) {
        return null;
    }
    return value;
}
export class BackgroundTypedElementTemplateInstance extends BackgroundElementTemplateInstance {
    constructor(type) {
        super(type);
    }
    emitCreate() {
        if (this.isMaterializedOnMainThread) {
            return;
        }
        if (__DEV__ && this.instanceId === 0) {
            lynx.reportError(new Error('ElementTemplate patch has illegal handleId 0.'));
            return;
        }
        pushOp(ElementTemplateUpdateOps.createTypedElement, this.instanceId, this.type, this.getTypedAttributesForCreate(), this.getElementSlotsForCreate(), this.getRuntimeOptionsForCreate());
        this.isMaterializedOnMainThread = true;
    }
    setAttribute(key, value) {
        if (key !== 'attributes') {
            super.setAttribute(key, value);
            return;
        }
        const previousValue = this.attributeSlots[0];
        const nextValue = toTypedAttributesCommand(value);
        this.attributeSlots = [nextValue];
        if (isElementTemplateHydrated()
            && this.isMaterializedOnMainThread
            && !isDirectOrDeepEqual(previousValue, nextValue)) {
            pushOp(ElementTemplateUpdateOps.setAttribute, this.instanceId, 0, nextValue);
        }
    }
    getTypedAttributesForCreate() {
        return toTypedAttributesCommand(this.attributeSlots[0]);
    }
    getElementSlotsForCreate() {
        return null;
    }
    getRuntimeOptionsForCreate() {
        return null;
    }
}
export class BackgroundListElementTemplateInstance extends BackgroundTypedElementTemplateInstance {
    constructor() {
        super('list');
    }
    getRuntimeOptionsForCreate() {
        const listChildren = [];
        let child = this.firstChild;
        while (child) {
            listChildren.push(toUpdateTypedListItemCommand(child));
            child = child.nextSibling;
        }
        return {
            listChildren,
        };
    }
    insertBefore(child, beforeChild, silent) {
        const previousParent = child.parent;
        super.insertBefore(child, beforeChild, true);
        if (!silent) {
            if (previousParent instanceof BackgroundListElementTemplateInstance) {
                previousParent.emitTypedListItemRemove(child, EMPTY_REMOVED_SUBTREE_HANDLE_IDS);
            }
            this.emitTypedListItemInsert(child, beforeChild);
        }
    }
    removeChild(child, silent) {
        super.removeChild(child, true);
        if (!silent) {
            const canEmitUpdatePatch = this.canEmitUpdatePatch();
            const removedSubtreeHandleIds = canEmitUpdatePatch
                ? collectElementTemplateSubtreeHandleIds(child)
                : EMPTY_REMOVED_SUBTREE_HANDLE_IDS;
            this.cleanupDetachedChildForLifetimeRemoval(child, canEmitUpdatePatch);
            this.emitTypedListItemRemove(child, removedSubtreeHandleIds);
        }
    }
    notifyLogicalChildUpdated(child) {
        this.emitTypedListItemUpdate(child);
    }
    emitTypedListItemInsert(child, beforeChild) {
        if (!isElementTemplateHydrated() || !this.isMaterializedOnMainThread) {
            return;
        }
        emitMainThreadCreateRecursive(child);
        pushOp(ElementTemplateUpdateOps.insertTypedListItem, this.instanceId, toUpdateTypedListItemCommand(child), beforeChild?.instanceId ?? 0);
    }
    emitTypedListItemRemove(child, removedSubtreeHandleIds) {
        if (!isElementTemplateHydrated() || !this.isMaterializedOnMainThread) {
            return;
        }
        pushOp(ElementTemplateUpdateOps.removeTypedListItem, this.instanceId, child.instanceId, removedSubtreeHandleIds);
    }
    emitTypedListItemUpdate(child) {
        if (!isElementTemplateHydrated() || !this.isMaterializedOnMainThread) {
            return;
        }
        pushOp(ElementTemplateUpdateOps.updateTypedListItem, this.instanceId, toUpdateTypedListItemCommand(child));
    }
}
export function toUpdateTypedListItemCommand(child) {
    return {
        __etHandleRef: child.instanceId,
        type: child.type,
        platformInfo: child.getListItemPlatformInfo(),
    };
}
export function collectElementTemplateSubtreeHandleIds(root) {
    const handles = [];
    collectElementTemplateSubtreeHandleIdsImpl(root, handles);
    return handles;
}
function collectElementTemplateSubtreeHandleIdsImpl(instance, handles) {
    if (instance.instanceId !== 0) {
        handles.push(instance.instanceId);
    }
    let child = instance.firstChild;
    while (child) {
        collectElementTemplateSubtreeHandleIdsImpl(child, handles);
        child = child.nextSibling;
    }
}
function emitMainThreadCreateRecursive(instance) {
    if (!isElementTemplateHydrated()
        || instance.instanceId === 0) {
        return;
    }
    // Walk children in linked-list order; the slot-grouped view would just be
    // discarded here since we recurse into every child regardless of slot.
    let child = instance.firstChild;
    while (child) {
        emitMainThreadCreateRecursive(child);
        child = child.nextSibling;
    }
    instance.emitMainThreadCreateIfNeeded();
}
//# sourceMappingURL=instance.js.map