///|
pub struct CompiledStyle {
  entries : Array[(String, String?)]
  inline_vars : Array[(String, String)]
}

///|
/// A generated marker is both a style input (it applies its stable marker
/// class) and a typed selector identity consumable by relational conditions.
pub struct SelectorMarker {
  module_name : String
  name : String
  class_name : String
  compiled : CompiledStyle
}

///|
pub fn selector_marker(
  module_name : String,
  name : String,
  class_name : String,
) -> SelectorMarker {
  {
    module_name,
    name,
    class_name,
    compiled: {
      entries: [("marker|" + module_name + "|" + name, Some(class_name))],
      inline_vars: [],
    },
  }
}

///|
pub fn SelectorMarker::module_name(self : SelectorMarker) -> String {
  self.module_name
}

///|
pub fn SelectorMarker::name(self : SelectorMarker) -> String {
  self.name
}

///|
pub fn SelectorMarker::class_name(self : SelectorMarker) -> String {
  self.class_name
}

///|
pub(all) struct AppliedStyle {
  class_name : String
  inline_vars : Array[(String, String)]
} derive(Eq, Debug)

///|
pub(all) struct DynamicValueError {
  variable : String
  code : String
  label : String
} derive(Eq, Debug)

///|
pub type StyleResult = Result[CompiledStyle, Array[DynamicValueError]]

///|
pub struct StyleList {
  styles : Array[CompiledStyle]
  errors : Array[DynamicValueError]
}

///|
pub(open) trait StyleInput {
  fn append_to(Self, StyleList) -> StyleList
}

///|
pub(open) trait StaticStyleInput {
  fn to_compiled(Self) -> CompiledStyle
}

///|
pub fn style_list() -> StyleList {
  { styles: [], errors: [], }
}

///|
pub fn[T : StyleInput] compose(initial : T) -> StyleList {
  initial.append_to(style_list())
}

///|
#deprecated("Use compose(input) for a non-empty composition or style_list() for an empty composition", skip_current_package=true)
pub fn[T : StyleInput] styles(initial : Array[T]) -> StyleList {
  compose(initial)
}

///|
fn StyleList::append_style(
  self : StyleList,
  style : CompiledStyle,
) -> StyleList {
  let next_styles = self.styles.copy()
  next_styles.push(style)
  { styles: next_styles, errors: self.errors.copy(), }
}

///|
fn StyleList::append_errors(
  self : StyleList,
  errors : Array[DynamicValueError],
) -> StyleList {
  let next_errors = self.errors.copy()
  for error in errors {
    next_errors.push(error)
  }
  { styles: self.styles.copy(), errors: next_errors, }
}

///|
fn StyleList::append_list(self : StyleList, other : StyleList) -> StyleList {
  let next_styles = self.styles.copy()
  for style in other.styles {
    next_styles.push(style)
  }
  let next_errors = self.errors.copy()
  for error in other.errors {
    next_errors.push(error)
  }
  { styles: next_styles, errors: next_errors, }
}

///|
pub impl StyleInput for CompiledStyle with fn append_to(self, list) {
  list.append_style(self)
}

///|
pub impl StaticStyleInput for CompiledStyle with fn to_compiled(self) {
  self
}

///|
pub impl StyleInput for SelectorMarker with fn append_to(self, list) {
  list.append_style(self.compiled)
}

///|
pub impl StaticStyleInput for SelectorMarker with fn to_compiled(self) {
  self.compiled
}

///|
pub impl[T : StyleInput] StyleInput for Result[T, Array[DynamicValueError]] with fn append_to(
  self,
  list,
) {
  match self {
    Ok(input) => input.append_to(list)
    Err(errors) => list.append_errors(errors)
  }
}

///|
pub impl StyleInput for StyleList with fn append_to(self, list) {
  list.append_list(self)
}

///|
pub impl[T : StyleInput] StyleInput for Array[T] with fn append_to(self, list) {
  let mut result = list
  for input in self {
    result = input.append_to(result)
  }
  result
}

///|
pub impl[T : StyleInput] StyleInput for T? with fn append_to(self, list) {
  match self {
    Some(input) => input.append_to(list)
    None => list
  }
}

///|
pub fn[T : StyleInput] StyleList::add(self : StyleList, input : T) -> StyleList {
  input.append_to(self)
}

///|
pub fn[T : StyleInput] StyleList::when(
  self : StyleList,
  enabled : Bool,
  input : T,
) -> StyleList {
  if enabled {
    input.append_to(self)
  } else {
    self
  }
}

///|
pub fn dynamic_value_error(
  variable : String,
  code : String,
  label : String,
) -> DynamicValueError {
  { variable, code, label, }
}

///|
pub let none : CompiledStyle = { entries: [], inline_vars: [], }

///|
pub fn CompiledStyle::when(
  self : CompiledStyle,
  enabled : Bool,
) -> CompiledStyle {
  if enabled {
    self
  } else {
    none
  }
}

///|
pub fn checked_when(style : StyleResult, enabled : Bool) -> StyleResult {
  if enabled {
    style
  } else {
    Ok(none)
  }
}

///|
pub fn compiled(
  entries : Array[(String, String)],
  inline_vars~ : Array[(String, String)] = [],
) -> CompiledStyle {
  { entries: entries.map(entry => (entry.0, Some(entry.1))), inline_vars, }
}

///|
pub fn compiled_v2(
  entries : Array[(String, String?)],
  inline_vars~ : Array[(String, String)] = [],
) -> CompiledStyle {
  { entries, inline_vars, }
}

///|
pub fn compiled_v3(
  entries : Array[(String, String?)],
  inline_vars~ : Array[(String, String)] = [],
) -> CompiledStyle {
  { entries, inline_vars, }
}

///|
pub fn compiled_v5(
  entries : Array[(String, String?)],
  inline_vars~ : Array[(String, String)] = [],
) -> CompiledStyle {
  { entries, inline_vars, }
}

///|
pub fn bind_length(
  variable : @values.CssVar[@values.Length],
  value : @values.Length,
) -> (String, String) {
  let css = value.to_css()
  (variable.name(), if css == "0" { "0px" } else { css })
}

///|
pub fn bind_color(
  variable : @values.CssVar[@values.Color],
  value : @values.Color,
) -> (String, String) {
  (variable.name(), value.to_css())
}

///|
pub fn bind_number(
  variable : @values.CssVar[@values.Number],
  value : @values.Number,
) -> (String, String) {
  (variable.name(), value.to_css())
}

///|
pub fn bind_percentage(
  variable : @values.CssVar[@values.Percentage],
  value : @values.Percentage,
) -> (String, String) {
  (variable.name(), value.to_css())
}

///|
fn merge_compiled_entry(
  entries : Array[(String, String?)],
  key : String,
  value : String?,
) -> Unit {
  match entries.search_by(entry => entry.0 == key) {
    Some(index) => entries[index] = (key, value)
    None => entries.push((key, value))
  }
}

///|
fn merge_inline_var(
  entries : Array[(String, String)],
  key : String,
  value : String,
) -> Unit {
  match entries.search_by(entry => entry.0 == key) {
    Some(index) => entries[index] = (key, value)
    None => entries.push((key, value))
  }
}

///|
pub fn[T : StaticStyleInput] props(styles : Array[T]) -> AppliedStyle {
  props_compiled(styles.map(style => style.to_compiled()))
}

///|
fn props_compiled(styles : Array[CompiledStyle]) -> AppliedStyle {
  let merged : Array[(String, String?)] = []
  let inline_vars : Array[(String, String)] = []
  for style in styles {
    for entry in style.entries {
      if !entry.0.contains("|") {
        let conditional_prefix = entry.0 + "|"
        merged.retain(existing => !existing.0.has_prefix(conditional_prefix))
      }
    }
    for entry in style.entries {
      merge_compiled_entry(merged, entry.0, entry.1)
    }
    for entry in style.inline_vars {
      merge_inline_var(inline_vars, entry.0, entry.1)
    }
  }
  let classes : Array[String] = []
  for entry in merged {
    match entry.1 {
      Some(class_names) =>
        for class_name_view in class_names.split(" ") {
          let class_name = class_name_view.to_owned()
          if !class_name.is_empty() && !classes.contains(class_name) {
            classes.push(class_name)
          }
        }
      None => ()
    }
  }
  { class_name: classes.join(" "), inline_vars, }
}

///|
pub fn StyleList::resolve(
  self : StyleList,
) -> Result[AppliedStyle, Array[DynamicValueError]] {
  if self.errors.is_empty() {
    Ok(props_compiled(self.styles))
  } else {
    Err(self.errors.copy())
  }
}

///|
pub fn props_checked(
  checked_styles : Array[StyleResult],
  base_styles~ : Array[CompiledStyle] = [],
) -> Result[AppliedStyle, Array[DynamicValueError]] {
  compose(base_styles).add(checked_styles).resolve()
}

///|
fn dynamic_error_message(errors : Array[DynamicValueError]) -> String {
  let details = errors.map(error => {
    error.variable + ": " + error.code + " (" + error.label + ")"
  })
  "Invalid dynamic style values: " + details.join(", ")
}

///|
pub fn[T] expect_dynamic(result : Result[T, Array[DynamicValueError]]) -> T {
  match result {
    Ok(value) => value
    Err(errors) => abort(dynamic_error_message(errors))
  }
}