///|
pub(all) enum DataValidationType {
  NoneType
  Custom
  Date
  Decimal
  List
  TextLength
  Time
  Whole
} derive(Debug)

///|
pub(all) enum DataValidationErrorStyle {
  Stop
  Warning
  Information
} derive(Debug)

///|
pub(all) enum DataValidationOperator {
  Between
  Equal
  GreaterThan
  GreaterThanOrEqual
  LessThan
  LessThanOrEqual
  NotBetween
  NotEqual
} derive(Debug)

///|
pub(all) enum DataValidationFormula {
  IntValue(Int)
  DoubleValue(Double)
  TextValue(String)
} derive(Debug)

///|
pub struct DataValidation {
  allow_blank : Bool
  mut error : String?
  mut error_style : String?
  mut error_title : String?
  mut operator : String
  mut prompt : String?
  mut prompt_title : String?
  mut show_drop_down : Bool
  mut show_error_message : Bool
  mut show_input_message : Bool
  mut sqref : String
  mut validation_type : String
  mut formula1 : String
  mut formula2 : String
} derive(Debug)

///|
fn data_validation_type_text(value : DataValidationType) -> String {
  match value {
    NoneType => "none"
    Custom => "custom"
    Date => "date"
    Decimal => "decimal"
    List => "list"
    TextLength => "textLength"
    Time => "time"
    Whole => "whole"
  }
}

///|
fn data_validation_error_style_text(value : DataValidationErrorStyle) -> String {
  match value {
    Stop => "stop"
    Warning => "warning"
    Information => "information"
  }
}

///|
fn data_validation_operator_text(value : DataValidationOperator) -> String {
  match value {
    Between => "between"
    Equal => "equal"
    GreaterThan => "greaterThan"
    GreaterThanOrEqual => "greaterThanOrEqual"
    LessThan => "lessThan"
    LessThanOrEqual => "lessThanOrEqual"
    NotBetween => "notBetween"
    NotEqual => "notEqual"
  }
}

///|
fn data_validation_abs_double(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
fn data_validation_formula_text(
  value : DataValidationFormula,
) -> String raise XlsxError {
  let max_float32 = 3.4028234663852886e38
  match value {
    IntValue(v) => v.to_string()
    DoubleValue(v) =>
      if data_validation_abs_double(v) > max_float32 {
        raise InvalidDataValidation(msg="data validation range exceeds limit")
      } else {
        v.to_string()
      }
    TextValue(v) => v
  }
}

///|
fn escape_data_validation_formula(value : StringView) -> String {
  escape_xml_text(value)
}

///|
fn unescape_data_validation_formula(value : StringView) -> String {
  let text = unescape_xml_text(value)
  if text.has_prefix("\"") {
    text.replace_all(old="\"\"", new="\"")
  } else {
    text
  }
}

///|
pub fn DataValidation::new(allow_blank : Bool) -> DataValidation {
  {
    allow_blank,
    error: None,
    error_style: None,
    error_title: None,
    operator: "",
    prompt: None,
    prompt_title: None,
    show_drop_down: false,
    show_error_message: false,
    show_input_message: false,
    sqref: "",
    validation_type: "",
    formula1: "",
    formula2: "",
  }
}

///|
pub fn DataValidation::set_error(
  self : DataValidation,
  style : DataValidationErrorStyle,
  title : String,
  msg : String,
) -> Unit {
  self.error = Some(msg)
  self.error_title = Some(title)
  self.error_style = Some(data_validation_error_style_text(style))
  self.show_error_message = true
}

///|
pub fn DataValidation::set_input(
  self : DataValidation,
  title : String,
  msg : String,
) -> Unit {
  self.show_input_message = true
  self.prompt_title = Some(title)
  self.prompt = Some(msg)
}

///|
pub fn DataValidation::set_drop_list(
  self : DataValidation,
  values : ArrayView[String],
) -> Unit raise XlsxError {
  let sb = StringBuilder::new()
  for i, value in values {
    if i > 0 {
      sb.write_char(',')
    }
    sb.write_view(value)
  }
  let formula = sb.to_string()
  if count_utf16_units(formula) > max_field_length {
    raise InvalidDataValidation(msg="data validation must be 0-255 characters")
  }
  self.validation_type = data_validation_type_text(List)
  if formula.has_prefix("=") {
    self.formula1 = escape_data_validation_formula(formula)
    return
  }
  let escaped = escape_data_validation_formula(formula).replace_all(
    old="\"",
    new="\"\"",
  )
  self.formula1 = "\"\{escaped}\""
}

///|
pub fn DataValidation::set_range(
  self : DataValidation,
  formula1 : DataValidationFormula,
  formula2 : DataValidationFormula,
  validation_type : DataValidationType,
  op : DataValidationOperator,
) -> Unit raise XlsxError {
  self.formula1 = data_validation_formula_text(formula1)
  self.formula2 = data_validation_formula_text(formula2)
  self.validation_type = data_validation_type_text(validation_type)
  self.operator = data_validation_operator_text(op)
}

///|
pub fn DataValidation::set_sqref_drop_list(
  self : DataValidation,
  sqref : String,
) -> Unit {
  self.formula1 = sqref
  self.validation_type = data_validation_type_text(List)
}

///|
pub fn DataValidation::set_sqref(
  self : DataValidation,
  sqref : String,
) -> Unit raise XlsxError {
  let normalized = normalize_sqref_for_write(sqref)
  if self.sqref == "" {
    self.sqref = normalized
  } else {
    self.sqref = "\{self.sqref} \{normalized}"
  }
}

///|
fn data_validation_bool_attr(value : Bool) -> String {
  if value {
    "1"
  } else {
    "0"
  }
}

///|
/// Writes ` name="escaped-value"` for a present optional string attribute, or
/// nothing when it is absent.
fn write_opt_xml_attr(
  sb : StringBuilder,
  name : String,
  value : String?,
) -> Unit {
  match value {
    Some(v) => {
      sb.write_view(" \{name}=\"")
      sb.write_view(escape_xml_attr(v))
      sb.write_view("\"")
    }
    None => ()
  }
}

///|
fn data_validation_xml(dv : DataValidation) -> String raise XlsxError {
  if dv.sqref == "" {
    raise InvalidDataValidation(msg="data validation sqref empty")
  }
  // Keep the serializer as a defense-in-depth boundary for values reconstructed
  // from raw XML or older in-memory state, even though typed setters normalize.
  let sqref = normalize_sqref_for_write(dv.sqref)
  let sb = StringBuilder::new()
  sb.write_view("")
  if dv.formula1 != "" {
    sb.write_view("")
    sb.write_view(escape_data_validation_formula(dv.formula1))
    sb.write_view("")
  }
  if dv.formula2 != "" {
    sb.write_view("")
    sb.write_view(escape_data_validation_formula(dv.formula2))
    sb.write_view("")
  }
  sb.write_view("")
  sb.to_string()
}

///|
fn parse_xml_bool(text : StringView) -> Bool {
  match text.to_lower() {
    "1" | "true" => true
    _ => false
  }
}

///|
fn data_validation_from_xml(xml : StringView) -> DataValidation raise XlsxError {
  let tag = match tag_attributes_in(xml, "dataValidation") {
    Some(value) => value
    None => raise InvalidXml(msg="data validation tag missing")
  }
  let allow_blank = match attr_value(tag, "allowBlank") {
    Some(value) => parse_xml_bool(value)
    None => false
  }
  let dv = DataValidation::new(allow_blank)
  match attr_value(tag, "error") {
    Some(value) => dv.error = Some(unescape_xml_text(value))
    None => ()
  }
  match attr_value(tag, "errorStyle") {
    Some(value) => dv.error_style = Some(unescape_xml_text(value))
    None => ()
  }
  match attr_value(tag, "errorTitle") {
    Some(value) => dv.error_title = Some(unescape_xml_text(value))
    None => ()
  }
  match attr_value(tag, "operator") {
    Some(value) => dv.operator = unescape_xml_text(value)
    None => ()
  }
  match attr_value(tag, "prompt") {
    Some(value) => dv.prompt = Some(unescape_xml_text(value))
    None => ()
  }
  match attr_value(tag, "promptTitle") {
    Some(value) => dv.prompt_title = Some(unescape_xml_text(value))
    None => ()
  }
  match attr_value(tag, "showDropDown") {
    Some(value) => dv.show_drop_down = parse_xml_bool(value)
    None => ()
  }
  match attr_value(tag, "showErrorMessage") {
    Some(value) => dv.show_error_message = parse_xml_bool(value)
    None => ()
  }
  match attr_value(tag, "showInputMessage") {
    Some(value) => dv.show_input_message = parse_xml_bool(value)
    None => ()
  }
  dv.sqref = match attr_value(tag, "sqref") {
    Some(value) => unescape_xml_text(value)
    None => raise InvalidXml(msg="data validation sqref missing")
  }
  match attr_value(tag, "type") {
    Some(value) => dv.validation_type = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body_from(xml, "formula1") {
    Some(value) => dv.formula1 = unescape_data_validation_formula(value)
    None => ()
  }
  match extract_tag_body_from(xml, "formula2") {
    Some(value) => dv.formula2 = unescape_data_validation_formula(value)
    None => ()
  }
  dv
}

///|
test "data validation wb: data_validation_xml rejects empty sqref" {
  let dv = DataValidation::new(true)
  let result : Result[String, Error] = Ok(data_validation_xml(dv)) catch {
    e => Err(e)
  }
  inspect(result is Err(XlsxError::InvalidDataValidation(_)), content="true")
}

///|
test "data validation wb: data_validation_xml emits showDropDown when enabled" {
  let dv = DataValidation::new(true)
  dv.show_drop_down = true
  dv.set_sqref("A1")
  let xml = data_validation_xml(dv) catch {
    err => fail("data_validation_xml failed: \{repr(err)}")
  }
  inspect(xml.contains(" showDropDown=\"1\""), content="true")
}

///|
test "data validation wb: parse requires dataValidation tag" {
  let result : Result[DataValidation, Error] = Ok(
    data_validation_from_xml(""),
  ) catch {
    e => Err(e)
  }
  inspect(result is Err(XlsxError::InvalidXml(_)), content="true")
}

///|
test "data validation wb: parse defaults with minimal tag and optional attrs" {
  let dv = data_validation_from_xml("") catch {
    err => fail("data_validation_from_xml failed: \{repr(err)}")
  }
  inspect(dv.allow_blank, content="false")
  inspect(dv.validation_type, content="")
  inspect(dv.formula1, content="")

  let dv_show = data_validation_from_xml(
    "",
  ) catch {
    err =>
      fail("data_validation_from_xml with showDropDown failed: \{repr(err)}")
  }
  inspect(dv_show.show_drop_down, content="true")
}

///|
test "data validation wb: parse requires sqref attribute" {
  let result : Result[DataValidation, Error] = Ok(
    data_validation_from_xml(""),
  ) catch {
    e => Err(e)
  }
  inspect(result is Err(XlsxError::InvalidXml(_)), content="true")
}