import { Matrix } from '../../maths/matrix/Matrix';
import { InstructionSet } from '../../rendering/renderers/shared/instructions/InstructionSet';
import { TexturePool } from '../../rendering/renderers/shared/texture/TexturePool';
import { type Renderer } from '../../rendering/renderers/types';
import type { Instruction } from '../../rendering/renderers/shared/instructions/Instruction';
import type { Texture } from '../../rendering/renderers/shared/texture/Texture';
import type { BatchableSprite } from '../sprite/BatchableSprite';
import type { ViewContainer } from '../view/ViewContainer';
import type { Bounds } from './bounds/Bounds';
import type { Container } from './Container';
/**
 * Options for caching a container as a texture.
 * @memberof rendering
 * @see RenderGroup#textureOptions
 */
export interface CacheAsTextureOptions
{
    /**
     * If true, the texture will be antialiased. This smooths out the edges of the texture.
     * @default false
     */
    antialias?: boolean;
    /**
     * The resolution of the texture. A higher resolution means a sharper texture but uses more memory.
     * By default the resolution is 1 which is the same as the rendererers resolution.
     */
    resolution?: number;
}
/**
 * A RenderGroup is a class that is responsible for I generating a set of instructions that are used to render the
 * root container and its children. It also watches for any changes in that container or its children,
 * these changes are analysed and either the instruction set is rebuild or the instructions data is updated.
 * @memberof rendering
 */
export class RenderGroup implements Instruction
{
    public renderPipeId = 'renderGroup';
    public root: Container = null;
    public canBundle = false;
    public renderGroupParent: RenderGroup = null;
    public renderGroupChildren: RenderGroup[] = [];
    public worldTransform: Matrix = new Matrix();
    public worldColorAlpha = 0xffffffff;
    public worldColor = 0xffffff;
    public worldAlpha = 1;
    // these updates are transform changes..
    public readonly childrenToUpdate: Record<number, { list: Container[]; index: number; }> = Object.create(null);
    public updateTick = 0;
    public gcTick = 0;
    // these update are renderable changes..
    public readonly childrenRenderablesToUpdate: { list: Container[]; index: number; } = { list: [], index: 0 };
    // other
    public structureDidChange = true;
    public instructionSet: InstructionSet = new InstructionSet();
    private readonly _onRenderContainers: Container[] = [];
    /**
     * Indicates if the cached texture needs to be updated.
     * @default true
     */
    public textureNeedsUpdate = true;
    /**
     * Indicates if the container should be cached as a texture.
     * @default false
     */
    public isCachedAsTexture = false;
    /**
     * The texture used for caching the container. this is only set if isCachedAsTexture is true.
     * It can only be accessed after a render pass.
     * @type {Texture | undefined}
     */
    public texture?: Texture;
    /**
     * The bounds of the cached texture.
     * @type {Bounds | undefined}
     * @ignore
     */
    public _textureBounds?: Bounds;
    /**
     * The options for caching the container as a texture.
     * @type {CacheAsTextureOptions}
     */
    public textureOptions: CacheAsTextureOptions;
    /**
     *  holds a reference to the batchable render sprite
     *  @ignore
     */
    public _batchableRenderGroup: BatchableSprite;
    /**
     * Holds a reference to the closest parent RenderGroup that has isCachedAsTexture enabled.
     * This is used to properly transform coordinates when rendering into cached textures.
     * @type {RenderGroup | null}
     * @ignore
     */
    public _parentCacheAsTextureRenderGroup: RenderGroup;
    private _inverseWorldTransform: Matrix;
    private _textureOffsetInverseTransform: Matrix;
    private _inverseParentTextureTransform: Matrix;
    private _matrixDirty = 0b111;
    public init(root: Container)
    {
        this.root = root;
        if (root._onRender) this.addOnRender(root);
        root.didChange = true;
        const children = root.children;
        for (let i = 0; i < children.length; i++)
        {
            const child = children[i];
            // make sure the children are all updated on the first pass..
            child._updateFlags = 0b1111;
            this.addChild(child);
        }
    }
    public enableCacheAsTexture(options: CacheAsTextureOptions = {}): void
    {
        this.textureOptions = options;
        this.isCachedAsTexture = true;
        this.textureNeedsUpdate = true;
    }
    public disableCacheAsTexture(): void
    {
        this.isCachedAsTexture = false;
        if (this.texture)
        {
            TexturePool.returnTexture(this.texture);
            this.texture = null;
        }
    }
    public updateCacheTexture(): void
    {
        this.textureNeedsUpdate = true;
    }
    public reset()
    {
        this.renderGroupChildren.length = 0;
        for (const i in this.childrenToUpdate)
        {
            const childrenAtDepth = this.childrenToUpdate[i];
            childrenAtDepth.list.fill(null);
            childrenAtDepth.index = 0;
        }
        this.childrenRenderablesToUpdate.index = 0;
        this.childrenRenderablesToUpdate.list.fill(null);
        this.root = null;
        this.updateTick = 0;
        this.structureDidChange = true;
        this._onRenderContainers.length = 0;
        this.renderGroupParent = null;
        this.disableCacheAsTexture();
    }
    get localTransform()
    {
        return this.root.localTransform;
    }
    public addRenderGroupChild(renderGroupChild: RenderGroup)
    {
        if (renderGroupChild.renderGroupParent)
        {
            renderGroupChild.renderGroupParent._removeRenderGroupChild(renderGroupChild);
        }
        renderGroupChild.renderGroupParent = this;
        this.renderGroupChildren.push(renderGroupChild);
    }
    private _removeRenderGroupChild(renderGroupChild: RenderGroup)
    {
        const index = this.renderGroupChildren.indexOf(renderGroupChild);
        if (index > -1)
        {
            this.renderGroupChildren.splice(index, 1);
        }
        renderGroupChild.renderGroupParent = null;
    }
    public addChild(child: Container)
    {
        this.structureDidChange = true;
        child.parentRenderGroup = this;
        child.updateTick = -1;
        if (child.parent === this.root)
        {
            child.relativeRenderGroupDepth = 1;
        }
        else
        {
            child.relativeRenderGroupDepth = child.parent.relativeRenderGroupDepth + 1;
        }
        child.didChange = true;
        this.onChildUpdate(child);
        if (child.renderGroup)
        {
            this.addRenderGroupChild(child.renderGroup);
            return;
        }
        if (child._onRender) this.addOnRender(child);
        const children = child.children;
        for (let i = 0; i < children.length; i++)
        {
            this.addChild(children[i]);
        }
    }
    public removeChild(child: Container)
    {
        // remove all the children...
        this.structureDidChange = true;
        if (child._onRender)
        {
            // Remove the child to the onRender list under the following conditions:
            // 1. If the child is not a render group.
            // 2. If the child is a render group root of this render group - which it can't be removed from in this case.
            if (!child.renderGroup)
            {
                this.removeOnRender(child);
            }
        }
        child.parentRenderGroup = null;
        if (child.renderGroup)
        {
            this._removeRenderGroupChild(child.renderGroup);
            return;
        }
        const children = child.children;
        for (let i = 0; i < children.length; i++)
        {
            this.removeChild(children[i]);
        }
    }
    public removeChildren(children: Container[])
    {
        for (let i = 0; i < children.length; i++)
        {
            this.removeChild(children[i]);
        }
    }
    public onChildUpdate(child: Container)
    {
        let childrenToUpdate = this.childrenToUpdate[child.relativeRenderGroupDepth];
        if (!childrenToUpdate)
        {
            childrenToUpdate = this.childrenToUpdate[child.relativeRenderGroupDepth] = {
                index: 0,
                list: [],
            };
        }
        childrenToUpdate.list[childrenToUpdate.index++] = child;
    }
    public updateRenderable(renderable: ViewContainer)
    {
        if (renderable.globalDisplayStatus < 0b111) return;
        this.instructionSet.renderPipes[renderable.renderPipeId].updateRenderable(renderable);
        renderable.didViewUpdate = false;
    }
    public onChildViewUpdate(child: Container)
    {
        this.childrenRenderablesToUpdate.list[this.childrenRenderablesToUpdate.index++] = child;
    }
    get isRenderable(): boolean
    {
        return (this.root.localDisplayStatus === 0b111 && this.worldAlpha > 0);
    }
    /**
     * adding a container to the onRender list will make sure the user function
     * passed in to the user defined 'onRender` callBack
     * @param container - the container to add to the onRender list
     */
    public addOnRender(container: Container)
    {
        this._onRenderContainers.push(container);
    }
    public removeOnRender(container: Container)
    {
        this._onRenderContainers.splice(this._onRenderContainers.indexOf(container), 1);
    }
    public runOnRender(renderer: Renderer)
    {
        for (let i = 0; i < this._onRenderContainers.length; i++)
        {
            this._onRenderContainers[i]._onRender(renderer);
        }
    }
    public destroy()
    {
        this.disableCacheAsTexture();
        this.renderGroupParent = null;
        this.root = null;
        (this.childrenRenderablesToUpdate as any) = null;
        (this.childrenToUpdate as any) = null;
        (this.renderGroupChildren as any) = null;
        (this._onRenderContainers as any) = null;
        this.instructionSet = null;
    }
    public getChildren(out: Container[] = []): Container[]
    {
        const children = this.root.children;
        for (let i = 0; i < children.length; i++)
        {
            this._getChildren(children[i], out);
        }
        return out;
    }
    private _getChildren(container: Container, out: Container[] = []): Container[]
    {
        out.push(container);
        if (container.renderGroup) return out;
        const children = container.children;
        for (let i = 0; i < children.length; i++)
        {
            this._getChildren(children[i], out);
        }
        return out;
    }
    public invalidateMatrices()
    {
        this._matrixDirty = 0b111;
    }
    /**
     * Returns the inverse of the world transform matrix.
     * @returns {Matrix} The inverse of the world transform matrix.
     */
    public get inverseWorldTransform()
    {
        if ((this._matrixDirty & 0b001) === 0) return this._inverseWorldTransform;
        this._matrixDirty &= ~0b001;
        // TODO - add dirty flag
        this._inverseWorldTransform ||= new Matrix();
        return this._inverseWorldTransform
            .copyFrom(this.worldTransform)
            .invert();
    }
    /**
     * Returns the inverse of the texture offset transform matrix.
     * @returns {Matrix} The inverse of the texture offset transform matrix.
     */
    public get textureOffsetInverseTransform()
    {
        if ((this._matrixDirty & 0b010) === 0) return this._textureOffsetInverseTransform;
        this._matrixDirty &= ~0b010;
        this._textureOffsetInverseTransform ||= new Matrix();
        // TODO shared.. bad!
        return this._textureOffsetInverseTransform
            .copyFrom(this.inverseWorldTransform)
            .translate(
                -this._textureBounds.x,
                -this._textureBounds.y
            );
    }
    /**
     * Returns the inverse of the parent texture transform matrix.
     * This is used to properly transform coordinates when rendering into cached textures.
     * @returns {Matrix} The inverse of the parent texture transform matrix.
     */
    public get inverseParentTextureTransform()
    {
        if ((this._matrixDirty & 0b100) === 0) return this._inverseParentTextureTransform;
        this._matrixDirty &= ~0b100;
        const parentCacheAsTexture = this._parentCacheAsTextureRenderGroup;
        if (parentCacheAsTexture)
        {
            this._inverseParentTextureTransform ||= new Matrix();
            // Get relative transform by removing parent's world transform
            return this._inverseParentTextureTransform
                .copyFrom(this.worldTransform)
                .prepend(parentCacheAsTexture.inverseWorldTransform)
                // Offset by texture bounds
                .translate(
                    -parentCacheAsTexture._textureBounds.x,
                    -parentCacheAsTexture._textureBounds.y
                );
        }
        return this.worldTransform;
    }
    /**
     * Returns a matrix that transforms coordinates to the correct coordinate space of the texture being rendered to.
     * This is the texture offset inverse transform of the closest parent RenderGroup that is cached as a texture.
     * @returns {Matrix | null} The transform matrix for the cached texture coordinate space,
     * or null if no parent is cached as texture.
     */
    public get cacheToLocalTransform()
    {
        if (!this._parentCacheAsTextureRenderGroup) return null;
        return this._parentCacheAsTextureRenderGroup.textureOffsetInverseTransform;
    }
}