import {BaseInputEventNodeType} from '../../../nodes/event/_BaseInput';
import {SceneEventsDispatcher} from './EventsDispatcher';
import {EventData, EventType} from '../../../../core/event/EventData';
import {PointerEventType} from '../../../../core/event/PointerEventType';
import {CoreEventEmitter} from '../../../../core/event/CoreEventEmitter';
import {addToSetAtEntry} from '../../../../core/MapUtils';
import {ActorEvaluatorGenerator} from '../../../nodes/js/code/assemblers/actor/ActorEvaluatorGenerator';
import type {EventContext} from '../../../../core/event/EventContextType';

export abstract class BaseSceneEventsController<
	E extends Event,
	T extends BaseInputEventNodeType
	// JsNodeType extends BaseUserInputJsNodeType
> {
	private _activeEventDatas: EventData[] = [];
	private _activeEventDataTypes: Set<string> = new Set();
	protected _eventNodes: Set<T> = new Set();
	protected _requireCanvasEventListeners: boolean = false;
	protected _actorEvaluators: Set<ActorEvaluatorGenerator> = new Set();
	// protected _actorEventNames: Set<string> = new Set();
	protected _actorEvaluatorsByEventNames: Map<string, Map<CoreEventEmitter, Set<ActorEvaluatorGenerator>>> =
		new Map();
	constructor(protected dispatcher: SceneEventsDispatcher) {}

	registerEventNode(node: T) {
		this._eventNodes.add(node);
		this.updateViewerEventListeners();
	}
	unregisterEventNode(node: T) {
		this._eventNodes.delete(node);
		this.updateViewerEventListeners();
	}
	registerEvaluatorGenerator(evaluator: ActorEvaluatorGenerator) {
		this._actorEvaluators.add(evaluator);
		this._updateActorEvaluatorCache();
		this.updateViewerEventListeners();
	}
	unregisterEvaluatorGenerator(evaluator: ActorEvaluatorGenerator) {
		this._actorEvaluators.delete(evaluator);
		this._updateActorEvaluatorCache();
		this.updateViewerEventListeners();
	}

	private _updateActorEvaluatorCache() {
		// this._actorEventNames.clear();
		this._actorEvaluatorsByEventNames.clear();
		this._actorEvaluators.forEach((evaluator) => {
			const eventDatas = evaluator.eventDatas;
			if (eventDatas) {
				eventDatas.forEach((eventData) => {
					const eventName = eventData.type;
					const emitter = eventData.emitter;
					let mapForEventName = this._actorEvaluatorsByEventNames.get(eventName);
					if (!mapForEventName) {
						mapForEventName = new Map();
						this._actorEvaluatorsByEventNames.set(eventName, mapForEventName);
					}
					addToSetAtEntry(mapForEventName, emitter, evaluator);
				});
			}
			// const nodeEventNames = node.userInputEventNames();
			// const emitter = node.eventEmitter();
			// for (let eventName of nodeEventNames) {
			// 	// this._actorEventNames.add(eventName);

			// 	let mapForEventName = this._actorNodesByEventNames.get(eventName);
			// 	if (!mapForEventName) {
			// 		mapForEventName = new Map();
			// 		this._actorNodesByEventNames.set(eventName, mapForEventName);
			// 	}
			// 	MapUtils.addToSetAtEntry(mapForEventName, emitter, node);
			// }
		});
	}

	abstract type(): string;
	abstract acceptedEventTypes(): Set<string>;
	// abstract accepts_event(event: Event): boolean;

	processEvent(eventContext: EventContext<E>) {
		if (this._activeEventDatas.length == 0) {
			return;
		}
		const eventType = eventContext.event?.type;
		if (eventType) {
			// The check here if the eventType is active
			// is not necessary for canvas events (pointer, mouse, keyboard)
			// but currently necessary for scene events (such as tick)
			if (!this._activeEventDataTypes.has(eventType)) {
				return;
			}
		}

		this._eventNodes.forEach((node) => {
			node.processEvent(eventContext);
		});
	}

	updateViewerEventListeners() {
		this._updateActiveEventTypes();

		if (this._requireCanvasEventListeners) {
			this.dispatcher.scene.viewersRegister.traverseViewers((viewer) => {
				viewer.eventsController().updateEvents(this);
			});
		}
	}

	activeEventDatas() {
		return this._activeEventDatas;
	}

	private _updateActiveEventTypes() {
		const _storeEventData = (eventData: EventData) => {
			this._activeEventDatas.push(eventData);
			this._activeEventDataTypes.add(eventData.type);
		};

		const _reset = () => {
			this._activeEventDatas.splice(0, this._activeEventDatas.length);
			this._activeEventDataTypes.clear();
		};

		const _actorEventDatas = () => {
			let eventTypeByEmitter: Map<CoreEventEmitter, Set<EventType>> = new Map();

			this._actorEvaluatorsByEventNames.forEach((mapForEventName, eventName) => {
				mapForEventName.forEach((nodes, emitter) => {
					nodes.forEach((node) => {
						addToSetAtEntry(eventTypeByEmitter, emitter, eventName);
					});
				});
			});
			const eventDatas: EventData[] = [];
			eventTypeByEmitter.forEach((eventNames, emitter) => {
				for (const eventName of eventNames) {
					const eventData: EventData = {
						type: eventName,
						emitter,
					};
					eventDatas.push(eventData);
				}
			});
			return eventDatas;
		};

		const _updateActorNodesEventData = () => {
			const actorEventDatas = _actorEventDatas();
			if (actorEventDatas) {
				for (const data of actorEventDatas) {
					_storeEventData(data);
				}
			}
		};

		const _updateEventNodesEventData = () => {
			const activeNodeEventTypesState: Set<EventData> = new Set();
			this._eventNodes.forEach((node) => {
				if (node.parent()) {
					const nodeActiveEventDatas = node.activeEventDatas();
					for (const data of nodeActiveEventDatas) {
						activeNodeEventTypesState.add(data);
					}
				}
			});
			activeNodeEventTypesState.forEach((state, data) => {
				_storeEventData(data);
			});
		};

		_reset();

		// we always need the pointermove and touchmove
		// in order to always update the cursor
		_storeEventData({
			type: PointerEventType.pointermove,
			emitter: CoreEventEmitter.DOCUMENT,
		});
		_storeEventData({
			type: PointerEventType.touchmove,
			emitter: CoreEventEmitter.DOCUMENT,
		});

		_updateActorNodesEventData();
		_updateEventNodesEventData();
	}
}

export type BaseSceneEventsControllerType = BaseSceneEventsController<
	Event,
	BaseInputEventNodeType
	// BaseUserInputJsNodeType
>;
export class BaseSceneEventsControllerClass extends BaseSceneEventsController<
	Event,
	BaseInputEventNodeType
	// BaseUserInputJsNodeType
> {
	type() {
		return '';
	}
	acceptedEventTypes(): Set<string> {
		return new Set<string>();
	}
}
