import { float64, Input, InputEvent, InputEventJoypadButton, InputEventJoypadMotion, InputEventKey, InputEventMouse, int32, Node3D, Signal0, Signal1, Variant, Vector2 } from 'godot'
import { export_, signal } from 'godot.annotations'
import InputBuffer from './input_buffer'

export const PlayerAction = {
  MoveLeft: 'move_left',
  MoveRight: 'move_right',
  MoveUp: 'move_up',
  MoveDown: 'move_down',
  Run: 'run',
  Interact: 'interact',
  Aim: 'aim',
  Fire: 'fire'
} as const

enum Device {
  KeyboardMouse = 0,
  Gamepad = 1
}

export default class PlayerInput extends Node3D {
  @export_(Variant.Type.TYPE_INT)
  private input_buffer_size: int32 = 8

  @export_(Variant.Type.TYPE_FLOAT)
  private min_range: float64 = 0.1
  private _min_range_sqr: float64 = 0.01

  @export_(Variant.Type.TYPE_FLOAT)
  private max_range: float64 = 1
  private _max_range_sqr: float64 = 1

  private _last_known_device = Device.KeyboardMouse

  get last_known_device() { return this._last_known_device }

  private _input_buffer!: InputBuffer

  get input_buffer() {
    return this._input_buffer
  }

  get movement_input() {
    const dir = this._input_buffer.head()
    if (dir.is_zero_approx()) {
      return Vector2.ZERO
    } else {
      const len = dir.length_squared()

      if (len < this._min_range_sqr) {
        return Vector2.MULTIPLY(dir.normalized(), this.min_range)
      } else if (len > this._max_range_sqr) {
        return Vector2.MULTIPLY(dir.normalized(), this.max_range)
      } else {
        return dir
      }
    }
  }

  _running: boolean = false

  get is_running() {
    return this._running
  }

  @signal()
  readonly run!: Signal1<boolean>

  _interacting: boolean = false

  get is_interacting() {
    return this._interacting
  }

  @signal()
  readonly interact!: Signal0

  _aiming: boolean = false

  get is_aiming() {
    return this._aiming
  }

  @signal()
  readonly aim!: Signal1<boolean>

  _firing: boolean = false

  get is_firing() {
    return this._firing
  }

  @signal()
  readonly fire!: Signal1<boolean>

  private _changed_since_last_frame = false

  get changed_since_last_frame() {
    return this._changed_since_last_frame
  }

  _sqr(n: number) {
    return n * n
  }

  _ready(): void {
    this._min_range_sqr = this._sqr(this.min_range)
    this._max_range_sqr = this._sqr(this.max_range)

    this._input_buffer = new InputBuffer(this.input_buffer_size)
  }

  _process(_delta: float64): void {
    const next_movement = Input.get_vector(PlayerAction.MoveLeft, PlayerAction.MoveRight, PlayerAction.MoveUp, PlayerAction.MoveDown)

    this._changed_since_last_frame = !next_movement.is_equal_approx(this.movement_input)

    this._input_buffer.push(next_movement)
  }

  _input(event: InputEvent): void {
    if (event instanceof InputEventKey || event instanceof InputEventMouse) {
      this._last_known_device = Device.KeyboardMouse
    } else if (event instanceof InputEventJoypadButton || event instanceof InputEventJoypadMotion) {
      this._last_known_device = Device.Gamepad
    }

    const running = event.is_action_pressed(PlayerAction.Run)
    if (running !== this._running) {
      this._running = running
      this.run.emit(running)
    }

    const interacting = event.is_action_pressed(PlayerAction.Interact)

    if (interacting !== this._interacting) {
      this._interacting = interacting
      if (interacting) {
        this.interact.emit()
      }
    }

    const aiming = event.is_action_pressed(PlayerAction.Aim)
    if (aiming !== this._aiming) {
      this._aiming = aiming
      this.aim.emit(aiming)
    }

    const firing = event.is_action_pressed(PlayerAction.Fire)
    if (firing !== this._firing) {
      this._firing = firing
      this.fire.emit(firing)
    }
  }
}

