use mlua::{ExternalError, LuaSerdeExt};
use pastey::paste;

pub fn register_to_lua(lua: &mlua::Lua) -> mlua::Result<()> {
    json_encode(lua)?;
    json_decode(lua)?;

    json5_encode(lua)?;
    json5_decode(lua)?;

    yaml_encode(lua)?;
    yaml_decode(lua)?;

    ini_encode(lua)?;
    ini_decode(lua)?;

    toml_encode(lua)?;
    toml_decode(lua)?;

    csv_decode(lua)?;

    xml_encode(lua)?;
    xml_decode(lua)?;

    Ok(())
}

pub fn sanetize_lua_input(lua: &mlua::Lua, input: mlua::Value) -> mlua::Result<mlua::Value> {
    if let Some(input) = input.as_table() {
        let new_input = lua.create_table()?;

        for pair in input.pairs::<mlua::Value, mlua::Value>() {
            let (key, value) = pair?;
            if !value.is_function()
                && !value.is_light_userdata()
                && !value.is_userdata()
                && !value.is_error()
                && !value.is_thread()
            {
                new_input.set(key, value)?;
            }
        }

        lua.to_value(&new_input)
    } else {
        Ok(input)
    }
}

macro_rules! gen_methods {
    ($crate_name:ident, $name:ident) => {
        paste! {
            fn [<$name _encode>](lua: &mlua::Lua) -> mlua::Result<()> {
                lua.globals().set(
                    "astra_internal__".to_string() + stringify!($name) + "_encode",
                    lua.create_function(|lua, input: mlua::Value| {
                        let value =
                            lua.from_value::<serde_value::Value>(sanetize_lua_input(&lua, input)?)?;
                        match $crate_name::to_string(&value) {
                            Ok(serialized) => Ok(lua.to_value(&serialized)?),
                            Err(e) => Err(e.into_lua_err()),
                        }
                    })?,
                )
            }

            fn [<$name _decode>](lua: &mlua::Lua) -> mlua::Result<()> {
                lua.globals().set(
                    "astra_internal__".to_string() + stringify!($name) + "_decode",
                    lua.create_function(|lua, input: String| {
                        match $crate_name::from_str::<serde_value::Value>(&input) {
                            Ok(deserialized) => lua.to_value(&deserialized),
                            Err(e) => Err(e.into_lua_err()),
                        }
                    })?,
                )
            }
        }
    };
}

gen_methods!(serde_json, json);
gen_methods!(serde_json5, json5);
gen_methods!(serde_yaml, yaml);
gen_methods!(serde_ini, ini);
gen_methods!(toml, toml);

fn xml_encode(lua: &mlua::Lua) -> mlua::Result<()> {
    lua.globals().set(
        "astra_internal__xml_encode",
        lua.create_async_function(|lua, (root, input): (String, mlua::Value)| async move {
            //
            let value = lua.from_value::<serde_value::Value>(sanetize_lua_input(&lua, input)?)?;
            match quick_xml::se::to_string_with_root(&root, &value) {
                Ok(serialized) => Ok(lua.to_value(&serialized)?),
                Err(e) => Err(e.into_lua_err()),
            }
        })?,
    )
}

fn xml_decode(lua: &mlua::Lua) -> mlua::Result<()> {
    lua.globals().set(
        "astra_internal__xml_decode",
        lua.create_function(|lua, input: String| {
            let result = quick_xml::de::from_str::<serde_value::Value>(&input);

            match result {
                Ok(res) => lua.to_value(&res),
                Err(e) => Err(e.into_lua_err()),
            }
        })?,
    )
}

fn csv_decode(lua: &mlua::Lua) -> mlua::Result<()> {
    lua.globals().set(
        "astra_internal__csv_decode",
        lua.create_function(|lua, (input, settings): (String, Option<mlua::Table>)| {
            let mut reader = csv::ReaderBuilder::new();

            if let Some(settings) = settings {
                if let Ok(buffer_capacity) = settings.get::<usize>("buffer_capacity") {
                    reader.buffer_capacity(buffer_capacity);
                }

                if let Ok(value) = settings.get::<String>("delimiter")
                    && let Some(value) = value.as_bytes().first()
                {
                    reader.delimiter(*value);
                }

                if let Ok(value) = settings.get::<String>("quote")
                    && let Some(value) = value.as_bytes().first()
                {
                    reader.quote(*value);
                }

                macro_rules! gen_fields {
                    ($field:ident) => {
                        if let Ok(value) = settings.get::<bool>(stringify!($field)) {
                            reader.$field(value);
                        }
                    };
                    ($field:ident, $type:ty) => {
                        if let Ok(value) = settings.get::<$type>(stringify!($field)) {
                            reader.$field(value.as_bytes().first().cloned());
                        }
                    };
                }

                gen_fields!(flexible);
                gen_fields!(quoting);
                gen_fields!(double_quote);
                gen_fields!(has_headers);
                gen_fields!(escape, String);
                gen_fields!(comment, String);
            }

            let mut reader = reader.from_reader(input.as_bytes());

            let header = reader
                .headers()
                .and_then(|i| i.deserialize::<Vec<serde_value::Value>>(None))
                .ok();
            let body = reader
                .into_byte_records()
                .filter_map(|x| {
                    x.and_then(|i| i.deserialize::<Vec<serde_value::Value>>(None))
                        .ok()
                })
                .collect::<Vec<_>>();

            lua.to_value(&(body, header))
        })?,
    )
}
