///|
/// Structured failures produced while compiling a bounded pattern-list.
pub(all) suberror PatternError {
  EmptyPatternList
  EmptyPattern(index~ : Int)
  PatternTooLong(length~ : Int, limit~ : Int)
  TooManyPatterns(count~ : Int, limit~ : Int)
  MissingMatchArgument(condition~ : String)
  InvalidAllCondition
  UnsupportedMatchCondition(name~ : String)
} derive(Debug, Eq)

///|
struct CompiledPattern {
  source_index : Int
  negated : Bool
  glob : String
} derive(Debug, Eq)

///|
/// A checked OpenSSH pattern-list. Construct it with `compile_pattern_list`
/// before using it in a resolver or Match condition.
pub struct PatternList {
  patterns : Array[CompiledPattern]
} derive(Debug, Eq)

///|
/// Explainable result of evaluating a pattern-list. Indices refer to the
/// original input array, are zero-based, and remain populated even when a
/// negative item excludes a matching positive item.
pub(all) struct PatternDecision {
  matched : Bool
  positive_index : Int?
  negative_index : Int?
} derive(Debug, Eq)

///|
fn compile_item(item : String, source_index : Int) -> CompiledPattern {
  if item.length() > 0 && item[0] == '!' {
    { source_index, negated: true, glob: lower_ascii(item[1:].to_owned()) }
  } else {
    { source_index, negated: false, glob: lower_ascii(item) }
  }
}

///|
/// Compile a pattern-list using the P0 resource limits.
pub fn compile_pattern_list(
  patterns : Array[String],
) -> PatternList raise PatternError {
  compile_pattern_list_with_limits(patterns)
}

///|
/// Compile a pattern-list with caller-supplied limits. This is useful for
/// fuzzing and for embedders which need tighter request budgets.
pub fn compile_pattern_list_with_limits(
  patterns : Array[String],
  pattern_length_limit? : Int = default_pattern_length_limit,
  pattern_count_limit? : Int = default_pattern_list_limit,
) -> PatternList raise PatternError {
  if patterns.length() == 0 {
    raise EmptyPatternList
  }
  if patterns.length() > pattern_count_limit {
    raise TooManyPatterns(count=patterns.length(), limit=pattern_count_limit)
  }
  let compiled : Array[CompiledPattern] = []
  for index = 0; index < patterns.length(); index = index + 1 {
    let item = patterns[index]
    if item.is_empty() || item == "!" {
      raise EmptyPattern(index~)
    }
    if item.length() > pattern_length_limit {
      raise PatternTooLong(length=item.length(), limit=pattern_length_limit)
    }
    compiled.push(compile_item(item, index))
  }
  { patterns: compiled }
}

///|
/// Evaluate a checked list against a value. A matching negative pattern has
/// precedence regardless of where it appears in the list.
pub fn PatternList::decide(
  self : PatternList,
  value : String,
) -> PatternDecision {
  if value.length() > default_match_value_length_limit {
    return { matched: false, positive_index: None, negative_index: None }
  }
  let normalized_value = lower_ascii(value)
  let mut positive_index : Int? = None
  let mut negative_index : Int? = None
  for item in self.patterns {
    if glob_from(item.glob, 0, normalized_value, 0) {
      if item.negated {
        if negative_index is None {
          negative_index = Some(item.source_index)
        }
      } else if positive_index is None {
        positive_index = Some(item.source_index)
      }
    }
  }
  {
    matched: positive_index is Some(_) && negative_index is None,
    positive_index,
    negative_index,
  }
}

///|
/// Evaluate a checked list without recording explain information.
pub fn PatternList::matches(self : PatternList, value : String) -> Bool {
  self.decide(value).matched
}