// A platformer-friendly character controller, as an aspect on a Node2D. Built on a fixed-rotation
// dynamic Physics2D body: you drive horizontal intent + jump, and Box2D handles gravity, falling, and
// wall/floor collision response; a short downward ray reports `grounded`.
//
//   const hero = new Sprite({ texture, anchor: [0.5, 1] })
//     .aspect(Shape2D, { capsule: { from: [0, 6], to: [0, 26], radius: 6 } })
//     .aspect(Physics2D, { motion: 'dynamic', fixedRotation: true })
//     .aspect(CharacterController2D, { speed: 200, jumpSpeed: 520 })
//   Input.on('left',  d => hero.controller.move(d ? -1 : 0))
//   Input.on('jump',  () => hero.controller.jump())
//
// Velocity-control (set velocity each frame) is the standard, robust approach for platformers and
// reuses the physics solver. For pixel-tight movement without any solver bounce/seam quirks, a
// kinematic move-and-slide controller can be added later (see creator-2d PLAN.md, Phase G).

import { Aspect } from "../core/Aspect"
import { Vec2 } from "../math/vec"
import { Physics2D } from "./Physics2D"
import { Shape2D } from "./Shape2D"
import type { Node2D } from "./Node2D"

export class CharacterController2D extends Aspect<"controller", Node2D> {
  static readonly aspect = "controller"

  /** Horizontal move speed (world units/s). */
  speed = 200
  /** Jump take-off speed (world units/s). */
  jumpSpeed = 500
  /** Extra probe distance below the body's feet used for the grounded check (world units). */
  groundProbe = 6
  /** Distance from the node origin down to the feet. Defaults to half the sprite height. */
  footOffset?: number

  private _moveX = 0
  private _jumpQueued = false
  private _grounded = false

  /** Horizontal intent in [-1, 1] (e.g. from input). Sticky — set 0 to stop. Applied next frame. */
  move(dir: number): void { this._moveX = dir < -1 ? -1 : dir > 1 ? 1 : dir }
  /** Queue a jump; consumed on the next frame if grounded. */
  jump(): void { this._jumpQueued = true }
  /** True while standing on something (updated each frame). */
  get grounded(): boolean { return this._grounded }

  private get phys(): Physics2D | undefined { return this.node.get(Physics2D) }

  onAttach(): void {
    // Ensure a Shape2D (auto box from the sprite) + a dynamic, fixed-rotation body if not already set up.
    if (!this.node.has(Shape2D)) this.node.aspect(Shape2D, {})
    if (!this.node.has(Physics2D)) this.node.aspect(Physics2D, { motion: "dynamic", fixedRotation: true })
  }

  update(_dt: number): void {
    const phys = this.phys
    if (!phys) return

    // Grounded probe: a short ray straight down from the feet. The ray originates inside the body,
    // so Box2D won't report the body's own shape; we exclude self defensively too.
    const pos = this.node.worldPosition
    const foot = this.footOffset ?? this._spriteHalfHeight()
    const hit = Physics2D.raycast([pos.x, pos.y - foot * 0.5], [pos.x, pos.y - foot - this.groundProbe])
    this._grounded = !!hit && hit.node !== this.node

    let vy = phys.velocity.y
    if (this._jumpQueued && this._grounded) { vy = this.jumpSpeed; this._grounded = false }
    this._jumpQueued = false

    // Drive horizontal velocity directly; let the sim own vertical (gravity / jump / landing).
    phys.velocity = new Vec2(this._moveX * this.speed, vy)
  }

  private _spriteHalfHeight(): number {
    const size = (this.node as { size?: Vec2 }).size
    return size ? size.y * 0.5 : 8
  }
}
