// Software-rendered demo built ONLY from the coordinate-system library: prints a PPM image.
//   moon run cmd/demo --target native > demo.ppm
// What it exercises: look_at / orbit camera, reversed-Z infinite projection, viewport transform,
// frustum culling, TRS model matrices + normal matrix, perspective-correct barycentric
// interpolation, picking rays for the sky (cube-map lookup) and ground plane, axis gizmo.

///|
struct Framebuffer {
  w : Int
  h : Int
  color : FixedArray[Int] // packed 0xRRGGBB
  depth : FixedArray[Double] // reversed-Z: larger = nearer, cleared to 0
}

///|
fn Framebuffer::new(w : Int, h : Int) -> Framebuffer {
  {
    w,
    h,
    color: FixedArray::make(w * h, 0),
    depth: FixedArray::make(w * h, 0.0),
  }
}

///|
fn pack(r : Double, g : Double, b : Double) -> Int {
  let c = fn(x : Double) -> Int {
    (@graph.clamp(x, 0.0, 1.0) * 255.0 + 0.5).to_int()
  }
  (c(r) << 16) | (c(g) << 8) | c(b)
}

///|
fn checker(u : Double, v : Double, n : Double) -> Bool {
  let iu = @math.floor(u * n).to_int()
  let iv = @math.floor(v * n).to_int()
  ((iu + iv) & 1) == 0
}

///|
/// Sky colour from a world direction through the cube-map coordinate system: every face gets
/// its own hue and a UV grid, so seams and orientation are visible.
fn sky(dir : @graph.Vec3) -> Int {
  let c = @graph.cube_from_direction(dir)
  let hues : Array[(Double, Double, Double)] = [
    (0.85, 0.35, 0.35),
    (0.55, 0.20, 0.20),
    (0.35, 0.80, 0.40),
    (0.15, 0.40, 0.20),
    (0.35, 0.45, 0.90),
    (0.18, 0.22, 0.50),
  ]
  let (r, g, b) = hues[c.face]
  let line = c.u * 8.0 - @math.floor(c.u * 8.0) < 0.04 ||
    c.v * 8.0 - @math.floor(c.v * 8.0) < 0.04
  let k = if line { 1.0 } else { 0.55 }
  // darker toward the horizon-less bottom hemisphere
  pack(r * k, g * k, b * k)
}

///|
fn draw_triangle(
  fb : Framebuffer,
  cam : @graph.Camera,
  p : Array[@graph.Vec3], // world positions
  uv : Array[@graph.Vec2],
  normal_world : @graph.Vec3,
  base : (Double, Double, Double),
) -> Unit {
  let scr : Array[@graph.Vec3] = []
  let ws : Array[Double] = []
  for v in p {
    match cam.world_to_screen(v) {
      Some(s) => {
        scr.push(s)
        ws.push(cam.world_to_clip(v).w)
      }
      None => return // behind camera: a real renderer would clip; the demo keeps cubes in front
    }
  }
  let minx = @math.floor(scr[0].x.min(scr[1].x).min(scr[2].x))
    .to_int()
    .clamp(min=0, max=fb.w - 1)
  let maxx = @math.floor(scr[0].x.max(scr[1].x).max(scr[2].x))
    .to_int()
    .clamp(min=0, max=fb.w - 1)
  let miny = @math.floor(scr[0].y.min(scr[1].y).min(scr[2].y))
    .to_int()
    .clamp(min=0, max=fb.h - 1)
  let maxy = @math.floor(scr[0].y.max(scr[1].y).max(scr[2].y))
    .to_int()
    .clamp(min=0, max=fb.h - 1)
  let light = @graph.Vec3::new(-0.4, 0.8, 0.5).normalize()
  let lam = @graph.Vec3::new(0.0, 0.0, 0.0)
  let _ = lam
  let (br, bg, bb) = base
  let shade = 0.25 + 0.75 * @graph.saturate(normal_world.dot(light))
  for y in miny..<=maxy {
    for x in minx..<=maxx {
      let c = @graph.pixel_center(x, y)
      match @graph.barycentric_2d(c, scr[0].xy(), scr[1].xy(), scr[2].xy()) {
        Some(b) =>
          if @graph.bary_inside(b, 0.0) {
            let z = @graph.interpolate1(b, scr[0].z, scr[1].z, scr[2].z)
            let i = y * fb.w + x
            if z > fb.depth[i] {
              let pc = @graph.perspective_correct(b, ws[0], ws[1], ws[2])
              let t = @graph.interpolate2(pc, uv[0], uv[1], uv[2])
              let k = if checker(t.x, t.y, 4.0) { 1.0 } else { 0.55 }
              fb.depth[i] = z
              fb.color[i] = pack(br * shade * k, bg * shade * k, bb * shade * k)
            }
          }
        None => ()
      }
    }
  }
}

///|
/// Draw the six faces of a unit cube [-0.5, 0.5]^3 under model matrix `m`.
fn draw_cube(
  fb : Framebuffer,
  cam : @graph.Camera,
  m : @graph.Mat4,
  base : (Double, Double, Double),
) -> Unit {
  // frustum culling with the world-space AABB of the transformed cube
  let box = @graph.Aabb::from_center_half(
    @graph.Vec3::zero(),
    @graph.Vec3::splat(0.5),
  ).transform(m)
  if cam.frustum().classify_aabb(box) == Outside {
    return
  }
  // normals transform with the inverse transpose (correct under non-uniform scale)
  let nm = m.normal_matrix().unwrap()
  // face: normal, u axis, v axis (right-handed), 4 corners
  let faces : Array[(@graph.Vec3, @graph.Vec3, @graph.Vec3)] = [
    (@graph.Vec3::unit_x(), -@graph.Vec3::unit_z(), @graph.Vec3::unit_y()),
    (-@graph.Vec3::unit_x(), @graph.Vec3::unit_z(), @graph.Vec3::unit_y()),
    (@graph.Vec3::unit_y(), @graph.Vec3::unit_x(), -@graph.Vec3::unit_z()),
    (-@graph.Vec3::unit_y(), @graph.Vec3::unit_x(), @graph.Vec3::unit_z()),
    (@graph.Vec3::unit_z(), @graph.Vec3::unit_x(), @graph.Vec3::unit_y()),
    (-@graph.Vec3::unit_z(), -@graph.Vec3::unit_x(), @graph.Vec3::unit_y()),
  ]
  for (n, u, v) in faces {
    let c = n.scale(0.5)
    let q = [
      c - u.scale(0.5) - v.scale(0.5),
      c + u.scale(0.5) - v.scale(0.5),
      c + u.scale(0.5) + v.scale(0.5),
      c - u.scale(0.5) + v.scale(0.5),
    ]
    let wq = q.map(fn(p) { m.mul_point(p) })
    let nw = nm.mul_vec3(n).normalize()
    // back-face culling in world space
    if nw.dot(wq[0] - cam.eye()) >= 0.0 {
      continue
    }
    let uvs = [
      @graph.Vec2::new(0.0, 0.0),
      @graph.Vec2::new(1.0, 0.0),
      @graph.Vec2::new(1.0, 1.0),
      @graph.Vec2::new(0.0, 1.0),
    ]
    draw_triangle(
      fb,
      cam,
      [wq[0], wq[1], wq[2]],
      [uvs[0], uvs[1], uvs[2]],
      nw,
      base,
    )
    draw_triangle(
      fb,
      cam,
      [wq[0], wq[2], wq[3]],
      [uvs[0], uvs[2], uvs[3]],
      nw,
      base,
    )
  }
}

///|
fn draw_line(
  fb : Framebuffer,
  a : @graph.Vec3,
  b : @graph.Vec3,
  color : Int,
) -> Unit {
  let steps = @math.hypot(b.x - a.x, b.y - a.y).to_int() + 1
  for i in 0..<=steps {
    let t = Double::from_int(i) / Double::from_int(steps)
    let x = @math.floor(a.x + (b.x - a.x) * t).to_int()
    let y = @math.floor(a.y + (b.y - a.y) * t).to_int()
    if x >= 0 && x < fb.w && y >= 0 && y < fb.h {
      fb.color[y * fb.w + x] = color
    }
  }
}

///|
fn main {
  let w = 480
  let h = 300
  let fb = Framebuffer::new(w, h)
  let proj = @graph.Projection::perspective(
    1.0,
    Double::from_int(w) / Double::from_int(h),
    0.1,
    @double.infinity,
    ReversedZ,
  )
  let cam = @graph.Camera::orbit(
    @graph.Vec3::new(0.0, 0.6, 0.0),
    7.5,
    -0.55, // yaw: look toward +X-ish (negative yaw turns right)
    -0.28, // pitch: look slightly down
    proj,
    @graph.Viewport::full(Double::from_int(w), Double::from_int(h)),
  )
  // 1. background + ground by picking rays through every pixel centre
  let ground = @graph.Plane::from_point_normal(
    @graph.Vec3::zero(),
    @graph.Vec3::unit_y(),
  )
  for y in 0.. {
          let hit = ray.at(t)
          let fade = 1.0 / (1.0 + t * 0.03)
          let k = if checker(hit.x * 0.5, hit.z * 0.5, 1.0) {
            0.75
          } else {
            0.45
          }
          fb.color[y * w + x] = pack(
            0.55 * k * fade + 0.1,
            0.6 * k * fade + 0.12,
            0.55 * k * fade + 0.15,
          )
          // write ground depth so cubes are depth-tested against it
          let s = cam.world_to_screen(hit).unwrap()
          fb.depth[y * w + x] = s.z
        }
        None => fb.color[y * w + x] = sky(ray.dir)
      }
    }
  }
  // 2. cubes with different TRS (one mirrored scale to prove the normal matrix is right)
  let cubes : Array[(@graph.Mat4, (Double, Double, Double))] = [
    (
      @graph.Mat4::from_trs(
        @graph.Vec3::new(0.0, 0.5, 0.0),
        @graph.Quat::from_y(0.5),
        @graph.Vec3::splat(1.0),
      ),
      (0.95, 0.85, 0.25),
    ),
    (
      @graph.Mat4::from_trs(
        @graph.Vec3::new(2.4, 0.4, -1.5),
        @graph.Quat::from_axis_angle(@graph.Vec3::new(1.0, 1.0, 0.0), 0.6),
        @graph.Vec3::new(0.8, 0.8, 0.8),
      ),
      (0.9, 0.35, 0.35),
    ),
    (
      @graph.Mat4::from_trs(
        @graph.Vec3::new(-2.2, 1.0, -1.0),
        @graph.Quat::from_y(-0.3),
        @graph.Vec3::new(1.0, 2.0, 1.0),
      ),
      (0.35, 0.65, 0.95),
    ),
    (
      @graph.Mat4::from_trs(
        @graph.Vec3::new(1.2, 0.3, 2.0),
        @graph.Quat::from_y(0.9),
        @graph.Vec3::new(0.6, 0.6, 0.6),
      ),
      (0.5, 0.9, 0.5),
    ),
    (
      @graph.Mat4::from_trs(
        @graph.Vec3::new(0.0, 0.5, -5.0),
        @graph.Quat::identity(),
        @graph.Vec3::new(1.0, 1.0, 1.0),
      ),
      (0.9, 0.9, 0.9),
    ),
    // behind the camera / outside the view: must be frustum-culled
    (
      @graph.Mat4::translation(@graph.Vec3::new(0.0, 0.5, 30.0)),
      (1.0, 0.0, 1.0),
    ),
  ]
  for (m, col) in cubes {
    draw_cube(fb, cam, m, col)
  }
  // 3. world axis gizmo from the origin: +X red, +Y green, +Z blue
  let o = cam.world_to_screen(@graph.Vec3::zero()).unwrap()
  for
    (axis, col) in [
      (@graph.Vec3::unit_x(), 0xFF2020),
      (@graph.Vec3::unit_y(), 0x20FF20),
      (@graph.Vec3::unit_z(), 0x3030FF),
    ] {
    match cam.world_to_screen(axis.scale(2.5)) {
      Some(e) => draw_line(fb, o, e, col)
      None => ()
    }
  }
  // 4. output PPM (P3)
  let sb = StringBuilder()
  sb.write_string("P3\n\{w} \{h}\n255\n")
  for i in 0..<(w * h) {
    let c = fb.color[i]
    sb.write_string("\{(c >> 16) & 255} \{(c >> 8) & 255} \{c & 255}\n")
  }
  println(sb.to_string())
}