///|
extern "js" fn load_shiki() -> @js.Promise =
  #| (async () => {
  #|   const [
  #|     { createHighlighterCore },
  #|     { createJavaScriptRegexEngine },
  #|   ] = await Promise.all([
  #|     import('https://esm.sh/shiki@3.23.0/core'),
  #|     import('https://esm.sh/shiki@3.23.0/engine/javascript'),
  #|   ])
  #|
  #|   return createHighlighterCore({
  #|     langs: [],
  #|     themes: [],
  #|     engine: createJavaScriptRegexEngine(),
  #|   })
  #| })

///|
extern "js" fn load_langs(
  highlighter : @js.Value,
  langs : Array[String],
) -> @js.Promise =
  #| (hl, langs) =>
  #|   hl.loadLanguage(
  #|     ...langs.map(lang =>
  #|       import(/* @vite-ignore */ ('https://esm.sh/shiki@3.23.0/langs/' + lang))
  #|     )
  #|   )

///|
extern "js" fn load_themes(
  highlighter : @js.Value,
  themes : Array[String],
) -> @js.Promise =
  #| (hl, themes) =>
  #|   hl.loadTheme(
  #|     ...themes.map(theme =>
  #|       import(/* @vite-ignore */ ('https://esm.sh/shiki@3.23.0/themes/' + theme))
  #|     )
  #|   )

///|
extern "js" fn code_to_html(
  highlighter : @js.Value,
  code : String,
  lang : String,
  theme : String,
) -> String =
  #| (hl, code, lang, theme) => {
  #|   try {
  #|     return hl.codeToHtml(code, { lang, theme })
  #|   } catch (e) {
  #|     console.warn(`[shiki] highlight failed: lang=${lang}, theme=${theme}`, e)
  #|     return ""
  #|   }
  #| }

///|
using @rabbita {type Cmd}

///|
struct Highlighter(@js.Value)

///|
extern "js" fn is_highlighter(x : @js.Value) -> Bool =
  #| (x) => !!x
  #|   && typeof x.codeToHtml === 'function'
  #|   && typeof x.loadLanguage === 'function'
  #|   && typeof x.loadTheme === 'function'

///|
pub fn load(
  langs : Array[String],
  themes : Array[String],
  msg : (Result[Highlighter, Error]) -> Cmd,
) -> Cmd {
  @rabbita.attempt(msg, async fn() {
    let highlighter = load_shiki().wait()
    guard is_highlighter(highlighter) else {
      fail("shiki highlighter init failed: invalid object")
    }
    if !(langs is []) {
      try load_langs(highlighter, langs).wait() |> ignore catch {
        e => fail("shiki load langs failed: \{e}")
      }
    }
    if !(themes is []) {
      try load_themes(highlighter, themes).wait() |> ignore catch {
        e => fail("shiki load themes failed: \{e}")
      }
    }
    Highlighter(highlighter)
  })
}

///|
pub async fn Highlighter::add_theme(self : Self, theme : String) -> Unit {
  load_themes(self.0, [theme]).wait() |> ignore
}

///|
pub async fn Highlighter::add_lang(self : Self, lang : String) -> Unit {
  load_langs(self.0, [lang]).wait() |> ignore
}

///|
/// Render a code to html, with language and theme options.
#warnings("-alert_xss_vulnerable")
pub fn Highlighter::code_to_html(
  self : Self,
  code : String,
  lang~ : String,
  theme~ : String,
) -> @rabbita.Html {
  let str = code_to_html(self.0, code, lang, theme)
  @html.code(attrs=@html.Attrs::build().inner_html(str), @html.nothing)
}

///|
/// Render code using dual light/dark themes. Outputs CSS variables
/// (--shiki-light, --shiki-dark, etc.) so the host page can switch
/// themes by toggling a CSS rule, e.g. [data-theme=dark] pre span.
#warnings("-alert_xss_vulnerable")
pub fn Highlighter::code_to_html_dual_themes(
  self : Self,
  code : String,
  lang~ : String,
  light~ : String,
  dark~ : String,
) -> @rabbita.Html {
  let str = code_to_html_dual_theme(self.0, code, lang, light, dark)
  @html.code(attrs=@html.Attrs::build().inner_html(str), @html.nothing)
}

///|
extern "js" fn code_to_html_dual_theme(
  highlighter : @js.Value,
  code : String,
  lang : String,
  light_theme : String,
  dark_theme : String,
) -> String =
  #| (hl, code, lang, lightTheme, darkTheme) => {
  #|   try {
  #|     return hl.codeToHtml(code, {
  #|       lang,
  #|       themes: { light: lightTheme, dark: darkTheme },
  #|     })
  #|   } catch (e) {
  #|     console.warn(`[shiki] dual highlight failed: lang=${lang}`, e)
  #|     return ""
  #|   }
  #| }