use anyhow::{bail, Result};
use askama::Template;
use camino::Utf8Path;
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::collections::HashMap;
use crate::bindings::ruby;
use crate::interface::*;
use crate::{BindingGenerator, BindingsConfig};
pub struct RubyBindingGenerator;
impl BindingGenerator for RubyBindingGenerator {
    type Config = Config;
    fn write_bindings(
        &self,
        ci: &ComponentInterface,
        config: &Config,
        out_dir: &Utf8Path,
        try_format_code: bool,
    ) -> Result<()> {
        ruby::write_bindings(config, ci, out_dir, try_format_code)
    }
    fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()> {
        if cdylib_name.is_none() {
            bail!("Generate bindings for Ruby requires a cdylib, but {library_path} was given");
        }
        Ok(())
    }
}
const RESERVED_WORDS: &[&str] = &[
    "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else",
    "elsif", "END", "end", "ensure", "false", "for", "if", "module", "next", "nil", "not", "or",
    "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless",
    "until", "when", "while", "yield", "__FILE__", "__LINE__",
];
fn is_reserved_word(word: &str) -> bool {
    RESERVED_WORDS.contains(&word)
}
pub fn canonical_name(t: &Type) -> String {
    match t {
        Type::Int8 => "i8".into(),
        Type::UInt8 => "u8".into(),
        Type::Int16 => "i16".into(),
        Type::UInt16 => "u16".into(),
        Type::Int32 => "i32".into(),
        Type::UInt32 => "u32".into(),
        Type::Int64 => "i64".into(),
        Type::UInt64 => "u64".into(),
        Type::Float32 => "f32".into(),
        Type::Float64 => "f64".into(),
        Type::String => "string".into(),
        Type::Bytes => "bytes".into(),
        Type::Boolean => "bool".into(),
        Type::Object { name, .. } => format!("Type{name}"),
        Type::Enum { name, .. } => format!("Type{name}"),
        Type::Record { name, .. } => format!("Type{name}"),
        Type::CallbackInterface { name, .. } => format!("CallbackInterface{name}"),
        Type::Timestamp => "Timestamp".into(),
        Type::Duration => "Duration".into(),
        Type::Optional { inner_type } => format!("Optional{}", canonical_name(inner_type)),
        Type::Sequence { inner_type } => format!("Sequence{}", canonical_name(inner_type)),
        Type::Map {
            key_type,
            value_type,
        } => format!(
            "Map{}{}",
            canonical_name(key_type).to_upper_camel_case(),
            canonical_name(value_type).to_upper_camel_case()
        ),
        Type::External { name, .. } | Type::Custom { name, .. } => format!("Type{name}"),
    }
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
    cdylib_name: Option<String>,
    cdylib_path: Option<String>,
}
impl Config {
    pub fn cdylib_name(&self) -> String {
        self.cdylib_name
            .clone()
            .unwrap_or_else(|| "uniffi".to_string())
    }
    pub fn custom_cdylib_path(&self) -> bool {
        self.cdylib_path.is_some()
    }
    pub fn cdylib_path(&self) -> String {
        self.cdylib_path.clone().unwrap_or_default()
    }
}
impl BindingsConfig for Config {
    fn update_from_ci(&mut self, ci: &ComponentInterface) {
        self.cdylib_name
            .get_or_insert_with(|| format!("uniffi_{}", ci.namespace()));
    }
    fn update_from_cdylib_name(&mut self, cdylib_name: &str) {
        self.cdylib_name
            .get_or_insert_with(|| cdylib_name.to_string());
    }
    fn update_from_dependency_configs(&mut self, _config_map: HashMap<&str, &Self>) {}
}
#[derive(Template)]
#[template(syntax = "rb", escape = "none", path = "wrapper.rb")]
pub struct RubyWrapper<'a> {
    config: Config,
    ci: &'a ComponentInterface,
    canonical_name: &'a dyn Fn(&Type) -> String,
}
impl<'a> RubyWrapper<'a> {
    pub fn new(config: Config, ci: &'a ComponentInterface) -> Self {
        Self {
            config,
            ci,
            canonical_name: &canonical_name,
        }
    }
}
mod filters {
    use super::*;
    pub use crate::backend::filters::*;
    pub fn type_ffi(type_: &FfiType) -> Result<String, askama::Error> {
        Ok(match type_ {
            FfiType::Int8 => ":int8".to_string(),
            FfiType::UInt8 => ":uint8".to_string(),
            FfiType::Int16 => ":int16".to_string(),
            FfiType::UInt16 => ":uint16".to_string(),
            FfiType::Int32 => ":int32".to_string(),
            FfiType::UInt32 => ":uint32".to_string(),
            FfiType::Int64 => ":int64".to_string(),
            FfiType::UInt64 => ":uint64".to_string(),
            FfiType::Float32 => ":float".to_string(),
            FfiType::Float64 => ":double".to_string(),
            FfiType::Handle => ":uint64".to_string(),
            FfiType::RustArcPtr(_) => ":pointer".to_string(),
            FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(),
            FfiType::RustCallStatus => "RustCallStatus".to_string(),
            FfiType::ForeignBytes => "ForeignBytes".to_string(),
            FfiType::Callback(_) => unimplemented!("FFI Callbacks not implemented"),
            FfiType::Reference(_) => ":pointer".to_string(),
            FfiType::VoidPointer => ":pointer".to_string(),
            FfiType::Struct(_) => {
                unimplemented!("Structs are not implemented")
            }
        })
    }
    pub fn literal_rb(literal: &Literal) -> Result<String, askama::Error> {
        Ok(match literal {
            Literal::Boolean(v) => {
                if *v {
                    "true".into()
                } else {
                    "false".into()
                }
            }
            Literal::String(s) => format!("\"{s}\""),
            Literal::None => "nil".into(),
            Literal::Some { inner } => literal_rb(inner)?,
            Literal::EmptySequence => "[]".into(),
            Literal::EmptyMap => "{}".into(),
            Literal::Enum(v, type_) => match type_ {
                Type::Enum { name, .. } => {
                    format!("{}::{}", class_name_rb(name)?, enum_name_rb(v)?)
                }
                _ => panic!("Unexpected type in enum literal: {type_:?}"),
            },
            Literal::Int(i, radix, _) => match radix {
                Radix::Octal => format!("0o{i:o}"),
                Radix::Decimal => format!("{i}"),
                Radix::Hexadecimal => format!("{i:#x}"),
            },
            Literal::UInt(i, radix, _) => match radix {
                Radix::Octal => format!("0o{i:o}"),
                Radix::Decimal => format!("{i}"),
                Radix::Hexadecimal => format!("{i:#x}"),
            },
            Literal::Float(string, _type_) => string.clone(),
        })
    }
    pub fn class_name_rb(nm: &str) -> Result<String, askama::Error> {
        Ok(nm.to_string().to_upper_camel_case())
    }
    pub fn fn_name_rb(nm: &str) -> Result<String, askama::Error> {
        Ok(nm.to_string().to_snake_case())
    }
    pub fn var_name_rb(nm: &str) -> Result<String, askama::Error> {
        let nm = nm.to_string();
        let prefix = if is_reserved_word(&nm) { "_" } else { "" };
        Ok(format!("{prefix}{}", nm.to_snake_case()))
    }
    pub fn enum_name_rb(nm: &str) -> Result<String, askama::Error> {
        Ok(nm.to_string().to_shouty_snake_case())
    }
    pub fn coerce_rb(nm: &str, ns: &str, type_: &Type) -> Result<String, askama::Error> {
        Ok(match type_ {
            Type::Int8 => format!("{ns}::uniffi_in_range({nm}, \"i8\", -2**7, 2**7)"),
            Type::Int16 => format!("{ns}::uniffi_in_range({nm}, \"i16\", -2**15, 2**15)"),
            Type::Int32 => format!("{ns}::uniffi_in_range({nm}, \"i32\", -2**31, 2**31)"),
            Type::Int64 => format!("{ns}::uniffi_in_range({nm}, \"i64\", -2**63, 2**63)"),
            Type::UInt8 => format!("{ns}::uniffi_in_range({nm}, \"u8\", 0, 2**8)"),
            Type::UInt16 => format!("{ns}::uniffi_in_range({nm}, \"u16\", 0, 2**16)"),
            Type::UInt32 => format!("{ns}::uniffi_in_range({nm}, \"u32\", 0, 2**32)"),
            Type::UInt64 => format!("{ns}::uniffi_in_range({nm}, \"u64\", 0, 2**64)"),
            Type::Float32 | Type::Float64 => nm.to_string(),
            Type::Boolean => format!("{nm} ? true : false"),
            Type::Object { .. } | Type::Enum { .. } | Type::Record { .. } => nm.to_string(),
            Type::String => format!("{ns}::uniffi_utf8({nm})"),
            Type::Bytes => format!("{ns}::uniffi_bytes({nm})"),
            Type::Timestamp | Type::Duration => nm.to_string(),
            Type::CallbackInterface { .. } => {
                panic!("No support for coercing callback interfaces yet")
            }
            Type::Optional { inner_type: t } => format!("({nm} ? {} : nil)", coerce_rb(nm, ns, t)?),
            Type::Sequence { inner_type: t } => {
                let coerce_code = coerce_rb("v", ns, t)?;
                if coerce_code == "v" {
                    nm.to_string()
                } else {
                    format!("{nm}.map {{ |v| {coerce_code} }}")
                }
            }
            Type::Map { value_type: t, .. } => {
                let k_coerce_code = coerce_rb("k", ns, &Type::String)?;
                let v_coerce_code = coerce_rb("v", ns, t)?;
                if k_coerce_code == "k" && v_coerce_code == "v" {
                    nm.to_string()
                } else {
                    format!(
                        "{nm}.each.with_object({{}}) {{ |(k, v), res| res[{k_coerce_code}] = {v_coerce_code} }}"
                    )
                }
            }
            Type::External { .. } => panic!("No support for external types, yet"),
            Type::Custom { .. } => panic!("No support for custom types, yet"),
        })
    }
    pub fn check_lower_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> {
        Ok(match type_ {
            Type::Object { name, .. } => {
                format!("({}.uniffi_check_lower {nm})", class_name_rb(name)?)
            }
            Type::Enum { .. }
            | Type::Record { .. }
            | Type::Optional { .. }
            | Type::Sequence { .. }
            | Type::Map { .. } => format!(
                "RustBuffer.check_lower_{}({})",
                class_name_rb(&canonical_name(type_))?,
                nm
            ),
            _ => "".to_owned(),
        })
    }
    pub fn lower_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> {
        Ok(match type_ {
            Type::Int8
            | Type::UInt8
            | Type::Int16
            | Type::UInt16
            | Type::Int32
            | Type::UInt32
            | Type::Int64
            | Type::UInt64
            | Type::Float32
            | Type::Float64 => nm.to_string(),
            Type::Boolean => format!("({nm} ? 1 : 0)"),
            Type::String => format!("RustBuffer.allocFromString({nm})"),
            Type::Bytes => format!("RustBuffer.allocFromBytes({nm})"),
            Type::Object { name, .. } => format!("({}.uniffi_lower {nm})", class_name_rb(name)?),
            Type::CallbackInterface { .. } => {
                panic!("No support for lowering callback interfaces yet")
            }
            Type::Enum { .. }
            | Type::Record { .. }
            | Type::Optional { .. }
            | Type::Sequence { .. }
            | Type::Timestamp
            | Type::Duration
            | Type::Map { .. } => format!(
                "RustBuffer.alloc_from_{}({})",
                class_name_rb(&canonical_name(type_))?,
                nm
            ),
            Type::External { .. } => panic!("No support for lowering external types, yet"),
            Type::Custom { .. } => panic!("No support for lowering custom types, yet"),
        })
    }
    pub fn lift_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> {
        Ok(match type_ {
            Type::Int8
            | Type::UInt8
            | Type::Int16
            | Type::UInt16
            | Type::Int32
            | Type::UInt32
            | Type::Int64
            | Type::UInt64 => format!("{nm}.to_i"),
            Type::Float32 | Type::Float64 => format!("{nm}.to_f"),
            Type::Boolean => format!("1 == {nm}"),
            Type::String => format!("{nm}.consumeIntoString"),
            Type::Bytes => format!("{nm}.consumeIntoBytes"),
            Type::Object { name, .. } => format!("{}.uniffi_allocate({nm})", class_name_rb(name)?),
            Type::CallbackInterface { .. } => {
                panic!("No support for lifting callback interfaces, yet")
            }
            Type::Enum { .. } => {
                format!(
                    "{}.consumeInto{}",
                    nm,
                    class_name_rb(&canonical_name(type_))?
                )
            }
            Type::Record { .. }
            | Type::Optional { .. }
            | Type::Sequence { .. }
            | Type::Timestamp
            | Type::Duration
            | Type::Map { .. } => format!(
                "{}.consumeInto{}",
                nm,
                class_name_rb(&canonical_name(type_))?
            ),
            Type::External { .. } => panic!("No support for lifting external types, yet"),
            Type::Custom { .. } => panic!("No support for lifting custom types, yet"),
        })
    }
}
#[cfg(test)]
mod test_type {
    use super::*;
    #[test]
    fn test_canonical_names() {
        assert_eq!(canonical_name(&Type::UInt8), "u8");
        assert_eq!(canonical_name(&Type::String), "string");
        assert_eq!(canonical_name(&Type::Bytes), "bytes");
        assert_eq!(
            canonical_name(&Type::Optional {
                inner_type: Box::new(Type::Sequence {
                    inner_type: Box::new(Type::Object {
                        module_path: "anything".to_string(),
                        name: "Example".into(),
                        imp: ObjectImpl::Struct,
                    })
                })
            }),
            "OptionalSequenceTypeExample"
        );
    }
}
#[cfg(test)]
mod tests;