/*
 * Copyright (C) 2026  Behdad Esfahbod
 *
 *  This is part of HarfBuzz, a text shaping library.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and its documentation for any purpose, provided that the
 * above copyright notice and the following two paragraphs appear in
 * all copies of this software.
 *
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
 * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
 * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 *
 * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
 * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
 * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 */


/* Paint-renderer fragment shader (WGSL).
 *
 * Assumes the shared fragment helpers (hb-gpu-fragment.wgsl) and
 * the draw-renderer fragment helpers (hb-gpu-draw-fragment.wgsl)
 * are prepended to this source.
 *
 * atlas is passed as an explicit storage-buffer pointer parameter,
 * matching WGSL's scoping rules.
 */


fn _hb_gpu_stop_color (hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>,
                       stops_base: i32, i: i32, foreground: vec4f,
                       offset: ptr<function, f32>) -> vec4f
{
  let a = hb_gpu_fetch (hb_gpu_atlas, stops_base + i * 2);
  *offset = f32 (a.r) / 32767.0;
  let b = hb_gpu_fetch (hb_gpu_atlas, stops_base + i * 2 + 1);
  if ((a.g & 1) != 0) {
    return vec4f (foreground.rgb, foreground.a * (f32 (b.a) / 32767.0));
  }
  return vec4f (b) / 32767.0;
}

fn _hb_gpu_extend_t (t: f32, extend: i32) -> f32
{
  if (extend == 1) { return t - floor (t); }
  if (extend == 2) {
    let u = t - 2.0 * floor (t * 0.5);
    if (u > 1.0) { return 2.0 - u; }
    return u;
  }
  return clamp (t, 0.0, 1.0);
}

fn _hb_gpu_eval_stops (hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>,
                       stops_base: i32, stop_count: i32,
                       t: f32, foreground: vec4f) -> vec4f
{
  var off_prev: f32;
  var col_prev = _hb_gpu_stop_color (hb_gpu_atlas, stops_base, 0, foreground, &off_prev);
  if (t <= off_prev) { return col_prev; }
  for (var i: i32 = 1; i < stop_count; i = i + 1)
  {
    var off: f32;
    let col = _hb_gpu_stop_color (hb_gpu_atlas, stops_base, i, foreground, &off);
    if (t <= off)
    {
      let span = off - off_prev;
      var f: f32 = 0.0;
      if (span > 1e-6) { f = (t - off_prev) / span; }
      let p0 = vec4f (col_prev.rgb * col_prev.a, col_prev.a);
      let p1 = vec4f (col.rgb * col.a, col.a);
      let pm = mix (p0, p1, f);
      if (pm.a > 1e-6) { return vec4f (pm.rgb / pm.a, pm.a); }
      return vec4f (0.0);
    }
    col_prev = col;
    off_prev = off;
  }
  return col_prev;
}

/* Apply the stored 2x2 M^-1 (row-major i16 Q10) to a vector. */
fn _hb_gpu_apply_minv (m: vec4<i32>, v: vec2f) -> vec2f
{
  let mf = vec4f (m) * (1.0 / 1024.0);
  return vec2f (mf.x * v.x + mf.y * v.y,
                mf.z * v.x + mf.w * v.y);
}

fn _hb_gpu_sample_linear (renderCoord: vec2f, grad_base: i32,
                          stop_count: i32, extend: i32, foreground: vec4f,
                          hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>) -> vec4f
{
  let t0 = hb_gpu_fetch (hb_gpu_atlas, grad_base);
  let m  = hb_gpu_fetch (hb_gpu_atlas, grad_base + 1);
  let p0_r = vec2f (f32 (t0.r), f32 (t0.g));
  let d    = vec2f (f32 (t0.b), f32 (t0.a));
  let denom = dot (d, d);
  if (denom < 1e-6) { return vec4f (0.0); }
  let p = _hb_gpu_apply_minv (m, renderCoord - p0_r);
  var t = dot (p, d) / denom;
  t = _hb_gpu_extend_t (t, extend);
  return _hb_gpu_eval_stops (hb_gpu_atlas, grad_base + 2, stop_count, t, foreground);
}

fn _hb_gpu_sample_radial (renderCoord: vec2f, grad_base: i32,
                          stop_count: i32, extend: i32, foreground: vec4f,
                          hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>) -> vec4f
{
  let t0 = hb_gpu_fetch (hb_gpu_atlas, grad_base);
  let t1 = hb_gpu_fetch (hb_gpu_atlas, grad_base + 1);
  let m  = hb_gpu_fetch (hb_gpu_atlas, grad_base + 2);
  let c0_r = vec2f (f32 (t0.r), f32 (t0.g));
  let cd   = vec2f (f32 (t0.b), f32 (t0.a));
  let r0 = f32 (t1.r);
  let r1 = f32 (t1.g);

  let dr = r1 - r0;
  let p  = _hb_gpu_apply_minv (m, renderCoord - c0_r);

  let A = dot (cd, cd) - dr * dr;
  let B = -2.0 * (dot (p, cd) + r0 * dr);
  let C = dot (p, p) - r0 * r0;

  var t: f32;
  if (abs (A) > 1e-6)
  {
    let disc = B * B - 4.0 * A * C;
    if (disc < 0.0) { return vec4f (0.0); }
    let sq = sqrt (disc);
    let t1r = (-B + sq) / (2.0 * A);
    let t2r = (-B - sq) / (2.0 * A);
    if (r0 + t1r * dr >= 0.0) { t = t1r; } else { t = t2r; }
  }
  else
  {
    if (abs (B) < 1e-6) { return vec4f (0.0); }
    t = -C / B;
  }
  t = _hb_gpu_extend_t (t, extend);
  return _hb_gpu_eval_stops (hb_gpu_atlas, grad_base + 3, stop_count, t, foreground);
}

fn _hb_gpu_sample_sweep (renderCoord: vec2f, grad_base: i32,
                         stop_count: i32, extend: i32, foreground: vec4f,
                         hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>) -> vec4f
{
  let t0 = hb_gpu_fetch (hb_gpu_atlas, grad_base);
  let m  = hb_gpu_fetch (hb_gpu_atlas, grad_base + 1);
  let c_r = vec2f (f32 (t0.r), f32 (t0.g));
  let a0 = f32 (t0.b) / 16384.0;
  let a1 = f32 (t0.a) / 16384.0;
  let span = a1 - a0;
  if (abs (span) < 1e-6) { return vec4f (0.0); }

  let p = _hb_gpu_apply_minv (m, renderCoord - c_r);
  var ang = atan2 (p.y, p.x) / 3.14159265358979;
  if (ang < 0.0) { ang = ang + 2.0; }
  var t = (ang - a0) / span;
  t = _hb_gpu_extend_t (t, extend);
  return _hb_gpu_eval_stops (hb_gpu_atlas, grad_base + 2, stop_count, t, foreground);
}

fn _hb_gpu_composite (src: vec4f, dst: vec4f, mode_in: i32) -> vec4f
{
  var r = src + dst * (1.0 - src.a);  /* SRC_OVER default */

  /* Mode numbers match hb_paint_composite_mode_t.  Approximate
   * unsupported modes with the nearest Porter-Duff mode we do
   * implement; DIFFERENCE / EXCLUSION / HSL_* still fall through to
   * SRC_OVER below. */
  var mode = mode_in;
  if      (mode == 14 || mode == 18 || mode == 19) { mode = 23; } /* OVERLAY / COLOR_BURN / HARD_LIGHT -> MULTIPLY */
  else if (mode == 17 || mode == 20)               { mode = 13; } /* COLOR_DODGE / SOFT_LIGHT -> SCREEN */

  if      (mode == 0)  { r = vec4f (0.0); }                    /* CLEAR */
  else if (mode == 1)  { r = src; }                            /* SRC */
  else if (mode == 2)  { r = dst; }                            /* DST */
  else if (mode == 4)  { r = dst + src * (1.0 - dst.a); }      /* DST_OVER */
  else if (mode == 5)  { r = src * dst.a; }                    /* SRC_IN */
  else if (mode == 6)  { r = dst * src.a; }                    /* DST_IN */
  else if (mode == 7)  { r = src * (1.0 - dst.a); }            /* SRC_OUT */
  else if (mode == 8)  { r = dst * (1.0 - src.a); }            /* DST_OUT */
  else if (mode == 9)  { r = src * dst.a + dst * (1.0 - src.a); }  /* SRC_ATOP */
  else if (mode == 10) { r = dst * src.a + src * (1.0 - dst.a); }  /* DST_ATOP */
  else if (mode == 11) { r = src * (1.0 - dst.a) + dst * (1.0 - src.a); }  /* XOR */
  else if (mode == 12) { r = min (src + dst, vec4f (1.0)); }   /* PLUS */
  else if (mode == 13) {                                       /* SCREEN (premul) */
    r = vec4f (src.rgb + dst.rgb - src.rgb * dst.rgb,
               src.a + dst.a - src.a * dst.a);
  }
  else if (mode == 15) {                                       /* DARKEN */
    r = vec4f (min (src.rgb * dst.a, dst.rgb * src.a)
             + src.rgb * (1.0 - dst.a) + dst.rgb * (1.0 - src.a),
               src.a + dst.a - src.a * dst.a);
  }
  else if (mode == 16) {                                       /* LIGHTEN */
    r = vec4f (max (src.rgb * dst.a, dst.rgb * src.a)
             + src.rgb * (1.0 - dst.a) + dst.rgb * (1.0 - src.a),
               src.a + dst.a - src.a * dst.a);
  }
  else if (mode == 23) {                                       /* MULTIPLY (premul) */
    r = vec4f (src.rgb * (1.0 - dst.a) + dst.rgb * (1.0 - src.a)
             + src.rgb * dst.rgb,
               src.a + dst.a - src.a * dst.a);
  }
  /* SRC_OVER (3) and DIFFERENCE / EXCLUSION / HSL_* (21, 22, 24-27)
   * fall through to the SRC_OVER default. */
  return r;
}

/* Wrap _hb_gpu_slug with a sub-glyph extents bail-out.  Many
 * paint layers cover a small region of the outer glyph quad; for
 * fragments outside the layer's bbox (with an AA + MSAA-spread
 * margin) the slug coverage is exactly 0, so we can skip the
 * band/curve walk entirely. */
fn _hb_gpu_slug_clipped (renderCoord: vec2f, pixelsPerEm: vec2f, glyphLoc_: u32,
                         hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>) -> f32
{
  let header0 = hb_gpu_fetch (hb_gpu_atlas, i32 (glyphLoc_));
  let ext = vec4f (header0) * HB_GPU_INV_UNITS;
  let margin = 2.0 / pixelsPerEm;
  if (any (renderCoord < ext.xy - margin) ||
      any (renderCoord > ext.zw + margin)) {
    return 0.0;
  }
  return _hb_gpu_slug (renderCoord, pixelsPerEm, glyphLoc_, hb_gpu_atlas);
}

/* Combine slug coverages from all clip outlines on the layer.
 * Factored so the shader has one set of inlined slug walks
 * instead of two (one per LAYER op type).  flags bits: 0x100 =
 * HAS_CLIP2; 0x200 = HAS_CLIP3 (HAS_CLIP3 implies HAS_CLIP2). */
fn _hb_gpu_layer_coverage (renderCoord: vec2f, pixelsPerEm: vec2f,
                           base: i32, flags: i32,
                           clip1_payload: i32, clip2_payload: i32, clip3_payload: i32,
                           hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>) -> f32
{
  var cov = _hb_gpu_slug_clipped (renderCoord, pixelsPerEm,
                                       u32 (base + clip1_payload), hb_gpu_atlas);
  if ((flags & 0x100) != 0) {
    cov = cov * _hb_gpu_slug_clipped (renderCoord, pixelsPerEm,
                                          u32 (base + clip2_payload), hb_gpu_atlas);
    if ((flags & 0x200) != 0) {
      cov = cov * _hb_gpu_slug_clipped (renderCoord, pixelsPerEm,
                                            u32 (base + clip3_payload), hb_gpu_atlas);
    }
  }
  return cov;
}

const HB_GPU_PAINT_GROUP_DEPTH: i32 = 4;

fn hb_gpu_paint (renderCoord: vec2f, glyphLoc_: u32, foreground: vec4f,
                 hb_gpu_atlas: ptr<storage, array<vec4<i32>>, read>,
                 coverage: ptr<function, f32>) -> vec4f
{
  /* Compute pixelsPerEm once here at uniform control flow.  WGSL
   * rejects fwidth inside a loop-conditional branch, so we call
   * _hb_gpu_slug (the MSAA-aware implementation that takes a
   * pre-computed pixelsPerEm) instead of the top-level
   * hb_gpu_draw() which would re-call fwidth. */
  let emsPerPixel = fwidth (renderCoord);
  let pixelsPerEm = 1.0 / emsPerPixel;

  let base    = i32 (glyphLoc_);
  let h0      = hb_gpu_fetch (hb_gpu_atlas, base);     // (num_ops, _, _, _)
  let h2      = hb_gpu_fetch (hb_gpu_atlas, base + 2); // (ops_offset, _, _, _)
  let num_ops = h0.r;
  var cursor  = base + h2.r;

  var acc = vec4f (0.0);
  var group_stack: array<vec4f, HB_GPU_PAINT_GROUP_DEPTH>;
  var sp: i32 = 0;
  *coverage = 0.0;

  for (var i: i32 = 0; i < num_ops; i = i + 1)
  {
    let op      = hb_gpu_fetch (hb_gpu_atlas, cursor);
    let op_type = op.r;
    let aux     = op.g;
    let payload = (op.b << 16) | (op.a & 0xffff);

    if (op_type == 0) {  // LAYER_SOLID
      // texel 1: (clip2_hi, clip2_lo, clip3_hi, clip3_lo) -- valid
      //          per HAS_CLIP2 / HAS_CLIP3 flag bits.
      // texel 2: RGBA as signed Q15.
      let op2 = hb_gpu_fetch (hb_gpu_atlas, cursor + 1);
      let clip2_payload = (op2.r << 16) | (op2.g & 0xffff);
      let clip3_payload = (op2.b << 16) | (op2.a & 0xffff);
      let ct = hb_gpu_fetch (hb_gpu_atlas, cursor + 2);
      var col: vec4f;
      if ((aux & 1) != 0) {
        col = vec4f (foreground.rgb, foreground.a * (f32 (ct.a) / 32767.0));
      } else {
        col = vec4f (ct) / 32767.0;
      }

      let cov = _hb_gpu_layer_coverage (renderCoord, pixelsPerEm,
                                        base, aux,
                                        payload, clip2_payload, clip3_payload,
                                        hb_gpu_atlas);
      *coverage = max (*coverage, cov);
      let src = vec4f (col.rgb * col.a, col.a) * cov;
      acc = src + acc * (1.0 - src.a);

      cursor = cursor + 3;
    } else if (op_type == 1) {  // LAYER_GRADIENT
      let op2 = hb_gpu_fetch (hb_gpu_atlas, cursor + 1);
      let clip2_payload = (op2.r << 16) | (op2.g & 0xffff);
      let clip3_payload = (op2.b << 16) | (op2.a & 0xffff);
      let op3 = hb_gpu_fetch (hb_gpu_atlas, cursor + 2);
      let grad_payload = (op3.r << 16) | (op3.g & 0xffff);
      let extend = op3.b;
      let stop_count = op3.a;
      let subtype = aux & 0xff;

      var col = vec4f (0.0);
      if (subtype == 0) {
        col = _hb_gpu_sample_linear (renderCoord,
                                     base + grad_payload,
                                     stop_count, extend, foreground,
                                     hb_gpu_atlas);
      } else if (subtype == 1) {
        col = _hb_gpu_sample_radial (renderCoord,
                                     base + grad_payload,
                                     stop_count, extend, foreground,
                                     hb_gpu_atlas);
      } else if (subtype == 2) {
        col = _hb_gpu_sample_sweep  (renderCoord,
                                     base + grad_payload,
                                     stop_count, extend, foreground,
                                     hb_gpu_atlas);
      }

      let cov = _hb_gpu_layer_coverage (renderCoord, pixelsPerEm,
                                        base, aux,
                                        payload, clip2_payload, clip3_payload,
                                        hb_gpu_atlas);
      *coverage = max (*coverage, cov);
      let src = vec4f (col.rgb * col.a, col.a) * cov;
      acc = src + acc * (1.0 - src.a);

      cursor = cursor + 3;
    } else if (op_type == 2) {  // PUSH_GROUP
      if (sp < HB_GPU_PAINT_GROUP_DEPTH) {
        group_stack[sp] = acc;
        sp = sp + 1;
      }
      acc = vec4f (0.0);
      cursor = cursor + 1;
    } else if (op_type == 3) {  // POP_GROUP
      if (sp > 0) {
        sp = sp - 1;
        let src = acc;
        let dst = group_stack[sp];
        acc = _hb_gpu_composite (src, dst, aux);
      }
      cursor = cursor + 1;
    } else {
      break;
    }
  }

  return acc;
}
