Source: scene/container/container-mixins/childrenHelperMixin.ts

import { removeItems } from '../../../utils/data/removeItems';
import { deprecation, v8_0_0 } from '../../../utils/logging/deprecation';

import type { Container, ContainerChild } from '../Container';

export interface ChildrenHelperMixin<C = ContainerChild>
{
    allowChildren: boolean;
    addChild<U extends C[]>(...children: U): U[0];
    removeChild<U extends C[]>(...children: U): U[0];
    removeChildren(beginIndex?: number, endIndex?: number): C[];
    removeChildAt<U extends C>(index: number): U;
    getChildAt<U extends C>(index: number): U;
    setChildIndex(child: C, index: number): void;
    getChildIndex(child: C): number;
    addChildAt<U extends C>(child: U, index: number): U;
    swapChildren<U extends C>(child: U, child2: U): void;
    removeFromParent(): void;

    reparentChild<U extends C[]>(...child: U): U[0];
    reparentChildAt<U extends C>(child: U, index: number): U;
}

export const childrenHelperMixin: Partial<Container> = {

    allowChildren: true,

    /**
     * Removes all children from this container that are within the begin and end indexes.
     * @param beginIndex - The beginning position.
     * @param endIndex - The ending position. Default value is size of the container.
     * @returns - List of removed children
     * @memberof scene.Container#
     */
    removeChildren(beginIndex = 0, endIndex?: number): ContainerChild[]
    {
        const end = endIndex ?? this.children.length;
        const range = end - beginIndex;
        const removed: ContainerChild[] = [];

        if (range > 0 && range <= end)
        {
            for (let i = end - 1; i >= beginIndex; i--)
            {
                const child = this.children[i];

                if (!child) continue;
                removed.push(child);
                child.parent = null;
            }

            removeItems(this.children, beginIndex, end);

            const renderGroup = this.renderGroup || this.parentRenderGroup;

            if (renderGroup)
            {
                renderGroup.removeChildren(removed);
            }

            for (let i = 0; i < removed.length; ++i)
            {
                this.emit('childRemoved', removed[i], this, i);
                removed[i].emit('removed', this);
            }

            return removed;
        }
        else if (range === 0 && this.children.length === 0)
        {
            return removed;
        }

        throw new RangeError('removeChildren: numeric values are outside the acceptable range.');
    },

    /**
     * Removes a child from the specified index position.
     * @param index - The index to get the child from
     * @returns The child that was removed.
     * @memberof scene.Container#
     */
    removeChildAt<U extends ContainerChild>(index: number): U
    {
        const child = this.getChildAt<U>(index);

        return this.removeChild(child);
    },

    /**
     * Returns the child at the specified index
     * @param index - The index to get the child at
     * @returns - The child at the given index, if any.
     * @memberof scene.Container#
     */
    getChildAt<U extends ContainerChild>(index: number): U
    {
        if (index < 0 || index >= this.children.length)
        {
            throw new Error(`getChildAt: Index (${index}) does not exist.`);
        }

        return this.children[index] as U;
    },

    /**
     * Changes the position of an existing child in the container container
     * @param child - The child Container instance for which you want to change the index number
     * @param index - The resulting index number for the child container
     * @memberof scene.Container#
     */
    setChildIndex(child: ContainerChild, index: number): void
    {
        if (index < 0 || index >= this.children.length)
        {
            throw new Error(`The index ${index} supplied is out of bounds ${this.children.length}`);
        }

        this.getChildIndex(child); // check if child exists
        this.addChildAt(child, index);
    },

    /**
     * Returns the index position of a child Container instance
     * @param child - The Container instance to identify
     * @returns - The index position of the child container to identify
     * @memberof scene.Container#
     */
    getChildIndex(child: ContainerChild): number
    {
        const index = this.children.indexOf(child);

        if (index === -1)
        {
            throw new Error('The supplied Container must be a child of the caller');
        }

        return index;
    },

    /**
     * Adds a child to the container at a specified index. If the index is out of bounds an error will be thrown.
     * If the child is already in this container, it will be moved to the specified index.
     * @param {Container} child - The child to add.
     * @param {number} index - The absolute index where the child will be positioned at the end of the operation.
     * @returns {Container} The child that was added.
     * @memberof scene.Container#
     */
    addChildAt<U extends ContainerChild>(child: U, index: number): U
    {
        // #if _DEBUG
        if (!this.allowChildren)
        {
            deprecation(v8_0_0, 'addChildAt: Only Containers will be allowed to add children in v8.0.0');
        }
        // #endif

        const { children } = this;

        if (index < 0 || index > children.length)
        {
            throw new Error(`${child}addChildAt: The index ${index} supplied is out of bounds ${children.length}`);
        }

        // TODO - check if child is already in the list?
        // we should be able to optimise this!

        if (child.parent)
        {
            const currentIndex = child.parent.children.indexOf(child);

            // If this child is in the container and in the same position, do nothing
            if (child.parent === this && currentIndex === index)
            {
                return child;
            }

            if (currentIndex !== -1)
            {
                child.parent.children.splice(currentIndex, 1);
            }
        }

        if (index === children.length)
        {
            children.push(child);
        }
        else
        {
            children.splice(index, 0, child);
        }

        child.parent = this;
        child.didChange = true;
        child.didViewUpdate = false;
        child._updateFlags = 0b1111;

        const renderGroup = this.renderGroup || this.parentRenderGroup;

        if (renderGroup)
        {
            renderGroup.addChild(child);
        }

        if (this.sortableChildren) this.sortDirty = true;

        this.emit('childAdded', child, this, index);
        child.emit('added', this);

        return child;
    },
    /**
     * Swaps the position of 2 Containers within this container.
     * @param child - First container to swap
     * @param child2 - Second container to swap
     */
    swapChildren<U extends ContainerChild>(child: U, child2: U): void
    {
        if (child === child2)
        {
            return;
        }

        const index1 = this.getChildIndex(child);
        const index2 = this.getChildIndex(child2);

        this.children[index1] = child2;
        this.children[index2] = child;

        const renderGroup = this.renderGroup || this.parentRenderGroup;

        if (renderGroup)
        {
            renderGroup.structureDidChange = true;
        }

        this._didChangeId++;
    },
    /**
     * Remove the Container from its parent Container. If the Container has no parent, do nothing.
     * @memberof scene.Container#
     */
    removeFromParent()
    {
        this.parent?.removeChild(this);
    },

    /**
     * Reparent the child to this container, keeping the same worldTransform.
     * @param child - The child to reparent
     * @returns The first child that was reparented.
     * @memberof scene.Container#
     */
    reparentChild<U extends ContainerChild[]>(...child: U): U[0]
    {
        if (child.length === 1)
        {
            return this.reparentChildAt(child[0], this.children.length);
        }

        child.forEach((c) => this.reparentChildAt(c, this.children.length));

        return child[0];
    },

    /**
     * Reparent the child to this container at the specified index, keeping the same worldTransform.
     * @param child - The child to reparent
     * @param index - The index to reparent the child to
     * @memberof scene.Container#
     */
    reparentChildAt<U extends ContainerChild>(child: U, index: number): U
    {
        if (child.parent === this)
        {
            this.setChildIndex(child, index);

            return child;
        }

        const childMat = child.worldTransform.clone();

        child.removeFromParent();
        this.addChildAt(child, index);

        const newMatrix = this.worldTransform.clone();

        newMatrix.invert();
        childMat.prepend(newMatrix);

        child.setFromMatrix(childMat);

        return child;
    }
} as Container;