import { createEmitter } from "@nebula/core-utils";

/**
 * @typedef {"gamepad" | "keyboard" | "mouse" | "touch" | "unknown"} InputSource
 */

/**
 * @typedef {"pressed" | "released" | "moved" | "axis"} InputEventType
 */

/**
 * Normalized input event shape.
 * @typedef {object} InputEvent
 * @property {InputSource} source
 * @property {string} control - Physical control identifier (e.g. "a", "dpad-up", "left-stick-x").
 * @property {InputEventType} type
 * @property {number} value - Normalized 0..1 for digital, -1..1 for analog.
 * @property {number} [timestamp]
 */

/**
 * Binding definition for mapping physical controls to actions.
 * @typedef {object} InputBinding
 * @property {InputSource} source
 * @property {string} control
 * @property {InputEventType | "any"} [type]
 * @property {number} [threshold] - Digital activation threshold for analog controls.
 */

/**
 * @typedef {object} ActionUpdate
 * @property {string} action
 * @property {number} value
 * @property {boolean} active
 * @property {InputEvent} event
 */

/**
 * @typedef {object} ActionState
 * @property {number} value
 * @property {boolean} active
 */

/**
 * @typedef {object} ActionMapperOptions
 * @property {Record<string, InputBinding[]>} bindings
 * @property {number} [deadzone] - Analog deadzone applied to -1..1 axes.
 */

/**
 * Create an action-based mapper that converts raw input events into action state updates.
 * @param {ActionMapperOptions} options
 * @returns {{
 *   mapEvent: (event: InputEvent) => ActionUpdate[],
 *   getActionState: (action: string) => ActionState,
 *   onAction: (listener: (update: ActionUpdate) => void) => () => void,
 *   reset: () => void
 * }}
 */
export function createActionMapper(options) {
  const deadzone = options.deadzone ?? 0.2;
  /** @type {Map<string, ActionState>} */
  const state = new Map();
  const updates = createEmitter();

  /** @param {string} action */
  function ensureState(action) {
    if (!state.has(action)) {
      state.set(action, { value: 0, active: false });
    }
    return /** @type {ActionState} */ (state.get(action));
  }

  /** @param {number} value */
  function applyDeadzone(value) {
    if (Math.abs(value) < deadzone) return 0;
    return value;
  }

  return {
    mapEvent(event) {
      /** @type {ActionUpdate[]} */
      const result = [];

      for (const [action, bindings] of Object.entries(options.bindings)) {
        for (const binding of bindings) {
          if (binding.source !== event.source) continue;
          if (binding.control !== event.control) continue;
          if (binding.type && binding.type !== "any" && binding.type !== event.type) continue;

          const threshold = binding.threshold ?? 0.5;
          const value = event.type === "axis" || event.type === "moved"
            ? applyDeadzone(event.value)
            : event.type === "released"
              ? 0
              : 1;

          const active = Math.abs(value) >= threshold;
          const current = ensureState(action);
          current.value = value;
          current.active = active;

          const update = { action, value, active, event };
          result.push(update);
          updates.emit(update);
        }
      }

      return result;
    },
    getActionState(action) {
      return ensureState(action);
    },
    onAction(listener) {
      return updates.on(listener);
    },
    reset() {
      state.clear();
    }
  };
}
