import { Callable, Callable1, Camera3D, float64, Input, InputEvent, InputEventJoypadButton, InputEventJoypadMotion, InputEventKey, InputEventMouse, InputEventMouseMotion, int32, Node3D, Signal0, Signal1, Variant, Vector2, Vector3 } from 'godot'
import { export_, signal } from 'godot.annotations'
import InputBuffer from './input_buffer'
import MainCamera from './main_camera'
import MessageBus from './message_bus'

export const PlayerMovement = Object.freeze({
  MoveLeft: 'move_left',
  MoveRight: 'move_right',
  MoveUp: 'move_up',
  MoveDown: 'move_down',
} as const)

export const PlayerLook = Object.freeze({
  LookLeft: 'look_left',
  LookRight: 'look_right',
  LookUp: 'look_up',
  LookDown: 'look_down',
} as const)

class LookDirection {
  static Empty = new LookDirection(Vector2.ZERO, false)

  _value: Vector2
  needs_projection: boolean
  camera?: Camera3D

  get value() {
    return this._value
  }

  set value(val: Vector2) {
    this._value = val
  }

  constructor(value: Vector2, needs_projection: boolean) {
    this._value = value
    this.needs_projection = needs_projection
  }

  accumulate(delta: Vector2) {
    this.value = Vector2.ADD(this._value, delta)
  }

  project(to_position: Vector3): Vector3 {
    if (this.camera) {
      // TODO: this performance a sqrt operation,
      // check if this is necessary
      const distance = this.camera.global_position.distance_to(to_position)
      return this.camera?.project_position(this._value, distance) ?? Vector3.ZERO
    } else {
      return Vector3.ZERO
    }
  }
}

export const PlayerAction = Object.freeze({
  Run: 'run',
  Interact: 'interact',
  Aim: 'aim',
  Fire: 'fire'
} as const)

export const Device = Object.freeze({
  KeyboardMouse: 0,
  Gamepad: 1
} as const)

type PlayerActionType = typeof PlayerAction[keyof typeof PlayerAction]

class InputDesc {
  readonly type: PlayerActionType
  readonly value: boolean

  constructor(type: PlayerActionType, value: boolean) {
    this.type = type
    this.value = value
  }
}

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

  @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: typeof Device[keyof typeof Device] = Device.KeyboardMouse

  get last_known_device() { return this._last_known_device }

  private _movement_buffer!: InputBuffer<Vector2>

  get movement_buffer() {
    return this._movement_buffer
  }

  get movement_input() {
    const dir = this._movement_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
      }
    }
  }

  private _look_direction: LookDirection = LookDirection.Empty

  get look_direction() {
    return this._look_direction
  }

  private _input_buffer!: InputBuffer<InputDesc>

  get input_buffer() {
    return this._input_buffer
  }

  private _changed_since_last_frame = false

  get changed_since_last_frame() {
    return this._changed_since_last_frame
  }

  private _camera!: Camera3D

  private _running: boolean = false
  private _interacting: boolean = false
  private _aiming: boolean = false
  private _firing: boolean = false

  get is_running() {
    return this._running
  }

  private set is_running(value: boolean) {
    if (value !== this._running) {
      this._running = value
      this.run.emit(value)
    }
  }

  get is_interacting() {
    return this._interacting
  }

  private set is_interacting(value) {
    if (value !== this._interacting) {
      this._interacting = value
      if (value) {
        this.interact.emit()
      }
    }
  }

  get is_aiming() {
    return this._aiming
  }

  private set is_aiming(value) {
    if (value !== this._aiming) {
      this._aiming = value
      this.aim.emit(value)
    }
  }

  get is_firing() {
    return this._firing
  }

  private set is_firing(value) {
    if (value !== this._firing) {
      this._firing = value
      if (value) {
        this.fire.emit()
      }
    }
  }

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

  @signal()
  readonly interact!: Signal0

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

  @signal()
  readonly fire!: Signal0

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

  private _await_camera_callable!: Callable1<Camera3D>

  _ready(): void {
    this._camera = MainCamera.instance

    this._await_camera_callable = Callable.create(this, this._await_camera)
    MessageBus.instance.active_camera_changed.connect(this._await_camera_callable)

    this._look_direction.camera = this._camera

    this._min_range_sqr = this._sqr(this.min_range)
    this._max_range_sqr = this._sqr(this.max_range)

    this._movement_buffer = new InputBuffer(this.movement_buffer_size, Vector2.ZERO)
    this._input_buffer = new InputBuffer(this.input_buffer_size)
  }

  private _await_camera(camera: Camera3D) {
    console.log('got camera', camera)
    this._camera = camera
    this._look_direction.camera = camera
  }

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

    let dir = Vector2.ZERO

    switch (this._last_known_device) {
      case Device.Gamepad:
        dir = Input.get_vector(
          PlayerLook.LookLeft,
          PlayerLook.LookRight,
          PlayerLook.LookUp,
          PlayerLook.LookDown
        )

        this._look_direction.value = dir
        this._look_direction.needs_projection = false
      case Device.KeyboardMouse:
        dir = this.get_viewport().get_mouse_position()
        this._look_direction.value = dir
        this._look_direction.needs_projection = true
        break
    }

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

    this._movement_buffer.push(next_movement)
  }

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

    for (const action of Object.values(PlayerAction)) {
      if (event.is_action_released(action)) {
        this._input_buffer.push(new InputDesc(action, false))
      }

      if (event.is_action_pressed(action)) {
        this._input_buffer.push(new InputDesc(action, true))
      }
    }
  }

  process_queue() {
    while (!this.input_buffer.is_empty()) {
      const input = this.input_buffer.pop()
      switch (input.type) {
        case PlayerAction.Run:
          this.is_running = input.value
          break
        case PlayerAction.Interact:
          this.is_interacting = input.value
          break
        case PlayerAction.Aim:
          this.is_aiming = input.value
          break
        case PlayerAction.Fire:
          this.is_firing = input.value
          break
      }
    }
  }
}

