import { Actor } from "../..";
import type { Game } from '../game';
import type { Container } from 'pixi.js';
import { contains } from '../utils/array-utils';
import { EngineEvent } from '../events/engine-events';
import { Constructor } from '../utils/types';

/**
 * If a clip is supplied to the Group, it will act as the parent
 * of all Actor clips added to the group.
 */
export class Group<T extends Game = Game> {


	get actor() {
		return this._actor;
	}

	/**
	  * @property Optional clip associated with group.
	  */
	readonly clip?: Container | null;

	/**
	 * @property {string} name
	 */
	name?: string;


	/**
	 * @property {boolean} enabled
	 */
	get enabled() { return this._enabled; }

	/**
	 * Subgroups of this group.
	 */
	readonly subgroups: Group[] = [];

	/**
	 * Objects in group.
	 */
	readonly objects: Actor[] = [];

	/**
	 * Actor to hold group components.
	 */
	private readonly _actor?: Actor<Container>;

	private _game?: T;
	/**
	 * Game group is added to, if any.
	 */
	public get game() { return this._game }

	get parent() { return this._parent }

	/**
	 * Parent group, if any.
	 */
	private _parent?: Group;

	protected isDestroyed: boolean = false;

	private _enabled: boolean = false;

	/**
	 * Whether group should enable when added to engine.
	 * Used to track enable state before actually added.
	 */
	private _shouldEnable: boolean;

	/**
	 *
	 * @param actor -actor to assign to group, or container to use as group container,
	 * or 'true' to create a group container.
	 * @param enabled
	 */
	constructor(actor?: Container | boolean | undefined | null, enabled: boolean = true) {

		this._shouldEnable = enabled;
		if (actor) {
			this._actor = this.makeGroupActor(actor);
			this.clip = this._actor.clip;
		}

	}

	/**
	  * Ensure the group has its own group Actor.
	  */
	private makeGroupActor(clip: Actor<Container> | Container | boolean): Actor<Container> {

		let actor: Actor<Container>;
		if (typeof clip === 'boolean') {
			actor = new Actor<Container>();
		} else if (clip instanceof Actor) {
			actor = clip;
		} else {
			actor = new Actor<Container>(clip);
		}

		if (this._game) {
			this._game.addActor(actor);
		}
		return actor;
	}

	public enable() {

		this._shouldEnable = true;
		if (this._enabled === true) return;

		for (const g of this.subgroups) {
			g.enable();
		}

		this._enabled = true;

	}

	public disable() {

		this._shouldEnable = false;
		if (this._enabled === false) return;

		this._enabled = false;
		for (const g of this.subgroups) {
			g.disable();
		}

	}



	/**
	 * Override in subclasses for notification of when
	 * group is added to game.
	 */
	public onAdded() { }

	/**
	 * Override in subclasses to be notified when group is removed.
	 */
	public onRemoved() { }

	/**
	 * Internal message of group being added to game.
	 * Do not call directly.
	 * Override onAdded() in subclasses for the event.
	 */
	_onAdded(game: T) {

		if (this._game !== game) {

			this._game = game;
			if (this._actor && !this._actor.isAdded) {
				/// add actor to group.
				game.addActor(this._actor);
			}

			/// Add all objects in group.
			for (const a of this.objects) {
				game.addActor(a);
			}

			for (const s of this.subgroups) {
				game.addGroup(s);
			}

			this.onAdded();
			if (this._shouldEnable) {
				this.enable();
			}

		}

	}


	/**
	 * Show all the objects in the group and subgroups.
	 */
	show() {

		if (this.actor) {
			this.actor.visible = true;
		}

		for (let i = this.subgroups.length - 1; i >= 0; i--) {
			this.subgroups[i].show();
		}

	}

	/**
	 * Hide all actors in this group and subgroups.
	 */
	hide() {

		if (this.actor) {
			this.actor.visible = false;
		}

		for (let i = this.subgroups.length - 1; i >= 0; i--) {
			this.subgroups[i].hide();
		}

	}

	/**
	 * Get group by class. Searches this group then
	 * recurses up the parent chain, and then to the
	 * current game, if Group has been added to game.
	 * @param type 
	 * @returns 
	 */
	getGroup<G extends Group>(type: Constructor<G>): G | undefined {

		for (let i = this.subgroups.length - 1; i >= 0; i--) {

			if (this.subgroups[i] instanceof type) {
				return this.subgroups[i] as G;
			}
		}

		if (this._parent) {
			return this._parent.getGroup(type)
		} else if (this._game) {
			return this._game.getGroup(type);
		}
	}

	/**
	 * Find subgroup of this group.
	 * @param gname 
	 * @returns 
	 */
	findGroup(gname: string): Group | undefined {

		for (let i = this.subgroups.length - 1; i >= 0; i--) {
			if (this.subgroups[i].name == gname) return this.subgroups[i];
		}

		return undefined;
	}

	/**
	 * Return first subgroup found of type.
	 */
	get<GType extends Group<T> = Group<T>>(kind: Constructor<GType>) {

		for (let i = this.subgroups.length - 1; i >= 0; i--) {

			if (this.subgroups[i] instanceof kind) {
				return this.subgroups[i] as GType;
			}

		}
		return null;

	}

	/**
	 * Add subgroup to this group.
	 * @param {Group} sub
	 */
	addGroup(sub: Group) {

		if (!contains(this.subgroups, sub)) {

			if (sub._parent) {

				if (sub._parent === this) return;
				sub._parent.removeGroup(sub);
			}
			sub._parent = this;


			this.subgroups.push(sub);
			this.game?.addGroup(sub)


		}

	}

	/**
	 * Remove Actor from group, but not Game or Engine.
	 * @param {Actor} obj
	 */
	remove(obj: Actor) {

		const ind = this.objects.indexOf(obj);
		if (ind < 0) return;

		this.objects.splice(ind, 1);

		obj.off(EngineEvent.ActorDestroyed, this.remove, this);
		obj.group = null;

	}

	/**
	 *
	 * @param {Actor} obj
	 * @returns {Actor} the object.
	 */
	add(obj: Actor): Actor {

		obj.group = this;
		obj.on(EngineEvent.ActorDestroyed, this.remove, this);

		this.objects.push(obj);
		this._game?.engine.add(obj);

		return obj;

	}

	/**
	 * Internal message of group being removed from game.
	 * Do not call directly.
	 * Override onRemoved() in subclasses for the event.
	 */
	_onRemoved() {

		/// Save previous enabled state in case group is re-added.
		const curEnable = this._enabled;

		this.disable();
		this._shouldEnable = curEnable;

		const game = this._game;
		if (game) {

			this.onRemoved();
			this._game = undefined;

			for (const a of this.objects) {
				game.engine.remove(a);
			}

			for (const s of this.subgroups) {
				game.removeGroup(s);
			}
		}


	}

	/**
	 * Remove subgroup from this group.
	 * @param {Group} sub
	 */
	removeGroup(sub: Group) {

		if (sub._parent !== this) {
			return;
		}
		sub._parent = undefined;

		for (let i = this.subgroups.length - 1; i >= 0; i--) {

			if (this.subgroups[i] == sub) {
				this.subgroups.splice(i, 1);
				break;
			}
		}

		this.game?.removeGroup(sub);


	}


	/**
	 * Override in subclasses to cleanup before group destroyed.
	 */
	onDestroy?(): void;

	destroy() {

		this.isDestroyed = true;

		for (let i = this.subgroups.length - 1; i >= 0; i--) {
			this.subgroups[i].destroy();
		}
		for (let i = this.objects.length - 1; i >= 0; i--) {
			// Don't listen to the remove event since we're already looping.
			this.objects[i].off(EngineEvent.ActorDestroyed, this.remove, this);
			this.objects[i].destroy();
		}
		this.objects.length = 0;
		this.subgroups.length = 0;


		/// workaround to ensure 'game' exists in the onDestroy()
		/// function since _onRemoved() clears it.
		const tempGame = this._game;
		if (this._parent && !this._parent.isDestroyed) {
			this._parent.removeGroup(this);
		} else {
			this._game?.removeGroup(this);
		}

		this._game = tempGame;
		this.onDestroy?.();

		this._actor?.destroy();

		this._parent = undefined;
		this._game = undefined;

	}

}