use crate::{
    complete_output, get_compiler,
    util::{CtxtExt, MapErr},
};
use anyhow::{anyhow, bail, Context, Error};
use napi::{CallContext, JsObject, Task};
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use swc::{
    config::{util::BoolOrObject, SourceMapsConfig},
    try_with_handler, TransformOutput,
};
use swc_atoms::JsWord;
use swc_bundler::{Bundler, ModuleData, ModuleRecord};
use swc_common::{collections::AHashMap, errors::Handler, BytePos, FileName, SourceMap, Span};
use swc_ecma_loader::{
    resolvers::{lru::CachingResolver, node::NodeModulesResolver},
    TargetEnv, NODE_BUILTINS,
};
use swc_ecmascript::{
    ast::*,
    parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax},
    visit::{noop_visit_type, Visit, VisitWith},
};

#[js_function(1)]
pub fn bundle(cx: CallContext) -> napi::Result<JsObject> {
    let option = cx.get_buffer_as_string(0)?;

    let task = BundleTask {
        c: get_compiler(&cx),
        config: option,
    };

    cx.env.spawn(task).map(|t| t.promise_object())
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct BundleOption {
    entry: PathBuf,
}

struct BundleTask {
    c: Arc<swc::Compiler>,
    config: String,
}

impl Task for BundleTask {
    type Output = TransformOutput;

    type JsValue = JsObject;

    fn compute(&mut self) -> napi::Result<Self::Output> {
        let option: BundleOption = crate::util::deserialize_json(&self.config).convert_err()?;

        try_with_handler(self.c.cm.clone(), true, |handler| {
            let builtins = NODE_BUILTINS
                .to_vec()
                .into_iter()
                .map(JsWord::from)
                .collect::<Vec<_>>();

            //
            let mut bundler = Bundler::new(
                self.c.globals(),
                self.c.cm.clone(),
                CustomLoader {
                    cm: self.c.cm.clone(),
                    handler,
                },
                make_resolver(),
                swc_bundler::Config {
                    require: true,
                    disable_inliner: false,
                    external_modules: builtins,
                    module: swc_bundler::ModuleType::Es,
                    ..Default::default()
                },
                Box::new(CustomHook),
            );

            let mut entries = HashMap::default();
            let path: PathBuf = option.entry;
            let path = path
                .canonicalize()
                .context("failed to canonicalize entry file")?;
            entries.insert("main".to_string(), FileName::Real(path));
            let outputs = bundler.bundle(entries)?;

            let output = outputs
                .into_iter()
                .next()
                .ok_or_else(|| anyhow!("swc_bundler::Bundle::bundle returned empty result"))?;

            let source_map_names = {
                let mut v = SourceMapIdentCollector {
                    names: Default::default(),
                };

                output.module.visit_with(&mut v);

                v.names
            };

            let code = self.c.print(
                &output.module,
                None,
                None,
                true,
                EsVersion::Es5,
                SourceMapsConfig::Bool(true),
                &source_map_names,
                None,
                false,
                Some(BoolOrObject::Bool(true)),
            )?;

            Ok(code)
        })
        .convert_err()
    }

    fn resolve(self, env: napi::Env, output: Self::Output) -> napi::Result<Self::JsValue> {
        complete_output(&env, output)
    }
}

type Resolver = Arc<CachingResolver<NodeModulesResolver>>;

fn make_resolver() -> Resolver {
    static CACHE: Lazy<Resolver> = Lazy::new(|| {
        // TODO: Make target env and alias configurable
        let r = NodeModulesResolver::new(TargetEnv::Node, Default::default(), true);
        let r = CachingResolver::new(256, r);
        Arc::new(r)
    });

    (*CACHE).clone()
}

struct CustomLoader<'a> {
    handler: &'a Handler,
    cm: Arc<SourceMap>,
}

impl swc_bundler::Load for CustomLoader<'_> {
    fn load(&self, f: &FileName) -> Result<ModuleData, Error> {
        let fm = match f {
            FileName::Real(path) => self.cm.load_file(path)?,
            _ => unreachable!(),
        };

        let lexer = Lexer::new(
            Syntax::Es(EsConfig {
                ..Default::default()
            }),
            EsVersion::Es2020,
            StringInput::from(&*fm),
            None,
        );

        let mut parser = Parser::new_from(lexer);
        let module = parser.parse_module().map_err(|err| {
            err.into_diagnostic(self.handler).emit();
            anyhow!("failed to parse")
        })?;

        Ok(ModuleData {
            fm,
            module,
            helpers: Default::default(),
        })
    }
}

struct CustomHook;

impl swc_bundler::Hook for CustomHook {
    fn get_import_meta_props(
        &self,
        _span: Span,
        _module_record: &ModuleRecord,
    ) -> Result<Vec<KeyValueProp>, Error> {
        bail!("`import.meta` is not supported yet")
    }
}

pub struct SourceMapIdentCollector {
    names: AHashMap<BytePos, JsWord>,
}

impl Visit for SourceMapIdentCollector {
    noop_visit_type!();

    fn visit_ident(&mut self, ident: &Ident) {
        self.names.insert(ident.span.lo, ident.sym.clone());
    }
}
