// A one-way surface, as an aspect on a Node2D that already carries a Shape2D and a Physics2D body:
// solid from one side, passable from the other. The semisolid ledge of every platformer.
//
//   const ledge = new Sprite({ texture: plank, size: [128, 8] })
//     .aspect(Shape2D, {})
//     .aspect(Physics2D, { motion: 'static' })
//     .aspect(OneWay2D)                      // jump up through it, stand on top
//
// It is an ASPECT and not a flag on Shape2D on purpose: Shape2D is geometry, and which side of a
// surface is solid is behaviour. Keeping them apart is also what lets `normal` generalise the thing —
// a one-way WALL you can only walk through from the left is the same aspect turned sideways.
//
// Honoured by the character's mover AND, through a native pre-solve callback, by ordinary rigid
// bodies (a crate falls onto the ledge from above and passes up through it from below). Neither path
// costs any JS per frame.
//
// Dropping DOWN through one is the character's business, not the surface's — see
// `CharacterController2D.dropThrough`.

import { Aspect } from "../core/Aspect"
import { cx, cy, type Vec2Like } from "../math/vec"
import { Physics2D } from "./Physics2D"
import type { Node2D } from "./Node2D"

export class OneWay2D extends Aspect<"oneWay", Node2D> {
  static readonly aspect = "oneWay"

  private _normal: Vec2Like = [ 0, 1 ]
  private _arc = 90
  private _enabled = true

  /** Which side is solid. Default `[0, 1]` — solid from above, i.e. a floor you pass through from
   *  below. `[1, 0]` is a wall you can only enter from the left. */
  get normal(): Vec2Like { return this._normal }
  set normal(v: Vec2Like) { this._normal = v; this._push() }

  /** How far off `normal` an approach still counts as coming from the solid side, in degrees.
   *  Default 90 — the whole half-plane, which is what a platform wants. */
  get arc(): number { return this._arc }
  set arc(v: number) { this._arc = v; this._push() }

  /** Turn the one-way behaviour off, making the surface solid from both sides again. */
  get enabled(): boolean { return this._enabled }
  set enabled(v: boolean) { this._enabled = v; this._push() }

  onAttach(): void {
    if (!this.node.has(Physics2D)) {
      throw new Error("OneWay2D needs a Physics2D body on the same node — add it first: node.aspect(Shape2D, {…}).aspect(Physics2D, { motion: 'static' }).aspect(OneWay2D)")
    }
    this._push()
  }

  onDetach(): void {
    const id = this.node.get(Physics2D)?.id
    if (id) _creator2d.physicsSetOneWay(id, 0, 1, 90, false)
  }

  onReconfigure(): void { this._push() }

  private _push(): void {
    const id = this.node.get(Physics2D)?.id
    // via the public accessors on purpose: an accessor pair that is only ever an object-literal key
    // gets tree-shaken away, and the option would silently do nothing (see Physics2D.onAttach).
    if (id) _creator2d.physicsSetOneWay(id, cx(this.normal), cy(this.normal), this.arc, this.enabled)
  }
}
