///|
/// Deterministic raster textures for preview generation and material studies.
pub(all) struct TextureRecipe {
  name : String
  width : Int
  height : Int
  seed : UInt
  scale : Double
  contrast : Double
  brightness : Double
  palette : Palette
} derive(Eq, Debug, ToJson)

///|
pub(all) enum TextureBlend {
  TextureNormal
  TextureMultiply
  TextureScreen
  TextureOverlay
  TextureDifference
} derive(Eq, Debug, ToJson)

///|
pub fn TextureRecipe::new(
  name : String,
  width : Int,
  height : Int,
  seed : UInt,
  palette : Palette,
) -> TextureRecipe {
  {
    name,
    width: width.max(0),
    height: height.max(0),
    seed,
    scale: 0.03,
    contrast: 1.0,
    brightness: 0.0,
    palette,
  }
}

///|
pub fn TextureRecipe::with_scale(
  self : TextureRecipe,
  scale : Double,
) -> TextureRecipe {
  { ..self, scale: scale.max(0.0001) }
}

///|
pub fn TextureRecipe::with_tone(
  self : TextureRecipe,
  contrast : Double,
  brightness : Double,
) -> TextureRecipe {
  { ..self, contrast: contrast.max(0.0), brightness }
}

///|
fn tx_hash(x : Int, y : Int, seed : UInt) -> Double {
  let mut value = seed.reinterpret_as_int() ^ (x * 374761393) ^ (y * 668265263)
  value = (value ^ (value / 127)) * 1274126177
  ((value ^ (value / 8192)) % 100000).abs().to_double() / 50000.0 - 1.0
}

///|
fn tx_color_at(palette : Palette, index : Int) -> Color {
  palette.at(index).unwrap_or(Color::rgb8(0, 0, 0))
}

///|
pub fn texture_noise(recipe : TextureRecipe) -> Raster {
  let raster = Raster::new(recipe.width, recipe.height)
  let sampler = TrigNoise::new(recipe.scale)
  for y in 0.. Raster {
  let raster = Raster::new(
    recipe.width,
    recipe.height,
    fill=tx_color_at(recipe.palette, 0),
  )
  let amount = strength.max(0.0).min(1.0)
  for y in 0.. 0.0 { 1 } else { 0 }
      let _ = raster.set(x, y, tx_color_at(recipe.palette, index))
    }
  }
  raster
}

///|
pub fn texture_stripes(
  recipe : TextureRecipe,
  angle : Double,
  spacing : Double,
) -> Raster {
  let raster = Raster::new(recipe.width, recipe.height)
  let c = @math.cos(angle)
  let s = @math.sin(angle)
  let step = spacing.max(1.0)
  for y in 0.. Raster {
  let raster = Raster::new(recipe.width, recipe.height)
  let size = cell.max(1)
  for y in 0.. Raster {
  let raster = Raster::new(recipe.width, recipe.height)
  for y in 0.. Raster {
  let raster = Raster::new(recipe.width, recipe.height)
  for y in 0.. Array[Vec2] {
  let result = []
  let safe_margin = margin.max(0.0)
  for index in 0.. Raster {
  let width = base.width.min(top.width)
  let height = base.height.min(top.height)
  let result = base.copy()
  let amount = opacity.max(0.0).min(1.0)
  for y in 0.. above
        TextureMultiply => color_blend_multiply(below, above)
        TextureScreen => color_blend_screen(below, above)
        TextureOverlay => color_blend_overlay(below, above)
        TextureDifference => color_blend_difference(below, above)
      }
      let _ = result.set(x, y, below.mix(blended, amount))
    }
  }
  result
}

///|
pub fn texture_tone(
  raster : Raster,
  contrast : Double,
  brightness : Double,
) -> Raster {
  raster.map_colors(fn(color) {
    Color::rgba(
      (color.r - 0.5) * contrast + 0.5 + brightness,
      (color.g - 0.5) * contrast + 0.5 + brightness,
      (color.b - 0.5) * contrast + 0.5 + brightness,
      a=color.a,
    )
  })
}

///|
pub fn texture_posterize(raster : Raster, levels : Int) -> Raster {
  let count = levels.max(2)
  raster.map_colors(fn(color) {
    let quantize = fn(value : Double) {
      (value * (count - 1).to_double()).to_int().to_double() /
      (count - 1).to_double()
    }
    Color::rgba(
      quantize(color.r),
      quantize(color.g),
      quantize(color.b),
      a=color.a,
    )
  })
}

///|
pub fn texture_colorize(raster : Raster, palette : Palette) -> Raster {
  let normalized = raster.grayscale()
  let result = normalized.copy()
  for y in 0.. Raster {
  let edges = raster.sobel_edges()
  let result = raster.copy()
  for y in 0.. threshold {
        let _ = result.set(x, y, color)
      }
    }
  }
  result
}

///|
pub fn texture_paper(recipe : TextureRecipe, grain : Double) -> Raster {
  let base = texture_noise(recipe)
  let fibers = texture_fibers(recipe, grain, 11)
  texture_blend(base, fibers, TextureOverlay, grain.max(0.0).min(1.0))
}

///|
pub fn texture_moire(
  recipe : TextureRecipe,
  frequency : Double,
  phase : Double,
) -> Raster {
  let raster = Raster::new(recipe.width, recipe.height)
  for y in 0.. Raster {
  if layers.length() == 0 {
    Raster::new(0, 0)
  } else {
    let mut result = layers[0].0
    for layer in layers[1:] {
      result = texture_blend(result, layer.0, layer.1, layer.2)
    }
    result
  }
}

///|
pub fn texture_edge_mask(
  raster : Raster,
  low : Double,
  high : Double,
) -> Raster {
  let edges = raster.sobel_edges()
  edges.threshold(high.max(low))
}

///|
pub fn texture_recipe_report(recipe : TextureRecipe) -> String {
  "name=" +
  recipe.name +
  " size=" +
  recipe.width.to_string() +
  "x" +
  recipe.height.to_string() +
  " seed=" +
  recipe.seed.to_string() +
  " scale=" +
  recipe.scale.to_string() +
  " contrast=" +
  recipe.contrast.to_string() +
  " brightness=" +
  recipe.brightness.to_string() +
  " palette=" +
  recipe.palette.name
}

///|
pub fn texture_quality(recipe : TextureRecipe) -> RasterQualityMetrics {
  raster_quality_metrics(texture_paper(recipe, 0.35), 0.04)
}

///|
pub fn texture_preview(recipe : TextureRecipe, max_size : Int) -> Raster {
  let texture = texture_paper(recipe, 0.3)
  let factor = texture.width.max(texture.height) / max_size.max(1)
  if factor <= 1 {
    texture
  } else {
    texture.downsample(factor)
  }
}