use crate::core::Context;
use crate::input::{Gamepad, GamepadAxis, GamepadButton, GamepadStatus};
use crate::lua::LuaModule;
use fey_lua::{create_fill, UserDataOf};
use mlua::prelude::{LuaError, LuaResult};
use mlua::{FromLua, IntoLua, Lua, Table, UserData, UserDataMethods, UserDataRef, Value};

pub type GamepadData = UserDataOf<Gamepad>;
pub type GamepadRef = UserDataRef<Gamepad>;

pub struct GamepadModule;

impl LuaModule for GamepadModule {
    const PATH: &'static str = "Gamepad";

    #[inline]
    fn load(lua: &Lua) -> LuaResult<Value> {
        Self.into_lua(lua)
    }
}

impl UserData for GamepadModule {
    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
        methods.add_function("count", |lua, _: ()| {
            let ctx = Context::from_lua(lua);
            Ok(ctx.gamepads.count())
        });
        methods.add_function("all", |lua, fill: Option<Table>| {
            let ctx = Context::from_lua(lua);
            let fill = create_fill(lua, fill)?;
            if ctx.gamepads.count() > 0 {
                for pad in ctx.gamepads.all_lua() {
                    fill.raw_push(pad)?;
                }
                Ok(Some(fill))
            } else {
                Ok(None)
            }
        });
        methods.add_function("newly_connected", |lua, fill: Option<Table>| {
            let ctx = Context::from_lua(lua);
            let fill = create_fill(lua, fill)?;
            if ctx.gamepads.count() > 0 {
                for pad in ctx.gamepads.newly_connected_lua() {
                    fill.raw_push(pad)?;
                }
                Ok(Some(fill))
            } else {
                Ok(None)
            }
        });
        methods.add_function("last_active", |lua, _: ()| {
            let ctx = Context::from_lua(lua);
            Ok(ctx.gamepads.last_active_lua())
        });
        add_methods(methods);
    }
}

impl UserData for Gamepad {
    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
        add_methods(methods);
    }
}

fn add_methods<T, M: UserDataMethods<T>>(methods: &mut M) {
    methods.add_function("name", |lua, this: GamepadRef| this.name().into_lua(lua));
    methods.add_function("was_connected", |_, this: GamepadRef| {
        Ok(this.was_connected())
    });
    methods.add_function("charging_status", |lua, this: GamepadRef| {
        Ok(match this.charging_status() {
            GamepadStatus::Unknown => ("unknown".into_lua(lua)?, None),
            GamepadStatus::Wired => ("wired".into_lua(lua)?, None),
            GamepadStatus::Draining(p) => ("draining".into_lua(lua)?, Some(p)),
            GamepadStatus::Charging(p) => ("charging".into_lua(lua)?, Some(p)),
            GamepadStatus::Charged => ("charged".into_lua(lua)?, None),
        })
    });
    methods.add_function("down", |_, (this, btn): (GamepadRef, GamepadButton)| {
        Ok(this.down(btn))
    });
    methods.add_function("pressed", |_, (this, btn): (GamepadRef, GamepadButton)| {
        Ok(this.pressed(btn))
    });
    methods.add_function("released", |_, (this, btn): (GamepadRef, GamepadButton)| {
        Ok(this.released(btn))
    });
    methods.add_function("repeated", |_, (this, btn): (GamepadRef, GamepadButton)| {
        Ok(this.repeated(btn))
    });
    methods.add_function(
        "btn_changed",
        |_, (this, btn): (GamepadRef, GamepadButton)| Ok(this.btn_changed(btn)),
    );
    methods.add_function("value", |_, (this, btn): (GamepadRef, GamepadButton)| {
        Ok(this.value(btn))
    });
    methods.add_function("axis", |_, (this, axis): (GamepadRef, GamepadAxis)| {
        Ok(this.axis(axis))
    });
    methods.add_function(
        "axis_changed",
        |_, (this, axis): (GamepadRef, GamepadAxis)| Ok(this.axis_changed(axis)),
    );
}

pub struct GamepadButtonModule;

impl LuaModule for GamepadButtonModule {
    const PATH: &'static str = "GamepadButton";

    fn load(lua: &Lua) -> LuaResult<Value> {
        let m = lua.create_table()?;
        m.raw_set("SOUTH", GamepadButton::South)?;
        m.raw_set("EAST", GamepadButton::East)?;
        m.raw_set("WEST", GamepadButton::West)?;
        m.raw_set("NORTH", GamepadButton::North)?;
        m.raw_set("SELECT", GamepadButton::Select)?;
        m.raw_set("MENU", GamepadButton::Menu)?;
        m.raw_set("START", GamepadButton::Start)?;
        m.raw_set("LEFT_THUMB", GamepadButton::LeftThumb)?;
        m.raw_set("RIGHT_THUMB", GamepadButton::RightThumb)?;
        m.raw_set("LEFT_BUMPER", GamepadButton::LeftBumper)?;
        m.raw_set("RIGHT_BUMPER", GamepadButton::RightBumper)?;
        m.raw_set("LEFT_TRIGGER", GamepadButton::LeftTrigger)?;
        m.raw_set("RIGHT_TRIGGER", GamepadButton::RightTrigger)?;
        m.raw_set("DPAD_UP", GamepadButton::DPadUp)?;
        m.raw_set("DPAD_DOWN", GamepadButton::DPadDown)?;
        m.raw_set("DPAD_LEFT", GamepadButton::DPadLeft)?;
        m.raw_set("DPAD_RIGHT", GamepadButton::DPadRight)?;
        Ok(Value::Table(m))
    }
}

pub struct GamepadAxisModule;

impl LuaModule for GamepadAxisModule {
    const PATH: &'static str = "GamepadAxis";

    fn load(lua: &Lua) -> LuaResult<Value> {
        let m = lua.create_table()?;
        m.raw_set("LEFT_X", GamepadAxis::LeftX)?;
        m.raw_set("LEFT_Y", GamepadAxis::LeftY)?;
        m.raw_set("RIGHT_X", GamepadAxis::RightX)?;
        m.raw_set("RIGHT_Y", GamepadAxis::RightY)?;
        m.raw_set("DPAD_X", GamepadAxis::DPadX)?;
        m.raw_set("DPAD_Y", GamepadAxis::DPadY)?;
        Ok(Value::Table(m))
    }
}

impl FromLua for GamepadButton {
    #[inline]
    fn from_lua(value: Value, lua: &Lua) -> LuaResult<Self> {
        Self::from_repr(usize::from_lua(value, lua)?)
            .ok_or_else(|| LuaError::runtime("invalid gamepad button"))
    }
}

impl IntoLua for GamepadButton {
    #[inline]
    fn into_lua(self, _lua: &Lua) -> LuaResult<Value> {
        Ok(Value::Integer(self as _))
    }
}

impl FromLua for GamepadAxis {
    #[inline]
    fn from_lua(value: Value, lua: &Lua) -> LuaResult<Self> {
        Self::from_repr(usize::from_lua(value, lua)?)
            .ok_or_else(|| LuaError::runtime("invalid gamepad axis"))
    }
}

impl IntoLua for GamepadAxis {
    #[inline]
    fn into_lua(self, _lua: &Lua) -> LuaResult<Value> {
        Ok(Value::Integer(self as _))
    }
}
