///|
/// Shared runtime helpers used by generated Vapor Moon code.
/// Component contracts themselves are generated as plain typed arguments.

///|
/// Declarative hydration strategies encoded into SSR and DOM markers.
pub(all) enum HydrationMode {
  Manual
  Load
  Idle
  Visible
  Media(String)
  Only
  ServerDefer
} derive(Eq, Show)

///|
/// Typed handle for a DOM node addressed through `ref` / `use_template_ref`.
pub struct TemplateRef[T] {
  name : String
  phantom : T?
}

///|
/// Map a component name to the client module path expected by the island runtime.
pub fn client_module_path(component_name : String) -> String {
  "/_vapor_moon/" + sanitize_component_name(component_name) + ".js"
}

///|
/// Map a component name to the Web Components island host tag.
pub fn client_element_name(component_name : String) -> String {
  "vm-" + sanitize_component_name(component_name).to_lower()
}

///|
/// Build the deterministic `data-vm-scope` suffix for one component.
pub fn make_scope_id(component_name : String) -> String {
  "vm-" + hash_string(component_name)
}

///|
/// Create a typed template-ref token that can be resolved later in the DOM runtime.
pub fn[T] use_template_ref(name : String) -> TemplateRef[T] {
  { name, phantom: None }
}

///|
/// Generate stable element ids for compiler-emitted `use_id` calls.
pub fn use_id(component_name : String, index : Int) -> String {
  sanitize_component_name(component_name) + "-id-" + index.to_string()
}

///|
/// Expose the raw template-ref lookup key.
pub fn[T] TemplateRef::name(self : TemplateRef[T]) -> String {
  self.name
}

///|
/// Serialize island props into a compact JSON object without pulling in a JSON library.
pub fn props_to_json(entries : Array[(String, String)]) -> String {
  if entries.is_empty() {
    return "{}"
  }
  let parts : Array[String] = []
  for entry in entries {
    parts.push(escape_json_string(entry.0) + ":" + escape_json_value(entry.1))
  }
  "{" + parts.join(",") + "}"
}

///|
/// Scope raw CSS text by prefixing selectors with the component scope attribute.
pub fn scope_css(css : String, scope_id : String) -> String {
  scope_css_rules(css, scope_id)
}

///|
/// Merge conditional visibility into an inline style string.
pub fn show_style(visible : Bool, base : String) -> String {
  let base_style = normalize_style(base)
  if visible {
    return base_style
  }
  if base_style.length() == 0 {
    "display: none;"
  } else if base_style.has_suffix(";") {
    base_style + " display: none;"
  } else {
    base_style + "; display: none;"
  }
}

///|
/// Join class fragments while trimming empty segments.
pub fn merge_class_names(parts : Array[String]) -> String {
  let merged : Array[String] = []
  for part in parts {
    let trimmed = part.trim().to_string()
    if trimmed.length() > 0 {
      merged.push(trimmed)
    }
  }
  merged.join(" ")
}

///|
/// Join inline style fragments into normalized semicolon-terminated CSS.
pub fn merge_styles(parts : Array[String]) -> String {
  let merged : Array[String] = []
  for part in parts {
    let trimmed = normalize_style(part)
    if trimmed.length() == 0 {
      continue
    }
    if trimmed.has_suffix(";") {
      merged.push(trimmed)
    } else {
      merged.push(trimmed + ";")
    }
  }
  merged.join(" ")
}

///|
/// Quote one JSON string literal with the escaping required by generated props payloads.
fn escape_json_string(source : String) -> String {
  let sb = StringBuilder::new()
  sb.write_char('"')
  for ch in source {
    match ch {
      '"' => sb.write_string("\\\"")
      '\\' => sb.write_string("\\\\")
      '\n' => sb.write_string("\\n")
      '\r' => sb.write_string("\\r")
      '\t' => sb.write_string("\\t")
      _ => sb.write_char(ch)
    }
  }
  sb.write_char('"')
  sb.to_string()
}

///|
/// Preserve JSON literals such as booleans/numbers, otherwise quote the value.
fn escape_json_value(source : String) -> String {
  if source == "true" || source == "false" || source == "null" {
    return source
  }
  let all_digits = source.length() > 0 &&
    source
    .to_array()
    .all(fn(ch) { (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' })
  if all_digits {
    source
  } else {
    escape_json_string(source)
  }
}

///|
/// Shared string hash used for deterministic runtime ids.
fn hash_string(source : String) -> String {
  let mut hash : UInt = 5381
  for ch in source {
    hash = (hash << 5) + hash + ch.to_uint()
  }
  hash.to_string()
}

///|
/// Normalize component names for use in filenames, ids, and DOM attributes.
fn sanitize_component_name(component_name : String) -> String {
  let sb = StringBuilder::new()
  for ch in component_name {
    if (ch >= 'a' && ch <= 'z') ||
      (ch >= 'A' && ch <= 'Z') ||
      (ch >= '0' && ch <= '9') ||
      ch == '_' ||
      ch == '-' {
      sb.write_char(ch)
    } else {
      sb.write_char('_')
    }
  }
  sb.to_string() + "-" + hash_string(component_name)
}

///|
/// Trim an inline style fragment without changing its CSS semantics.
fn normalize_style(source : String) -> String {
  source.trim().to_string()
}

///|
/// CSS scoping engine used by the shared runtime and generated helpers.
fn scope_css_rules(css : String, scope_id : String) -> String {
  let scoped_chunks : Array[String] = []
  let mut index = 0
  while index < css.length() {
    index = skip_css_ignored(css, index)
    if index >= css.length() {
      break
    }
    if css[index] == '/' && index + 1 < css.length() && css[index + 1] == '*' {
      let next_index = skip_css_comment(css, index)
      scoped_chunks.push(
        String::unsafe_substring(css, start=index, end=next_index)
        .trim()
        .to_string(),
      )
      index = next_index
      continue
    }
    let header_start = index
    let mut in_string : UInt16? = None
    let mut escaped = false
    while index < css.length() {
      let ch = css[index]
      match in_string {
        Some(quote) => {
          if escaped {
            escaped = false
          } else if ch == '\\' {
            escaped = true
          } else if ch == quote {
            in_string = None
          }
          index += 1
        }
        None =>
          if ch == '"' || ch == '\'' {
            in_string = Some(ch)
            index += 1
          } else if ch == '/' &&
            index + 1 < css.length() &&
            css[index + 1] == '*' {
            index = skip_css_comment(css, index)
          } else if ch == '{' || ch == ';' {
            break
          } else {
            index += 1
          }
      }
    }
    let header = String::unsafe_substring(css, start=header_start, end=index)
      .trim()
      .to_string()
    if index >= css.length() {
      if header.length() > 0 {
        scoped_chunks.push(header)
      }
      break
    }
    let delimiter = css[index]
    if delimiter == ';' {
      if header.length() > 0 {
        scoped_chunks.push(header + ";")
      }
      index += 1
      continue
    }
    let (body, next_index) = read_css_block(css, index)
    index = next_index
    if header.length() == 0 {
      continue
    }
    if header.has_prefix("@") {
      let nested = if scopes_nested_css_rules(header) {
        scope_css_rules(body, scope_id)
      } else {
        body.trim().to_string()
      }
      scoped_chunks.push(wrap_css_block(header, nested))
    } else {
      let selectors = split_css_selector_list(header)
      let scoped_selectors : Array[String] = []
      for selector in selectors {
        let trimmed = selector.trim().to_string()
        if trimmed.length() > 0 {
          let rewritten = rewrite_scoped_selector(trimmed)
          if rewritten.unscoped {
            scoped_selectors.push(rewritten.selector)
          } else {
            scoped_selectors.push(
              "[data-vm-scope=\"" + scope_id + "\"] " + rewritten.selector,
            )
          }
        }
      }
      if not(scoped_selectors.is_empty()) {
        scoped_chunks.push(
          wrap_css_block(scoped_selectors.join(", "), body.trim().to_string()),
        )
      }
    }
  }
  scoped_chunks.join("\n")
}

///|
/// Read the body of a CSS block, preserving nested braces and strings.
fn read_css_block(source : String, open_brace_index : Int) -> (String, Int) {
  let mut index = open_brace_index + 1
  let mut depth = 1
  let mut in_string : UInt16? = None
  let mut escaped = false
  while index < source.length() {
    let ch = source[index]
    match in_string {
      Some(quote) => {
        if escaped {
          escaped = false
        } else if ch == '\\' {
          escaped = true
        } else if ch == quote {
          in_string = None
        }
        index += 1
      }
      None =>
        if ch == '"' || ch == '\'' {
          in_string = Some(ch)
          index += 1
        } else if ch == '/' &&
          index + 1 < source.length() &&
          source[index + 1] == '*' {
          index = skip_css_comment(source, index)
        } else if ch == '{' {
          depth += 1
          index += 1
        } else if ch == '}' {
          depth -= 1
          if depth == 0 {
            return (
              String::unsafe_substring(
                source,
                start=open_brace_index + 1,
                end=index,
              ),
              index + 1,
            )
          }
          index += 1
        } else {
          index += 1
        }
    }
  }
  (
    String::unsafe_substring(
      source,
      start=open_brace_index + 1,
      end=source.length(),
    ),
    source.length(),
  )
}

///|
/// Split a selector list on top-level commas only.
fn split_css_selector_list(source : String) -> Array[String] {
  let selectors : Array[String] = []
  let mut start = 0
  let mut index = 0
  let mut in_string : UInt16? = None
  let mut escaped = false
  let mut paren_depth = 0
  let mut bracket_depth = 0
  while index < source.length() {
    let ch = source[index]
    match in_string {
      Some(quote) => {
        if escaped {
          escaped = false
        } else if ch == '\\' {
          escaped = true
        } else if ch == quote {
          in_string = None
        }
        index += 1
      }
      None =>
        if ch == '"' || ch == '\'' {
          in_string = Some(ch)
          index += 1
        } else if ch == '/' &&
          index + 1 < source.length() &&
          source[index + 1] == '*' {
          index = skip_css_comment(source, index)
        } else if ch == '(' {
          paren_depth += 1
          index += 1
        } else if ch == ')' {
          if paren_depth > 0 {
            paren_depth -= 1
          }
          index += 1
        } else if ch == '[' {
          bracket_depth += 1
          index += 1
        } else if ch == ']' {
          if bracket_depth > 0 {
            bracket_depth -= 1
          }
          index += 1
        } else if ch == ',' && paren_depth == 0 && bracket_depth == 0 {
          selectors.push(String::unsafe_substring(source, start~, end=index))
          start = index + 1
          index += 1
        } else {
          index += 1
        }
    }
  }
  selectors.push(String::unsafe_substring(source, start~, end=source.length()))
  selectors
}

///|
priv struct ScopedSelector {
  selector : String
  unscoped : Bool
}

///|
fn rewrite_scoped_selector(selector : String) -> ScopedSelector {
  let normalized = rewrite_scoped_pseudo_functions(selector).trim().to_owned()
  if selector_is_pure_pseudo(selector, ":global") {
    { selector: normalized, unscoped: true }
  } else {
    { selector: normalized, unscoped: false }
  }
}

///|
fn rewrite_scoped_pseudo_functions(selector : String) -> String {
  let sb = StringBuilder::new()
  let mut index = 0
  while index < selector.length() {
    match scoped_pseudo_name_at(selector, index) {
      Some(name) => {
        let open_index = index + name.length()
        match matching_close_paren(selector, open_index) {
          Some(close_index) => {
            let inner = String::unsafe_substring(
              selector,
              start=open_index + 1,
              end=close_index,
            )
            sb.write_string(inner.trim().to_owned())
            index = close_index + 1
          }
          None => {
            sb.write_string(
              String::unsafe_substring(selector, start=index, end=index + 1),
            )
            index += 1
          }
        }
      }
      None => {
        sb.write_string(
          String::unsafe_substring(selector, start=index, end=index + 1),
        )
        index += 1
      }
    }
  }
  normalize_selector_spacing(sb.to_string())
}

///|
fn scoped_pseudo_name_at(selector : String, index : Int) -> String? {
  if selector_has_prefix_at(selector, ":deep(", index) {
    Some(":deep")
  } else if selector_has_prefix_at(selector, ":global(", index) {
    Some(":global")
  } else if selector_has_prefix_at(selector, ":slotted(", index) {
    Some(":slotted")
  } else {
    None
  }
}

///|
fn selector_is_pure_pseudo(selector : String, pseudo : String) -> Bool {
  let trimmed = selector.trim().to_owned()
  if !trimmed.has_prefix(pseudo + "(") {
    return false
  }
  match matching_close_paren(trimmed, pseudo.length()) {
    Some(close_index) => close_index == trimmed.length() - 1
    None => false
  }
}

///|
fn selector_has_prefix_at(
  selector : String,
  prefix : String,
  index : Int,
) -> Bool {
  if index + prefix.length() > selector.length() {
    return false
  }
  String::unsafe_substring(selector, start=index, end=index + prefix.length()) ==
  prefix
}

///|
fn matching_close_paren(selector : String, open_index : Int) -> Int? {
  if open_index >= selector.length() || selector[open_index] != '(' {
    return None
  }
  let mut index = open_index + 1
  let mut depth = 1
  let mut in_string : UInt16? = None
  let mut escaped = false
  while index < selector.length() {
    let ch = selector[index]
    match in_string {
      Some(quote) =>
        if escaped {
          escaped = false
        } else if ch == '\\' {
          escaped = true
        } else if ch == quote {
          in_string = None
        }
      None =>
        if ch == '"' || ch == '\'' {
          in_string = Some(ch)
        } else if ch == '(' {
          depth += 1
        } else if ch == ')' {
          depth -= 1
          if depth == 0 {
            return Some(index)
          }
        }
    }
    index += 1
  }
  None
}

///|
fn normalize_selector_spacing(selector : String) -> String {
  selector
  .replace_all(old="  ", new=" ")
  .replace_all(old=" >", new=">")
  .replace_all(old="> ", new=">")
  .trim()
  .to_owned()
}

///|
/// Skip ASCII whitespace between CSS tokens.
fn skip_css_ignored(source : String, start : Int) -> Int {
  let mut index = start
  while index < source.length() {
    let ch = source[index]
    if ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' {
      index += 1
      continue
    }
    break
  }
  index
}

///|
/// Skip a CSS block comment and return the first index after it.
fn skip_css_comment(source : String, start : Int) -> Int {
  let mut index = start + 2
  while index + 1 < source.length() {
    if source[index] == '*' && source[index + 1] == '/' {
      return index + 2
    }
    index += 1
  }
  source.length()
}

///|
/// Return whether an at-rule contains nested selectors that still need scoping.
fn scopes_nested_css_rules(header : String) -> Bool {
  not(
    header.has_prefix("@keyframes") ||
    header.has_prefix("@-webkit-keyframes") ||
    header.has_prefix("@font-face") ||
    header.has_prefix("@page") ||
    header.has_prefix("@property"),
  )
}

///|
/// Reassemble a CSS rule header and body after scoping.
fn wrap_css_block(header : String, body : String) -> String {
  header + " { " + body.trim().to_string() + " }"
}