///|
/// Normalize a measured column to the unit interval.
pub fn normalize_column(
  values : ArrayView[Double],
) -> Array[Double] raise @domain.ExtractionError {
  if values.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  for value in values {
    @domain.require_finite("feature value", value)
  }
  let mut lower = values[0]
  let mut upper = values[0]
  for value in values {
    lower = lower.min(value)
    upper = upper.max(value)
  }
  if upper <= lower {
    [
      for _ in values => 0.0
    ]
  } else {
    [
      for value in values => (value - lower) / (upper - lower)
    ]
  }
}

///|
pub fn standardize_column(
  values : ArrayView[Double],
) -> Array[Double] raise @domain.ExtractionError {
  if values.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let mean = values.fold(init=0.0, (sum, value) => sum + value) /
    values.length().to_double()
  let variance = values.fold(init=0.0, (sum, value) => {
      sum + (value - mean) * (value - mean)
    }) /
    values.length().to_double()
  let scale = variance.sqrt().max(1.0e-12)
  [
    for value in values => (value - mean) / scale
  ]
}

///|
pub fn winsorize_column(
  values : ArrayView[Double],
  lower_probability~ : Double,
  upper_probability~ : Double,
) -> Array[Double] raise @domain.ExtractionError {
  if lower_probability < 0.0 ||
    upper_probability > 1.0 ||
    lower_probability > upper_probability {
    raise @domain.ExtractionError::InvalidArgument(
      message="winsorization probabilities are invalid",
    )
  }
  let sorted = values.to_owned()
  sorted.sort()
  let lower = sorted[(lower_probability * (sorted.length() - 1).to_double())
    .floor()
    .to_int()]
  let upper = sorted[(upper_probability * (sorted.length() - 1).to_double())
    .ceil()
    .to_int()]
  [
    for value in values => value.max(lower).min(upper)
  ]
}

///|
pub fn first_difference(values : ArrayView[Double]) -> Array[Double] {
  if values.length() < 2 {
    []
  } else {
    [
      for index in 1.. values[index] - values[index - 1]
    ]
  }
}

///|
pub fn relative_difference(values : ArrayView[Double]) -> Array[Double] {
  if values.length() < 2 {
    []
  } else {
    [
      for index in 1.. {
        (values[index] - values[index - 1]) /
        values[index - 1].abs().max(1.0e-12)
      }
    ]
  }
}

///|
pub(all) struct FeatureVector {
  id : String
  features : Array[Double]
  target : Double
} derive(Debug, Eq, ToJson)

///|
pub fn table_features(table : MeasurementTable) -> Array[FeatureVector] {
  let xs = normalize_column(table.x_values()) catch { _ => table.x_values() }
  let ys = normalize_column(table.y_values()) catch { _ => table.y_values() }
  [
    for index, row in table.rows() => {
      {
        id: row.id,
        features: [
          xs[index],
          ys[index],
          xs[index] * xs[index],
          xs[index] * ys[index],
        ],
        target: row.y,
      }
    }
  ]
}

///|
pub fn feature_matrix(table : MeasurementTable) -> Array[Array[Double]] {
  [
    for vector in table_features(table) => vector.features
  ]
}

///|
pub fn aggregate_replicates(
  table : MeasurementTable,
) -> MeasurementTable raise @domain.ExtractionError {
  let sorted = table.sorted_by_x().rows()
  let result : Array[MeasurementRow] = []
  let mut index = 0
  while index < sorted.length() {
    let first = sorted[index]
    let mut cursor = index + 1
    let mut sum_y = first.y
    let mut sum_temperature = first.temperature_kelvin
    let mut sum_pressure = first.pressure_pascal
    while cursor < sorted.length() && sorted[cursor].id == first.id {
      sum_y += sorted[cursor].y
      sum_temperature += sorted[cursor].temperature_kelvin
      sum_pressure += sorted[cursor].pressure_pascal
      cursor += 1
    }
    let count = cursor - index
    result.push(
      MeasurementRow::new(
        id=first.id,
        x=first.x,
        y=sum_y / count.to_double(),
        temperature_kelvin=sum_temperature / count.to_double(),
        pressure_pascal=sum_pressure / count.to_double(),
        replicate=count,
      ),
    )
    index = cursor
  }
  MeasurementTable::new(name=table.name(), rows=result)
}

///|
pub fn rescale_column(
  values : ArrayView[Double],
  source_lower~ : Double,
  source_upper~ : Double,
  target_lower~ : Double,
  target_upper~ : Double,
) -> Array[Double] raise @domain.ExtractionError {
  if source_upper <= source_lower {
    raise @domain.ExtractionError::InvalidArgument(
      message="source scale is invalid",
    )
  }
  [
    for value in values => {
      target_lower +
      (value - source_lower) /
      (source_upper - source_lower) *
      (target_upper - target_lower)
    }
  ]
}

///|
pub fn rolling_variance(
  values : ArrayView[Double],
  window~ : Int,
) -> Array[Double] raise @domain.ExtractionError {
  if window < 1 || window > values.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="variance window is invalid",
    )
  }
  [
    for index in 0.. {
      let start = (index + 1 - window).max(0)
      let count = index - start + 1
      let mut sum = 0.0
      for cursor in start..<=index {
        sum += values[cursor]
      }
      let mean = sum / count.to_double()
      let mut variance = 0.0
      for cursor in start..<=index {
        let difference = values[cursor] - mean
        variance += difference * difference
      }
      variance / count.to_double()
    }
  ]
}

///|
pub fn detect_plateau(
  values : ArrayView[Double],
  tolerance~ : Double,
) -> Array[Bool] {
  [
    for index in 0.. {
      if index == 0 {
        false
      } else {
        (values[index] - values[index - 1]).abs() <= tolerance
      }
    }
  ]
}

///|
pub fn monotone_direction(values : ArrayView[Double]) -> Int {
  let mut positive = 0
  let mut negative = 0
  for index in 1.. values[index - 1] {
      positive += 1
    }
    if values[index] < values[index - 1] {
      negative += 1
    }
  }
  if positive > negative {
    1
  } else if negative > positive {
    -1
  } else {
    0
  }
}