///|
pub(all) struct AutoFilterOption {
  column : String
  expression : String
} derive(Debug)

///|
pub type AutoFilterOptions = AutoFilterOption

///|
pub struct AutoFilterCustomFilter {
  operator : String
  value : String
} derive(Debug)

///|
pub struct AutoFilterCustomFilters {
  and_filter : Bool
  filters : Array[AutoFilterCustomFilter]
} derive(Debug)

///|
pub struct AutoFilterColumn {
  col : Int
  filters : Array[String]?
  custom_filters : AutoFilterCustomFilters?
} derive(Debug)

///|
pub struct AutoFilter {
  range_ref : String
  columns : Array[AutoFilterColumn]
} derive(Debug)

///|
fn is_whitespace(ch : Char) -> Bool {
  match ch {
    ' ' | '\t' | '\n' | '\r' => true
    _ => false
  }
}

///|
fn tokenize_filter_expression(expression : StringView) -> Array[String] {
  let tokens : Array[String] = []
  let chars = expression.to_array()
  let mut current = StringBuilder::new()
  let mut has_content = false
  let mut in_quotes = false
  let mut i = 0
  while i < chars.length() {
    let ch = chars[i]
    if in_quotes {
      current.write_char(ch)
      has_content = true
      if ch == '"' {
        if i + 1 < chars.length() && chars[i + 1] == '"' {
          current.write_char('"')
          i = i + 1
        } else {
          in_quotes = false
        }
      }
      i = i + 1
      continue
    }
    if is_whitespace(ch) {
      if has_content {
        tokens.push(current.to_string())
        current = StringBuilder::new()
        has_content = false
      }
      i = i + 1
      continue
    }
    if ch == '"' {
      in_quotes = true
      current.write_char(ch)
      has_content = true
      i = i + 1
      continue
    }
    current.write_char(ch)
    has_content = true
    i = i + 1
  }
  if has_content {
    tokens.push(current.to_string())
  }
  tokens
}

///|
fn parse_filter_tokens(
  tokens : ArrayView[String],
) -> (Int, String) raise XlsxError {
  if tokens.length() != 3 {
    raise InvalidAutoFilter(msg="auto filter expression invalid")
  }
  let op_text = tokens[1].to_lower()
  let operator = match op_text {
    "==" | "=" | "=~" | "eq" => 2
    "!=" | "!~" | "ne" | "<>" => 5
    "<" => 1
    "<=" => 3
    ">" => 4
    ">=" => 6
    _ => raise InvalidAutoFilter(msg="auto filter operator invalid")
  }
  let mut op_value = operator
  let mut token = tokens[2]
  let lower_token = token.to_lower()
  if lower_token == "blanks" || lower_token == "nonblanks" {
    if op_value != 2 && op_value != 5 {
      raise InvalidAutoFilter(msg="auto filter operator invalid")
    }
    token = lower_token
    if token == "blanks" {
      if op_value == 5 {
        token = " "
      }
    } else if op_value == 5 {
      op_value = 2
      token = "blanks"
    } else {
      op_value = 5
      token = " "
    }
  }
  if op_value == 2 && (token.contains("*") || token.contains("?")) {
    op_value = 22
  }
  (op_value, token)
}

///|
fn parse_filter_expression(
  tokens : ArrayView[String],
) -> (Array[Int], Array[String]) raise XlsxError {
  if tokens.length() == 7 {
    let cond = tokens[3].to_lower()
    let conditional = if cond == "or" || tokens[3] == "||" { 1 } else { 0 }
    let (op1, token1) = parse_filter_tokens(tokens[:3])
    let (op2, token2) = parse_filter_tokens(tokens[4:7])
    ([op1, conditional, op2], [token1, token2])
  } else if tokens.length() == 3 {
    let (op, token) = parse_filter_tokens(tokens)
    ([op], [token])
  } else {
    raise InvalidAutoFilter(msg="auto filter expression invalid")
  }
}

///|
fn custom_filter_operator(op : Int) -> String raise XlsxError {
  match op {
    1 => "lessThan"
    2 => "equal"
    3 => "lessThanOrEqual"
    4 => "greaterThan"
    5 => "notEqual"
    6 => "greaterThanOrEqual"
    22 => "equal"
    _ => raise InvalidAutoFilter(msg="auto filter operator invalid")
  }
}

///|
fn auto_filter_column_from_expression(
  col : Int,
  expression : StringView,
) -> AutoFilterColumn raise XlsxError {
  let tokens = tokenize_filter_expression(expression)
  let (ops, values) = parse_filter_expression(tokens)
  if ops.length() == 1 && ops[0] == 2 {
    return { col, filters: Some(values), custom_filters: None }
  }
  if ops.length() == 3 && ops[0] == 2 && ops[1] == 1 && ops[2] == 2 {
    return { col, filters: Some(values), custom_filters: None }
  }
  let custom_filters : Array[AutoFilterCustomFilter] = []
  if ops.length() == 1 {
    let operator = custom_filter_operator(ops[0])
    custom_filters.push({ operator, value: values[0] })
  } else {
    let operator1 = custom_filter_operator(ops[0])
    let operator2 = custom_filter_operator(ops[2])
    custom_filters.push({ operator: operator1, value: values[0] })
    custom_filters.push({ operator: operator2, value: values[1] })
  }
  let and_value = ops.length() == 3 && ops[1] == 0
  {
    col,
    filters: None,
    custom_filters: Some({ and_filter: and_value, filters: custom_filters }),
  }
}

///|

///|
fn auto_filter_from_options(
  range_ref : String,
  options : ArrayView[AutoFilterOption],
) -> AutoFilter raise XlsxError {
  let normalized = normalize_range_ref(range_ref)
  let (_min_row, min_col, _max_row, max_col) = parse_range_ref(normalized)
  let columns : Array[AutoFilterColumn] = []
  for opt in options {
    if opt.column == "" || opt.expression == "" {
      continue
    }
    let col = column_name_to_number(opt.column) catch {
      _ => raise InvalidAutoFilter(msg="auto filter column invalid")
    }
    if col < min_col || col > max_col {
      raise InvalidAutoFilter(msg="auto filter column out of range")
    }
    let column = auto_filter_column_from_expression(col, opt.expression)
    let mut replaced = false
    for i, existing in columns {
      if existing.col == column.col {
        columns[i] = column
        replaced = true
        break
      }
    }
    if !replaced {
      columns.push(column)
    }
  }
  { range_ref: normalized, columns }
}

///|
fn clone_auto_filter(filter : AutoFilter) -> AutoFilter {
  let columns : Array[AutoFilterColumn] = []
  for column in filter.columns {
    let filters = match column.filters {
      Some(values) => {
        let copied : Array[String] = []
        for value in values {
          copied.push(value)
        }
        Some(copied)
      }
      None => None
    }
    let custom_filters = match column.custom_filters {
      Some(custom) => {
        let copied_filters : Array[AutoFilterCustomFilter] = []
        for entry in custom.filters {
          copied_filters.push({ operator: entry.operator, value: entry.value })
        }
        Some({ and_filter: custom.and_filter, filters: copied_filters })
      }
      None => None
    }
    columns.push({ col: column.col, filters, custom_filters })
  }
  { range_ref: filter.range_ref, columns }
}

///|
test "auto filter wb: parse_filter_tokens rejects non-3 token input" {
  let bad_tokens : Result[(Int, String), Error] = Ok(
    parse_filter_tokens(["x", "=="]),
  ) catch {
    e => Err(e)
  }
  inspect(bad_tokens is Err(XlsxError::InvalidAutoFilter(_)), content="true")
}

///|
test "auto filter wb: custom_filter_operator equal and invalid branches" {
  inspect(custom_filter_operator(2), content="equal")
  let bad_op : Result[String, Error] = Ok(custom_filter_operator(99)) catch {
    e => Err(e)
  }
  inspect(bad_op is Err(XlsxError::InvalidAutoFilter(_)), content="true")
}