// Hand-drawn rendering: the graphic shapes of a scenegraph (rectangles, lines, paths,
// symbols) are drawn by moon_rough, a port of rough.js; text stays as it is.
//
//   let draw = @sketch.shape_drawer(@sketch.SketchOptions::new(seed=42))
//   @svg.render_svg(scene, @svg.SvgOptions::new(w, h, shapes=draw))
//
// Every shape gets a seed of its own, derived from the base seed and the position of the
// shape in the scenegraph (see `shape_seed`), so the same scenegraph and base seed always
// give the same bytes, and editing one shape does not change the others.

///|
/// How rough.js fills a shape: `Hachure` (the default), `Solid`, `Zigzag`, `CrossHatch`, `Dots`,
/// `Dashed` or `ZigzagLine`.
pub using @moon_rough {type FillStyle}

///|
/// Options of the hand-drawn style.
pub struct SketchOptions {
  /// the base seed; each shape derives its own from it
  seed : Int32
  /// how far the lines stray from the true shape (rough.js `roughness`, default 1)
  roughness : Double
  /// how much the lines bow (rough.js `bowing`, default 1)
  bowing : Double
  fill_style : FillStyle
  /// distance between hachure lines; rough.js uses four times the stroke width when absent
  hachure_gap : Double?
  /// width of the hachure lines; rough.js uses half the stroke width when absent
  fill_weight : Double?
  /// round the coordinates to this many decimals (rough.js `fixedDecimalPlaceDigits`), which makes
  /// the SVG much smaller; absent: full precision
  fixed_decimals : Int32?
}

///|
pub fn SketchOptions::new(
  seed~ : Int32 = 1,
  roughness~ : Double = 1.0,
  bowing~ : Double = 1.0,
  fill_style~ : FillStyle = Hachure,
  hachure_gap? : Double,
  fill_weight? : Double,
  fixed_decimals? : Int32,
) -> SketchOptions {
  {
    seed,
    roughness,
    bowing,
    fill_style,
    hachure_gap,
    fill_weight,
    fixed_decimals,
  }
}

///|
/// One round of a 32-bit integer hash (the "lowbias" finaliser), computed in `Int64`.
fn mix(x : Int64) -> Int64 {
  let m = 0xFFFFFFFFL
  let mut h = x & m
  h = ((h ^ (h >> 16)) * 0x45D9F3BL) & m
  h = ((h ^ (h >> 16)) * 0x45D9F3BL) & m
  h ^ (h >> 16)
}

///|
/// The seed of the shape at `position` (see `@svg.Shape`): the base seed is hashed together
/// with every index of the position, and the result lies in `[1, 2^31 - 2]`, the range of
/// rough.js' generator (0 would mean "random"). Seeds are hashed, not added, because the
/// first numbers the generator draws from neighbouring seeds are almost equal.
pub fn shape_seed(base : Int32, position : Array[Int32]) -> Int32 {
  let mut h = mix(base.to_int64())
  for i in position {
    h = mix(h * 31L + i.to_int64() + 1L)
  }
  (h % 2147483646L).to_int() + 1
}

///|
priv enum Paint {
  Nothing
  Solid(String)
  Gradient
}

///|
fn paint_of(item : @scene.Item, key : String) -> Paint {
  match item.get(key) {
    Null | Undef => Nothing
    Str("none") | Str("transparent") => Nothing
    Str(c) => Solid(c)
    Obj(_) => Gradient
    v => Solid(v.to_js_string())
  }
}

///|
fn finite(x : Double) -> Bool {
  !x.is_nan() && !x.is_inf()
}

///|
fn dash_of(item : @scene.Item) -> Array[Double]? {
  match item.get("strokeDash") {
    Arr(a) if a.length() > 0 => {
      let out : Array[Double] = []
      for v in a {
        out.push(v.to_number())
      }
      Some(out)
    }
    _ => None
  }
}

///|
/// The `opacity` of the wrapper element: the item's opacity, times its fill opacity when the
/// shape has no stroke (its outline and hatching then stand for the fill).
fn opacity_of(item : @scene.Item, fill_only : Bool) -> Double? {
  let base = item.opt_num("opacity")
  let fill = if fill_only { item.opt_num("fillOpacity") } else { None }
  match (base, fill) {
    (None, None) => None
    (Some(a), None) => Some(a)
    (None, Some(b)) => Some(b)
    (Some(a), Some(b)) => Some(a * b)
  }
}

///|
/// Draw one shape: the markup replacing the standard element, `None` to keep the
/// standard element (gradients, shapes rough cannot draw).
fn draw_shape(o : SketchOptions, shape : @svg.Shape) -> String? {
  let item = shape.item
  let fill = paint_of(item, "fill")
  let stroke = paint_of(item, "stroke")
  if fill is Gradient || stroke is Gradient {
    return None
  }
  // a shape with a fill but no stroke gets an outline in the colour of its fill
  let (stroke_color, stroke_width, fill_only) = match (stroke, fill) {
    (Solid(c), _) => (c, item.num_or("strokeWidth", 1.0), false)
    (Nothing, Solid(c)) => (c, 1.0, true)
    _ => ("none", 0.0, false)
  }
  let fill_color = match fill {
    Solid(c) => Some(c)
    _ => None
  }
  if stroke_color == "none" && fill_color is None {
    return Some("") // nothing to see
  }
  let seed = shape_seed(o.seed, shape.position)
  let options = @moon_rough.Options::new(
    seed~,
    roughness=o.roughness,
    bowing=o.bowing,
    stroke=stroke_color,
    stroke_width~,
    fill?=fill_color,
    fill_style=o.fill_style,
    hachure_gap?=o.hachure_gap,
    fill_weight?=o.fill_weight,
    fixed_decimal_place_digits?=o.fixed_decimals,
    stroke_line_dash?=dash_of(item),
  )
  let gen = @moon_rough.generator(
    source=@moon_rough.RandomSource::park_miller(seed),
  )
  let drawable = match shape.geometry {
    Box(x, y, w, h) => {
      if !(finite(x) && finite(y) && finite(w) && finite(h)) {
        return Some("")
      }
      // rough.js wants a positive size
      let x0 = if w < 0.0 { x + w } else { x }
      let y0 = if h < 0.0 { y + h } else { y }
      gen.rectangle(x0, y0, w.abs(), h.abs(), options~)
    }
    Segment(x1, y1, x2, y2) => {
      if !(finite(x1) && finite(y1) && finite(x2) && finite(y2)) {
        return Some("")
      }
      gen.line(x1, y1, x2, y2, options~)
    }
    Path(d) => {
      if d == "" {
        return Some("")
      }
      gen.path(d, options~) catch {
        _ => return None
      }
    }
  }
  let inner = @moon_rough.drawable_to_svg(drawable)
  let attrs = StringBuilder()
  match shape.transform {
    Some(t) => attrs.write_string(" transform=\"" + t + "\"")
    None => ()
  }
  match opacity_of(item, fill_only) {
    Some(a) =>
      attrs.write_string(" opacity=\"" + @util.js_number_to_string(a) + "\"")
    None => ()
  }
  let a = attrs.to_string()
  if a == "" {
    Some(inner)
  } else {
    Some("" + inner + "")
  }
}

///|
/// The function to pass as `shapes` to `@svg.SvgOptions::new`.
pub fn shape_drawer(options : SketchOptions) -> (@svg.Shape) -> String? {
  fn(shape) { draw_shape(options, shape) }
}