///|
// All views in one generation share the same interleaved Float64 buffer.
priv struct Line2DCache {
  flat : @ticker.TickValues
  xs : @ticker.TickValues
  ys : @ticker.TickValues
}

///|
// This is captured source metadata, distinct from Style's effective appearance.
// A width edit rescales this pattern, never the current config template.
priv struct LineDashPattern {
  offset : Double
  dashes : Array[Double]?
}

///|
/// A stable Line data handle with copied originals and lazy converted views.
pub struct Line2D {
  priv points : Array[@core.Point]
  priv mut style : @core.Style
  priv mut unscaled_dash_pattern : LineDashPattern
  // Only source container provenance; effective appearance stays in Style.
  priv mut scaled_dashes_as_list : Bool
  priv mut source_color : Bool
  priv mut x : LineData
  priv mut y : LineData
  priv mut cache : Line2DCache
  priv mut drawstyle : String
  priv mut path_values : @ticker.TickValues
  priv mut invalid_x : Bool
  priv mut invalid_y : Bool
  priv mut owner : Axes?
  priv mut removal_parent : Axes?
  priv mut visible : Bool
  priv mut stale : Bool
}

///|
fn line2d_cache_from_flat(values : Array[Double]) -> Line2DCache {
  let flat = @ticker.TickValues::from_array(values)
  let length = values.length() / 2
  // Builders supply an even, private buffer whose shape cannot be resized by
  // a borrowed TickValues. Empty views also accept their unused start value.
  { flat, xs: try! flat.view(0, 2, length), ys: try! flat.view(1, 2, length), }
}

///|
fn line2d_flat_length(length : Int32) -> Int32 raise @core.DataError {
  let cells = length.to_int64() * 2L
  if cells > 2147483647L {
    raise @core.DataError::InvalidTicks(
      message="ResourceError: Line Float64 cache exceeds Array addressability",
    )
  }
  cells.to_int()
}

///|
fn line2d_cache_from_points(
  points : Array[@core.Point],
) -> Line2DCache raise @core.DataError {
  let values = Array(capacity=line2d_flat_length(points.length()))
  for point in points {
    values.push(point.x())
    values.push(point.y())
  }
  line2d_cache_from_flat(values)
}

///|
/// Borrow the copied original x input without converting it.
pub fn Line2D::get_xdata(self : Line2D) -> LineData {
  self.x
}

///|
pub fn Line2D::get_ydata(self : Line2D) -> LineData {
  self.y
}

///|
pub fn Line2D::get_data(self : Line2D) -> (LineData, LineData) {
  (self.get_xdata(), self.get_ydata())
}

///|
/// Copy new x originals and invalidate only x; limits are updated separately.
pub fn Line2D::set_xdata(self : Line2D, values : LineData) -> Unit {
  self.x = values.copy()
  self.invalid_x = true
  self.stale = true
}

///|
pub fn Line2D::set_ydata(self : Line2D, values : LineData) -> Unit {
  self.y = values.copy()
  self.invalid_y = true
  self.stale = true
}

///|
pub fn Line2D::set_data(self : Line2D, x : LineData, y : LineData) -> Unit {
  self.set_xdata(x)
  self.set_ydata(y)
}

///|
pub fn Line2D::stale(self : Line2D) -> Bool {
  self.stale
}

///|
pub fn Line2D::get_visible(self : Line2D) -> Bool {
  self.visible
}

///|
/// Change drawing visibility without invalidating data or requesting new limits.
pub fn Line2D::set_visible(self : Line2D, visible : Bool) -> Unit {
  if self.visible != visible {
    self.visible = visible
    self.stale = true
  }
}

///|
pub fn Line2D::get_linewidth(self : Line2D) -> Double {
  self.style.width()
}

///|
pub fn Line2D::get_drawstyle(self : Line2D) -> String {
  self.drawstyle
}

///|
/// Change the path construction mode without eagerly converting coordinates.
pub fn Line2D::set_drawstyle(
  self : Line2D,
  drawstyle : String,
) -> Unit raise @core.DataError {
  match drawstyle {
    "default" | "steps-mid" | "steps-pre" | "steps-post" | "steps" => ()
    _ =>
      raise @core.DataError::InvalidTicks(
        message=line_drawstyle_invalid_message(drawstyle),
      )
  }
  if self.drawstyle != drawstyle {
    self.stale = true
    self.invalid_x = true
  }
  self.drawstyle = drawstyle
}

///|
pub fn Line2D::get_linestyle(self : Line2D) -> String {
  self.style.line_stroke().linestyle()
}

///|
pub fn Line2D::is_dashed(self : Line2D) -> Bool {
  match self.get_linestyle() {
    "--" | "-." | ":" => true
    _ => false
  }
}

///|
pub fn Line2D::get_gapcolor(self : Line2D) -> String? {
  self.style.line_gapcolor()
}

///|
/// Validate first, then retain the raw optional spelling and always mark stale.
pub fn Line2D::set_gapcolor(
  self : Line2D,
  gapcolor : String?,
) -> Unit raise @core.DataError {
  if gapcolor is Some(value) {
    // The original checker uses color=gapcolor, including its error text.
    line_color_validate(value)
  }
  let style = self.style
  self.style = @core.style(
    style.color(),
    width=style.width(),
    radius=style.radius(),
    fill=style.fill(),
    font_size=style.font_size(),
    marker=style.marker(),
    line_alpha=style.line_alpha(),
    line_stroke=style.line_stroke(),
    line_gapcolor=gapcolor,
  )
  self.stale = true
}

///|
// Normalize the source initial integer-zero offset with its float sum. Keep
// this shared by rc String capture and the immutable tuple-input projection.
fn line_style_zero_offset(values : Array[Double]) -> Double {
  let total = line_style_float_sum(values)
  if total.is_nan() {
    0.0 / 0.0
  } else if total < 0.0 {
    -0.0
  } else {
    0.0
  }
}

///|
fn line_style_capture(canonical : String) -> LineDashPattern {
  let dashes = match canonical {
    "--" => Some(@config.lines_dashed_pattern())
    "-." => Some(@config.lines_dashdot_pattern())
    ":" => Some(@config.lines_dotted_pattern())
    _ => None
  }
  let offset = dashes.map(line_style_zero_offset).unwrap_or(0.0)
  { offset, dashes, }
}

///|
fn LineDashPattern::scaled(
  self : LineDashPattern,
  canonical : String,
  width : Double,
  scale_dashes : Bool,
) -> @core.LineStroke {
  if scale_dashes {
    @core.line_stroke(
      canonical,
      offset=self.offset * width,
      dashes=self.dashes.map(values => values.map(value => value * width)),
    )
  } else {
    @core.line_stroke(canonical, offset=self.offset, dashes=self.dashes)
  }
}

///|
fn Line2D::replace_line_stroke(
  self : Line2D,
  stroke : @core.LineStroke,
) -> Unit {
  let style = self.style
  self.style = @core.style(
    style.color(),
    width=style.width(),
    radius=style.radius(),
    fill=style.fill(),
    font_size=style.font_size(),
    marker=style.marker(),
    line_alpha=style.line_alpha(),
    line_stroke=stroke,
    line_gapcolor=style.line_gapcolor(),
  )
}

///|
pub fn Line2D::set_linestyle(
  self : Line2D,
  linestyle : String,
) -> Unit raise @core.DataError {
  let canonical = match linestyle {
    "-" | "solid" => "-"
    "--" | "dashed" => "--"
    "-." | "dashdot" => "-."
    ":" | "dotted" => ":"
    "" | " " | "none" | "None" => "None"
    _ =>
      raise @core.DataError::InvalidTicks(
        message=line_style_invalid_message(linestyle),
      )
  }
  // Preserve the original assignment order: canonical appearance first,
  // captured unscaled metadata second, effective scaling third, then stale.
  let previous = self.style.line_stroke()
  self.replace_line_stroke(
    @core.line_stroke(
      canonical,
      offset=previous.dash_offset(),
      dashes=previous.dash_sequence(),
    ),
  )
  self.unscaled_dash_pattern = line_style_capture(canonical)
  let scale_dashes = @config.lines_scale_dashes()
  self.scaled_dashes_as_list = scale_dashes
  self.replace_line_stroke(
    self.unscaled_dash_pattern.scaled(
      canonical,
      self.style.width(),
      scale_dashes,
    ),
  )
  self.stale = true
}

///|
/// Snapshot numeric dashes as an immutable tuple-input profile. Validation of
/// unusual numeric patterns remains draw-late, as for captured rc patterns.
pub fn Line2D::set_dashes(self : Line2D, seq : Array[Double]) -> Unit {
  let snapshot = seq.copy()
  if snapshot.is_empty() {
    // This literal is always valid; retain the full solid scaling route.
    try! self.set_linestyle("-")
    return
  }
  let previous = self.style.line_stroke()
  self.replace_line_stroke(
    @core.line_stroke(
      "--",
      offset=previous.dash_offset(),
      dashes=previous.dash_sequence(),
    ),
  )
  self.unscaled_dash_pattern = {
    offset: line_style_zero_offset(snapshot),
    dashes: Some(snapshot),
  }
  let scale_dashes = @config.lines_scale_dashes()
  self.scaled_dashes_as_list = scale_dashes
  self.replace_line_stroke(
    self.unscaled_dash_pattern.scaled("--", self.style.width(), scale_dashes),
  )
  self.stale = true
}

///|
pub fn Line2D::get_alpha(self : Line2D) -> Double? {
  self.style.line_alpha()
}

///|
/// Equal alpha values are a complete no-op, including the sign of stored zero.
pub fn Line2D::set_alpha(
  self : Line2D,
  alpha : Double?,
) -> Unit raise @core.DataError {
  if alpha is Some(value) && !(value >= 0.0 && value <= 1.0) {
    raise @core.DataError::InvalidTicks(
      message="ValueError: alpha (" +
        @ticker.python_float_repr(value) +
        ") is outside 0-1 range",
    )
  }
  let style = self.style
  let same = match (style.line_alpha(), alpha) {
    (None, None) => true
    (Some(previous), Some(next)) => previous == next
    _ => false
  }
  if same {
    return
  }
  self.style = @core.style(
    style.color(),
    width=style.width(),
    radius=style.radius(),
    fill=style.fill(),
    font_size=style.font_size(),
    marker=style.marker(),
    line_alpha=alpha,
    line_stroke=style.line_stroke(),
    line_gapcolor=style.line_gapcolor(),
  )
  self.stale = true
}

///|
/// Return the original color spelling, without resolving a color-cycle token.
pub fn Line2D::get_color(self : Line2D) -> String {
  self.style.color()
}

///|
/// Validate a source color and preserve its spelling for later drawing.
pub fn Line2D::set_color(
  self : Line2D,
  color : String,
) -> Unit raise @core.DataError {
  line_color_validate(color)
  let style = self.style
  self.style = @core.style(
    color,
    width=style.width(),
    radius=style.radius(),
    fill=style.fill(),
    font_size=style.font_size(),
    marker=style.marker(),
    line_alpha=style.line_alpha(),
    line_stroke=style.line_stroke(),
    line_gapcolor=style.line_gapcolor(),
  )
  self.source_color = true
  self.stale = true
}

///|
/// Set linewidth and refresh scaling of its captured unscaled dash metadata.
pub fn Line2D::set_linewidth(self : Line2D, width : Double) -> Unit {
  let style = self.style
  if style.width() != width {
    self.stale = true
  }
  // Assignment is unconditional: equal signed zeroes still have distinct bits.
  self.style = @core.style(
    style.color(),
    width~,
    radius=style.radius(),
    fill=style.fill(),
    font_size=style.font_size(),
    marker=style.marker(),
    line_alpha=style.line_alpha(),
    line_stroke=style.line_stroke(),
    line_gapcolor=style.line_gapcolor(),
  )
  let scale_dashes = @config.lines_scale_dashes()
  self.scaled_dashes_as_list = scale_dashes
  self.replace_line_stroke(
    self.unscaled_dash_pattern.scaled(self.get_linestyle(), width, scale_dashes),
  )
}

///|
fn Line2D::detach(self : Line2D) -> Unit {
  self.owner = None
}

///|
// Clear only currently attached children through this operation. Ordinary
// remove deliberately retains its old removal binding for repeated errors.
fn Line2D::clear_attachment(self : Line2D) -> Unit {
  self.removal_parent = None
  self.detach()
}

///|
/// Remove this exact attached Line without recomputing limits or legend state.
pub fn Line2D::remove(self : Line2D) -> Unit raise @core.DataError {
  let parent = match self.removal_parent {
    Some(parent) => parent
    None =>
      raise @core.DataError::InvalidTicks(
        message="NotImplementedError: cannot remove artist",
      )
  }
  let artists = parent.state.val.artists
  let mut found = None
  for index in 0.. !physical_equal(line, self))
  self.detach()
}

///|
fn Line2D::mark_drawn(self : Line2D) -> Unit {
  self.stale = false
}

///|
fn Line2D::convert_coordinate(
  self : Line2D,
  values : LineData,
  horizontal : Bool,
) -> Array[Double] raise @core.DataError {
  match self.owner {
    Some(axes) =>
      values.convert(if horizontal { axes.x_axis() } else { axes.y_axis() })
    None => values.convert_detached()
  }
}

///|
/// Rebuild one generation. Conversion side effects survive a later failure.
pub fn Line2D::recache_checked(
  self : Line2D,
  always~ : Bool = false,
) -> Unit raise @core.DataError {
  let xs = if always || self.invalid_x {
    @ticker.TickValues::from_array(self.convert_coordinate(self.x, true))
  } else {
    self.cache.xs
  }
  let ys = if always || self.invalid_y {
    @ticker.TickValues::from_array(self.convert_coordinate(self.y, false))
  } else {
    self.cache.ys
  }
  let length = if xs.length() == ys.length() {
    xs.length()
  } else if xs.length() == 1 {
    ys.length()
  } else if ys.length() == 1 {
    xs.length()
  } else {
    raise @core.DataError::InvalidTicks(
      message="shape mismatch: objects cannot be broadcast to a single shape.  Mismatch is between arg 0 with shape (\{xs.length()},) and arg 1 with shape (\{ys.length()},).",
    )
  }
  let values = Array(capacity=line2d_flat_length(length))
  for index in 0.. @ticker.TickValues raise @core.DataError {
  if self.invalid_x {
    self.recache_checked()
  }
  self.cache.xs
}

///|
pub fn Line2D::y_values_checked(
  self : Line2D,
) -> @ticker.TickValues raise @core.DataError {
  if self.invalid_y {
    self.recache_checked()
  }
  self.cache.ys
}

///|
/// Read x then y, preserving the possibility of two different generations.
pub fn Line2D::data_values_checked(
  self : Line2D,
) -> (@ticker.TickValues, @ticker.TickValues) raise @core.DataError {
  let xs = self.x_values_checked()
  let ys = self.y_values_checked()
  (xs, ys)
}

///|
/// Borrow flat interleaved [x0,y0,x1,y1,...] Float64 storage.
pub fn Line2D::xy_values_checked(
  self : Line2D,
) -> @ticker.TickValues raise @core.DataError {
  if self.invalid_x || self.invalid_y {
    self.recache_checked()
  }
  self.cache.flat
}

///|
/// Borrow cached path vertices as flat [x0,y0,...] Float64 storage.
/// Processed coordinate queries remain unexpanded; default shares XY storage.
pub fn Line2D::path_values_checked(
  self : Line2D,
) -> @ticker.TickValues raise @core.DataError {
  if self.invalid_x || self.invalid_y {
    self.recache_checked()
  }
  self.path_values
}

///|
fn Line2D::materialize_points(self : Line2D) -> Unit {
  self.points.clear()
  let length = self.path_values.length() / 2
  for index in 0.. Array[@core.Point] raise @core.DataError {
  ignore(self.path_values_checked())
  self.materialize_points()
  copy_points(self.points)
}

///|
fn LineData::update_initial_units(
  self : LineData,
  axis : @axis.Axis,
) -> Unit raise @core.DataError {
  if !axis.have_units() {
    match self {
      Numeric(_) => ()
      Category(values) => ignore(axis.update_category_units(values))
      Date(values) => ignore(axis.update_date_units(values))
    }
  }
}

///|
/// Attach a typed Line with the existing equal-length initial plot contract.
pub fn Axes::plot_line(
  self : Axes,
  x : LineData,
  y : LineData,
  color~ : String = "",
  label~ : String = "",
) -> Line2D raise @core.DataError {
  if x.length() != y.length() {
    raise @core.DataError::LengthMismatch(
      expected=x.length(),
      actual=y.length(),
    )
  }
  x.update_initial_units(self.x_axis())
  y.update_initial_units(self.y_axis())
  let xs = x.convert(self.x_axis())
  let ys = y.convert(self.y_axis())
  let points = @core.series(xs, ys).points()
  self.attach_line_handle(points, x, y, color~, label~)
}

///|
pub fn Figure::plot_line(
  self : Figure,
  x : LineData,
  y : LineData,
  color~ : String = "",
  label~ : String = "",
) -> Line2D raise @core.DataError {
  self.axes().plot_line(x, y, color~, label~)
}