import { AnyCallable, AnySignal, Callable, Callable0, Callable1, Camera3D, CharacterBody3D, Color, float64, NodePath, ProjectSettings, Signal, Signal0, Signal1, Variant, Vector2, Vector3 } from 'godot'
import { export_, help, onready } from 'godot.annotations'
import PlayerInput from './player_input'
import DebugDraw from './debug_draw'
import Interactor from './interactor'
import PlayerAnimation, { PlayerAnimationName } from './player_animation'

export default class Player extends CharacterBody3D {
  readonly gravity = ProjectSettings.get_setting('physics/3d/default_gravity')

  @onready("Input")
  readonly player_input!: PlayerInput

  @onready("AnimationTree")
  readonly player_animation!: PlayerAnimation

  @onready("Interactor")
  readonly interactor!: Interactor

  @help('Forward walk speed in units per second')
  @export_(Variant.Type.TYPE_FLOAT)
  readonly walk_speed = 3

  @help('Run speed in units per second')
  @export_(Variant.Type.TYPE_FLOAT)
  readonly run_speed = 6

  @help('Turn speed in rotations per second')
  @export_(Variant.Type.TYPE_FLOAT)
  readonly turn_speed = 1
  private _rotation_speed = 2 * Math.PI

  @help('Aim turn speed in rotations per second')
  @export_(Variant.Type.TYPE_FLOAT)
  readonly aim_turn_speed = 2
  private _aim_rotation_speed = 2 * Math.PI * 2

  is_moving() {
    return !this.velocity.is_zero_approx()
  }

  is_running() {
    return this.player_input.is_running && this.is_moving()
  }

  is_aiming() {
    return this.player_input.is_aiming && !this.is_moving()
  }

  is_firing() {
    return this._wants_to_fire
  }

  private _can_act: boolean = true
  private _wants_to_fire: boolean = false

  // TODO: abstract this horrible shit away
  private _try_interact_callable!: Callable0
  private _queue_fire_callable!: Callable0
  private _disable_action_callable!: Callable1<string>
  private _enable_action_callable!: Callable1<string>

  _ready(): void {
    this._rotation_speed = this.turn_speed * 2 * Math.PI
    this._aim_rotation_speed = this.aim_turn_speed * 2 * Math.PI

    this._try_interact_callable = Callable.create(
      this,
      this._try_interact
    )

    this._queue_fire_callable = Callable.create(
      this,
      this._queue_fire
    )

    this.player_input.interact.connect(this._try_interact_callable)
    this.player_input.fire.connect(this._queue_fire_callable)

    this._disable_action_callable = Callable.create(
      this,
      this._disable_action
    )

    this._enable_action_callable = Callable.create(
      this,
      this._enable_action
    )

    this.player_animation.animation_started.connect(this._disable_action_callable)
    this.player_animation.animation_finished.connect(this._enable_action_callable)
  }

  private _disable_action(value: string) {
    if (PlayerAnimationName.Fire === value) {
      this._can_act = false
    }
  }

  private _enable_action(value: string) {
    if (PlayerAnimationName.Fire === value) {
      this._can_act = true
    }
  }

  private _try_interact() {
    this.interactor.try_interact_nearest()
  }

  private async _queue_fire() {
    if (this.is_aiming()) {
      this._wants_to_fire = true
      await this.get_tree().process_frame.as_promise()
      this._wants_to_fire = false
    }
  }

  private _get_movement(input: Vector2): Vector3 {
    let speed = this.walk_speed

    if (this.is_running()) {
      input = input.normalized()
      speed = this.run_speed
    }

    return Vector3.MULTIPLY(speed, new Vector3(
      input.x,
      0,
      input.y
    ))
  }

  private _get_gravitational_force(delta: float64): float64 {
    return -this.gravity * delta
  }

  private _draw_movement(movement: Vector3) {
    DebugDraw.draw_line(
      Vector3.ADD(
        Vector3.UP,
        this.position,
      ),
      Vector3.ADD(
        Vector3.UP,
        Vector3.ADD(this.position, movement)
      ),
      Color.GREEN
    )
  }

  private _rotate_toward(direction: Vector3, _delta: float64) {
    if (!direction.is_zero_approx()) {
      this.look_at(
        Vector3.ADD(this.global_position, direction),
        Vector3.UP,
        true
      )
    }
  }

  private _move_toward(movement: Vector3, delta: float64) {
    this.velocity = new Vector3(
      movement.x,
      this.velocity.y + this._get_gravitational_force(delta),
      movement.z
    )

    this.move_and_slide()
  }

  // TODO: combine this with _rotate_toward as a general
  // rotate_over_time_toward method
  // right now, both rotate instantly
  private _aim_toward(delta: float64) {
    // TODO: this performs a costly sqrt operation. necessary?
    const look = this.player_input.look_direction.project(this.global_position)
    look.y = this.global_position.y
    this.look_at(look, Vector3.UP, true)
  }

  _physics_process(delta: float64): void {
    this.player_input.process_queue()

    if (!this._can_act) {
      return
    }

    if (this.player_input.is_aiming) {
      this.velocity = Vector3.ZERO
      this._aim_toward(delta)
      return
    }

    const input = this.player_input.movement_input

    const movement = this._get_movement(input)

    this._draw_movement(movement)

    this._rotate_toward(movement, delta)
    this._move_toward(movement, delta)
  }
}

