use crate::hash::ShortHash;
use proc_macro2::{Ident, Span};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;
use syn::ext::IdentExt;

use crate::ast;
use crate::Diagnostic;

#[derive(Clone)]
pub enum EncodeChunk {
    EncodedBuf(Vec<u8>),
    StrExpr(syn::Expr),
    // TODO: support more expr type;
}

pub struct EncodeResult {
    pub custom_section: Vec<EncodeChunk>,
    pub included_files: Vec<PathBuf>,
}

pub fn encode(program: &ast::Program) -> Result<EncodeResult, Diagnostic> {
    let mut e = Encoder::new();
    let i = Interner::new();
    shared_program(program, &i)?.encode(&mut e);
    let custom_section = e.finish();
    let included_files = i
        .files
        .borrow()
        .values()
        .map(|p| &p.path)
        .cloned()
        .collect();
    Ok(EncodeResult {
        custom_section,
        included_files,
    })
}

struct Interner {
    bump: bumpalo::Bump,
    files: RefCell<HashMap<String, LocalFile>>,
    root: PathBuf,
    crate_name: String,
    has_package_json: Cell<bool>,
}

struct LocalFile {
    path: PathBuf,
    definition: Span,
    new_identifier: String,
    linked_module: bool,
}

impl Interner {
    fn new() -> Interner {
        let root = env::var_os("CARGO_MANIFEST_DIR")
            .expect("should have CARGO_MANIFEST_DIR env var")
            .into();
        let crate_name = env::var("CARGO_PKG_NAME").expect("should have CARGO_PKG_NAME env var");
        Interner {
            bump: bumpalo::Bump::new(),
            files: RefCell::new(HashMap::new()),
            root,
            crate_name,
            has_package_json: Cell::new(false),
        }
    }

    fn intern(&self, s: &Ident) -> &str {
        self.intern_str(&s.to_string())
    }

    fn intern_str(&self, s: &str) -> &str {
        // NB: eventually this could be used to intern `s` to only allocate one
        // copy, but for now let's just "transmute" `s` to have the same
        // lifetime as this struct itself (which is our main goal here)
        self.bump.alloc_str(s)
    }

    /// Given an import to a local module `id` this generates a unique module id
    /// to assign to the contents of `id`.
    ///
    /// Note that repeated invocations of this function will be memoized, so the
    /// same `id` will always return the same resulting unique `id`.
    fn resolve_import_module(
        &self,
        id: &str,
        span: Span,
        linked_module: bool,
    ) -> Result<ImportModule<'_>, Diagnostic> {
        let mut files = self.files.borrow_mut();
        if let Some(file) = files.get(id) {
            return Ok(ImportModule::Named(self.intern_str(&file.new_identifier)));
        }
        self.check_for_package_json();
        let path = if let Some(id) = id.strip_prefix('/') {
            self.root.join(id)
        } else if id.starts_with("./") || id.starts_with("../") {
            let msg = "relative module paths aren't supported yet";
            return Err(Diagnostic::span_error(span, msg));
        } else {
            return Ok(ImportModule::RawNamed(self.intern_str(id)));
        };

        // Generate a unique ID which is somewhat readable as well, so mix in
        // the crate name, hash to make it unique, and then the original path.
        let new_identifier = format!("{}{id}", self.unique_crate_identifier());
        let file = LocalFile {
            path,
            definition: span,
            new_identifier,
            linked_module,
        };
        files.insert(id.to_string(), file);
        drop(files);
        self.resolve_import_module(id, span, linked_module)
    }

    fn unique_crate_identifier(&self) -> String {
        format!("{}-{}", self.crate_name, ShortHash(0))
    }

    fn check_for_package_json(&self) {
        if self.has_package_json.get() {
            return;
        }
        let path = self.root.join("package.json");
        if path.exists() {
            self.has_package_json.set(true);
        }
    }
}

fn shared_program<'a>(
    prog: &'a ast::Program,
    intern: &'a Interner,
) -> Result<Program<'a>, Diagnostic> {
    Ok(Program {
        exports: prog
            .exports
            .iter()
            .map(|a| shared_export(a, intern))
            .collect::<Result<Vec<_>, _>>()?,
        structs: prog
            .structs
            .iter()
            .map(|a| shared_struct(a, intern))
            .collect(),
        enums: prog.enums.iter().map(|a| shared_enum(a, intern)).collect(),
        imports: prog
            .imports
            .iter()
            .map(|a| shared_import(a, intern))
            .collect::<Result<Vec<_>, _>>()?,
        typescript_custom_sections: prog
            .typescript_custom_sections
            .iter()
            .map(|x| shared_lit_or_expr(x, intern))
            .collect(),
        linked_modules: prog
            .linked_modules
            .iter()
            .enumerate()
            .map(|(i, a)| shared_linked_module(&prog.link_function_name(i), a, intern))
            .collect::<Result<Vec<_>, _>>()?,
        local_modules: intern
            .files
            .borrow()
            .values()
            .map(|file| {
                fs::read_to_string(&file.path)
                    .map(|s| LocalModule {
                        identifier: intern.intern_str(&file.new_identifier),
                        contents: intern.intern_str(&s),
                        linked_module: file.linked_module,
                    })
                    .map_err(|e| {
                        let msg = format!("failed to read file `{}`: {e}", file.path.display());
                        Diagnostic::span_error(file.definition, msg)
                    })
            })
            .collect::<Result<Vec<_>, _>>()?,
        inline_js: prog
            .inline_js
            .iter()
            .map(|js| intern.intern_str(js))
            .collect(),
        unique_crate_identifier: intern.intern_str(&intern.unique_crate_identifier()),
        package_json: if intern.has_package_json.get() {
            Some(intern.intern_str(intern.root.join("package.json").to_str().unwrap()))
        } else {
            None
        },
    })
}

fn shared_export<'a>(
    export: &'a ast::Export,
    intern: &'a Interner,
) -> Result<Export<'a>, Diagnostic> {
    let consumed = matches!(export.method_self, Some(ast::MethodSelf::ByValue));
    let method_kind = from_ast_method_kind(&export.function, intern, &export.method_kind)?;
    Ok(Export {
        class: export.js_class.as_deref(),
        comments: export.comments.iter().map(|s| &**s).collect(),
        consumed,
        function: shared_function(&export.function, intern),
        js_namespace: export
            .js_namespace
            .as_ref()
            .map(|ns| ns.iter().map(|s| &**s).collect()),
        method_kind,
        start: export.start,
    })
}

fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Function<'a> {
    let args =
        func.arguments
            .iter()
            .enumerate()
            .map(|(idx, arg)| FunctionArgumentData {
                // use argument's "js_name" if it was provided via attributes
                // if not use the original Rust argument ident
                name: arg.js_name.clone().unwrap_or(
                    if let syn::Pat::Ident(x) = &*arg.pat_type.pat {
                        x.ident.unraw().to_string()
                    } else {
                        format!("arg{idx}")
                    },
                ),
                ty_override: arg.js_type.as_deref(),
                optional: arg.optional,
                desc: arg.desc.as_deref(),
            })
            .collect::<Vec<_>>();

    Function {
        args,
        asyncness: func.r#async,
        name: &func.name,
        generate_typescript: func.generate_typescript,
        generate_jsdoc: func.generate_jsdoc,
        variadic: func.variadic,
        ret_ty_override: func.ret.as_ref().and_then(|v| v.js_type.as_deref()),
        ret_desc: func.ret.as_ref().and_then(|v| v.desc.as_deref()),
    }
}

fn shared_enum<'a>(e: &'a ast::Enum, intern: &'a Interner) -> Enum<'a> {
    Enum {
        name: &e.js_name,
        signed: e.signed,
        variants: e
            .variants
            .iter()
            .map(|v| shared_variant(v, intern))
            .collect(),
        comments: e.comments.iter().map(|s| &**s).collect(),
        generate_typescript: e.generate_typescript,
        js_namespace: e
            .js_namespace
            .as_ref()
            .map(|ns| ns.iter().map(|s| &**s).collect()),
        private: e.private,
    }
}

fn shared_variant<'a>(v: &'a ast::Variant, intern: &'a Interner) -> EnumVariant<'a> {
    EnumVariant {
        name: intern.intern(&v.name),
        value: v.value,
        comments: v.comments.iter().map(|s| &**s).collect(),
    }
}

fn shared_import<'a>(i: &'a ast::Import, intern: &'a Interner) -> Result<Import<'a>, Diagnostic> {
    // Resolve reexport name: use explicit rename if provided, otherwise use the import's name
    let reexport = i.reexport.as_ref().map(|rename_opt| {
        rename_opt.clone().unwrap_or_else(|| {
            // Get the default name from the import kind
            match &i.kind {
                ast::ImportKind::Type(t) => t.js_name.clone(),
                ast::ImportKind::Function(f) => f.function.name.clone(),
                ast::ImportKind::Static(s) => s.js_name.clone(),
                _ => unreachable!("reexport only supported on types, functions, and statics"),
            }
        })
    });

    // Determine whether TypeScript should be generated for this import.
    // For functions, this is stored on the Function struct; for types and statics,
    // skip_typescript is not currently supported so we default to true.
    let generate_typescript = match &i.kind {
        ast::ImportKind::Function(f) => f.function.generate_typescript,
        _ => true,
    };

    Ok(Import {
        module: i
            .module
            .as_ref()
            .map(|m| shared_module(m, intern, false))
            .transpose()?,
        js_namespace: i.js_namespace.clone(),
        reexport,
        generate_typescript,
        kind: shared_import_kind(&i.kind, intern)?,
    })
}

fn shared_lit_or_expr<'a>(i: &'a ast::LitOrExpr, _intern: &'a Interner) -> LitOrExpr<'a> {
    match i {
        ast::LitOrExpr::Lit(lit) => LitOrExpr::Lit(lit),
        ast::LitOrExpr::Expr(expr) => LitOrExpr::Expr(expr),
    }
}

fn shared_linked_module<'a>(
    name: &str,
    i: &'a ast::ImportModule,
    intern: &'a Interner,
) -> Result<LinkedModule<'a>, Diagnostic> {
    Ok(LinkedModule {
        module: shared_module(i, intern, true)?,
        link_function_name: intern.intern_str(name),
    })
}

fn shared_module<'a>(
    m: &'a ast::ImportModule,
    intern: &'a Interner,
    linked_module: bool,
) -> Result<ImportModule<'a>, Diagnostic> {
    Ok(match m {
        ast::ImportModule::Named(m, span) => {
            intern.resolve_import_module(m, *span, linked_module)?
        }
        ast::ImportModule::RawNamed(m, _span) => ImportModule::RawNamed(intern.intern_str(m)),
        ast::ImportModule::Inline(idx) => ImportModule::Inline(*idx as u32),
    })
}

fn shared_import_kind<'a>(
    i: &'a ast::ImportKind,
    intern: &'a Interner,
) -> Result<ImportKind<'a>, Diagnostic> {
    Ok(match i {
        ast::ImportKind::Function(f) => ImportKind::Function(shared_import_function(f, intern)?),
        ast::ImportKind::Static(f) => ImportKind::Static(shared_import_static(f, intern)),
        ast::ImportKind::String(f) => ImportKind::String(shared_import_string(f, intern)),
        ast::ImportKind::Type(f) => ImportKind::Type(shared_import_type(f, intern)),
        ast::ImportKind::Enum(f) => ImportKind::Enum(shared_import_enum(f, intern)),
    })
}

fn shared_import_function<'a>(
    i: &'a ast::ImportFunction,
    intern: &'a Interner,
) -> Result<ImportFunction<'a>, Diagnostic> {
    let method = match &i.kind {
        ast::ImportFunctionKind::Method { class, kind, .. } => {
            let kind = from_ast_method_kind(&i.function, intern, kind)?;
            Some(MethodData { class, kind })
        }
        ast::ImportFunctionKind::Normal => None,
    };

    Ok(ImportFunction {
        shim: intern.intern(&i.shim),
        catch: i.catch,
        method,
        assert_no_shim: i.assert_no_shim,
        structural: i.structural,
        function: shared_function(&i.function, intern),
        variadic: i.variadic,
    })
}

fn shared_import_static<'a>(i: &'a ast::ImportStatic, intern: &'a Interner) -> ImportStatic<'a> {
    ImportStatic {
        name: &i.js_name,
        shim: intern.intern(&i.shim),
    }
}

fn shared_import_string<'a>(i: &'a ast::ImportString, intern: &'a Interner) -> ImportString<'a> {
    ImportString {
        shim: intern.intern(&i.shim),
        string: &i.string,
    }
}

fn shared_import_type<'a>(i: &'a ast::ImportType, intern: &'a Interner) -> ImportType<'a> {
    ImportType {
        name: &i.js_name,
        instanceof_shim: &i.instanceof_shim,
        vendor_prefixes: i.vendor_prefixes.iter().map(|x| intern.intern(x)).collect(),
    }
}

fn shared_import_enum<'a>(i: &'a ast::StringEnum, _intern: &'a Interner) -> StringEnum<'a> {
    StringEnum {
        name: &i.export_name,
        generate_typescript: i.generate_typescript,
        variant_values: i.variant_values.iter().map(|x| &**x).collect(),
        comments: i.comments.iter().map(|s| &**s).collect(),
        js_namespace: i
            .js_namespace
            .as_ref()
            .map(|ns| ns.iter().map(|s| &**s).collect()),
    }
}

fn shared_struct<'a>(s: &'a ast::Struct, intern: &'a Interner) -> Struct<'a> {
    Struct {
        name: &s.js_name,
        rust_name: intern.intern(&s.rust_name),
        fields: s
            .fields
            .iter()
            .map(|s| shared_struct_field(s, intern))
            .collect(),
        comments: s.comments.iter().map(|s| &**s).collect(),
        is_inspectable: s.is_inspectable,
        generate_typescript: s.generate_typescript,
        js_namespace: s
            .js_namespace
            .as_ref()
            .map(|ns| ns.iter().map(|s| &**s).collect()),
        private: s.private,
    }
}

fn shared_struct_field<'a>(s: &'a ast::StructField, _intern: &'a Interner) -> StructField<'a> {
    StructField {
        name: &s.js_name,
        readonly: s.readonly,
        comments: s.comments.iter().map(|s| &**s).collect(),
        generate_typescript: s.generate_typescript,
        generate_jsdoc: s.generate_jsdoc,
    }
}

trait Encode {
    fn encode(&self, dst: &mut Encoder);
}

struct Encoder {
    dst: Vec<EncodeChunk>,
}

enum LitOrExpr<'a> {
    Expr(&'a syn::Expr),
    Lit(&'a str),
}

impl Encode for LitOrExpr<'_> {
    fn encode(&self, dst: &mut Encoder) {
        match self {
            LitOrExpr::Expr(expr) => {
                dst.dst.push(EncodeChunk::StrExpr((*expr).clone()));
            }
            LitOrExpr::Lit(s) => s.encode(dst),
        }
    }
}

impl Encoder {
    fn new() -> Encoder {
        Encoder { dst: vec![] }
    }

    fn finish(self) -> Vec<EncodeChunk> {
        self.dst
    }

    fn byte(&mut self, byte: u8) {
        if let Some(EncodeChunk::EncodedBuf(buf)) = self.dst.last_mut() {
            buf.push(byte);
        } else {
            self.dst.push(EncodeChunk::EncodedBuf(vec![byte]));
        }
    }

    fn extend_from_slice(&mut self, slice: &[u8]) {
        if let Some(EncodeChunk::EncodedBuf(buf)) = self.dst.last_mut() {
            buf.extend_from_slice(slice);
        } else {
            self.dst.push(EncodeChunk::EncodedBuf(slice.to_owned()));
        }
    }
}

impl Encode for bool {
    fn encode(&self, dst: &mut Encoder) {
        dst.byte(*self as u8);
    }
}

impl Encode for u32 {
    fn encode(&self, dst: &mut Encoder) {
        let mut val = *self;
        while (val >> 7) != 0 {
            dst.byte((val as u8) | 0x80);
            val >>= 7;
        }
        assert_eq!(val >> 7, 0);
        dst.byte(val as u8);
    }
}

impl Encode for usize {
    fn encode(&self, dst: &mut Encoder) {
        assert!(*self <= u32::MAX as usize);
        (*self as u32).encode(dst);
    }
}

impl Encode for &[u8] {
    fn encode(&self, dst: &mut Encoder) {
        self.len().encode(dst);
        dst.extend_from_slice(self);
    }
}

impl Encode for &str {
    fn encode(&self, dst: &mut Encoder) {
        self.as_bytes().encode(dst);
    }
}

impl Encode for String {
    fn encode(&self, dst: &mut Encoder) {
        self.as_bytes().encode(dst);
    }
}

impl<T: Encode> Encode for Vec<T> {
    fn encode(&self, dst: &mut Encoder) {
        self.len().encode(dst);
        for item in self {
            item.encode(dst);
        }
    }
}

impl<T: Encode> Encode for Option<T> {
    fn encode(&self, dst: &mut Encoder) {
        match self {
            None => dst.byte(0),
            Some(val) => {
                dst.byte(1);
                val.encode(dst)
            }
        }
    }
}

macro_rules! encode_struct {
    ($name:ident ($($lt:tt)*) $($field:ident: $ty:ty,)*) => {
        struct $name $($lt)* {
            $($field: $ty,)*
        }

        impl $($lt)* Encode for $name $($lt)* {
            fn encode(&self, _dst: &mut Encoder) {
                $(self.$field.encode(_dst);)*
            }
        }
    }
}

macro_rules! encode_enum {
    ($name:ident ($($lt:tt)*) $($fields:tt)*) => (
        enum $name $($lt)* { $($fields)* }

        impl$($lt)* Encode for $name $($lt)* {
            fn encode(&self, dst: &mut Encoder) {
                use self::$name::*;
                encode_enum!(@arms self dst (0) () $($fields)*)
            }
        }
    );

    (@arms $me:ident $dst:ident ($cnt:expr) ($($arms:tt)*)) => (
        encode_enum!(@expr match $me { $($arms)* })
    );

    (@arms $me:ident $dst:ident ($cnt:expr) ($($arms:tt)*) $name:ident, $($rest:tt)*) => (
        encode_enum!(
            @arms
            $me
            $dst
            ($cnt+1)
            ($($arms)* $name => $dst.byte($cnt),)
            $($rest)*
        )
    );

    (@arms $me:ident $dst:ident ($cnt:expr) ($($arms:tt)*) $name:ident($t:ty), $($rest:tt)*) => (
        encode_enum!(
            @arms
            $me
            $dst
            ($cnt+1)
            ($($arms)* $name(val) => { $dst.byte($cnt); val.encode($dst) })
            $($rest)*
        )
    );

    (@expr $e:expr) => ($e);
}

macro_rules! encode_api {
    () => ();
    (struct $name:ident<'a> { $($fields:tt)* } $($rest:tt)*) => (
        encode_struct!($name (<'a>) $($fields)*);
        encode_api!($($rest)*);
    );
    (struct $name:ident { $($fields:tt)* } $($rest:tt)*) => (
        encode_struct!($name () $($fields)*);
        encode_api!($($rest)*);
    );
    (enum $name:ident<'a> { $($variants:tt)* } $($rest:tt)*) => (
        encode_enum!($name (<'a>) $($variants)*);
        encode_api!($($rest)*);
    );
    (enum $name:ident { $($variants:tt)* } $($rest:tt)*) => (
        encode_enum!($name () $($variants)*);
        encode_api!($($rest)*);
    );
}
wasm_bindgen_shared::shared_api!(encode_api);

fn from_ast_method_kind<'a>(
    function: &'a ast::Function,
    intern: &'a Interner,
    method_kind: &'a ast::MethodKind,
) -> Result<MethodKind<'a>, Diagnostic> {
    Ok(match method_kind {
        ast::MethodKind::Constructor => MethodKind::Constructor,
        ast::MethodKind::Operation(ast::Operation { is_static, kind }) => {
            let is_static = *is_static;
            let kind = match kind {
                ast::OperationKind::Getter(g) => {
                    let g = g.as_ref().map(|g| intern.intern_str(g));
                    OperationKind::Getter(g.unwrap_or_else(|| function.infer_getter_property()))
                }
                ast::OperationKind::Regular => OperationKind::Regular,
                ast::OperationKind::RegularThis => OperationKind::RegularThis,
                ast::OperationKind::Setter(s) => {
                    let s = s.as_ref().map(|s| intern.intern_str(s));
                    OperationKind::Setter(match s {
                        Some(s) => s,
                        None => intern.intern_str(&function.infer_setter_property()?),
                    })
                }
                ast::OperationKind::IndexingGetter => OperationKind::IndexingGetter,
                ast::OperationKind::IndexingSetter => OperationKind::IndexingSetter,
                ast::OperationKind::IndexingDeleter => OperationKind::IndexingDeleter,
            };
            MethodKind::Operation(Operation { is_static, kind })
        }
    })
}
