use crate::{NodeZoomLayerMap, TilePoint, Zoom};
use mlua::{Lua, Result as LuaResult, UserData};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

pub fn load_lua_script(lua: &Lua, lua_code: &str) -> LuaResult<mlua::Function> {
    // Execute the Lua script to define the functions
    lua.load(lua_code).exec()?;

    // Get the `node_function` from the Lua globals
    let globals = lua.globals();
    let node_function: mlua::Function = globals.get("node_function")?;
    Ok(node_function)
}

pub struct LuaState {
    pub lua: Lua,
    pub node_function: mlua::Function,
}

pub enum LuaProcess {
    None,
    Some(LuaState),
}

#[derive(Debug, Clone)]
pub struct OsmNode {
    pub id: i64,
    pub zoom: Zoom,
    pub tile_point: TilePoint,
    tags: HashMap<String, String>,
    osm_type: String,
}

impl OsmNode {
    pub fn new(id: i64, zoom: Zoom, tile_point: TilePoint, tags: HashMap<String, String>) -> Self {
        Self {
            id,
            zoom,
            tile_point,
            tags,
            osm_type: "node".to_string(),
        }
    }
}

// Implement `UserData` for `OsmNode` to expose it to Lua
impl UserData for OsmNode {
    fn add_methods<'lua, M: mlua::UserDataMethods<Self>>(methods: &mut M) {
        methods.add_method("find", |_, node, key: String| {
            // Return the value for the given key, or nil if the key doesn't exist
            Ok(node.tags.get(&key).cloned())
        });
        methods.add_method("get_id", |_, node, ()| Ok(node.id));
        methods.add_method("id", |_, node, ()| Ok(node.id));
        methods.add_method("get_lat", |_, node, ()| Ok(node.tile_point.x));
        methods.add_method("get_lon", |_, node, ()| Ok(node.tile_point.y));
        methods.add_method("osm_type", |_, node, ()| Ok(node.osm_type.clone()));
        methods.add_method("holds", |_, node, (key, value): (String, Option<String>)| {
            Ok(node.tags.get(&key).map_or(false, |v| value.map_or(true, |val| v == &val)))
        });
    }
}

pub fn call_node_function(
    lua: &Lua,
    node_function: &mlua::Function,
    node: OsmNode,
    layered_nodes: Rc<RefCell<NodeZoomLayerMap>>,
) -> LuaResult<()> {
    // Create a Lua table for the output function
    let output = lua.create_function(|_, (node,): (mlua::Value,)| {
        println!("Output node: {:?}", node);
        Ok(())
    })?;

    let find_function = create_find_function(lua, node.clone())?;
    lua.globals().set("Find", find_function)?;

    let all_tags_function = create_all_tags_function(lua, node.clone())?;
    lua.globals().set("AllTags", all_tags_function)?;

    let layer_function = create_layer_function(lua, layered_nodes.clone(), node.clone())?;
    lua.globals().set("Layer", layer_function)?;

    let attribute_function = create_attribute_function(lua, layered_nodes.clone(), node.clone())?;
    lua.globals().set("Attribute", attribute_function)?;

    let id_function = create_id_function(lua, node.clone())?;
    lua.globals().set("Id", id_function)?;

    let osm_type_function = create_osm_type_function(lua, node.clone())?;
    lua.globals().set("OsmType", osm_type_function)?;

    let holds_function = create_holds_function(lua, node.clone())?;
    lua.globals().set("Holds", holds_function)?;

    // Call the Lua function with the node and output
    node_function.call((node, output))
}

fn create_find_function(lua: &Lua, node: OsmNode) -> LuaResult<mlua::Function> {
    lua.create_function(move |_, key: String| Ok(node.tags.get(&key).cloned()))
}

fn create_id_function(lua: &Lua, node: OsmNode) -> LuaResult<mlua::Function> {
    lua.create_function(move |_, ()| Ok(node.id))
}

fn create_osm_type_function(lua: &Lua, node: OsmNode) -> LuaResult<mlua::Function> {
    lua.create_function(move |_, ()| Ok(node.osm_type.clone()))
}

fn create_holds_function(lua: &Lua, node: OsmNode) -> LuaResult<mlua::Function> {
    use mlua::MultiValue;
    lua.create_function(move |_, args: MultiValue| {
        let args: Vec<mlua::Value> = args.into_vec();
        if args.is_empty() {
            return Err(mlua::Error::RuntimeError(
                "Holds expects at least 1 argument (key)".to_string(),
            ));
        }
        let key = args[0].to_string()?;
        let value: Option<String> = if args.len() >= 2 {
            match &args[1] {
                mlua::Value::String(s) => Some(s.to_str()?.to_owned()),
                mlua::Value::Nil => None,
                _ => {
                    return Err(mlua::Error::RuntimeError(
                        "Holds expects string or nil as second argument".to_string(),
                    ));
                }
            }
        } else {
            None
        };
        Ok(node.tags.get(&key).map_or(false, |v| value.map_or(true, |val| v == &val)))
    })
}

fn create_all_tags_function(lua: &Lua, node: OsmNode) -> LuaResult<mlua::Function> {
    lua.create_function(move |lua_ctx, ()| {
        let table = lua_ctx.create_table()?;
        for (key, value) in &node.tags {
            table.set(key.clone(), value.clone())?;
        }
        Ok(table)
    })
}

fn create_attribute_function(
    lua: &Lua,
    layered_nodes: Rc<RefCell<NodeZoomLayerMap>>,
    node: OsmNode,
) -> LuaResult<mlua::Function> {
    use mlua::MultiValue;
    lua.create_function(move |_, args: MultiValue| {
        let args: Vec<mlua::Value> = args.into_vec();
        if args.is_empty() {
            return Err(mlua::Error::RuntimeError(
                "Attribute expects at least 1 argument (key)".to_string(),
            ));
        }
        let key = args[0].to_string()?;
        let value_str = if args.len() >= 2 {
            match &args[1] {
                mlua::Value::String(s) => s.to_str()?.to_owned(),
                mlua::Value::Number(n) => n.to_string(),
                mlua::Value::Boolean(b) => b.to_string(),
                mlua::Value::Nil => String::new(),
                _ => {
                    return Err(mlua::Error::RuntimeError(
                        "Unsupported attribute value type".to_string(),
                    ));
                }
            }
        } else {
            String::new() // No value provided, use empty string
        };

        Ok(layered_nodes
            .clone()
            .borrow_mut()
            .add_attribute(&node, &key, &value_str))
    })
}

fn create_layer_function(
    lua: &Lua,
    layered_nodes: Rc<RefCell<NodeZoomLayerMap>>,
    node: OsmNode,
) -> LuaResult<mlua::Function> {
    lua.create_function(move |_, layer_name: String| {
        Ok(layered_nodes.clone().borrow_mut().insert(&node, layer_name))
    })
}
