// From https://github.com/swc-project/swc/blob/main/crates/binding_core_wasm/src/lib.rs

use anyhow::{Context, Error};
use once_cell::sync::Lazy;
use std::sync::Arc;
use swc::{
  config::{
    ErrorFormat, JsMinifyOptions, Options, ParseOptions, SourceMapsConfig,
  },
  try_with_handler, Compiler,
};
use swc_common::{comments::Comments, FileName, FilePathMapping, SourceMap};
use swc_ecmascript::ast::{EsVersion, Program};
use wasm_bindgen::prelude::*;

fn convert_err(err: Error, error_format: ErrorFormat) -> JsValue {
  error_format.format(&err).into()
}

#[wasm_bindgen(js_name = "minifySync")]
pub fn minify_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
  console_error_panic_hook::set_once();

  let c = compiler();

  try_with_handler(
    c.cm.clone(),
    swc::HandlerOpts {
      ..Default::default()
    },
    |handler| {
      c.run(|| {
        let opts: JsMinifyOptions =
          opts.into_serde().context("failed to parse options")?;

        let fm = c.cm.new_source_file(FileName::Anon, s.into());
        let program = c
          .minify(fm, handler, &opts)
          .context("failed to minify file")?;

        JsValue::from_serde(&program).context("failed to serialize json")
      })
    },
  )
  .map_err(|e| convert_err(e, ErrorFormat::Normal))
}

#[wasm_bindgen(js_name = "parseSync")]
pub fn parse_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
  console_error_panic_hook::set_once();

  let c = compiler();

  try_with_handler(
    c.cm.clone(),
    swc::HandlerOpts {
      ..Default::default()
    },
    |handler| {
      c.run(|| {
        let opts: ParseOptions =
          opts.into_serde().context("failed to parse options")?;

        let fm = c.cm.new_source_file(FileName::Anon, s.into());

        let cmts = c.comments().clone();
        let comments = if opts.comments {
          Some(&cmts as &dyn Comments)
        } else {
          None
        };

        let program = c
          .parse_js(
            fm,
            handler,
            opts.target,
            opts.syntax,
            opts.is_module,
            comments,
          )
          .context("failed to parse code")?;

        JsValue::from_serde(&program).context("failed to serialize json")
      })
    },
  )
  .map_err(|e| convert_err(e, ErrorFormat::Normal))
}

#[wasm_bindgen(js_name = "printSync")]
pub fn print_sync(s: JsValue, opts: JsValue) -> Result<JsValue, JsValue> {
  console_error_panic_hook::set_once();

  let c = compiler();

  try_with_handler(
    c.cm.clone(),
    swc::HandlerOpts {
      ..Default::default()
    },
    |_handler| {
      c.run(|| {
        let opts: Options =
          opts.into_serde().context("failed to parse options")?;

        let program: Program =
          s.into_serde().context("failed to deserialize program")?;

        let s = c
          .print(
            &program,
            None,
            None,
            true,
            opts.codegen_target().unwrap_or(EsVersion::Es2020),
            opts
              .source_maps
              .clone()
              .unwrap_or(SourceMapsConfig::Bool(false)),
            &Default::default(),
            None,
            opts.config.minify.into(),
            None,
            opts.config.emit_source_map_columns.into_bool(),
            false,
          )
          .context("failed to print code")?;

        JsValue::from_serde(&s).context("failed to serialize json")
      })
    },
  )
  .map_err(|e| convert_err(e, ErrorFormat::Normal))
}

#[wasm_bindgen(js_name = "transformSync")]
pub fn transform_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
  console_error_panic_hook::set_once();

  let c = compiler();
  let opts: Options = opts
    .into_serde()
    .context("failed to parse options")
    .map_err(|e| convert_err(e, ErrorFormat::Normal))?;

  let error_format = opts.experimental.error_format.unwrap_or_default();

  try_with_handler(
    c.cm.clone(),
    swc::HandlerOpts {
      ..Default::default()
    },
    |handler| {
      c.run(|| {
        let fm = c.cm.new_source_file(
          if opts.filename.is_empty() {
            FileName::Anon
          } else {
            FileName::Real(opts.filename.clone().into())
          },
          s.into(),
        );
        let out = c
          .process_js_file(fm, handler, &opts)
          .context("failed to process input file")?;

        JsValue::from_serde(&out).context("failed to serialize json")
      })
    },
  )
  .map_err(|e| convert_err(e, error_format))
}

/// Get global sourcemap
fn compiler() -> Arc<Compiler> {
  static C: Lazy<Arc<Compiler>> = Lazy::new(|| {
    let cm = Arc::new(SourceMap::new(FilePathMapping::empty()));

    Arc::new(Compiler::new(cm))
  });

  C.clone()
}
