///|
/// A declarative export workflow for applications that need a stable GIF
/// pipeline instead of assembling encoder flags at each call site.
///
/// The workflow keeps visual stages, timing edits, safety checks, and the
/// export profile in one value. It is intentionally independent of files or
/// operating-system APIs, so the same plan can run in a CLI, a browser host,
/// or an embedded MoonBit service.
pub(all) enum GifWorkflowStep {
  Visual(stage~ : PipelineStage)
  Resize(width~ : Int, height~ : Int)
  SetDelay(delay_cs~ : Int)
  SetLoop(loop_count~ : Int?)
  SetBackground(color~ : Rgba)
  SetProfile(profile~ : ExportProfile)
  ClampLimits
  NormalizeTimeline
} derive(Debug)

///|
/// A reusable, inspectable GIF export plan.
pub(all) struct GifWorkflow {
  steps : Array[GifWorkflowStep]
  profile : ExportProfile
  limits : GifLimits
  comment : String?
  reject_empty : Bool
} derive(Debug)

///|
/// Metrics returned by a workflow before or after encoding.
pub(all) struct GifWorkflowReport {
  valid : Bool
  issues : Array[String]
  input_width : Int
  input_height : Int
  output_width : Int
  output_height : Int
  input_frames : Int
  output_frames : Int
  input_pixels : Int
  output_pixels : Int
  encoded_bytes : Int
  estimated_bytes : Int
  comment_bytes : Int
  stage_count : Int
  profile : ExportProfile
} derive(Debug)

///|
pub fn GifWorkflow::new() -> GifWorkflow {
  {
    steps: [],
    profile: Balanced,
    limits: GifLimits::default(),
    comment: None,
    reject_empty: true,
  }
}

///|
pub fn GifWorkflow::default() -> GifWorkflow {
  GifWorkflow::new()
}

///|
/// Append a low-level visual pipeline stage.
pub fn GifWorkflow::visual(
  self : GifWorkflow,
  stage : PipelineStage,
) -> GifWorkflow {
  self.steps.push(Visual(stage~))
  self
}

///|
/// Append an explicit nearest-neighbour resize stage.
pub fn GifWorkflow::resize(
  self : GifWorkflow,
  width : Int,
  height : Int,
) -> GifWorkflow {
  self.steps.push(Resize(width~, height~))
  self
}

///|
pub fn GifWorkflow::grayscale(self : GifWorkflow) -> GifWorkflow {
  self.visual(Gray)
}

///|
pub fn GifWorkflow::invert(self : GifWorkflow) -> GifWorkflow {
  self.visual(InvertColors)
}

///|
pub fn GifWorkflow::blur(self : GifWorkflow, radius : Int) -> GifWorkflow {
  self.visual(Blur(radius~))
}

///|
pub fn GifWorkflow::gaussian_blur(self : GifWorkflow) -> GifWorkflow {
  self.visual(GaussianBlur)
}

///|
pub fn GifWorkflow::sharpen(self : GifWorkflow) -> GifWorkflow {
  self.visual(Sharpen)
}

///|
pub fn GifWorkflow::edge_map(self : GifWorkflow) -> GifWorkflow {
  self.visual(Edge)
}

///|
pub fn GifWorkflow::posterize(self : GifWorkflow, levels : Int) -> GifWorkflow {
  self.visual(Posterize(levels~))
}

///|
pub fn GifWorkflow::threshold(self : GifWorkflow, level : Byte) -> GifWorkflow {
  self.visual(Threshold(level~))
}

///|
pub fn GifWorkflow::normalize_luma(self : GifWorkflow) -> GifWorkflow {
  self.visual(Normalize)
}

///|
pub fn GifWorkflow::ordered_dither(
  self : GifWorkflow,
  levels : Int,
) -> GifWorkflow {
  self.visual(Dither(levels~))
}

///|
pub fn GifWorkflow::pixelate(self : GifWorkflow, block : Int) -> GifWorkflow {
  self.visual(Pixelate(block~))
}

///|
pub fn GifWorkflow::grain(
  self : GifWorkflow,
  amount : Int,
  seed : Int,
) -> GifWorkflow {
  self.visual(Grain(amount~, seed~))
}

///|
pub fn GifWorkflow::tint(
  self : GifWorkflow,
  color : Rgba,
  strength : Int,
) -> GifWorkflow {
  self.visual(Tint(color~, strength~))
}

///|
pub fn GifWorkflow::adjust(
  self : GifWorkflow,
  adjustment : ColorAdjustment,
) -> GifWorkflow {
  self.visual(Adjust(adjustment~))
}

///|
pub fn GifWorkflow::convolve(
  self : GifWorkflow,
  kernel : ConvolutionKernel,
) -> GifWorkflow {
  self.visual(Convolve(kernel~))
}

///|
pub fn GifWorkflow::dilate(
  self : GifWorkflow,
  radius : Int,
  mode : MorphologyMode,
) -> GifWorkflow {
  self.visual(Dilate(radius~, mode~))
}

///|
pub fn GifWorkflow::erode(
  self : GifWorkflow,
  radius : Int,
  mode : MorphologyMode,
) -> GifWorkflow {
  self.visual(Erode(radius~, mode~))
}

///|
pub fn GifWorkflow::chroma_key(
  self : GifWorkflow,
  key : Rgba,
  tolerance : Int,
) -> GifWorkflow {
  self.visual(ChromaKey(key~, tolerance~))
}

///|
pub fn GifWorkflow::crop(
  self : GifWorkflow,
  x : Int,
  y : Int,
  width : Int,
  height : Int,
) -> GifWorkflow {
  self.visual(Crop(x~, y~, width~, height~))
}

///|
pub fn GifWorkflow::flip_horizontal(self : GifWorkflow) -> GifWorkflow {
  self.visual(FlipHorizontal)
}

///|
pub fn GifWorkflow::flip_vertical(self : GifWorkflow) -> GifWorkflow {
  self.visual(FlipVertical)
}

///|
/// Set a constant frame delay in centiseconds.
pub fn GifWorkflow::delay(self : GifWorkflow, delay_cs : Int) -> GifWorkflow {
  self.steps.push(SetDelay(delay_cs~))
  self
}

///|
pub fn GifWorkflow::loop_count(self : GifWorkflow, count : Int?) -> GifWorkflow {
  self.steps.push(SetLoop(loop_count=count))
  self
}

///|
pub fn GifWorkflow::background(self : GifWorkflow, color : Rgba) -> GifWorkflow {
  self.steps.push(SetBackground(color~))
  self
}

///|
pub fn GifWorkflow::profile(
  self : GifWorkflow,
  profile : ExportProfile,
) -> GifWorkflow {
  self.steps.push(SetProfile(profile~))
  { ..self, profile, }
}

///|
pub fn GifWorkflow::clamp_limits(self : GifWorkflow) -> GifWorkflow {
  self.steps.push(ClampLimits)
  self
}

///|
pub fn GifWorkflow::normalize_timeline(self : GifWorkflow) -> GifWorkflow {
  self.steps.push(NormalizeTimeline)
  self
}

///|
/// Add a UTF-8 comment extension to the final GIF.
pub fn GifWorkflow::comment(self : GifWorkflow, text : String?) -> GifWorkflow {
  { ..self, comment: text }
}

///|
pub fn GifWorkflow::with_limits(
  self : GifWorkflow,
  limits : GifLimits,
) -> GifWorkflow {
  { ..self, limits, }
}

///|
/// Empty animations can be useful as intermediate values, but export jobs
/// normally reject them so a missing input cannot silently create a trailer.
pub fn GifWorkflow::allow_empty(self : GifWorkflow) -> GifWorkflow {
  { ..self, reject_empty: false }
}

///|
pub fn GifWorkflow::step_count(self : GifWorkflow) -> Int {
  self.steps.length()
}

///|
pub fn GifWorkflow::steps(self : GifWorkflow) -> ArrayView[GifWorkflowStep] {
  self.steps[:]
}

///|
pub fn GifWorkflow::selected_profile(self : GifWorkflow) -> ExportProfile {
  self.profile
}

///|
pub fn GifWorkflow::limits(self : GifWorkflow) -> GifLimits {
  self.limits
}

///|
pub fn GifWorkflow::comment_text(self : GifWorkflow) -> String? {
  self.comment
}

///|
pub fn GifWorkflow::has_comment(self : GifWorkflow) -> Bool {
  self.comment is Some(_)
}

///|
fn workflow_step_label(step : GifWorkflowStep) -> String {
  match step {
    Visual(stage~) =>
      match stage {
        Gray => "grayscale"
        InvertColors => "invert"
        Blur(_) => "blur"
        GaussianBlur => "gaussian-blur"
        Sharpen => "sharpen"
        Edge => "edge"
        Posterize(_) => "posterize"
        Threshold(_) => "threshold"
        Normalize => "normalize"
        Dither(_) => "dither"
        Pixelate(_) => "pixelate"
        Grain(..) => "grain"
        Tint(..) => "tint"
        Adjust(..) => "adjust"
        Convolve(..) => "convolve"
        Dilate(..) => "dilate"
        Erode(..) => "erode"
        ChromaKey(..) => "chroma-key"
        Crop(..) => "crop"
        Resize(..) => "resize"
        FlipHorizontal => "flip-horizontal"
        FlipVertical => "flip-vertical"
      }
    Resize(..) => "resize"
    SetDelay(..) => "delay"
    SetLoop(..) => "loop"
    SetBackground(..) => "background"
    SetProfile(..) => "profile"
    ClampLimits => "clamp-limits"
    NormalizeTimeline => "normalize-timeline"
  }
}

///|
fn workflow_profile_label(profile : ExportProfile) -> String {
  match profile {
    Preview => "preview"
    Balanced => "balanced"
    SmallFile => "small-file"
    Archive => "archive"
    Sticker => "sticker"
  }
}

///|
/// Return stable labels suitable for logs, telemetry, and a CLI dry run.
pub fn GifWorkflow::labels(self : GifWorkflow) -> Array[String] {
  let result : Array[String] = []
  for step in self.steps {
    result.push(workflow_step_label(step))
  }
  result
}

///|
fn workflow_visual_pipeline(workflow : GifWorkflow) -> CanvasPipeline {
  let pipeline = CanvasPipeline::new()
  for step in workflow.steps {
    match step {
      Visual(stage~) => ignore(pipeline.add(stage))
      Resize(width~, height~) => ignore(pipeline.resize(width, height))
      _ => ()
    }
  }
  pipeline
}

///|
/// Apply only visual stages to one canvas. Timing and metadata steps are
/// intentionally ignored here so callers can preview an image independently.
pub fn GifWorkflow::apply_canvas(self : GifWorkflow, canvas : Canvas) -> Canvas {
  workflow_visual_pipeline(self).apply(canvas)
}

///|
fn workflow_apply_metadata(
  workflow : GifWorkflow,
  animation : Animation,
) -> Animation {
  let mut result = animation
  for step in workflow.steps {
    match step {
      SetDelay(delay_cs~) => result = result.with_delay(delay_cs)
      SetLoop(loop_count~) => result = result.with_loop_count(loop_count~)
      SetBackground(color~) => result = result.with_background(background=color)
      ClampLimits => result = result.clamp_limits(workflow.limits)
      NormalizeTimeline => result = result.normalize_delays()
      SetProfile(_) | Visual(_) | Resize(_) => ()
    }
  }
  result
}

///|
/// Apply visual stages while keeping timing, disposal, offsets, and loop data.
pub fn GifWorkflow::apply_animation(
  self : GifWorkflow,
  animation : Animation,
) -> Animation {
  let prepared = workflow_apply_metadata(self, animation)
  let pipeline = workflow_visual_pipeline(self)
  let frames : Array[Frame] = []
  let mut output_width = prepared.width
  let mut output_height = prepared.height
  for frame in prepared.frames {
    let transformed = pipeline.apply(frame.canvas())
    if frames.length() == 0 {
      output_width = transformed.width
      output_height = transformed.height
    }
    frames.push(frame.with_canvas(transformed))
  }
  let result = Animation::new(width=output_width, height=output_height)
    .with_loop_count(loop_count=prepared.loop_count)
    .with_background(background=prepared.background)
  for frame in frames {
    ignore(result.add_frame(frame))
  }
  result
}

///|
fn workflow_comment_bytes(workflow : GifWorkflow) -> Int {
  match workflow.comment {
    None => 0
    Some(text) => @utf8.encode(text).length()
  }
}

///|
fn workflow_check(
  workflow : GifWorkflow,
  animation : Animation,
) -> Array[String] {
  let issues : Array[String] = []
  if workflow.reject_empty && animation.frame_count() == 0 {
    issues.push("animation has no frames")
  }
  if !workflow.limits.valid_dimensions(animation.width, animation.height) {
    issues.push("logical screen dimensions are outside configured limits")
  }
  if animation.frame_count() > workflow.limits.frames() {
    issues.push("frame count exceeds configured limits")
  }
  if workflow_comment_bytes(workflow) > workflow.limits.comment_bytes() {
    issues.push("comment exceeds configured byte limit")
  }
  for frame in animation.frames {
    let canvas = frame.canvas()
    if !workflow.limits.valid_dimensions(canvas.width, canvas.height) {
      issues.push("frame dimensions are outside configured limits")
    }
    if frame.delay_cs() > workflow.limits.delay() {
      issues.push("frame delay exceeds configured limits")
    }
  }
  issues
}

///|
/// Validate a source animation without changing it or touching the encoder.
pub fn GifWorkflow::validate(
  self : GifWorkflow,
  animation : Animation,
) -> Array[String] {
  workflow_check(self, animation)
}

///|
pub fn GifWorkflowReport::valid(self : GifWorkflowReport) -> Bool {
  self.valid
}

///|
pub fn GifWorkflowReport::issues(self : GifWorkflowReport) -> ArrayView[String] {
  self.issues[:]
}

///|
pub fn GifWorkflowReport::input_dimensions(
  self : GifWorkflowReport,
) -> (Int, Int) {
  (self.input_width, self.input_height)
}

///|
pub fn GifWorkflowReport::output_dimensions(
  self : GifWorkflowReport,
) -> (Int, Int) {
  (self.output_width, self.output_height)
}

///|
pub fn GifWorkflowReport::input_frames(self : GifWorkflowReport) -> Int {
  self.input_frames
}

///|
pub fn GifWorkflowReport::output_frames(self : GifWorkflowReport) -> Int {
  self.output_frames
}

///|
pub fn GifWorkflowReport::input_pixels(self : GifWorkflowReport) -> Int {
  self.input_pixels
}

///|
pub fn GifWorkflowReport::output_pixels(self : GifWorkflowReport) -> Int {
  self.output_pixels
}

///|
pub fn GifWorkflowReport::encoded_bytes(self : GifWorkflowReport) -> Int {
  self.encoded_bytes
}

///|
pub fn GifWorkflowReport::estimated_bytes(self : GifWorkflowReport) -> Int {
  self.estimated_bytes
}

///|
pub fn GifWorkflowReport::comment_bytes(self : GifWorkflowReport) -> Int {
  self.comment_bytes
}

///|
pub fn GifWorkflowReport::stage_count(self : GifWorkflowReport) -> Int {
  self.stage_count
}

///|
pub fn GifWorkflowReport::profile(self : GifWorkflowReport) -> ExportProfile {
  self.profile
}

///|
/// Build a report from transformed frames and the exact optimizer estimate.
pub fn GifWorkflow::report(
  self : GifWorkflow,
  animation : Animation,
) -> GifWorkflowReport raise GifError {
  let issues = self.validate(animation)
  let transformed = self.apply_animation(animation)
  let plan = transformed.plan_export(self.profile)
  let optimized = transformed.optimize_for_size(plan.optimization())
  let estimated = optimized
    .optimization_report(plan.optimization())
    .estimated_bytes()
  let mut output_pixels = 0
  for frame in optimized.frames {
    output_pixels += frame.canvas().len()
  }
  GifWorkflowReport::{
    valid: issues.length() == 0,
    issues,
    input_width: animation.width,
    input_height: animation.height,
    output_width: transformed.width,
    output_height: transformed.height,
    input_frames: animation.frame_count(),
    output_frames: optimized.frame_count(),
    input_pixels: animation.width * animation.height * animation.frame_count(),
    output_pixels,
    encoded_bytes: 0,
    estimated_bytes: estimated + workflow_comment_bytes(self),
    comment_bytes: workflow_comment_bytes(self),
    stage_count: self.step_count(),
    profile: self.profile,
  }
}

///|
/// Encode the workflow and return its exact output. Validation is reported by
/// the normal GIF error channel only when the source is structurally invalid;
/// callers can inspect `validate` first when they need all diagnostics.
pub fn GifWorkflow::encode(
  self : GifWorkflow,
  animation : Animation,
) -> Bytes raise GifError {
  let transformed = self.apply_animation(animation)
  let plan = transformed.plan_export(self.profile)
  let optimized = transformed.optimize_for_size(plan.optimization())
  match self.comment {
    None => encode_with_options(optimized, plan.encode())
    Some(text) => encode_with_comment(optimized, text, options=plan.encode())
  }
}

///|
/// Encode and fill the report's measured byte field after the exact export.
pub fn GifWorkflow::encode_report(
  self : GifWorkflow,
  animation : Animation,
) -> GifWorkflowReport raise GifError {
  let report = self.report(animation)
  let encoded = self.encode(animation)
  { ..report, encoded_bytes: encoded.length() }
}

///|
/// Produce a compact deterministic dry-run line for a command-line preview.
pub fn GifWorkflow::dry_run(
  self : GifWorkflow,
  animation : Animation,
) -> String {
  let transformed = self.apply_animation(animation)
  let labels = self.labels()
  let label_text = if labels.length() == 0 {
    "none"
  } else {
    let mut result = labels[0]
    for i in 1..(?[^\/<>]+?)\<\/a\>/g
          },
          hljs.COMMENT(
            '//', // begin
            '\n', // end
          )
        ]
      }
    }

    hljs.registerLanguage('moonbit', moonbitLanguageFn);
    hljs.highlightAll();
    hljs.initLineNumbersOnLoad();

    const number = window.location.href.split('#')[1];

    function waitForLineNumbers() {
      setTimeout(function () {
        const target = document.querySelector(`.hljs-ln-line[data-line-number="${number}"]`);
        if (target == null) waitForLineNumbers();
        else target.scrollIntoView();
      }, 50);
    }

    waitForLineNumbers()