// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Glob patterns and filters for diago IR
///
/// Supports:
/// - Single glob (*): matches any characters at one level
/// - Double glob (**): matches all descendants recursively
/// - Triple glob (***): matches all descendants including boards
/// - Filters (&key: value): conditionally apply properties

///|
/// Reserved keywords that shouldn't be matched by globs
let reserved_keywords : Array[String] = [
  "label", "shape", "icon", "style", "width", "height", "top", "left", "near", "tooltip",
  "link", "class", "classes", "vars", "source-arrowhead", "target-arrowhead", "direction",
  "grid-rows", "grid-columns", "grid-gap", "horizontal-gap", "vertical-gap", "grid-column-span",
  "grid-row-span", "constraint", "layers", "scenarios", "steps",
]

///|
/// Board keywords (for triple glob)
let board_keywords : Array[String] = ["layers", "scenarios", "steps"]

///|
/// Check if a keyword is reserved
pub fn is_reserved_keyword(name : String) -> Bool {
  let lower = name.to_lower()
  for kw in reserved_keywords {
    if kw == lower {
      return true
    }
  }
  false
}

///|
/// Check if a keyword is a board keyword
pub fn is_board_keyword(name : String) -> Bool {
  let lower = name.to_lower()
  for kw in board_keywords {
    if kw == lower {
      return true
    }
  }
  false
}

///|
/// Check if pattern represents a double glob (**)
/// Parser stores ** as ["*", "", "*"]
pub fn is_double_glob(pattern : Array[String]) -> Bool {
  if pattern.length() == 1 && pattern[0] == "**" {
    return true
  }
  // Parser format: ["*", "", "*"]
  if pattern.length() == 3 &&
    pattern[0] == "*" &&
    pattern[1] == "" &&
    pattern[2] == "*" {
    return true
  }
  false
}

///|
/// Check if pattern represents a triple glob (***)
/// Parser stores *** as ["*", "", "*", "", "*"]
pub fn is_triple_glob(pattern : Array[String]) -> Bool {
  if pattern.length() == 1 && pattern[0] == "***" {
    return true
  }
  // Parser format: ["*", "", "*", "", "*"]
  if pattern.length() == 5 &&
    pattern[0] == "*" &&
    pattern[1] == "" &&
    pattern[2] == "*" &&
    pattern[3] == "" &&
    pattern[4] == "*" {
    return true
  }
  false
}

///|
/// Collect all fields matching a double glob (**) pattern
/// This recursively collects all descendant fields except board keywords
pub fn Map::double_glob(self : Map) -> Array[Field] {
  let result : Array[Field] = []
  self.double_glob_helper(result)
  result
}

///|
fn Map::double_glob_helper(self : Map, result : Array[Field]) -> Unit {
  for f in self.fields {
    // Skip reserved keywords (but check if it's a board keyword)
    if f.is_reserved_keyword_name() {
      if f.is_board_keyword_name() {
        // Skip board keywords entirely for double glob
        continue
      }
      // For other reserved keywords, descend but don't add
      match f.composite {
        Some(Map(m)) => m.double_glob_helper(result)
        _ => ()
      }
      continue
    }
    // Add this field
    result.push(f)
    // Recursively collect from children
    match f.composite {
      Some(Map(m)) => m.double_glob_helper(result)
      _ => ()
    }
  }
}

///|
/// Collect all fields matching a triple glob (***) pattern
/// This recursively collects all descendant fields including boards
pub fn Map::triple_glob(self : Map) -> Array[Field] {
  let result : Array[Field] = []
  self.triple_glob_helper(result)
  result
}

///|
fn Map::triple_glob_helper(self : Map, result : Array[Field]) -> Unit {
  for f in self.fields {
    if f.is_board_keyword_name() {
      match f.composite {
        Some(Map(boards)) =>
          for board in boards.fields {
            match board.composite {
              Some(Map(board_map)) => board_map.triple_glob_helper(result)
              _ => ()
            }
          }
        _ => ()
      }
      continue
    }
    // Skip non-board reserved keywords
    if f.is_reserved_keyword_name() {
      match f.composite {
        Some(Map(m)) => m.triple_glob_helper(result)
        _ => ()
      }
      continue
    }
    result.push(f)
    // Recursively collect from children
    match f.composite {
      Some(Map(m)) => m.triple_glob_helper(result)
      _ => ()
    }
  }
}

///|
/// Collect fields matching a multi-glob pattern (** or ***)
/// Returns (fields, is_glob_pattern)
pub fn Map::multi_glob(
  self : Map,
  pattern : Array[String],
) -> (Array[Field], Bool) {
  if is_double_glob(pattern) {
    (self.double_glob(), true)
  } else if is_triple_glob(pattern) {
    (self.triple_glob(), true)
  } else {
    ([], false)
  }
}

///|
/// Match a string against a glob pattern
/// Pattern is an array where elements are either:
/// - literal text to match
/// - "*" to match any characters
pub fn match_pattern(s : String, pattern : Array[String]) -> Bool {
  match_pattern_inner(s, pattern, false)
}

///|
fn match_pattern_inner(
  s : String,
  pattern : Array[String],
  allow_reserved : Bool,
) -> Bool {
  if !allow_reserved && is_reserved_keyword(s) {
    return false
  }
  if pattern.length() == 0 {
    return true
  }
  let s_lower = s.to_lower()
  let mut remaining = s_lower
  let mut i = 0
  while i < pattern.length() {
    if pattern[i] == "*" {
      // Wildcard: match next literal if there is one
      if i != pattern.length() - 1 {
        let next_pattern = pattern[i + 1].to_lower()
        match string_index_of(remaining, next_pattern) {
          Some(j) => {
            remaining = string_substring(remaining, j + next_pattern.length())
            i = i + 2
          }
          None => return false
        }
      } else {
        // Trailing *, matches everything remaining
        i = i + 1
      }
    } else {
      // Literal match at start of remaining
      let pattern_lower = pattern[i].to_lower()
      if !remaining.has_prefix(pattern_lower) {
        return false
      }
      remaining = string_substring(remaining, pattern_lower.length())
      i = i + 1
    }
  }
  true
}

///|
/// Find index of substring in string (case-sensitive)
fn string_index_of(s : String, sub : String) -> Int? {
  if sub.length() == 0 {
    return Some(0)
  }
  if s.length() < sub.length() {
    return None
  }
  for i in 0..<(s.length() - sub.length() + 1) {
    let mut matches = true
    for j in 0.. String {
  if start >= s.length() {
    return ""
  }
  let buf = StringBuilder::new()
  for i in start.. Array[Field] {
  let result : Array[Field] = []
  for f in self.fields {
    if f.is_reserved_keyword_name() {
      continue
    }
    if match_pattern_inner(f.name, pattern, true) {
      result.push(f)
    }
  }
  result
}

///|
priv struct GlobTarget {
  field : Field
  parent_map : Map
  level : Int
}

///|
fn Map::glob_targets(self : Map, pattern : Array[String]) -> Array[GlobTarget] {
  let targets : Array[GlobTarget] = []
  if is_double_glob(pattern) {
    self.collect_multi_glob_targets(targets, false, 0)
  } else if is_triple_glob(pattern) {
    self.collect_multi_glob_targets(targets, true, 0)
  } else {
    for field in self.single_glob(pattern) {
      targets.push({ field, parent_map: self, level: 0 })
    }
  }
  targets
}

///|
fn Map::collect_multi_glob_targets(
  self : Map,
  targets : Array[GlobTarget],
  include_boards : Bool,
  level : Int,
) -> Unit {
  let snapshot = self.fields.copy()
  for field in snapshot {
    let child_map = match field.composite {
      Some(Map(map)) => Some(map)
      _ => None
    }
    if field.is_board_keyword_name() {
      if include_boards {
        match child_map {
          Some(boards) =>
            for board in boards.fields {
              match board.composite {
                Some(Map(board_map)) =>
                  board_map.collect_multi_glob_targets(
                    targets, include_boards, 0,
                  )
                _ => ()
              }
            }
          None => ()
        }
      }
      continue
    }
    if field.is_reserved_keyword_name() {
      if field.name == "vars" && !include_boards {
        match child_map {
          Some(map) =>
            map.collect_multi_glob_targets(targets, include_boards, level)
          None => ()
        }
      }
      continue
    }
    targets.push({ field, parent_map: self, level })
    match child_map {
      Some(map) =>
        map.collect_multi_glob_targets(targets, include_boards, level + 1)
      None => ()
    }
  }
}

///|
/// Filter context for evaluating ampersand filters
pub struct FilterContext {
  /// Current field being filtered
  field : Field
  /// Parent map containing the field
  parent_map : Map
}

///|
pub fn FilterContext::new(field : Field, parent_map : Map) -> FilterContext {
  { field, parent_map }
}

///|
/// Evaluate a filter condition against a field
/// Returns true if the field passes the filter
pub fn evaluate_filter(
  ctx : FilterContext,
  filter_key : String,
  filter_value : String,
) -> Bool {
  evaluate_filter_at_level(ctx, filter_key, filter_value, 0)
}

///|
fn evaluate_filter_at_level(
  ctx : FilterContext,
  filter_key : String,
  filter_value : String,
  level : Int,
) -> Bool {
  // Check simple property filters
  match filter_key.to_lower() {
    "level" => filter_value_matches(level.to_string(), filter_value)
    "shape" =>
      // Match against shape property
      match get_field_property(ctx.field, "shape") {
        Some(v) => filter_value_matches(v, filter_value)
        None => filter_value_matches("rectangle", filter_value) // default shape
      }
    "label" =>
      // Match against primary value (label)
      match ctx.field.primary {
        Some(s) => filter_value_matches(s.to_string(), filter_value)
        None =>
          filter_value == "" ||
          filter_value_matches(ctx.field.name, filter_value)
      }
    "class" =>
      match field_property(ctx.field, "class") {
        Some(class_field) =>
          field_contains_filter_value(class_field, filter_value)
        None => false
      }
    "connected" =>
      filter_value_matches(
        if field_is_connected(ctx.field, ctx.parent_map) {
          "true"
        } else {
          "false"
        },
        filter_value,
      )
    "leaf" =>
      filter_value_matches(
        if field_is_leaf(ctx.field) {
          "true"
        } else {
          "false"
        },
        filter_value,
      )
    "opacity" =>
      match get_field_property(ctx.field, "opacity") {
        Some(v) => filter_value_matches(v, filter_value)
        None => filter_value_matches("1", filter_value) // default opacity
      }
    "stroke-width" =>
      match get_field_property(ctx.field, "stroke-width") {
        Some(v) => filter_value_matches(v, filter_value)
        None => filter_value_matches("2", filter_value) // default stroke-width
      }
    "border-radius" | "stroke-dash" =>
      match get_field_property(ctx.field, filter_key) {
        Some(v) => filter_value_matches(v, filter_value)
        None => filter_value_matches("0", filter_value) // default
      }
    "shadow" | "multiple" | "3d" | "animated" | "filled" =>
      match get_field_property(ctx.field, filter_key) {
        Some(v) => filter_value_matches(v, filter_value)
        None => filter_value_matches("false", filter_value) // default
      }
    "icon" | "tooltip" | "link" =>
      match get_field_property(ctx.field, filter_key) {
        Some(v) => filter_value_matches(v, filter_value)
        None => filter_value == "" // absent properties do not satisfy wildcards
      }
    _ =>
      // Try to find the property in the field's map
      match get_field_property(ctx.field, filter_key) {
        Some(v) => filter_value_matches(v, filter_value)
        None => false
      }
  }
}

///|
pub fn filter_value_matches(actual : String, pattern : String) -> Bool {
  let actual = actual.to_lower()
  let pattern = pattern.to_lower()
  let mut actual_index = 0
  let mut pattern_index = 0
  let mut star_index = -1
  let mut star_match_index = 0
  while actual_index < actual.length() {
    if pattern_index < pattern.length() &&
      pattern.code_unit_at(pattern_index) != '*'.to_int().to_uint16() &&
      pattern.code_unit_at(pattern_index) == actual.code_unit_at(actual_index) {
      actual_index = actual_index + 1
      pattern_index = pattern_index + 1
    } else if pattern_index < pattern.length() &&
      pattern.code_unit_at(pattern_index) == '*'.to_int().to_uint16() {
      star_index = pattern_index
      star_match_index = actual_index
      pattern_index = pattern_index + 1
    } else if star_index >= 0 {
      pattern_index = star_index + 1
      star_match_index = star_match_index + 1
      actual_index = star_match_index
    } else {
      return false
    }
  }
  while pattern_index < pattern.length() &&
        pattern.code_unit_at(pattern_index) == '*'.to_int().to_uint16() {
    pattern_index = pattern_index + 1
  }
  pattern_index == pattern.length()
}

///|
fn field_contains_filter_value(field : Field, filter_value : String) -> Bool {
  match field.primary {
    Some(value) =>
      if filter_value_matches(value.to_string(), filter_value) {
        return true
      }
    None => ()
  }
  match field.composite {
    Some(Array(array)) =>
      for value in array.values {
        match value {
          Scalar(scalar) =>
            if filter_value_matches(scalar.to_string(), filter_value) {
              return true
            }
          _ => ()
        }
      }
    _ => ()
  }
  false
}

///|
fn field_is_connected(field : Field, parent_map : Map) -> Bool {
  for edge in parent_map.edges {
    if (
        !edge.id.src_path.is_empty() &&
        edge.id.src_path[0].to_lower() == field.name.to_lower()
      ) ||
      (
        !edge.id.dst_path.is_empty() &&
        edge.id.dst_path[0].to_lower() == field.name.to_lower()
      ) {
      return true
    }
  }
  false
}

///|
fn field_is_leaf(field : Field) -> Bool {
  match field.composite {
    Some(Map(map)) =>
      for child in map.fields {
        if !child.is_reserved_keyword_name() {
          return false
        }
      }
    _ => ()
  }
  true
}

///|
fn field_property(field : Field, key : String) -> Field? {
  match field.composite {
    Some(Map(map)) => field_property_in_map(map, key)
    _ => None
  }
}

///|
fn field_property_in_map(map : Map, key : String) -> Field? {
  let parts = key.split(".").collect()
  let mut current = map
  for i, part in parts {
    guard current.get_field(part.to_owned()) is Some(field) else {
      if parts.length() == 1 {
        guard current.get_field("style") is Some(style_field) else {
          return None
        }
        guard style_field.composite is Some(Map(style_map)) else { return None }
        return style_map.get_field(part.to_owned())
      }
      return None
    }
    if i == parts.length() - 1 {
      return Some(field)
    }
    match field.composite {
      Some(Map(next)) => current = next
      _ => return None
    }
  }
  None
}

///|
/// Get a scalar property value from a field's composite map.
fn get_field_property(field : Field, key : String) -> String? {
  match field_property(field, key) {
    Some(property) =>
      match property.primary {
        Some(value) => Some(value.to_string())
        None => None
      }
    None => None
  }
}

///|
/// Apply properties to fields matching a glob pattern
/// If filters are provided, only apply to fields that pass all filters
pub fn Map::apply_glob_properties(
  self : Map,
  pattern : Array[String],
  properties : Map,
  filters : Array[(Bool, String, String)],
) -> Unit {
  if is_double_glob(pattern) {
    self.apply_multi_glob_properties(properties, filters, false)
    return
  }
  if is_triple_glob(pattern) {
    self.apply_multi_glob_properties(properties, filters, true)
    return
  }

  // Single-level glob: apply only within this map
  let fields = self.single_glob(pattern)
  for f in fields {
    let ctx = FilterContext::new(f, self)
    let mut passes_all = true
    for filter in filters {
      let (negated, key, value) = filter
      let result = evaluate_filter(ctx, key, value)
      let passes = if negated { !result } else { result }
      if !passes {
        passes_all = false
        break
      }
    }
    if passes_all {
      let updated = apply_properties_to_field(f, properties)
      self.set_field(updated)
    }
  }
}

///|
/// Apply properties recursively for **/*** patterns.
/// `include_boards` matches the *** behavior: descend into boards but do not
/// apply to reserved keywords, and do not apply to the board field itself.
fn Map::apply_multi_glob_properties(
  self : Map,
  properties : Map,
  filters : Array[(Bool, String, String)],
  include_boards : Bool,
) -> Unit {
  // Snapshot fields so newly added properties (like `style`) aren't re-visited.
  let snapshot : Array[Field] = []
  for f in self.fields {
    snapshot.push(f)
  }
  for f in snapshot {
    // Capture original children before any mutation.
    let child_map = match f.composite {
      Some(Map(m)) => Some(m)
      _ => None
    }
    if include_boards {
      // Triple glob (***): descend into boards, but don't apply to reserved keywords.
      if f.is_board_keyword_name() {
        match child_map {
          Some(boards) =>
            for board in boards.fields {
              match board.composite {
                Some(Map(board_map)) =>
                  board_map.apply_multi_glob_properties(
                    properties, filters, include_boards,
                  )
                _ => ()
              }
            }
          None => ()
        }
        continue
      }
      if f.is_reserved_keyword_name() {
        continue
      }
      // Double glob (**): don't apply to reserved keywords (including `style`).
    } else if f.is_reserved_keyword_name() {
      match child_map {
        Some(m) =>
          m.apply_multi_glob_properties(properties, filters, include_boards)
        None => ()
      }
      continue
    }

    // Apply properties to this field if it passes filters.
    let ctx = FilterContext::new(f, self)
    let mut passes_all = true
    for filter in filters {
      let (negated, key, value) = filter
      let result = evaluate_filter(ctx, key, value)
      let passes = if negated { !result } else { result }
      if !passes {
        passes_all = false
        break
      }
    }
    if passes_all {
      let updated = apply_properties_to_field(f, properties)
      self.set_field(updated)
    }

    // Descend into original children (not newly added properties).
    match child_map {
      Some(m) =>
        m.apply_multi_glob_properties(properties, filters, include_boards)
      None => ()
    }
  }
}

///|
fn clone_value_for_properties(v : Value) -> Value {
  match v {
    Scalar(s) => Scalar(s)
    Array(a) => Array(clone_irarray_for_properties(a))
    Map(m) => Map(clone_map_for_properties(m))
  }
}

///|
fn clone_irarray_for_properties(a : IRArray) -> IRArray {
  let values : Array[Value] = []
  for v in a.values {
    values.push(clone_value_for_properties(v))
  }
  { values, ast: a.ast, source_path: a.source_path, path: a.path }
}

///|
fn clone_composite_for_properties(c : Composite) -> Composite {
  match c {
    Array(a) => Array(clone_irarray_for_properties(a))
    Map(m) => Map(clone_map_for_properties(m))
  }
}

///|
fn clone_field_for_properties(f : Field) -> Field {
  let composite = match f.composite {
    Some(c) => Some(clone_composite_for_properties(c))
    None => None
  }
  {
    name: f.name,
    name_syntax: f.name_syntax(),
    primary: f.primary,
    composite,
    references: f.references.copy(),
    import_ast: f.import_ast,
    path: f.path,
  }
}

///|
fn clone_map_for_properties(m : Map) -> Map {
  let out : Map = {
    fields: [],
    edges: [],
    ast: m.ast,
    import_ast: m.import_ast,
    source_path: m.source_path,
    path: m.path,
  }
  for f in m.fields {
    out.add_field(clone_field_for_properties(f))
  }
  // Edges are not expected in property maps.
  out
}

///|
fn merge_properties_into_map(target : Map, src : Map) -> Unit {
  for prop in src.fields {
    let existing_field = match prop.name_syntax() {
      Some(name_syntax) => target.get_field_by_syntax(name_syntax)
      None => target.get_field(prop.name)
    }
    match existing_field {
      Some(existing) => {
        let prop_follows = glob_property_follows_existing(existing, prop)
        let primary = if prop.primary is Some(_) && prop_follows {
          prop.primary
        } else if prop.composite is Some(Array(_)) && prop_follows {
          None
        } else {
          existing.primary
        }
        let composite = match prop.composite {
          Some(Map(src_map)) =>
            match existing.composite {
              Some(Map(dst_map)) => {
                merge_properties_into_map(dst_map, src_map)
                existing.composite
              }
              _ => Some(Map(clone_map_for_properties(src_map)))
            }
          Some(other) =>
            if prop_follows {
              Some(clone_composite_for_properties(other))
            } else {
              existing.composite
            }
          None =>
            if prop.primary is Some(_) &&
              prop_follows &&
              existing.composite is Some(Array(_)) {
              None
            } else {
              existing.composite
            }
        }
        let updated = Field::new(
          existing.name,
          primary,
          composite,
          {
            let refs = existing.references.copy()
            for reference in prop.references {
              refs.push(reference)
            }
            refs
          },
          name_syntax=existing.name_syntax(),
        )
        target.set_field(updated)
      }
      None => target.add_field(clone_field_for_properties(prop))
    }
  }
}

///|
fn glob_property_follows_existing(existing : Field, prop : Field) -> Bool {
  guard prop.last_primary_ref() is Some(prop_ref) else { return true }
  guard existing.last_primary_ref() is Some(existing_ref) else { return true }
  let prop_context = prop_ref.context()
  let existing_context = existing_ref.context()
  match (prop_context, existing_context) {
    (Some(prop_context), Some(existing_context)) =>
      if prop_context.source_path != existing_context.source_path {
        return true
      }
    _ => ()
  }
  reference_operation_range(prop_ref).start.offset >=
  reference_operation_range(existing_ref).start.offset
}

///|
fn reference_operation_range(reference : Reference) -> @lexer.Range {
  if reference.due_to_glob() || reference.due_to_lazy_glob() {
    match reference.context() {
      Some(context) =>
        match context.key {
          Some(key) => return key.range
          None => ()
        }
      None => ()
    }
  }
  reference.range()
}

///|
/// Apply properties from a source map to a field
fn apply_properties_to_field(field : Field, properties : Map) -> Field {
  // Ensure field has a composite map
  let (field, field_map) = match field.composite {
    Some(Map(m)) => (field, m)
    None => {
      let m = Map::new()
      let field = Field::new(
        field.name,
        field.primary,
        Some(Map(m)),
        field.references,
        name_syntax=field.name_syntax(),
      )
      (field, m)
    }
    Some(_) => return field // Can't apply to arrays
  }
  // Copy properties
  merge_properties_into_map(field_map, properties)
  field
}