use self::util::BoolOrObject;
use crate::{builder::PassBuilder, SwcComments, SwcImportResolver};
use anyhow::{bail, Context, Error};
use dashmap::DashMap;
use either::Either;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
    cell::RefCell,
    collections::{HashMap, HashSet},
    env,
    hash::BuildHasher,
    path::{Path, PathBuf},
    rc::Rc as RustRc,
    sync::Arc,
    usize,
};
use swc_atoms::JsWord;
pub use swc_common::chain;
use swc_common::{errors::Handler, FileName, Mark, SourceMap};
use swc_ecma_ast::{Expr, ExprStmt, ModuleItem, Stmt};
use swc_ecma_ext_transforms::jest;
use swc_ecma_loader::resolvers::{
    lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver,
};
use swc_ecma_minifier::option::{
    terser::{TerserCompressorOptions, TerserEcmaVersion},
    MangleOptions, ManglePropertiesOptions,
};
pub use swc_ecma_parser::JscTarget;
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
use swc_ecma_transforms::{
    hygiene, modules,
    modules::{hoist::import_hoister, path::NodeImportResolver, util::Scope},
    optimization::{const_modules, inline_globals, json_parse, simplifier},
    pass::{noop, Optional},
    proposals::{decorators, export_default_from},
    react, resolver_with_mark, typescript,
};
use swc_ecma_visit::Fold;

#[cfg(test)]
mod tests;
pub mod util;

#[derive(Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParseOptions {
    #[serde(default)]
    pub comments: bool,
    #[serde(flatten)]
    pub syntax: Syntax,

    #[serde(default = "default_is_module")]
    pub is_module: bool,

    #[serde(default)]
    pub target: JscTarget,
}

#[cfg(target_arch = "wasm32")]
fn default_as_true() -> bool {
    true
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Options {
    #[serde(flatten)]
    pub config: Config,

    #[serde(skip_deserializing, default)]
    pub skip_helper_injection: bool,

    #[cfg(not(target_arch = "wasm32"))]
    #[serde(skip_deserializing, default)]
    pub disable_hygiene: bool,

    #[cfg(target_arch = "wasm32")]
    #[serde(default = "default_as_true")]
    pub disable_hygiene: bool,

    #[serde(skip_deserializing, default)]
    pub disable_fixer: bool,

    #[serde(skip_deserializing, default)]
    pub global_mark: Option<Mark>,

    #[cfg(not(target_arch = "wasm32"))]
    #[serde(default = "default_cwd")]
    pub cwd: PathBuf,

    #[serde(default)]
    pub caller: Option<CallerOptions>,

    #[serde(default)]
    pub filename: String,

    #[serde(default)]
    pub config_file: Option<ConfigFile>,

    #[serde(default)]
    pub root: Option<PathBuf>,

    #[serde(default)]
    pub root_mode: RootMode,

    #[serde(default = "default_swcrc")]
    pub swcrc: bool,

    #[cfg(not(target_arch = "wasm32"))]
    #[serde(default)]
    pub swcrc_roots: Option<PathBuf>,

    #[serde(default = "default_env_name")]
    pub env_name: String,

    #[serde(default)]
    pub source_maps: Option<SourceMapsConfig>,

    #[serde(default)]
    pub source_file_name: Option<String>,

    #[serde(default)]
    pub source_root: Option<String>,

    #[serde(default = "default_is_module")]
    pub is_module: bool,

    #[serde(default)]
    pub output_path: Option<PathBuf>,
}

impl Options {
    pub fn codegen_target(&self) -> Option<JscTarget> {
        self.config.jsc.target
    }
}

fn default_is_module() -> bool {
    true
}

/// Configuration related to source map generated by swc.
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum SourceMapsConfig {
    Bool(bool),
    Str(String),
}

impl SourceMapsConfig {
    pub fn enabled(&self) -> bool {
        match *self {
            SourceMapsConfig::Bool(b) => b,
            SourceMapsConfig::Str(ref s) => {
                assert_eq!(s, "inline", "Source map must be true, false or inline");
                true
            }
        }
    }
}

impl Default for SourceMapsConfig {
    fn default() -> Self {
        SourceMapsConfig::Bool(true)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InputSourceMap {
    Bool(bool),
    Str(String),
}

impl Default for InputSourceMap {
    fn default() -> Self {
        InputSourceMap::Bool(false)
    }
}

impl Options {
    pub fn build<'a>(
        &self,
        cm: &Arc<SourceMap>,
        base: &FileName,
        output_path: Option<&Path>,
        source_file_name: Option<String>,
        handler: &Handler,
        is_module: bool,
        config: Option<Config>,
        comments: Option<&'a SwcComments>,
    ) -> BuiltConfig<impl 'a + swc_ecma_visit::Fold> {
        let mut config = config.unwrap_or_else(Default::default);
        config.merge(&self.config);

        let JscConfig {
            transform,
            syntax,
            external_helpers,
            target,
            loose,
            keep_class_names,
            base_url,
            paths,
            minify: js_minify,
        } = config.jsc;
        let target = target.unwrap_or_default();

        let syntax = syntax.unwrap_or_default();
        let mut transform = transform.unwrap_or_default();

        let preserve_comments = js_minify.as_ref().map(|v| v.format.comments.clone());

        if syntax.typescript() {
            transform.legacy_decorator = true;
        }
        let optimizer = transform.optimizer;
        let enable_optimizer = optimizer.is_some();

        let const_modules = {
            let enabled = transform.const_modules.is_some();
            let config = transform.const_modules.unwrap_or_default();

            let globals = config.globals;
            Optional::new(const_modules(cm.clone(), globals), enabled)
        };

        let json_parse_pass = {
            if let Some(ref cfg) = optimizer.as_ref().and_then(|v| v.jsonify) {
                Either::Left(json_parse(cfg.min_cost))
            } else {
                Either::Right(noop())
            }
        };

        let optimization = {
            let pass =
                if let Some(opts) = optimizer.map(|o| o.globals.unwrap_or_else(Default::default)) {
                    opts.build(cm, handler)
                } else {
                    GlobalPassOption::default().build(cm, handler)
                };

            pass
        };

        let top_level_mark = self
            .global_mark
            .unwrap_or_else(|| Mark::fresh(Mark::root()));

        let pass = chain!(
            // handle jsx
            Optional::new(
                react::react(cm.clone(), comments.clone(), transform.react),
                syntax.jsx()
            ),
            // Decorators may use type information
            Optional::new(
                decorators(decorators::Config {
                    legacy: transform.legacy_decorator,
                    emit_metadata: transform.decorator_metadata,
                }),
                syntax.decorators()
            ),
            Optional::new(typescript::strip(), syntax.typescript()),
            resolver_with_mark(top_level_mark),
            const_modules,
            optimization,
            Optional::new(export_default_from(), syntax.export_default_from()),
            Optional::new(simplifier(Default::default()), enable_optimizer),
            json_parse_pass
        );

        let pass = PassBuilder::new(&cm, &handler, loose, top_level_mark, pass)
            .target(target)
            .skip_helper_injection(self.skip_helper_injection)
            .minify(js_minify)
            .hygiene(if self.disable_hygiene {
                None
            } else {
                Some(hygiene::Config { keep_class_names })
            })
            .fixer(!self.disable_fixer)
            .preset_env(config.env)
            .finalize(
                base_url,
                paths.into_iter().collect(),
                base,
                syntax,
                config.module,
                comments,
            );

        let pass = chain!(pass, Optional::new(jest::jest(), transform.hidden.jest));

        BuiltConfig {
            minify: config.minify,
            pass,
            external_helpers,
            syntax,
            target,
            is_module,
            source_maps: self
                .source_maps
                .clone()
                .or(config.source_maps)
                .unwrap_or(SourceMapsConfig::Bool(false)),
            input_source_map: self.config.input_source_map.clone(),
            output_path: output_path.map(|v| v.to_path_buf()),
            source_file_name,
            preserve_comments,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RootMode {
    #[serde(rename = "root")]
    Root,
    #[serde(rename = "upward")]
    Upward,
    #[serde(rename = "upward-optional")]
    UpwardOptional,
}

impl Default for RootMode {
    fn default() -> Self {
        RootMode::Root
    }
}
const fn default_swcrc() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConfigFile {
    Bool(bool),
    Str(String),
}

impl Default for ConfigFile {
    fn default() -> Self {
        ConfigFile::Bool(true)
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallerOptions {
    pub name: String,
}

#[cfg(not(target_arch = "wasm32"))]
fn default_cwd() -> PathBuf {
    ::std::env::current_dir().unwrap()
}

/// `.swcrc` file
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged, rename = "swcrc")]
pub enum Rc {
    Single(Config),
    Multi(Vec<Config>),
}

impl Default for Rc {
    fn default() -> Self {
        Rc::Multi(vec![
            Config {
                env: None,
                test: None,
                exclude: Some(FileMatcher::Regex("\\.tsx?$".into())),
                jsc: JscConfig {
                    syntax: Some(Default::default()),
                    transform: None,
                    external_helpers: false,
                    target: Default::default(),
                    loose: false,
                    keep_class_names: false,
                    ..Default::default()
                },
                module: None,
                minify: false,
                source_maps: None,
                input_source_map: InputSourceMap::default(),
            },
            Config {
                env: None,
                test: Some(FileMatcher::Regex("\\.tsx$".into())),
                exclude: None,
                jsc: JscConfig {
                    syntax: Some(Syntax::Typescript(TsConfig {
                        tsx: true,
                        ..Default::default()
                    })),
                    transform: None,
                    external_helpers: false,
                    target: Default::default(),
                    loose: false,
                    keep_class_names: false,
                    ..Default::default()
                },
                module: None,
                minify: false,
                source_maps: None,
                input_source_map: InputSourceMap::default(),
            },
            Config {
                env: None,
                test: Some(FileMatcher::Regex("\\.ts$".into())),
                exclude: None,
                jsc: JscConfig {
                    syntax: Some(Syntax::Typescript(TsConfig {
                        tsx: false,
                        ..Default::default()
                    })),
                    transform: None,
                    external_helpers: false,
                    target: Default::default(),
                    loose: false,
                    keep_class_names: false,
                    ..Default::default()
                },
                module: None,
                minify: false,
                source_maps: None,
                input_source_map: InputSourceMap::default(),
            },
        ])
    }
}

impl Rc {
    /// This method returns `Ok(None)` if the file should be ignored.
    pub fn into_config(self, filename: Option<&Path>) -> Result<Option<Config>, Error> {
        let cs = match self {
            Rc::Single(mut c) => match filename {
                Some(filename) => {
                    if c.matches(filename)? {
                        c.adjust(filename);

                        return Ok(Some(c));
                    } else {
                        return Ok(None);
                    }
                }
                // TODO
                None => return Ok(Some(c)),
            },
            Rc::Multi(cs) => cs,
        };

        match filename {
            Some(filename) => {
                for mut c in cs {
                    if c.matches(filename)? {
                        c.adjust(filename);

                        return Ok(Some(c));
                    }
                }
            }
            None => return Ok(Some(Config::default())),
        }

        bail!(".swcrc exists but not matched")
    }
}

/// A single object in the `.swcrc` file
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Config {
    #[serde(default)]
    pub env: Option<swc_ecma_preset_env::Config>,

    #[serde(default)]
    pub test: Option<FileMatcher>,

    #[serde(default)]
    pub exclude: Option<FileMatcher>,

    #[serde(default)]
    pub jsc: JscConfig,

    #[serde(default)]
    pub module: Option<ModuleConfig>,

    #[serde(default)]
    pub minify: bool,

    #[serde(default)]
    pub input_source_map: InputSourceMap,

    /// Possible values are: `'inline'`, `true`, `false`.
    #[serde(default)]
    pub source_maps: Option<SourceMapsConfig>,
}

/// Second argument of `minify`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct JsMinifyOptions {
    #[serde(default)]
    pub compress: BoolOrObject<TerserCompressorOptions>,

    #[serde(default)]
    pub mangle: BoolOrObject<MangleOptions>,

    #[serde(default)]
    pub format: JsMinifyFormatOptions,

    #[serde(default)]
    pub ecma: TerserEcmaVersion,

    #[serde(default)]
    pub keep_classnames: bool,

    #[serde(default)]
    pub keep_fnames: bool,

    #[serde(default)]
    pub module: bool,

    #[serde(default)]
    pub safari10: bool,

    #[serde(default)]
    pub toplevel: bool,

    #[serde(default)]
    pub source_map: bool,

    #[serde(default)]
    pub output_path: Option<String>,
}

/// `jsc.minify.format`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct JsMinifyFormatOptions {
    pub comments: BoolOrObject<JsMinifyCommentOption>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JsMinifyCommentOption {
    #[serde(rename = "some")]
    PreserveSomeComments,
    #[serde(rename = "all")]
    PreserveAllComments,
}

impl Default for JsMinifyCommentOption {
    fn default() -> Self {
        JsMinifyCommentOption::PreserveSomeComments
    }
}

impl Config {
    /// Adjust config for `file`.
    ///
    ///
    ///
    /// - typescript: `tsx` will be modified if file extension is `ts`.
    fn adjust(&mut self, file: &Path) {
        match &mut self.jsc.syntax {
            Some(Syntax::Typescript(TsConfig { tsx, .. })) => {
                let is_ts = file.extension().map(|v| v == "ts").unwrap_or(false);
                if is_ts {
                    *tsx = false;
                }
            }
            _ => {}
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FileMatcher {
    Regex(String),
    Multi(Vec<FileMatcher>),
}

impl Default for FileMatcher {
    fn default() -> Self {
        Self::Regex(String::from(""))
    }
}

impl FileMatcher {
    pub fn matches(&self, filename: &Path) -> Result<bool, Error> {
        static CACHE: Lazy<DashMap<String, Regex>> = Lazy::new(Default::default);

        match self {
            FileMatcher::Regex(ref s) => {
                if s.is_empty() {
                    return Ok(false);
                }

                if !CACHE.contains_key(&*s) {
                    let re = Regex::new(&s).with_context(|| format!("invalid regex: {}", s))?;
                    CACHE.insert(s.clone(), re);
                }

                let re = CACHE.get(&*s).unwrap();

                let filename = if cfg!(target_os = "windows") {
                    filename.to_string_lossy().replace("\\", "/")
                } else {
                    filename.to_string_lossy().to_string()
                };

                Ok(re.is_match(&filename))
            }
            FileMatcher::Multi(ref v) => {
                //
                for m in v {
                    if m.matches(filename)? {
                        return Ok(true);
                    }
                }

                Ok(false)
            }
        }
    }
}

impl Config {
    pub fn matches(&self, filename: &Path) -> Result<bool, Error> {
        if let Some(ref exclude) = self.exclude {
            if exclude.matches(filename)? {
                return Ok(false);
            }
        }

        if let Some(ref include) = self.test {
            if include.matches(filename)? {
                return Ok(true);
            }
            return Ok(false);
        }

        Ok(true)
    }
}

/// One `BuiltConfig` per a directory with swcrc
pub struct BuiltConfig<P: swc_ecma_visit::Fold> {
    pub pass: P,
    pub syntax: Syntax,
    pub target: JscTarget,
    /// Minification for **codegen**. Minifier transforms will be inserted into
    /// `pass`.
    pub minify: bool,
    pub external_helpers: bool,
    pub source_maps: SourceMapsConfig,
    pub input_source_map: InputSourceMap,
    pub is_module: bool,
    pub output_path: Option<PathBuf>,

    pub source_file_name: Option<String>,

    pub preserve_comments: Option<BoolOrObject<JsMinifyCommentOption>>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct JscConfig {
    #[serde(rename = "parser", default)]
    pub syntax: Option<Syntax>,

    #[serde(default)]
    pub transform: Option<TransformConfig>,

    #[serde(default)]
    pub external_helpers: bool,

    #[serde(default)]
    pub target: Option<JscTarget>,

    #[serde(default)]
    pub loose: bool,

    #[serde(default)]
    pub keep_class_names: bool,

    #[serde(default)]
    pub base_url: String,

    #[serde(default)]
    pub paths: Paths,

    #[serde(default)]
    pub minify: Option<JsMinifyOptions>,
}

/// `paths` sectiob of `tsconfig.json`.
pub type Paths = HashMap<String, Vec<String>, ahash::RandomState>;
pub(crate) type CompiledPaths = Vec<(String, Vec<String>)>;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum ModuleConfig {
    #[serde(rename = "commonjs")]
    CommonJs(modules::common_js::Config),
    #[serde(rename = "umd")]
    Umd(modules::umd::Config),
    #[serde(rename = "amd")]
    Amd(modules::amd::Config),
    #[serde(rename = "es6")]
    Es6,
}

impl ModuleConfig {
    pub fn build(
        cm: Arc<SourceMap>,
        base_url: String,
        paths: CompiledPaths,
        base: &FileName,
        root_mark: Mark,
        config: Option<ModuleConfig>,
        scope: RustRc<RefCell<Scope>>,
    ) -> Box<dyn swc_ecma_visit::Fold> {
        let base = match base {
            FileName::Real(v) => {
                FileName::Real(v.canonicalize().unwrap_or_else(|_| v.to_path_buf()))
            }
            _ => base.clone(),
        };

        match config {
            None | Some(ModuleConfig::Es6) => Box::new(import_hoister()),
            Some(ModuleConfig::CommonJs(config)) => {
                if paths.is_empty() {
                    Box::new(modules::common_js::common_js(
                        root_mark,
                        config,
                        Some(scope),
                    ))
                } else {
                    let resolver = build_resolver(base_url, paths);

                    Box::new(modules::common_js::common_js_with_resolver(
                        resolver,
                        base,
                        root_mark,
                        config,
                        Some(scope),
                    ))
                }
            }
            Some(ModuleConfig::Umd(config)) => {
                if paths.is_empty() {
                    Box::new(modules::umd::umd(cm, root_mark, config))
                } else {
                    let resolver = build_resolver(base_url, paths);

                    Box::new(modules::umd::umd_with_resolver(
                        resolver, base, cm, root_mark, config,
                    ))
                }
            }
            Some(ModuleConfig::Amd(config)) => {
                if paths.is_empty() {
                    Box::new(modules::amd::amd(config))
                } else {
                    let resolver = build_resolver(base_url, paths);

                    Box::new(modules::amd::amd_with_resolver(resolver, base, config))
                }
            }
        }
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct TransformConfig {
    #[serde(default)]
    pub react: react::Options,

    #[serde(default)]
    pub const_modules: Option<ConstModulesConfig>,

    #[serde(default)]
    pub optimizer: Option<OptimizerConfig>,

    #[serde(default)]
    pub legacy_decorator: bool,

    #[serde(default)]
    pub decorator_metadata: bool,

    #[serde(default)]
    pub hidden: HiddenTransformConfig,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct HiddenTransformConfig {
    #[serde(default)]
    pub jest: bool,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ConstModulesConfig {
    #[serde(default)]
    pub globals: HashMap<JsWord, HashMap<JsWord, String>>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct OptimizerConfig {
    #[serde(default)]
    pub globals: Option<GlobalPassOption>,

    #[serde(default)]
    pub jsonify: Option<JsonifyOption>,
}

#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct JsonifyOption {
    #[serde(default = "default_jsonify_min_cost")]
    pub min_cost: usize,
}

fn default_jsonify_min_cost() -> usize {
    1024
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GlobalPassOption {
    #[serde(default)]
    pub vars: HashMap<String, String>,
    #[serde(default = "default_envs")]
    pub envs: HashSet<String>,
}

fn default_envs() -> HashSet<String> {
    let mut v = HashSet::default();
    v.insert(String::from("NODE_ENV"));
    v.insert(String::from("SWC_ENV"));
    v
}

impl GlobalPassOption {
    pub fn build(self, cm: &SourceMap, handler: &Handler) -> impl 'static + Fold {
        fn mk_map(
            cm: &SourceMap,
            handler: &Handler,
            values: impl Iterator<Item = (String, String)>,
            is_env: bool,
        ) -> HashMap<JsWord, Expr> {
            let mut m = HashMap::default();

            for (k, v) in values {
                let v = if is_env {
                    format!("'{}'", v)
                } else {
                    (*v).into()
                };
                let v_str = v.clone();
                let fm = cm.new_source_file(FileName::Custom(format!("GLOBAL.{}", k)), v);
                let lexer = Lexer::new(
                    Syntax::Es(Default::default()),
                    Default::default(),
                    StringInput::from(&*fm),
                    None,
                );

                let mut p = Parser::new_from(lexer);
                let module = p.parse_module();

                for e in p.take_errors() {
                    e.into_diagnostic(handler).emit()
                }

                let mut module = module
                    .map_err(|e| e.into_diagnostic(handler).emit())
                    .unwrap_or_else(|()| {
                        panic!(
                            "failed to parse global variable {}=`{}` as module",
                            k, v_str
                        )
                    });

                let expr = match module.body.pop() {
                    Some(ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. }))) => *expr,
                    _ => panic!("{} is not a valid expression", v_str),
                };

                m.insert((*k).into(), expr);
            }

            m
        }

        let envs = self.envs;
        inline_globals(
            if cfg!(target_arch = "wasm32") {
                mk_map(cm, handler, vec![].into_iter(), true)
            } else {
                mk_map(
                    cm,
                    handler,
                    env::vars().filter(|(k, _)| envs.contains(&*k)),
                    true,
                )
            },
            mk_map(cm, handler, self.vars.into_iter(), false),
        )
    }
}

fn default_env_name() -> String {
    match env::var("SWC_ENV") {
        Ok(v) => return v,
        Err(_) => {}
    }

    match env::var("NODE_ENV") {
        Ok(v) => v,
        Err(_) => "development".into(),
    }
}

pub trait Merge {
    /// Apply overrides from `from`
    fn merge(&mut self, from: &Self);
}

impl<T: Clone> Merge for Option<T>
where
    T: Merge,
{
    fn merge(&mut self, from: &Option<T>) {
        match *from {
            Some(ref from) => match *self {
                Some(ref mut v) => v.merge(from),
                None => *self = Some(from.clone()),
            },
            // no-op
            None => {}
        }
    }
}

impl Merge for Config {
    fn merge(&mut self, from: &Self) {
        self.jsc.merge(&from.jsc);
        self.module.merge(&from.module);
        self.minify.merge(&from.minify);
        self.env.merge(&from.env);
        self.source_maps.merge(&from.source_maps);
    }
}

impl Merge for JsMinifyOptions {
    fn merge(&mut self, from: &Self) {
        self.compress.merge(&from.compress);
        self.mangle.merge(&from.mangle);
        self.format.merge(&from.format);
        self.ecma.merge(&from.ecma);
        self.keep_classnames |= from.keep_classnames;
        self.keep_fnames |= from.keep_fnames;
        self.safari10 |= from.safari10;
        self.toplevel |= from.toplevel;
    }
}

impl Merge for TerserCompressorOptions {
    fn merge(&mut self, from: &Self) {
        self.defaults |= from.defaults;
        // TODO
    }
}

impl Merge for MangleOptions {
    fn merge(&mut self, from: &Self) {
        self.props.merge(&from.props);

        self.top_level |= from.top_level;
        self.keep_class_names |= from.keep_class_names;
        self.keep_fn_names |= from.keep_fn_names;
        self.keep_private_props |= from.keep_private_props;
        self.safari10 |= from.safari10;
    }
}

impl Merge for JsMinifyFormatOptions {
    fn merge(&mut self, from: &Self) {
        self.comments.merge(&from.comments);
    }
}

impl Merge for JsMinifyCommentOption {
    fn merge(&mut self, _: &Self) {}
}

impl Merge for ManglePropertiesOptions {
    fn merge(&mut self, from: &Self) {
        self.undeclared |= from.undeclared;
    }
}

impl Merge for TerserEcmaVersion {
    fn merge(&mut self, from: &Self) {
        *self = from.clone();
    }
}

impl Merge for SourceMapsConfig {
    fn merge(&mut self, from: &Self) {
        match self {
            SourceMapsConfig::Bool(false) => {
                *self = from.clone();
            }
            _ => {}
        }
    }
}

impl Merge for swc_ecma_preset_env::Config {
    fn merge(&mut self, from: &Self) {
        *self = from.clone();
    }
}

impl Merge for JscConfig {
    fn merge(&mut self, from: &Self) {
        self.syntax.merge(&from.syntax);
        self.transform.merge(&from.transform);
        self.target.merge(&from.target);
        self.external_helpers.merge(&from.external_helpers);
        self.keep_class_names.merge(&from.keep_class_names);
        self.paths.merge(&from.paths);
        self.minify.merge(&from.minify);
    }
}

impl<K, V, S> Merge for HashMap<K, V, S>
where
    K: Clone + Eq + std::hash::Hash,
    V: Clone,
    S: Clone + BuildHasher,
{
    fn merge(&mut self, from: &Self) {
        if self.is_empty() {
            *self = (*from).clone();
        } else {
            for (k, v) in from {
                self.entry(k.clone()).or_insert(v.clone());
            }
        }
    }
}

impl Merge for JscTarget {
    fn merge(&mut self, from: &Self) {
        if *self < *from {
            *self = *from
        }
    }
}

impl Merge for Option<ModuleConfig> {
    fn merge(&mut self, from: &Self) {
        match *from {
            Some(ref c2) => *self = Some(c2.clone()),
            None => {}
        }
    }
}

impl Merge for bool {
    fn merge(&mut self, from: &Self) {
        *self |= *from
    }
}

impl Merge for Syntax {
    fn merge(&mut self, from: &Self) {
        *self = *from;
    }
}

impl Merge for TransformConfig {
    fn merge(&mut self, from: &Self) {
        self.optimizer.merge(&from.optimizer);
        self.const_modules.merge(&from.const_modules);
        self.react.merge(&from.react);
    }
}

impl Merge for OptimizerConfig {
    fn merge(&mut self, from: &Self) {
        self.globals.merge(&from.globals)
    }
}

impl Merge for GlobalPassOption {
    fn merge(&mut self, from: &Self) {
        *self = from.clone();
    }
}

impl Merge for react::Options {
    fn merge(&mut self, from: &Self) {
        if *from != react::Options::default() {
            *self = from.clone();
        }
    }
}

impl Merge for ConstModulesConfig {
    fn merge(&mut self, from: &Self) {
        *self = from.clone()
    }
}

fn build_resolver(base_url: String, paths: CompiledPaths) -> SwcImportResolver {
    static CACHE: Lazy<DashMap<(String, CompiledPaths), SwcImportResolver>> =
        Lazy::new(|| Default::default());

    if let Some(cached) = CACHE.get(&(base_url.clone(), paths.clone())) {
        return (*cached).clone();
    }

    let r = {
        let r = TsConfigResolver::new(
            NodeModulesResolver::default(),
            base_url.clone().into(),
            paths.clone(),
        );
        let r = CachingResolver::new(40, r);

        let r = NodeImportResolver::new(r);
        Arc::new(r)
    };

    CACHE.insert((base_url, paths), r.clone());

    r
}
