// /// turns `src/a/b.rs` into `a_b`, (used inside macro)
// pub fn css_name_from_path(p: &str) -> String {
//   let mut s = p.to_owned();
//   if let Some(x) = s.strip_prefix("src/") {
//     s = x.to_string();
//   }
//   if let Some(x) = s.strip_suffix(".rs") {
//     s = x.to_string();
//   }
//   s.replace("::", "_").replace(['/', '.'], "_")
// }

///|
let class_name_in_tags : @hashset.HashSet[String] = @hashset.HashSet([])

///|
/// Generate a CSS class name from a SourceLoc.
/// Preferred format (JSON): `{"pkg":"tiye/respo/main","filename":"task.mbt","start_line":118,...}`
/// -> `tiye_respo_main_task_118`
/// Fallback format (string): `file.mbt:line:col-line:col@namespace/module`
/// -> `tiye_respo_ui_226_34-238_3`
fn gen_class_name_from_loc(loc : SourceLoc) -> String {
  // Try to parse JSON format first
  let json_str = loc.to_json_string()
  let json = try? @json.parse(json_str[:])
  match json {
    Ok(json) => {
      let pkg = if json is Object(obj) { obj.get("pkg") } else { None }
      let filename = if json is Object(obj) {
        obj.get("filename")
      } else {
        None
      }
      let start_line = if json is Object(obj) {
        obj.get("start_line")
      } else {
        None
      }
      match (pkg, filename, start_line) {
        (Some(String(pkg)), Some(String(filename)), Some(Number(start_line, ..))
        ) => {
          let pkg_clean = pkg.replace_all(old="/", new="_")
          let file_clean = filename.replace_all(old=".mbt", new="")
          let line = start_line.to_int().to_string()
          pkg_clean + "_" + file_clean + "_" + line
        }
        _ => gen_class_name_from_loc_fallback(loc)
      }
    }
    Err(_) => gen_class_name_from_loc_fallback(loc)
  }
}

///|
/// Fallback for old SourceLoc format without JSON support.
/// The SourceLoc format is: `file.mbt:line:col-line:col@namespace/module`
/// e.g., `ui.mbt:226:34-238:3@tiye/respo` -> `tiye_respo_ui_226_34-238_3`
fn gen_class_name_from_loc_fallback(loc : SourceLoc) -> String {
  let loc_str = loc.to_string()
  let parts : Array[_] = loc_str.split("@").collect()
  // clean file part: extract after /src/ and replace special chars
  fn clean_file_part(s : StringView) -> String {
    s
    .split("/src/")
    .last()
    .unwrap()
    .replace_all(old="/", new="_")
    .replace_all(old=".mbt", new="_")
    .replace_all(old=":", new="_")
    .replace_all(old=".", new="_")
    .to_owned()
  }

  if parts.length() >= 2 {
    let file_part = parts[0] // e.g., ui.mbt:226:34-238:3
    let ns_part = parts[1] // e.g., tiye/respo
    let ns_clean = ns_part
      .replace_all(old="/", new="_")
      .replace_all(old=".", new="_")
    (ns_clean + "_" + clean_file_part(file_part)).to_owned()
  } else {
    // fallback for old format without namespace
    clean_file_part(loc_str[:])
  }
}

///|
/// use `static_style` instead
#callsite(autofill(loc))
#deprecated
pub fn[U : Show] declare_static_style(
  rules : Array[(U, RespoStyle)],
  loc~ : SourceLoc,
) -> String {
  static_style(rules, loc~)
}

///|
/// Declare a static style in the head of the documentm for example
/// ```moonbit nocheck
/// let _style_demo : String = static_style([
///   ("&", @css.respo_style(margin=4 |> Px, background_color=Hsl(200, 90, 96))),
/// ])
/// ```
#callsite(autofill(loc))
pub fn[U : Show] static_style(
  rules : Array[(U, RespoStyle)],
  loc~ : SourceLoc,
) -> String {
  // @dom_ffi.warn_log("SourceLoc:" + loc.to_json_string())
  let gen_name = gen_class_name_from_loc(loc)
  if class_name_in_tags.contains(gen_name) {
    gen_name
  } else {
    let window = @dom_ffi.window()
    let document = window.document()
    let head = document.head()
    let style_tag = document.create_element("style")
    style_tag.set_attribute("id", gen_name)
    style_tag.set_attribute("loc", loc.to_string())
    let mut styles = ""
    for pair in rules {
      let (query, properties) = pair
      styles = styles +
        query
        .to_string()
        .replace(old="$0", new="." + gen_name)
        .replace(old="&", new="." + gen_name)
      styles = styles + " {\n"
      styles = styles + properties.to_string()
      styles = styles + "}\n"
    }
    style_tag.set_inner_html(styles)
    head.reinterpret_as_node().append_child(style_tag.reinterpret_as_node())
    class_name_in_tags.add(gen_name)
    gen_name
  }
}

///|
/// use `contained_static_style` instead
#callsite(autofill(loc))
#deprecated
pub fn[U : Show] declare_contained_style(
  rules : Array[(String?, U, RespoStyle)],
  loc~ : SourceLoc,
) -> String {
  contained_static_style(rules, loc~)
}

///|
/// Declare a static style in the head of the documentm for example
/// ```moonbit nocheck
/// let _style_demo : String = contained_static_style([
///   (
///     Some("@media only screen and (max-width: 600px)"),
///     "&",
///     @css.respo_style(margin=4 |> Px, background_color=Hsl(200, 90, 96)),
///   ),
/// ])
/// ```
#callsite(autofill(loc))
pub fn[U : Show] contained_static_style(
  rules : Array[(String?, U, RespoStyle)],
  loc~ : SourceLoc,
) -> String {
  let gen_name = gen_class_name_from_loc(loc)
  if class_name_in_tags.contains(gen_name) {
    gen_name
  } else {
    let window = @dom_ffi.window()
    let document = window.document()
    let head = document.head()
    let style_tag = document.create_element("style")
    style_tag.set_attribute("id", gen_name)
    style_tag.set_attribute("loc", loc.to_string())
    let mut styles = ""
    for pair in rules {
      let (container, query, properties) = pair
      let mut rule_style = query
        .to_string()
        .replace(old="$0", new="." + gen_name)
        .replace(old="&", new="." + gen_name)
      rule_style += " {\n"
      rule_style += properties.to_string()
      rule_style += "}\n"
      if container is Some(container) {
        styles += "\n" + container + " {\n" + rule_style + "}\n"
      } else {
        styles += rule_style
      }
    }
    style_tag.set_inner_html(styles)
    head.reinterpret_as_node().append_child(style_tag.reinterpret_as_node())
    class_name_in_tags.add(gen_name)
    gen_name
  }
}