import { Runner } from 'pixi.js';

import type { Service } from '../types';

export interface SceneLike
{
    sceneKey?: string;
    isCreated?: boolean;
    interactiveChildren?: boolean;
    inputs?: Record<string, unknown>;
    create(data?: unknown): void;
    load(onProgress?: (progress: number) => void): Promise<unknown>;
    resize(data?: unknown): void;
    run(): void;
    stop(): void;
}

export interface SceneRoot
{
    addChild(child: SceneLike): unknown;
    removeChild(child: SceneLike): unknown;
    add?(child: SceneLike): unknown;
}

/** Manages lifecycle hooks for registered game scenes. */
export default class Scenes implements Service
{
    public name = 'Scenes';
    public readonly serviceType = 'scenes';

    protected readonly root: SceneRoot;
    protected readonly _scenes: Record<string, SceneLike> = {};
    protected readonly _runners = {
        update: new Runner('update'),
        resize: new Runner('resize'),
    };

    protected _resizeData: unknown;
    protected updatingSceneNames: string[] = [];

    constructor(root: SceneRoot)
    {
        this.root = root;
    }

    public add(key: string, scene: SceneLike): void
    {
        if (this._scenes[key])
        {
            console.warn('Scene key already exists');

            return;
        }

        scene.sceneKey = key;
        this._scenes[key] = scene;
    }

    public create(sceneName: string, data?: unknown): SceneLike | undefined
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            console.warn('the scene not found');

            return undefined;
        }

        scene.create(data);
        scene.isCreated = true;
        scene.resize(this._resizeData);
        // @ts-expect-error type
        this._runners.resize.add(scene);

        return scene;
    }

    public get(sceneName: string): SceneLike | undefined
    {
        if (!this._scenes[sceneName])
        {
            console.warn('the scene not found');

            return undefined;
        }

        return this._scenes[sceneName];
    }

    public async load(sceneName: string, onProgress?: (progress: number) => void): Promise<unknown>
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            throw new Error(`Scene ${sceneName} not found`);
        }

        return scene.load(onProgress);
    }

    public pause(sceneName: string): SceneLike | undefined
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            return undefined;
        }

        // @ts-expect-error type
        this._runners.update.remove(scene);
        if (scene)
        {
            scene.interactiveChildren = false;
        }

        return scene;
    }

    public pauseAllUpdatingScenes(): void
    {
        this.updatingSceneNames = this.getUpdatingSceneNames();
        this.updatingSceneNames.forEach((sceneName) => this.pause(sceneName));
    }

    public remove(sceneName: string): void
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            return;
        }

        // @ts-expect-error type
        this._runners.update.remove(scene);
        // @ts-expect-error type
        this._runners.resize.remove(scene);
        delete this._scenes[sceneName];
    }

    public resume(sceneName: string): SceneLike | undefined
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            throw new Error(`Scene ${sceneName} not found`);
        }

        // @ts-expect-error type
        this._runners.update.add(scene);
        scene.interactiveChildren = true;

        return scene;
    }

    public resumeAllUpdatingScenes(): void
    {
        if (Array.isArray(this.updatingSceneNames))
        {
            this.updatingSceneNames.forEach((sceneName) => this.resume(sceneName));
        }

        this.updatingSceneNames = [];
    }

    public run(sceneName: string, data?: unknown): SceneLike | undefined
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            throw new Error(`Scene ${sceneName} not found`);
        }

        if (!scene.isCreated)
        {
            this.create(sceneName, data);
        }

        this.root.addChild(scene);
        scene.run();
        // @ts-expect-error type
        this._runners.update.add(scene);

        return scene;
    }

    public sleep(sceneName: string): SceneLike | undefined
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            return undefined;
        }

        // @ts-expect-error type
        this._runners.update.remove(scene);
        // @ts-expect-error type
        this._runners.resize.remove(scene);

        return scene;
    }

    public stop(sceneName: string): SceneLike | undefined
    {
        const scene = this._scenes[sceneName];

        if (!scene || !scene.isCreated)
        {
            return scene;
        }

        scene.stop();
        scene.isCreated = false;
        // @ts-expect-error type
        this._runners.update.remove(scene);
        // @ts-expect-error type
        this._runners.resize.remove(scene);
        this.root.removeChild(scene);

        return scene;
    }

    public stopAll(...exceptionScenes: string[]): void
    {
        Object.keys(this._scenes).forEach((sceneName) =>
        {
            if (exceptionScenes.includes(sceneName))
            {
                return;
            }

            this.stop(sceneName);
        });
    }

    public update(dt: number): void
    {
        this._runners.update.emit(dt);
    }

    public wake(sceneName: string): SceneLike | undefined
    {
        const scene = this._scenes[sceneName];

        if (!scene)
        {
            return undefined;
        }

        // @ts-expect-error type
        this._runners.update.add(scene);
        // @ts-expect-error type
        this._runners.resize.add(scene);
        (this.root.addChild ?? this.root.add)?.call(this.root, scene);
        scene.resize(this._resizeData);

        return scene;
    }

    public resize(resizeData: unknown): void
    {
        this._resizeData = resizeData;
        this._runners.resize.emit(this._resizeData);
    }

    public getUpdatingSceneNames(): string[]
    {
        return (this._runners.update.items as SceneLike[])
            .map((scene) => scene.sceneKey)
            .filter((sceneKey): sceneKey is string => Boolean(sceneKey) && sceneKey !== 'gameplayui');
    }

    public dispose(): void
    {
        this._runners.update.removeAll?.();
        this._runners.resize.removeAll?.();
        this.updatingSceneNames = [];
    }
}
