/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use super::shared::*;
use crate::{CallbackIds, Config, FunctionIds, ObjectIds};
use askama::Template;
use extend::ext;
use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase};
use uniffi_bindgen::interface::{
    Argument, CallbackInterface, ComponentInterface, Constructor, Enum, Error, Field, Function,
    Literal, Method, Object, Radix, Record, Type,
};

fn arg_names(args: &[&Argument]) -> String {
    args.iter()
        .map(|arg| {
            if let Some(default_value) = arg.default_value() {
                format!("{} = {}", arg.nm(), default_value.render())
            } else {
                arg.nm()
            }
        })
        .collect::<Vec<String>>()
        .join(",")
}

fn render_enum_literal(typ: &Type, variant_name: &str) -> String {
    if let Type::Enum(enum_name) = typ {
        // TODO: This does not support complex enum literals yet.
        return format!(
            "{}.{}",
            enum_name.to_upper_camel_case(),
            variant_name.to_shouty_snake_case()
        );
    } else {
        panic!("Rendering an enum literal on a type that is not an enum")
    }
}

#[derive(Template)]
#[template(path = "js/wrapper.sys.mjs", escape = "none")]
pub struct JSBindingsTemplate<'a> {
    pub ci: &'a ComponentInterface,
    pub config: &'a Config,
    pub function_ids: &'a FunctionIds<'a>,
    pub object_ids: &'a ObjectIds<'a>,
    pub callback_ids: &'a CallbackIds<'a>,
}

impl<'a> JSBindingsTemplate<'a> {
    pub fn js_module_name(&self) -> String {
        self.js_module_name_for_ci_namespace(self.ci.namespace())
    }

    fn external_type_module(&self, crate_name: &str) -> String {
        format!(
            "resource://gre/modules/{}",
            self.js_module_name_for_crate_name(crate_name),
        )
    }

    // TODO: Once https://phabricator.services.mozilla.com/D156116 is merged maybe the next two
    // functions should use a map from the config file

    fn js_module_name_for_ci_namespace(&self, namespace: &str) -> String {
        // The plain namespace name is a bit too generic as a module name for m-c, so we
        // prefix it with "Rust". Later we'll probably allow this to be customized.
        format!("Rust{}.sys.mjs", namespace.to_upper_camel_case())
    }

    fn js_module_name_for_crate_name(&self, crate_name: &str) -> String {
        let namespace = match crate_name {
            "uniffi_geometry" => "geometry",
            s => s,
        };
        self.js_module_name_for_ci_namespace(namespace)
    }
}

// Define extension traits with methods used in our template code

#[ext(name=LiteralJSExt)]
pub impl Literal {
    fn render(&self) -> String {
        match self {
            Literal::Boolean(inner) => inner.to_string(),
            Literal::String(inner) => format!("\"{}\"", inner),
            Literal::UInt(num, radix, _) => format!("{}", radix.render_num(num)),
            Literal::Int(num, radix, _) => format!("{}", radix.render_num(num)),
            Literal::Float(num, _) => num.clone(),
            Literal::Enum(name, typ) => render_enum_literal(typ, name),
            Literal::EmptyMap => "{}".to_string(),
            Literal::EmptySequence => "[]".to_string(),
            Literal::Null => "null".to_string(),
        }
    }
}

#[ext(name=RadixJSExt)]
pub impl Radix {
    fn render_num(
        &self,
        num: impl std::fmt::Display + std::fmt::LowerHex + std::fmt::Octal,
    ) -> String {
        match self {
            Radix::Decimal => format!("{}", num),
            Radix::Hexadecimal => format!("{:#x}", num),
            Radix::Octal => format!("{:#o}", num),
        }
    }
}

#[ext(name=RecordJSExt)]
pub impl Record {
    fn nm(&self) -> String {
        self.name().to_upper_camel_case()
    }

    fn constructor_field_list(&self) -> String {
        self.fields()
            .iter()
            .map(|field| {
                if let Some(default_value) = field.default_value() {
                    format!("{} = {}", field.nm(), default_value.render())
                } else {
                    field.nm()
                }
            })
            .collect::<Vec<String>>()
            .join(",")
    }
}

#[ext(name=CallbackInterfaceJSExt)]
pub impl CallbackInterface {
    fn nm(&self) -> String {
        self.name().to_upper_camel_case()
    }

    fn handler(&self) -> String {
        format!("callbackHandler{}", self.nm())
    }
}

#[ext(name=FieldJSExt)]
pub impl Field {
    fn nm(&self) -> String {
        self.name().to_lower_camel_case()
    }

    fn lower_fn(&self) -> String {
        self.type_().lower_fn()
    }

    fn lift_fn(&self) -> String {
        self.type_().lift_fn()
    }

    fn write_datastream_fn(&self) -> String {
        self.type_().write_datastream_fn()
    }

    fn read_datastream_fn(&self) -> String {
        self.type_().read_datastream_fn()
    }

    fn compute_size_fn(&self) -> String {
        self.type_().compute_size_fn()
    }

    fn ffi_converter(&self) -> String {
        self.type_().ffi_converter()
    }
}

#[ext(name=ArgumentJSExt)]
pub impl Argument {
    fn nm(&self) -> String {
        self.name().to_lower_camel_case()
    }

    fn lower_fn(&self) -> String {
        self.type_().lower_fn()
    }

    fn lift_fn(&self) -> String {
        self.type_().lift_fn()
    }

    fn write_datastream_fn(&self) -> String {
        self.type_().write_datastream_fn()
    }

    fn read_datastream_fn(&self) -> String {
        self.type_().read_datastream_fn()
    }

    fn compute_size_fn(&self) -> String {
        self.type_().compute_size_fn()
    }

    fn ffi_converter(&self) -> String {
        self.type_().ffi_converter()
    }
}

#[ext(name=TypeJSExt)]
pub impl Type {
    // Render an expression to check if two instances of this type are equal
    fn equals(&self, first: &str, second: &str) -> String {
        match self {
            Type::Record(_) => format!("{}.equals({})", first, second),
            _ => format!("{} == {}", first, second),
        }
    }

    fn lower_fn(&self) -> String {
        format!("{}.lower", self.ffi_converter())
    }

    fn lift_fn(&self) -> String {
        format!("{}.lift", self.ffi_converter())
    }

    fn write_datastream_fn(&self) -> String {
        format!("{}.write", self.ffi_converter())
    }

    fn read_datastream_fn(&self) -> String {
        format!("{}.read", self.ffi_converter())
    }

    fn compute_size_fn(&self) -> String {
        format!("{}.computeSize", self.ffi_converter())
    }

    fn ffi_converter(&self) -> String {
        format!(
            "FfiConverter{}",
            self.canonical_name().to_upper_camel_case()
        )
    }
}

#[ext(name=EnumJSExt)]
pub impl Enum {
    fn nm(&self) -> String {
        self.name().to_upper_camel_case()
    }
}

#[ext(name=FunctionJSExt)]
pub impl Function {
    fn arg_names(&self) -> String {
        arg_names(self.arguments().as_slice())
    }

    fn nm(&self) -> String {
        self.name().to_lower_camel_case()
    }
}

#[ext(name=ErrorJSExt)]
pub impl Error {
    fn nm(&self) -> String {
        self.name().to_upper_camel_case()
    }
}

#[ext(name=ObjectJSExt)]
pub impl Object {
    fn nm(&self) -> String {
        self.name().to_upper_camel_case()
    }
}

#[ext(name=ConstructorJSExt)]
pub impl Constructor {
    fn nm(&self) -> String {
        if self.is_primary_constructor() {
            "init".to_string()
        } else {
            self.name().to_lower_camel_case()
        }
    }

    fn arg_names(&self) -> String {
        arg_names(&self.arguments().as_slice())
    }
}

#[ext(name=MethodJSExt)]
pub impl Method {
    fn arg_names(&self) -> String {
        arg_names(self.arguments().as_slice())
    }

    fn nm(&self) -> String {
        self.name().to_lower_camel_case()
    }
}
