///|
/// A regex pattern used for matching parameter types in cucumber expressions.
pub(all) struct RegexPattern {
  priv value : String
} derive(Debug, Eq)

///|
pub fn RegexPattern::RegexPattern(value : String) -> RegexPattern {
  { value, }
}

///|
pub fn RegexPattern::to_string(self : RegexPattern) -> String {
  self.value
}

///|
/// Parameter types supported by cucumber expressions.
pub(all) enum ParamType {
  Int
  Float
  String_
  Word
  Anonymous
  Double_
  Long
  Byte
  Short
  BigDecimal
  BigInteger
  Custom(String)
} derive(Debug, Eq, ToJson, FromJson)

///|
/// A registered parameter type entry with name, type, regex patterns, and transformer.
///
/// `use_for_snippets` is true when `CucumberExpressionGenerator` can use the
/// type. `prefer_for_regexp_match` is true when a `RegularExpression` uses
/// this type before other types with the same regexp.
pub(all) struct ParamTypeEntry {
  name : String
  type_ : ParamType
  patterns : Array[RegexPattern]
  transformer : Transformer
  use_for_snippets : Bool
  prefer_for_regexp_match : Bool
} derive(Debug)

///|
pub impl Eq for ParamTypeEntry with fn equal(self, other) -> Bool {
  self.name == other.name &&
  self.type_ == other.type_ &&
  self.patterns == other.patterns
}

///|
/// Registry mapping parameter type names to their regex patterns.
pub(all) struct ParamTypeRegistry {
  priv entries : Array[ParamTypeEntry]
}

///|
pub fn ParamTypeRegistry::new() -> ParamTypeRegistry {
  { entries: [], }
}

///|
/// Create a registry with the 11 built-in parameter types pre-registered.
pub fn ParamTypeRegistry::default() -> ParamTypeRegistry {
  let reg = ParamTypeRegistry::new()
  // Integer types (all share the same regex patterns)
  let int_patterns = [RegexPattern("-?\\d+"), RegexPattern("\\d+")]
  reg.add_entry(
    "int",
    ParamType::Int,
    int_patterns,
    transformer=Transformer::new(fn(groups) raise {
      IntVal(@string.parse_int(groups[0]))
    }),
    use_for_snippets=true,
    prefer_for_regexp_match=true,
  )
  reg.add_entry(
    "long",
    ParamType::Long,
    int_patterns,
    transformer=Transformer::new(fn(groups) raise {
      LongVal(@string.parse_int64(groups[0]))
    }),
    use_for_snippets=false,
    prefer_for_regexp_match=false,
  )
  reg.add_entry(
    "byte",
    ParamType::Byte,
    int_patterns,
    transformer=Transformer::new(fn(groups) raise {
      ByteVal(parse_int_in_range(groups[0], "byte", -128, 127).to_byte())
    }),
    use_for_snippets=false,
    prefer_for_regexp_match=false,
  )
  reg.add_entry(
    "short",
    ParamType::Short,
    int_patterns,
    transformer=Transformer::new(fn(groups) raise {
      ShortVal(parse_int_in_range(groups[0], "short", -32768, 32767))
    }),
    use_for_snippets=false,
    prefer_for_regexp_match=false,
  )
  reg.add_entry(
    "biginteger",
    ParamType::BigInteger,
    int_patterns,
    transformer=Transformer::new(fn(groups) {
      BigIntegerVal(BigInt::from_string(groups[0]))
    }),
    use_for_snippets=false,
    prefer_for_regexp_match=false,
  )
  // Float types (all share the same regex pattern)
  let float_patterns = [
    RegexPattern("[-+]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:[eE][+-]?\\d+)?"),
  ]
  reg.add_entry(
    "float",
    ParamType::Float,
    float_patterns,
    transformer=Transformer::new(fn(groups) raise {
      FloatVal(@string.parse_double(groups[0]))
    }),
    use_for_snippets=true,
    prefer_for_regexp_match=false,
  )
  reg.add_entry(
    "double",
    ParamType::Double_,
    float_patterns,
    transformer=Transformer::new(fn(groups) raise {
      DoubleVal(@string.parse_double(groups[0]))
    }),
    use_for_snippets=false,
    prefer_for_regexp_match=true,
  )
  reg.add_entry(
    "bigdecimal",
    ParamType::BigDecimal,
    float_patterns,
    transformer=Transformer::new(fn(groups) raise {
      match @decimal.Decimal::from_string(groups[0]) {
        Some(d) => BigDecimalVal(d)
        None => raise Failure::Failure("Invalid bigdecimal: " + groups[0])
      }
    }),
    use_for_snippets=false,
    prefer_for_regexp_match=false,
  )
  // String: the transformer gets the double-quoted and the single-quoted
  // group. It unescapes \" and \' like the reference implementation.
  // Other backslash sequences stay unchanged.
  reg.add_entry(
    "string",
    ParamType::String_,
    [
      RegexPattern("\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\""),
      RegexPattern("'([^'\\\\]*(\\\\.[^'\\\\]*)*)'"),
    ],
    transformer=Transformer::new(fn(groups) {
      let s = if groups[0] != "" {
        groups[0]
      } else {
        groups.get(1).unwrap_or("")
      }
      StringVal(
        s.replace_all(old="\\\"", new="\"").replace_all(old="\\'", new="'"),
      )
    }),
    use_for_snippets=true,
    prefer_for_regexp_match=false,
  )
  // Word (identity transformer)
  reg.add_entry(
    "word",
    ParamType::Word,
    [RegexPattern("[^\\s]+")],
    transformer=Transformer::new(fn(groups) { WordVal(groups[0]) }),
    use_for_snippets=false,
    prefer_for_regexp_match=false,
  )
  reg.add_entry(
    "",
    ParamType::Anonymous,
    [RegexPattern(".*")],
    transformer=Transformer::new(fn(groups) { AnonymousVal(groups[0]) }),
    use_for_snippets=false,
    prefer_for_regexp_match=true,
  )
  reg
}

///|
/// Register a parameter type.
///
/// Raises an error when the name is already registered, when the name has
/// one of the characters `{`, `}`, `(`, `)`, `\` or `/`, when there are no
/// regexps, or when a preferential type already has one of the regexps.
///
/// `use_for_snippets` (default `true`) lets `CucumberExpressionGenerator`
/// use the type. `prefer_for_regexp_match` (default `false`) makes a
/// `RegularExpression` use this type before other types with the same
/// regexp.
pub fn ParamTypeRegistry::register(
  self : ParamTypeRegistry,
  name : String,
  type_ : ParamType,
  patterns : Array[RegexPattern],
  transformer? : Transformer = Transformer::new(fn(groups) {
    ParamValue::CustomVal(@any.of(groups[0]))
  }),
  use_for_snippets? : Bool = true,
  prefer_for_regexp_match? : Bool = false,
) -> Unit raise ParameterTypeError {
  if !is_valid_parameter_type_name(name) {
    raise IllegalParameterName(name~)
  }
  if patterns.is_empty() {
    raise NoRegexps(name~)
  }
  if self.get(name) is Some(_) {
    raise DuplicateParameterType(name~)
  }
  if prefer_for_regexp_match {
    for pattern in patterns {
      let regexp = pattern.to_string()
      if self.entries_with_regexp(regexp) is [first, ..] &&
        first.prefer_for_regexp_match {
        raise DuplicatePreferentialParameterType(
          regexp~,
          first=first.name,
          second=name,
        )
      }
    }
  }
  self.add_entry(
    name,
    type_,
    patterns,
    transformer~,
    use_for_snippets~,
    prefer_for_regexp_match~,
  )
}

///|
fn ParamTypeRegistry::add_entry(
  self : ParamTypeRegistry,
  name : String,
  type_ : ParamType,
  patterns : Array[RegexPattern],
  transformer~ : Transformer,
  use_for_snippets? : Bool = true,
  prefer_for_regexp_match? : Bool = false,
) -> Unit {
  self.entries.push({
    name,
    type_,
    patterns,
    transformer,
    use_for_snippets,
    prefer_for_regexp_match,
  })
}

///|
/// Order parameter types: preferential types first, then by name.
fn compare_parameter_types(a : ParamTypeEntry, b : ParamTypeEntry) -> Int {
  if a.prefer_for_regexp_match && !b.prefer_for_regexp_match {
    return -1
  }
  if b.prefer_for_regexp_match && !a.prefer_for_regexp_match {
    return 1
  }
  compare_text(a.name, b.name)
}

///|
/// Compare strings character by character. `String::compare` compares the
/// length first, which is not the order that the reference uses.
fn compare_text(a : String, b : String) -> Int {
  let length = if a.length() < b.length() { a.length() } else { b.length() }
  for i in 0.. Array[ParamTypeEntry] {
  let found = self.entries.filter(e => {
    e.patterns.iter().any(p => p.to_string() == regexp)
  })
  found.sort_by(compare_parameter_types)
  found
}

///|
/// Find the parameter type for a capture group of a regular expression.
///
/// `regexp` is the source of the capture group. `expression_regexp` and
/// `text` are used for the error message. Returns `None` when no type has
/// the regexp. Raises an error when more than one type has the regexp and
/// none of them is preferential.
pub fn ParamTypeRegistry::lookup_by_regexp(
  self : ParamTypeRegistry,
  regexp : String,
  expression_regexp : String,
  text : String,
) -> ParamTypeEntry? raise AmbiguousParameterTypeError {
  let found = self.entries_with_regexp(regexp)
  if found.is_empty() {
    return None
  }
  if found.length() > 1 && !found[0].prefer_for_regexp_match {
    let generated = CucumberExpressionGenerator::new(self).generate_expressions(
      text,
    )
    raise AmbiguousParameterType(
      regexp~,
      expression_regexp~,
      names=found.map(e => e.name),
      generated=generated.map(g => g.source()),
    )
  }
  Some(found[0])
}

///|
/// A name is valid when it has none of `{`, `}`, `(`, `)`, `\` and `/`,
/// after `\` is removed before the regex characters `[`, `]`, `$`, `.`,
/// `|`, `?`, `*` and `+`.
fn is_valid_parameter_type_name(name : String) -> Bool {
  let chars = name.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c == '\\' &&
      i + 1 < chars.length() &&
      "[]$.|?*+".contains_char(chars[i + 1]) {
      i = i + 2
      continue
    }
    if "{}()\\/".contains_char(c) {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn parse_int_in_range(
  text : String,
  type_name : String,
  min : Int,
  max : Int,
) -> Int raise {
  let n = @string.parse_int(text)
  if n < min || n > max {
    raise Failure::Failure(
      "\{text} is out of range for {\{type_name}}: \{min}..\{max}",
    )
  }
  n
}

///|
pub fn ParamTypeRegistry::get(
  self : ParamTypeRegistry,
  name : String,
) -> ParamTypeEntry? {
  for entry in self.entries {
    if entry.name == name {
      return Some(entry)
    }
  }
  None
}

///|
/// Read-only view of all registered parameter type entries.
pub fn ParamTypeRegistry::entries_view(
  self : ParamTypeRegistry,
) -> ArrayView[ParamTypeEntry] {
  self.entries[:]
}

///|
#deprecated
pub extend RegexPattern with Eq::{not_equal, equal}

///|
#deprecated
pub extend RegexPattern with @debug.Debug::{to_repr}

///|
pub extend ParamType with ToJson::{to_json}

///|
pub extend ParamType with @json.FromJson::{from_json}

///|
#deprecated
pub extend ParamType with Eq::{not_equal, equal}

///|
#deprecated
pub extend ParamType with @debug.Debug::{to_repr}

///|
#deprecated
pub extend ParamTypeEntry with Eq::{not_equal, equal}

///|
#deprecated
pub extend ParamTypeEntry with @debug.Debug::{to_repr}