use oden_js::{Error, Result};
use std::cell::RefCell;
use std::rc::Rc;

use swc_common::{
    self, comments::SingleThreadedComments, errors::Diagnostic, sync::Lrc, FileName, Globals, Mark,
    SourceMap, GLOBALS,
};
use swc_ecma_codegen::{text_writer::JsWriter, Emitter};
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
use swc_ecma_transforms_base::{fixer::fixer, hygiene::hygiene, resolver};
use swc_ecma_transforms_typescript::strip;
use swc_ecma_visit::FoldWith;

struct DiagnosticCollector {
    cell: Rc<RefCell<Vec<Diagnostic>>>,
}

impl DiagnosticCollector {
    pub fn into_handler(self) -> swc_common::errors::Handler {
        swc_common::errors::Handler::with_emitter(true, false, Box::new(self))
    }
}

impl swc_common::errors::Emitter for DiagnosticCollector {
    fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) {
        use std::ops::Deref;
        self.cell.borrow_mut().push(db.deref().clone());
    }
}

fn is_fatal_swc_diagnostic(diagnostic: &Diagnostic) -> bool {
    use swc_common::errors::Level;
    match diagnostic.level {
        Level::Bug
        | Level::Cancelled
        | Level::FailureNote
        | Level::Fatal
        | Level::PhaseFatal
        | Level::Error => true,
        Level::Help | Level::Note | Level::Warning => false,
    }
}
fn format_swc_diagnostic(source_map: &SourceMap, diagnostic: &Diagnostic) -> String {
    if let Some(span) = &diagnostic.span.primary_span() {
        let file_name = source_map.span_to_filename(*span);
        let loc = source_map.lookup_char_pos(span.lo);
        format!(
            "{} at {}:{}:{}",
            diagnostic.message(),
            file_name,
            loc.line,
            loc.col_display + 1,
        )
    } else {
        diagnostic.message()
    }
}

fn diagnostics_to_parse_error<'a>(
    name: &str,
    source_map: &SourceMap,
    diagnostics: impl Iterator<Item = &'a Diagnostic>,
) -> Error {
    Error::ParseError(
        name.into(),
        diagnostics
            .map(|d| format_swc_diagnostic(source_map, d))
            .collect::<Vec<_>>()
            .join("\n\n"),
    )
}

fn ensure_no_fatal_swc_diagnostics<'a>(
    name: &str,
    source_map: &SourceMap,
    diagnostics: impl Iterator<Item = &'a Diagnostic>,
) -> Result<()> {
    let fatal_diagnostics = diagnostics
        .filter(|d| is_fatal_swc_diagnostic(d))
        .collect::<Vec<_>>();
    if !fatal_diagnostics.is_empty() {
        Err(diagnostics_to_parse_error(
            name,
            source_map,
            fatal_diagnostics.into_iter(),
        ))
    } else {
        Ok(())
    }
}

pub fn transpile_to_javascript(path: &str, input: String) -> Result<String> {
    // NOTE: This was taken almost verbatim from
    //       https://github.com/swc-project/swc/blob/main/crates/swc_ecma_transforms_typescript/examples/ts_to_js.rs
    //       This appears to be similar to what deno_ast does, but this has
    //       the advantage of actually compiling. :P
    let cm: Lrc<SourceMap> = Default::default();
    let diagnostics_cell: Rc<RefCell<Vec<Diagnostic>>> = Rc::new(RefCell::new(Vec::new()));
    let handler = DiagnosticCollector {
        cell: diagnostics_cell.clone(),
    }
    .into_handler();

    let fm = cm.new_source_file(FileName::Custom(path.into()), input.into());
    let comments = SingleThreadedComments::default();

    let lexer = Lexer::new(
        Syntax::Typescript(Default::default()),
        Default::default(),
        StringInput::from(&*fm),
        Some(&comments),
    );

    let mut parser = Parser::new_from(lexer);

    for e in parser.take_errors() {
        e.into_diagnostic(&handler).emit();
    }

    let module = parser
        .parse_module()
        .map_err(|e| e.into_diagnostic(&handler))
        .map_err(|mut e| {
            e.emit();
            let diagnostics = diagnostics_cell.borrow();
            diagnostics_to_parse_error(path, &cm, diagnostics.iter())
        })?;

    let globals = Globals::default();
    GLOBALS.set(&globals, || {
        let unresolved_mark = Mark::new();
        let top_level_mark = Mark::new();

        // Optionally transforms decorators here before the resolver pass
        // as it might produce runtime declarations.

        // Conduct identifier scope analysis
        let module = module.fold_with(&mut resolver(unresolved_mark, top_level_mark, true));

        // Remove typescript types
        let module = module.fold_with(&mut strip(top_level_mark));

        // Fix up any identifiers with the same name, but different contexts
        let module = module.fold_with(&mut hygiene());

        // Ensure that we have enough parenthesis.
        let module = module.fold_with(&mut fixer(Some(&comments)));

        let mut buf = vec![];
        {
            let mut emitter = Emitter {
                cfg: swc_ecma_codegen::Config {
                    minify: false,
                    ..Default::default()
                },
                cm: cm.clone(),
                comments: Some(&comments),
                wr: JsWriter::new(cm.clone(), "\n", &mut buf, None),
            };

            emitter.emit_module(&module).unwrap();
        }

        let diagnostics = diagnostics_cell.borrow();
        ensure_no_fatal_swc_diagnostics(path, &cm, diagnostics.iter())?;
        Ok(String::from_utf8(buf).expect("non-utf8?"))
    })
}
