///|
pub enum PipelineStage {
  Normalize
  Denoise
  AdaptiveThreshold
  Morphology
  Edges
  ConnectedComponents
  Equalize
} derive(Debug, Eq)

///|
pub struct PipelineConfig {
  threshold : Int
  radius : Int
  minimum_component_area : Int
  stages : Array[PipelineStage]
} derive(Debug, Eq)

///|
pub fn PipelineConfig::new(
  threshold? : Int = 128,
  radius? : Int = 1,
  minimum_component_area? : Int = 4,
) -> PipelineConfig {
  {
    threshold: clamp(threshold, low=0, high=255),
    radius: max_i(0, radius),
    minimum_component_area: max_i(0, minimum_component_area),
    stages: [],
  }
}

///|
pub fn PipelineConfig::add(
  self : PipelineConfig,
  stage : PipelineStage,
) -> Unit {
  self.stages.push(stage)
}

///|
pub fn PipelineConfig::with_default_stages() -> PipelineConfig {
  let config = PipelineConfig::new()
  config.add(Normalize)
  config.add(Denoise)
  config.add(AdaptiveThreshold)
  config.add(Morphology)
  config.add(ConnectedComponents)
  config.add(Edges)
  config.add(Equalize)
  config
}

///|
pub struct PipelineTrace {
  stage : PipelineStage
  width : Int
  height : Int
  checksum : UInt
  mean : Int
} derive(Debug, Eq)

///|
pub fn PipelineTrace::from_image(
  stage : PipelineStage,
  image : GrayImage,
) -> PipelineTrace {
  let summary = image.stats()
  {
    stage,
    width: image.width,
    height: image.height,
    checksum: image_checksum(image),
    mean: summary.mean_x1000 / 1000,
  }
}

///|
pub struct PipelineResult {
  image : GrayImage
  traces : Array[PipelineTrace]
  components : Array[Component]
  valid : Bool
} derive(Debug, Eq)

///|
pub fn PipelineResult::new(
  image~ : GrayImage,
  traces~ : Array[PipelineTrace],
  components~ : Array[Component],
) -> PipelineResult {
  {
    image,
    traces,
    components,
    valid: validate_image(image, "pipeline.output").valid,
  }
}

///|
pub fn run_pipeline(
  image : GrayImage,
  config : PipelineConfig,
) -> PipelineResult {
  let mut current = image.copy()
  let traces : Array[PipelineTrace] = []
  let components : Array[Component] = []
  for stage in config.stages {
    match stage {
      Normalize => current = current.multiply_contrast(11, 10)
      Denoise => current = box_blur(current, config.radius)
      AdaptiveThreshold =>
        current = current.adaptive_threshold(config.radius, 0)
      Morphology => current = current.close(config.radius)
      Edges => current = current.edge_map(config.threshold)
      ConnectedComponents => {
        let mask = Mask::new(width=current.width, height=current.height)
        for y in 0..= config.threshold {
              mask.set(x, y, 1)
            }
          }
        }
        let found = connected_components(mask, 1)
        for component in found {
          if component.area >= config.minimum_component_area {
            components.push(component)
          }
        }
        current = components_to_image(components, image.size())
      }
      Equalize => current = current.equalize_histogram()
    }
    traces.push(PipelineTrace::from_image(stage, current))
  }
  PipelineResult::new(image=current, traces~, components~)
}

///|
fn components_to_image(components : Array[Component], size : Size) -> GrayImage {
  let image = GrayImage::new(width=size.width, height=size.height)
  for component in components {
    for point in component.pixels {
      image.set(point.x, point.y, Gray::white())
    }
  }
  image
}

///|
pub fn PipelineResult::stage_count(self : PipelineResult) -> Int {
  self.traces.length()
}

///|
pub fn PipelineResult::component_count(self : PipelineResult) -> Int {
  self.components.length()
}

///|
pub fn PipelineResult::trace(
  self : PipelineResult,
  index : Int,
) -> PipelineTrace? {
  if index < 0 || index >= self.traces.length() {
    None
  } else {
    Some(self.traces[index])
  }
}

///|
pub fn PipelineResult::to_json(self : PipelineResult) -> String {
  let sb = StringBuilder()
  sb.write_string("{\"valid\":\{self.valid},\"stages\":[")
  for i, trace in self.traces {
    if i > 0 {
      sb.write_string(",")
    }
    sb.write_string(
      "{\"stage\":\"\{pipeline_stage_name(trace.stage)}\",\"checksum\":\{trace.checksum},\"mean\":\{trace.mean}}",
    )
  }
  sb.write_string("] ,\"components\":\{self.components.length()}}")
  sb.to_string()
}

///|
pub fn pipeline_stage_name(stage : PipelineStage) -> String {
  match stage {
    Normalize => "normalize"
    Denoise => "denoise"
    AdaptiveThreshold => "adaptive-threshold"
    Morphology => "morphology"
    Edges => "edges"
    ConnectedComponents => "connected-components"
    Equalize => "equalize"
  }
}

///|
pub struct BatchPipelineResult {
  results : Array[PipelineResult]
  total_pixels : Int
  total_components : Int
} derive(Debug, Eq)

///|
pub fn run_pipeline_batch(
  images : Array[GrayImage],
  config : PipelineConfig,
) -> BatchPipelineResult {
  let results : Array[PipelineResult] = []
  let mut pixels = 0
  let mut components = 0
  for image in images {
    let result = run_pipeline(image, config)
    pixels += image.width * image.height
    components += result.components.length()
    results.push(result)
  }
  { results, total_pixels: pixels, total_components: components }
}

///|
pub fn BatchPipelineResult::count(self : BatchPipelineResult) -> Int {
  self.results.length()
}

///|
pub fn BatchPipelineResult::to_json(self : BatchPipelineResult) -> String {
  "{\"images\":\{self.results.length()},\"pixels\":\{self.total_pixels},\"components\":\{self.total_components}}"
}

///|
pub fn scene_pipeline(
  scene : SceneOutput,
  config : PipelineConfig,
) -> PipelineResult {
  run_pipeline(scene.image, config)
}

///|
pub fn validate_pipeline_config(config : PipelineConfig) -> ValidationReport {
  let report = ValidationReport::new()
  if config.threshold < 0 || config.threshold > 255 {
    report.error("pipeline threshold must be in the 8-bit range")
  }
  if config.radius < 0 {
    report.error("pipeline radius must not be negative")
  }
  if config.stages.length() == 0 {
    report.warning("pipeline has no stages and returns a copy")
  }
  report
}

///|
pub fn pipeline_checksum(result : PipelineResult) -> UInt {
  let mut checksum = image_checksum(result.image)
  for trace in result.traces {
    checksum = (checksum ^ trace.checksum) * 16777619
  }
  checksum
}