///|
#cfg(target="js")
#external
priv type JsValue

///|
#cfg(target="js")
fn[T] to_js_value(value : T) -> JsValue = "%identity"

///|
#cfg(target="js")
extern "js" fn js_object_new() -> JsValue = "() => ({})"

///|
#cfg(target="js")
extern "js" fn js_object_set(
  object : JsValue,
  key : String,
  value : JsValue,
) -> Unit = "(object, key, value) => { object[key] = value; }"

///|
#cfg(target="js")
extern "js" fn js_undefined() -> JsValue = "() => undefined"

///|
#cfg(target="js")
extern "js" fn js_lowercase_true_map(values : JsValue) -> JsValue =
  #| (values) => {
  #|   const object = {};
  #|   for (const value of values || []) {
  #|     if (value != null) object[String(value).toLowerCase()] = true;
  #|   }
  #|   return object;
  #| }

///|
#cfg(target="js")
extern "js" fn js_merge_add_tags(values : JsValue, check : JsValue) -> JsValue =
  #| (values, check) => {
  #|   const hasValues = Array.isArray(values);
  #|   const hasCheck = typeof check === 'function';
  #|   if (hasValues && hasCheck) {
  #|     const set = new Set(
  #|       values
  #|         .filter((value) => value != null)
  #|         .map((value) => String(value).toLowerCase()),
  #|     );
  #|     return (tagName) => set.has(String(tagName).toLowerCase()) || !!check(tagName);
  #|   }
  #|   if (hasCheck) return check;
  #|   return values;
  #| }

///|
#cfg(target="js")
extern "js" fn js_merge_add_attr(values : JsValue, check : JsValue) -> JsValue =
  #| (values, check) => {
  #|   const hasValues = Array.isArray(values);
  #|   const hasCheck = typeof check === 'function';
  #|   if (hasValues && hasCheck) {
  #|     const set = new Set(
  #|       values
  #|         .filter((value) => value != null)
  #|         .map((value) => String(value).toLowerCase()),
  #|     );
  #|     return (attributeName, tagName) =>
  #|       set.has(String(attributeName).toLowerCase()) || !!check(attributeName, tagName);
  #|   }
  #|   if (hasCheck) return check;
  #|   return values;
  #| }

///|
#cfg(target="js")
fn[T] has_value(value : T?) -> Bool {
  match value {
    Some(_) => true
    None => false
  }
}

///|
#cfg(target="js")
fn[T] optional_to_js_value(value : T?) -> JsValue {
  match value {
    Some(value) => to_js_value(value)
    None => js_undefined()
  }
}

///|
#cfg(target="js")
fn[T] set_optional_value(object : JsValue, key : String, value : T?) -> Unit {
  match value {
    Some(value) => js_object_set(object, key, to_js_value(value))
    None => ()
  }
}

///|
#cfg(target="js")
fn set_optional_lowercase_true_map(
  object : JsValue,
  key : String,
  value : Array[String]?,
) -> Unit {
  match value {
    Some(values) =>
      js_object_set(object, key, js_lowercase_true_map(to_js_value(values)))
    None => ()
  }
}

///|
#cfg(target="js")
fn ensure_dompurify_compatible(config : Config) -> Unit {
  match config.allowed_uri_check {
    Some(_) =>
      abort(
        "Config.allowed_uri_check is not supported when delegating to DOMPurify directly",
      )
    None => ()
  }
}

///|
#cfg(target="js")
fn config_to_js_value(config : Config) -> JsValue {
  ensure_dompurify_compatible(config)

  let object = js_object_new()

  js_object_set(object, "ALLOW_ARIA_ATTR", to_js_value(config.allow_aria_attr))
  js_object_set(object, "ALLOW_DATA_ATTR", to_js_value(config.allow_data_attr))
  js_object_set(
    object,
    "ALLOW_UNKNOWN_PROTOCOLS",
    to_js_value(config.allow_unknown_protocols),
  )
  js_object_set(
    object,
    "ALLOW_SELF_CLOSE_IN_ATTR",
    to_js_value(config.allow_self_close_in_attr),
  )
  js_object_set(object, "FORCE_BODY", to_js_value(config.force_body))
  js_object_set(object, "KEEP_CONTENT", to_js_value(config.keep_content))
  js_object_set(object, "NAMESPACE", to_js_value(config.namespace_uri))
  js_object_set(
    object,
    "PARSER_MEDIA_TYPE",
    to_js_value(config.parser_media_type),
  )
  js_object_set(
    object,
    "SAFE_FOR_TEMPLATES",
    to_js_value(config.safe_for_templates),
  )
  js_object_set(object, "SAFE_FOR_XML", to_js_value(config.safe_for_xml))
  js_object_set(object, "SANITIZE_DOM", to_js_value(config.sanitize_dom))
  js_object_set(
    object,
    "SANITIZE_NAMED_PROPS",
    to_js_value(config.sanitize_named_props),
  )
  js_object_set(object, "WHOLE_DOCUMENT", to_js_value(config.whole_document))

  set_optional_value(object, "ALLOWED_TAGS", config.allowed_tags)
  set_optional_value(object, "ALLOWED_ATTR", config.allowed_attr)
  set_optional_value(object, "ALLOWED_NAMESPACES", config.allowed_namespaces)
  set_optional_value(object, "ADD_DATA_URI_TAGS", config.add_data_uri_tags)
  set_optional_value(object, "ADD_URI_SAFE_ATTR", config.add_uri_safe_attr)
  set_optional_value(object, "FORBID_TAGS", config.forbid_tags)
  set_optional_value(object, "FORBID_ATTR", config.forbid_attr)
  set_optional_value(object, "FORBID_CONTENTS", config.forbid_contents)
  set_optional_value(object, "ADD_FORBID_CONTENTS", config.add_forbid_contents)
  set_optional_lowercase_true_map(
    object,
    "HTML_INTEGRATION_POINTS",
    config.html_integration_points,
  )
  set_optional_lowercase_true_map(
    object,
    "MATHML_TEXT_INTEGRATION_POINTS",
    config.mathml_text_integration_points,
  )

  if has_value(config.add_tags) || has_value(config.add_tag_check) {
    js_object_set(
      object,
      "ADD_TAGS",
      js_merge_add_tags(
        optional_to_js_value(config.add_tags),
        optional_to_js_value(config.add_tag_check),
      ),
    )
  }

  if has_value(config.add_attr) || has_value(config.add_attr_check) {
    js_object_set(
      object,
      "ADD_ATTR",
      js_merge_add_attr(
        optional_to_js_value(config.add_attr),
        optional_to_js_value(config.add_attr_check),
      ),
    )
  }

  match config.use_profiles {
    Some(use_profiles) => {
      let nested = js_object_new()
      js_object_set(nested, "html", to_js_value(use_profiles.html))
      js_object_set(nested, "svg", to_js_value(use_profiles.svg))
      js_object_set(nested, "svgFilters", to_js_value(use_profiles.svg_filters))
      js_object_set(nested, "mathMl", to_js_value(use_profiles.math_ml))
      js_object_set(object, "USE_PROFILES", nested)
    }
    None => ()
  }

  match config.custom_element_handling {
    Some(custom) => {
      let nested = js_object_new()
      set_optional_value(nested, "tagNameCheck", custom.tag_name_check)
      set_optional_value(
        nested,
        "attributeNameCheck",
        custom.attribute_name_check,
      )
      js_object_set(
        nested,
        "allowCustomizedBuiltInElements",
        to_js_value(custom.allow_customized_built_in_elements),
      )
      js_object_set(object, "CUSTOM_ELEMENT_HANDLING", nested)
    }
    None => ()
  }

  object
}

///|
#cfg(target="js")
extern "js" fn sanitize_ffi(input : String, config : JsValue) -> String =
  #| (function () {
  #|   let dompurify = null;
  #| 
  #|   function getHostWindow() {
  #|     if (typeof window !== 'undefined' && window.document) {
  #|       return window;
  #|     }
  #|     const { JSDOM } = require('jsdom');
  #|     return new JSDOM('').window;
  #|   }
  #| 
  #|   function getDOMPurify() {
  #|     if (dompurify && typeof dompurify.sanitize === 'function') {
  #|       return dompurify;
  #|     }
  #|     const root = getHostWindow();
  #|     const imported =
  #|       typeof require === 'function' ? require('dompurify') : root.DOMPurify;
  #|     dompurify =
  #|       typeof imported === 'function' && typeof imported.sanitize !== 'function'
  #|         ? imported(root)
  #|         : imported;
  #|     if (!dompurify || typeof dompurify.sanitize !== 'function') {
  #|       throw new Error(
  #|         'Failed to initialize DOMPurify with an available DOM host window.',
  #|       );
  #|     }
  #|     return dompurify;
  #|   }
  #| 
  #|   return (input, config) => getDOMPurify().sanitize(input, config);
  #| })()

///|
#cfg(target="js")
fn sanitize_js(input : String, config : Config) -> String {
  sanitize_ffi(input, config_to_js_value(config))
}

///|
#cfg(not(target="js"))
fn sanitize_js(_input : String, _config : Config) -> String {
  abort("dom_sanitizer only supports the JS target")
}