///|
/// A filter operation that can be composed into a [`Pipeline`].
///
/// Each constructor corresponds to one of the stable `apply_filter_id`
/// operations. Keeping the operation as an enum means callers cannot create a
/// pipeline with an unknown filter id, while the existing numeric API remains
/// available for host bindings.
pub(all) enum Filter {
  Grayscale
  Invert
  Brightness(Int)
  Contrast(Double)
  Blur
  Sharpen
  Emboss
  Edges
  Sobel
  Sepia
  Threshold(Int)
  Pixelate(Int)
  Median
  HistogramEqualize
  FlipHorizontal
  FlipVertical
  Posterize(Int)
  Gamma(Double)
  Vignette(Double)
  Scharr
  Canny(Int)
  Otsu
  DitherMono
}

///|
/// Converts a typed operation to the shared host dispatch representation.
/// Parameter defaults intentionally stay in `Image::apply_filter_id`, so a
/// pipeline and a direct host call have exactly the same boundary semantics.
fn Filter::dispatch(self : Filter) -> (Int, Double) {
  match self {
    Grayscale => (0, 0.0)
    Invert => (1, 0.0)
    Brightness(delta) => (2, delta.to_double())
    Contrast(factor) => (3, factor)
    Blur => (4, 0.0)
    Sharpen => (5, 0.0)
    Emboss => (6, 0.0)
    Edges => (7, 0.0)
    Sobel => (8, 0.0)
    Sepia => (9, 0.0)
    Threshold(level) => (10, level.to_double())
    Pixelate(block) => (11, block.to_double())
    Median => (12, 0.0)
    HistogramEqualize => (13, 0.0)
    FlipHorizontal => (14, 0.0)
    FlipVertical => (15, 0.0)
    Posterize(levels) => (16, levels.to_double())
    Gamma(value) => (17, value)
    Vignette(strength) => (18, strength)
    Scharr => (19, 0.0)
    Canny(high) => (20, high.to_double())
    Otsu => (21, 0.0)
    DitherMono => (22, 0.0)
  }
}

///|
/// An ordered, reusable sequence of image filters.
///
/// `append` and `then` return a new pipeline and leave their receiver's
/// operation list untouched. `apply` also leaves the source image unchanged,
/// matching the copy-on-write behavior of individual filters.
pub struct Pipeline {
  operations : Array[Filter]
}

///|
/// Creates an empty pipeline. Applying it returns a deep copy of the input.
pub fn Pipeline::new() -> Pipeline {
  { operations: [], }
}

///|
/// Creates a pipeline containing one operation.
pub fn Pipeline::from_filter(filter : Filter) -> Pipeline {
  { operations: [filter], }
}

///|
/// Returns a new pipeline with `filter` appended at the end.
pub fn Pipeline::append(self : Pipeline, filter : Filter) -> Pipeline {
  let next = self.operations.copy()
  next.push(filter)
  { operations: next, }
}

///|
/// Returns a new pipeline that applies `self`, then `other`.
pub fn Pipeline::then(self : Pipeline, other : Pipeline) -> Pipeline {
  let next = self.operations.copy()
  for filter in other.operations {
    next.push(filter)
  }
  { operations: next, }
}

///|
/// Number of operations in this pipeline.
pub fn Pipeline::length(self : Pipeline) -> Int {
  self.operations.length()
}

///|
/// Whether this pipeline contains no operations.
pub fn Pipeline::is_empty(self : Pipeline) -> Bool {
  self.operations.length() == 0
}

///|
/// Applies each operation in insertion order.
pub fn Pipeline::apply(self : Pipeline, image : Image) -> Image {
  if self.operations.length() == 0 {
    return image.copy()
  }
  let mut out = image
  for filter in self.operations {
    let (filter_id, amount) = filter.dispatch()
    out = out.apply_filter_id(filter_id, amount)
  }
  out
}

///|
/// Applies a typed filter pipeline to an image.
pub fn Image::apply_pipeline(self : Image, pipeline : Pipeline) -> Image {
  pipeline.apply(self)
}