/**
 * Sends events related to the scene
 *
 *
 */
import {PolyEventName} from './../../poly/utils/PolyEventName';
import {BaseNodeType} from '../_Base';
import {BaseParamType} from '../../params/_Base';
import {EventConnectionPoint, EventConnectionPointType, BaseEventConnectionPoint} from '../utils/io/connections/Event';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {CoreGraphNode} from '../../../core/graph/CoreGraphNode';
import {TypedEventNode} from './_Base';
import {ParamOptions} from '../../params/utils/OptionsController';
import {isBooleanTrue} from '../../../core/Type';
import {EventContext} from '../../../core/event/EventContextType';

enum SceneNodeInput {
	SET_FRAME = 'setFrame',
}
enum SceneNodeOutput {
	TICK = 'tick',
	TIME_REACHED = 'timeReached',
}
const UPDATE_SCENE_EVENT_PARAM_OPTIONS: ParamOptions = {
	visibleIf: {active: 1},
	callback: (node: BaseNodeType) => {
		SceneEventNode.PARAM_CALLBACK_updateSceneEventsController(node as SceneEventNode);
	},
};
const UPDATE_TIME_DEPENDENCY_PARAM_OPTIONS: ParamOptions = {
	visibleIf: {active: 1},
	callback: (node: BaseNodeType) => {
		SceneEventNode.PARAM_CALLBACK_updateTimeDependency(node as SceneEventNode);
	},
};

interface OnTickCallbackBuilderOptions {
	tick: boolean;
	treachedTime: boolean;
	reachedTime: number;
}

enum EventName {
	CREATED = 'created',
	READY = 'ready',
	PLAY = 'play',
	PAUSE = 'pause',
}
export const ACCEPTED_EVENT_TYPES: EventName[] = [EventName.CREATED, EventName.READY, EventName.PLAY, EventName.PAUSE];

class SceneEventParamsConfig extends NodeParamsConfig {
	/** @param toggle on to allow any event to be listened to */
	active = ParamConfig.BOOLEAN(true, {
		callback: (node: BaseNodeType, param: BaseParamType) => {
			SceneEventNode.PARAM_CALLBACK_updateActiveState(node as SceneEventNode);
		},
		separatorAfter: true,
	});
	/** @param set which element triggers the event */
	element = ParamConfig.INTEGER(0, {
		hidden: true,
	});
	/** @param toggle on to trigger an event when the scene has been created. This can be useful to initialize other nodes */
	created = ParamConfig.BOOLEAN(1, UPDATE_SCENE_EVENT_PARAM_OPTIONS);
	/** @param toggle on to trigger an event when every object in the scene has been loaded. This can be useful to initialize other nodes */
	ready = ParamConfig.BOOLEAN(1, UPDATE_SCENE_EVENT_PARAM_OPTIONS);
	/** @param toggle on to trigger an event when the scene starts playing */
	play = ParamConfig.BOOLEAN(1, UPDATE_SCENE_EVENT_PARAM_OPTIONS);
	/** @param toggle on to trigger an event when the scene pauses */
	pause = ParamConfig.BOOLEAN(1, UPDATE_SCENE_EVENT_PARAM_OPTIONS);
	/** @param toggle on to trigger an event on every tick */
	tick = ParamConfig.BOOLEAN(1, {
		separatorAfter: true,
		...UPDATE_TIME_DEPENDENCY_PARAM_OPTIONS,
	});
	/** @param toggle on to trigger an event on every tick */
	treachedTime = ParamConfig.BOOLEAN(0, UPDATE_TIME_DEPENDENCY_PARAM_OPTIONS);
	/** @param time to trigger an event */
	reachedTime = ParamConfig.FLOAT(10, {
		visibleIf: {treachedTime: 1},
		range: [0, 100],
		separatorAfter: true,
		...UPDATE_TIME_DEPENDENCY_PARAM_OPTIONS,
	});
	/** @param frame to set */
	setFrameValue = ParamConfig.INTEGER(1, {
		range: [0, 100],
	});
	/** @param button to set a specific frame */
	setFrame = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			SceneEventNode.PARAM_CALLBACK_setFrame(node as SceneEventNode);
		},
	});
}
const ParamsConfig = new SceneEventParamsConfig();

export class SceneEventNode extends TypedEventNode<SceneEventParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return 'scene';
	}

	private _graphNode: CoreGraphNode | undefined;

	override dispose() {
		this._graphNode?.dispose();
		super.dispose();
	}

	override initializeNode() {
		this.io.inputs.setNamedInputConnectionPoints([
			new EventConnectionPoint(
				SceneNodeInput.SET_FRAME,
				EventConnectionPointType.BASE,
				this._onSetFrame.bind(this)
			),
			new EventConnectionPoint(EventName.PLAY, EventConnectionPointType.BASE, this._play.bind(this)),
			new EventConnectionPoint(EventName.PAUSE, EventConnectionPointType.BASE, this._pause.bind(this)),
		]);
		const outConnectionPoints: BaseEventConnectionPoint[] = ACCEPTED_EVENT_TYPES.map((event_type) => {
			return new EventConnectionPoint(event_type, EventConnectionPointType.BASE);
		});
		outConnectionPoints.push(new EventConnectionPoint(SceneNodeOutput.TICK, EventConnectionPointType.BASE));
		outConnectionPoints.push(new EventConnectionPoint(SceneNodeOutput.TIME_REACHED, EventConnectionPointType.BASE));
		this.io.outputs.setNamedOutputConnectionPoints(outConnectionPoints);

		this.params.onParamsCreated('updateTimeDependency', () => {
			this._updateTimeDependency();
		});
		this._updateSceneEventsController();

		// register/unregister
		const register = () => {
			this._updateSceneEventsController();
		};
		const unregister = () => {
			const eventsController = this.scene().eventsDispatcher.sceneEventsController;
			eventsController.removeObserverFromAllEventTypes(this);
		};
		this.lifecycle.onAfterAdded(register);
		this.lifecycle.onBeforeDeleted(unregister);
	}

	override processEvent(eventContext: EventContext<Event>) {
		if (!this.pv.active) {
			return;
		}
		if (!eventContext.event) {
			return;
		}
		const eventType = eventContext.event.type;
		switch (eventType) {
			case PolyEventName.SCENE_CREATED: {
				return this.dispatchEventToOutput(EventName.CREATED, eventContext);
			}
			case PolyEventName.SCENE_READY: {
				return this.dispatchEventToOutput(EventName.READY, eventContext);
			}
			case PolyEventName.SCENE_PLAY: {
				return this.dispatchEventToOutput(EventName.PLAY, eventContext);
			}
			case PolyEventName.SCENE_PAUSE: {
				return this.dispatchEventToOutput(EventName.PAUSE, eventContext);
			}
		}

		this.dispatchEventToOutput(eventContext.event.type, eventContext);
	}

	private _onSetFrame(eventContext: EventContext<Event>) {
		this.scene().setFrame(this.pv.setFrameValue);
	}
	private _play(eventContext: EventContext<Event>) {
		this.scene().play();
	}
	private _pause(eventContext: EventContext<Event>) {
		this.scene().pause();
	}

	private _timeReached = false;
	private _onTickCheckTimeReached(time: number, reachedTime: number) {
		if (time >= this.pv.reachedTime) {
			if (!this._timeReached) {
				this._timeReached = true;
				this.dispatchEventToOutput(SceneNodeOutput.TIME_REACHED, {});
			}
		} else {
			this._timeReached = false;
		}
	}
	private _onTickEvent() {
		this.dispatchEventToOutput(SceneNodeOutput.TICK, {});
	}

	private _updateTimeDependency() {
		const timeGraphNode = this.scene().timeController.graphNode;
		this._graphNode?.removeGraphInput(timeGraphNode);
		if (!isBooleanTrue(this.pv.active)) {
			return;
		}

		if (isBooleanTrue(this.pv.treachedTime) || isBooleanTrue(this.pv.tick)) {
			this._graphNode = this._graphNode || new CoreGraphNode(this.scene(), 'sceneNodeTimeGraphNode');
			this._graphNode.addGraphInput(timeGraphNode);

			const options: OnTickCallbackBuilderOptions = {
				tick: this.pv.tick,
				treachedTime: this.pv.treachedTime,
				reachedTime: this.pv.reachedTime,
			};

			const callback = this._buildOnTickCallback(options)?.bind(this);
			if (callback) {
				const callbackName = 'timeUpdate';
				this._graphNode.removePostDirtyHook(callbackName);
				this._graphNode.addPostDirtyHook(callbackName, callback);
			}
		}
	}
	private _buildOnTickCallback(options: OnTickCallbackBuilderOptions) {
		if (isBooleanTrue(options.treachedTime) && isBooleanTrue(options.tick)) {
			return () => {
				const time = this.scene().time();
				this._onTickEvent();
				this._onTickCheckTimeReached(time, options.reachedTime);
			};
		} else {
			if (isBooleanTrue(options.treachedTime)) {
				return () => {
					const time = this.scene().time();
					this._onTickCheckTimeReached(time, options.reachedTime);
				};
			}
			if (isBooleanTrue(options.tick)) {
				return this._onTickEvent.bind(this);
			}
		}
	}

	static PARAM_CALLBACK_setFrame(node: SceneEventNode) {
		node._onSetFrame({});
	}
	static PARAM_CALLBACK_updateTimeDependency(node: SceneEventNode) {
		node._updateTimeDependency();
	}

	static PARAM_CALLBACK_updateSceneEventsController(node: SceneEventNode) {
		node._updateSceneEventsController();
	}
	static PARAM_CALLBACK_updateActiveState(node: SceneEventNode) {
		node._updateTimeDependency();
		node._updateSceneEventsController();
	}

	private _updateSceneEventsController() {
		const eventsController = this.scene().eventsDispatcher.sceneEventsController;
		eventsController.removeObserverFromAllEventTypes(this);
		if (!this.pv.active) {
			return;
		}
		this._updateTimeDependency();
		// if (isBooleanTrue(this.pv.tick)) {
		// 	eventsController.addObserver(this, SceneEventType.TICK);
		// }
		if (isBooleanTrue(this.pv.created)) {
			eventsController.addObserver(this, PolyEventName.SCENE_CREATED);
		}
		if (isBooleanTrue(this.pv.ready)) {
			eventsController.addObserver(this, PolyEventName.SCENE_READY);
		}
		if (isBooleanTrue(this.pv.play)) {
			eventsController.addObserver(this, PolyEventName.SCENE_PLAY);
		}
		if (isBooleanTrue(this.pv.pause)) {
			eventsController.addObserver(this, PolyEventName.SCENE_PAUSE);
		}
	}
}
