import * as PIXI from 'pixi.js';
import { Assets, Texture } from 'pixi.js';

export type DisplayObjectCtor<T extends PIXI.DisplayObject> = new (...args: any[]) => T;
export type DisplayChild = PIXI.DisplayObject | DisplayChild[];

export type CreateProps<T extends PIXI.DisplayObject> = Partial<T> & {
    args?: unknown[];
    ref?: (instance: T) => void;
    styles?: Record<string, unknown>;
    position?: [number, number];
    texture?: string | Texture;
    textures?: Array<string | Texture>;
};

/**
 * Creates a Pixi display object instance with props and children.
 * @param DisplayObject
 * @param props
 * @param {...any} children
 */
export const createElement = <T extends PIXI.Container>(
    // @ts-expect-error Type
    DisplayObject: DisplayObjectCtor<T>,
    // @ts-expect-error Type
    props: CreateProps<T> = {},
    ...children: DisplayChild[]
): T =>
{
    const { args = [], ref, ...rest } = props;
    const instance = new DisplayObject(...(args as []));

    if (ref)
    {
        ref(instance);
    }

    children.forEach((child) => addChild(instance, child));

    // @ts-expect-error type
    instance.once('added', () =>
    {
        if (rest)
        {
            // @ts-expect-error Type
            setProps(instance, rest);
        }
    });

    return instance;
};

/**
 * Adds a child or nested children to a container.
 * @param parent
 * @param children
 */
export const addChild = (parent: PIXI.Container, children: DisplayChild): void =>
{
    if (Array.isArray(children))
    {
        children.forEach((nestedChild) => addChild(parent, nestedChild));

        return;
    }

    parent.addChild(children);
};

/**
 * Applies dynamic properties to a display object.
 * @param object
 * @param props
 */
export function setProps<T extends PIXI.DisplayObject>(object: T, props: CreateProps<T>): void
{
    Object.entries(props).forEach(([property, value]) =>
    {
        switch (property)
        {
            case 'texture':
                if (value && 'texture' in object)
                {
                    (object as unknown as { texture: Texture }).texture = getTexture(value as string | Texture);
                }
                break;
            case 'textures':
                if (value && 'textures' in object)
                {
                    (object as unknown as { textures: Texture[] }).textures = getTextures(value as Array<string | Texture>);
                }
                break;
            case 'styles':
                applyStyles(object, props as CreateProps<T>);
                break;
            case 'position':
                if (Array.isArray(value))
                {
                    object.position.set(...value as [number, number]);
                }
                break;
            case 'args':
            case 'ref':
                break;
            default:
                (object as Record<string, unknown>)[property] = value;
        }
    });
}

/**
 * Resolves a texture from cache or returns the provided texture.
 * @param texture
 */
export function getTexture(texture?: string | Texture): Texture
{
    if (typeof texture === 'string')
    {
        return Assets.get(texture) as Texture;
    }

    if (texture instanceof Texture)
    {
        return texture;
    }

    return Texture.WHITE;
}

function getTextures(textures: Array<string | Texture>): Texture[]
{
    return textures.map((texture) => getTexture(texture));
}

/**
 * Applies style proxies to the target object.
 * @param object
 * @param props
 */
export function applyStyles<T extends PIXI.DisplayObject>(object: T, props: CreateProps<T>): void
{
    const target = object as T & { _styles?: Record<string, unknown>; styles?: Record<string, unknown> };

    if (!target.styles)
    {
        Object.defineProperty(target, 'styles', {
            get: () => target._styles,
            set: (value: Record<string, unknown>) =>
            {
                if (!target._styles)
                {
                    target._styles = {};
                }
                Object.assign(target._styles, value);
                const styles: Record<string, unknown> = {};

                for (const key of Object.keys(target._styles))
                {
                    const styleValue = target._styles[key];

                    styles[key] = typeof styleValue === 'function'
                        ? (styleValue as () => unknown)()
                        : styleValue;
                }
                setProps(object, styles as CreateProps<T>);
            },
        });
    }

    if (props.styles)
    {
        (target as { styles: Record<string, unknown> }).styles = props.styles;
    }
}
