import { Callable, Camera3D, CharacterBody3D, Color, float64, NodePath, ProjectSettings, Variant, Vector2, Vector3 } from 'godot'
import { export_, help } from 'godot.annotations'
import PlayerInput from './player_input'
import MessageBus from './message_bus'
import DebugDraw from './debug_draw'
import { CameraCache } from './camera_cache'
import Interactor from './interactor'

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

  @export_(Variant.Type.TYPE_NODE_PATH)
  player_input!: NodePath
  _player_input!: PlayerInput

  @export_(Variant.Type.TYPE_NODE_PATH)
  interactor!: NodePath
  _interactor!: Interactor

  is_running() { return this._player_input.is_running }

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

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

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

  _camera!: CameraCache

  _ready(): void {
    this._rotation_speed = this.turn_speed * 2 * Math.PI
    this._player_input = this.get_node(this.player_input) as PlayerInput

    const camera = this.get_viewport().get_camera_3d()
    this._camera = new CameraCache(camera)

    MessageBus.instance.active_camera_changed.connect(
      Callable.create(
        this,
        this.update_camera
      )
    )

    this._player_input.interact.connect(
      Callable.create(
        this,
        this.try_interact
      )
    )
  }

  try_interact() {

  }

  update_camera(cam: Camera3D) {
    this._camera.update_camera(cam)
  }

  get_movement(input: Vector2): Vector3 {
    const speed = this.is_running() ? this.run_speed : this.walk_speed
    return Vector3.MULTIPLY(speed, new Vector3(
      input.x,
      0,
      input.y
    ))
  }

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

  is_kinda_equal(a: Vector2, b: Vector2, e: float64 = 0.1) {
    return a.distance_to(b) <= e
  }

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

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

  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()
  }

  _physics_process(delta: float64): void {
    const buf = this._player_input.input_buffer
    const input = buf.tail()
    const prev = buf.at(-2)

    if (prev && !this.is_kinda_equal(input, prev)) {
      this._camera.recalculate()
    }

    const movement = this._camera.transform_vec(
      this.get_movement(input)
    )

    this._draw_movement(movement)

    this.rotate_toward(movement, delta)
    this.move_toward(movement, delta)
  }
}

