// paintop.js — PaintOp wire encoder + SkPath serializer for the MojoJS port.
// Loaded after raster.js. Layouts from cc/paint/paint_op_{reader,writer}.{h,cc},
// cc/paint/paint_op.{h,cc}, cc/paint/paint_flags.{h,cc},
// third_party/skia/src/core/SkPath_serial.cpp.
'use strict';

// ─── enums ──────────────────────────────────────────────────────────────────
// cc/paint/paint_op.h:85-122
const Op = Object.freeze({
  ClipRect: 2, Concat: 4, DrawColor: 8, DrawPath: 16,
  Noop: 25, Restore: 26, Rotate: 27, Save: 28, SaveLayer: 29,
  SaveLayerAlpha: 30, Scale: 32, SetMatrix: 33, Translate: 35,
});
// cc/paint/paint_cache.h:47-52
const PCE = Object.freeze({
  kEmpty: 0, kCached: 1, kInlined: 2, kInlinedDoNotCache: 3,
});
// third_party/skia/include/core/SkPathTypes.h:60-66
const Verb = Object.freeze({
  Move: 0, Line: 1, Quad: 2, Conic: 3, Cubic: 4, Close: 5,
});
// SkBlendMode.h
const Blend = Object.freeze({ kClear: 0, kSrc: 1, kDst: 2, kSrcOver: 3 });
// SkClipOp (SkClipOp.h): kDifference=0, kIntersect=1
const ClipOp = Object.freeze({ kDifference: 0, kIntersect: 1 });
// SkPathFillType (SkPathTypes.h)
const FillType = Object.freeze({
  kWinding: 0, kEvenOdd: 1, kInverseWinding: 2, kInverseEvenOdd: 3,
});

// paint_op_writer.h:110-130: renderer→GPU has enable_security_constraints=false
// → BufferAlignment() = kMaxAlignment = 16. Every op is padded to 16 bytes and
// the buffer base must be 16-aligned (matches XferPool default + the
// AlignUp(p,16) check in raster_decoder.cc:2963).
const kPaintOpAlign = 16;

// ─── SkPath serializer ──────────────────────────────────────────────────────
// SkPath::writeToMemory kGeneral_SerializationType (SkPath_serial.cpp:108-152):
//   u32 packed = (fillType<<8) | (kGeneral=0<<28) | kCurrent_Version=5
//   i32 pts; i32 cnx; i32 vbs;
//   pts × {f32 x, f32 y}; cnx × f32; vbs × u8; padToAlign4.
// readAsRRect (SkPath_serial.cpp:170) checks (packed>>28)==kRRect=1, so
// kGeneral always falls through to readGeneral.
class SkPathBuilder {
  constructor(fillType = FillType.kWinding) {
    this.fill = fillType;
    this.pts = [];      // [x,y, x,y, …]
    this.verbs = [];
    this.conics = [];
  }
  moveTo(x, y)  { this.verbs.push(Verb.Move);  this.pts.push(x, y); return this; }
  lineTo(x, y)  { this.verbs.push(Verb.Line);  this.pts.push(x, y); return this; }
  close()       { this.verbs.push(Verb.Close);                       return this; }
  byteSize() {
    const raw = 16 + this.pts.length*4 + this.conics.length*4 + this.verbs.length;
    return (raw + 3) & ~3;
  }
  // Writes into u8 at byte offset `off`; returns bytes written (== byteSize()).
  writeInto(u8, off) {
    const dv = new DataView(u8.buffer, u8.byteOffset + off);
    dv.setUint32(0,  (this.fill << 8) | 5, true);          // packed
    dv.setInt32 (4,  this.pts.length >> 1, true);          // point count
    dv.setInt32 (8,  this.conics.length,   true);
    dv.setInt32 (12, this.verbs.length,    true);
    let p = 16;
    for (const f of this.pts)    { dv.setFloat32(p, f, true); p += 4; }
    for (const f of this.conics) { dv.setFloat32(p, f, true); p += 4; }
    for (const v of this.verbs)  { dv.setUint8  (p, v);       p += 1; }
    while (p & 3) { dv.setUint8(p++, 0); }
    return p;
  }
}

// ─── SkM44 helpers ─────────────────────────────────────────────────────────
// Wire layout = SkM44::fMat[16] verbatim (PaintOp{Reader,Writer}::ReadSimple/
// WriteSimple memcpy), and fMat is **column-major** (SkM44.h:188). So
// element i is row (i&3), col (i>>2); translate lives at [12..14],
// perspective at [3],[7],[11].
function m44Identity() {
  return new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
}
function m44Translate(tx, ty, tz = 0) {
  const m = m44Identity(); m[12] = tx; m[13] = ty; m[14] = tz; return m;
}
// SkM44::Translate(Tx,Ty)·Scale(Sx,Sy).
function m44TS(Tx, Ty, Sx, Sy) {
  return new Float32Array([Sx,0,0,0, 0,Sy,0,0, 0,0,1,0, Tx,Ty,0,1]);
}

// ─── PaintWriter ────────────────────────────────────────────────────────────
// Writes a PaintOp stream into a slice from XferPool.alloc(n,16). Each op:
//   [u32 header = type | (serialized_size<<8)]  …body…  pad-to-16.
// PaintOpReader::ReadAndValidateOpHeader requires serialized_size ≥ 4 and a
// multiple of BufferAlignment()=16 (paint_op_reader.cc:131-155).
class PaintWriter {
  constructor(slice /* {u8,shmId,offset,size} from XferPool.alloc */) {
    this.slice = slice;
    this.u8  = slice.u8;
    this.dv  = new DataView(slice.u8.buffer, slice.u8.byteOffset, slice.u8.byteLength);
    this.cur = 0;
  }
  size() { return this.cur; }
  _need(n) {
    if (this.cur + n > this.u8.byteLength)
      throw 'PaintWriter overflow: need ' + n + ' @ ' + this.cur + '/' + this.u8.byteLength;
  }
  // Reserve header, return finisher that pads to 16 and back-patches it.
  _op(type) {
    this._need(4);
    const hdr = this.cur; this.cur += 4;
    return () => {
      const pad = (kPaintOpAlign - (this.cur & (kPaintOpAlign-1))) & (kPaintOpAlign-1);
      this._need(pad);
      for (let i = 0; i < pad; ++i) this.u8[this.cur++] = 0;
      const sz = this.cur - hdr;
      this.dv.setUint32(hdr, (type | (sz << 8)) >>> 0, true);
    };
  }
  // ── primitive field writers (all 4-aligned per ReadSimple/DidRead) ───────
  u32(v)  { this._need(4); this.dv.setUint32 (this.cur, v>>>0, true); this.cur += 4; }
  i32(v)  { this._need(4); this.dv.setInt32  (this.cur, v|0,   true); this.cur += 4; }
  f32(v)  { this._need(4); this.dv.setFloat32(this.cur, v,     true); this.cur += 4; }
  u8e(v)  { this.u32(v & 0xff); }                       // ReadEnum: u8 padded to 4
  bool(v) { this.u32(v ? 1 : 0); }
  sz(n)   { this.u32(n >>> 0); this.u32((n / 0x100000000) >>> 0); }   // ReadSize: lo,hi
  rect(l, t, r, b) { this.f32(l); this.f32(t); this.f32(r); this.f32(b); }
  color4f(r, g, b, a) { this.f32(r); this.f32(g); this.f32(b); this.f32(a); }
  m44(m /* Float32Array[16] col-major = SkM44::fMat */) {
    for (let i=0;i<16;++i) this.f32(m[i]);
  }
  // PaintFlags (paint_op_reader.cc:355-379): 52 bytes total.
  //   SkColor4f color_; f32 width_; f32 miter_limit_; u32 bitfields_uint_;
  //   f32 targeted_hdr_headroom_; PathEffect::Type; ColorFilter::Type;
  //   bool has_looper; PaintFilter::Type; bool has_shader.
  // bitfields_uint_ (LE bitfield order, paint_flags.h:192-216):
  //   aa:1 dither:1 cap:2 join:2 style:2 blend:5 fq:2 …
  // Default ctor (paint_flags.cc:37-45): blend=kSrcOver=3, rest 0 → 0x300.
  flags(opts = {}) {
    const aa    = opts.aa    ?? true;
    const cap   = opts.cap   ?? 0;          // kButt_Cap
    const style = opts.style ?? 0;          // kFill
    const blend = opts.blend ?? Blend.kSrcOver;
    const c     = opts.color ?? [0,0,0,1];
    const bf = (aa?1:0) | ((cap&3)<<2) | ((style&3)<<6) | ((blend&31)<<8);
    this.color4f(c[0], c[1], c[2], c[3]);   // color_ (validated finite, a∈[0,1])
    this.f32(opts.width ?? 0);              // width_
    this.f32(opts.miter ?? 4);              // miter_limit_
    this.u32(bf >>> 0);                     // bitfields_uint_
    this.f32(-1);                           // targeted_hdr_headroom_ = "from playback"
    this.u8e(0);                            // PathEffect::Type::kNull
    this.u8e(0);                            // ColorFilter::Type::kNull
    this.bool(false);                       // has_looper
    this.u8e(0);                            // PaintFilter::Type::kNullFilter
    this.bool(false);                       // has_shader
  }
  // SkPath via PaintCache (paint_op_reader.cc:295-353):
  //   u32 path_id; u32 entry_state;
  //   if Inlined/InlinedDoNotCache: ReadSize path_bytes; <path_bytes>; DidRead→pad4.
  path(builder, state = PCE.kInlinedDoNotCache, id = 0) {
    this.u32(id);
    this.u32(state);
    if (state === PCE.kInlined || state === PCE.kInlinedDoNotCache) {
      const n = builder.byteSize();
      this.sz(n);
      this._need(n);
      builder.writeInto(this.u8, this.cur);
      this.cur += n;            // already 4-aligned via byteSize()
    }
  }

  // ── op encoders ──────────────────────────────────────────────────────────
  noop()    { this._op(Op.Noop)(); }
  save()    { this._op(Op.Save)(); }
  restore() { this._op(Op.Restore)(); }
  scale(sx, sy)       { const f=this._op(Op.Scale);     this.f32(sx); this.f32(sy); f(); }
  translate(dx, dy)   { const f=this._op(Op.Translate); this.f32(dx); this.f32(dy); f(); }
  rotate(deg)         { const f=this._op(Op.Rotate);    this.f32(deg);              f(); }
  concat(m)           { const f=this._op(Op.Concat);    this.m44(m);                f(); }
  // SetMatrixOp::Serialize writes original_ctm*matrix; we hand-craft the wire,
  // so pass the *final* canvas matrix here (canvas starts at identity).
  setMatrix(m)        { const f=this._op(Op.SetMatrix); this.m44(m);                f(); }
  clipRect(l,t,r,b, op = ClipOp.kIntersect, aa = true) {
    const f=this._op(Op.ClipRect);
    this.rect(l,t,r,b); this.u8e(op); this.bool(aa);
    f();
  }
  drawColor(rgba, mode = Blend.kSrcOver) {
    const f=this._op(Op.DrawColor);
    this.color4f(rgba[0],rgba[1],rgba[2],rgba[3]); this.u8e(mode);
    f();
  }
  // SaveLayerOp{PaintFlags flags, SkRect bounds}.
  // IsValid: flags.IsValid() && IsValidOrUnsetRect(bounds).
  saveLayer(bounds /* [l,t,r,b] */, flagOpts = {}) {
    const f=this._op(Op.SaveLayer);
    this.flags(flagOpts);
    this.rect(bounds[0],bounds[1],bounds[2],bounds[3]);
    f();
  }
  // DrawPathOp{PaintFlags flags, SkPath path, SkPathFillType sk_path_fill_type}.
  // IsValid: flags.IsValid() && IsValidPath(path).
  drawPath(builder, flagOpts = {}, state = PCE.kInlinedDoNotCache, id = 0) {
    const f=this._op(Op.DrawPath);
    this.flags(flagOpts);
    this.path(builder, state, id);
    this.u8e(builder.fill);
    f();
  }
}
