use std::io::Read;

use lua_ffi::c_str;
use lua_ffi::ffi::luaL_Reg;
use lua_ffi::lua_method;
use lua_ffi::{c_int, LuaObject, State};

use crate::common::{Direction, Rect};
use crate::framework::filesystem;
use crate::rng::RNG;
use crate::scene::game_scene::LightingMode;
use crate::scripting::lua::{check_status, LuaScriptingState, DRS_RUNTIME_GLOBAL};

pub struct Doukutsu {
    pub ptr: *mut LuaScriptingState,
}

#[allow(unused)]
impl Doukutsu {
    pub fn new(ptr: *mut LuaScriptingState) -> Doukutsu {
        Doukutsu { ptr }
    }

    unsafe fn lua_play_sfx(&self, state: &mut State) -> c_int {
        if let Some(index) = state.to_int(2) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            game_state.sound_manager.play_sfx(index as u8);
        }

        0
    }

    unsafe fn lua_play_sfx_loop(&self, state: &mut State) -> c_int {
        if let Some(index) = state.to_int(2) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            game_state.sound_manager.loop_sfx(index as u8);
        }

        0
    }

    unsafe fn lua_play_song(&self, state: &mut State) -> c_int {
        if let Some(index) = state.to_int(2) {
            let game_state = &mut (*(*self.ptr).state_ptr);
            let ctx = &mut (*(*self.ptr).ctx_ptr);

            let _ =
                game_state.sound_manager.play_song(index as usize, &game_state.constants, &game_state.settings, ctx);
        }

        0
    }

    unsafe fn lua_set_setting(&self, state: &mut State) -> c_int {
        if let Some(index) = state.to_int(2) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            state.push(game_state.get_flag(index.max(0) as usize));
        } else {
            state.push_nil();
        }

        1
    }

    unsafe fn lua_get_flag(&self, state: &mut State) -> c_int {
        if let Some(index) = state.to_int(2) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            state.push(game_state.get_flag(index.max(0) as usize));
        } else {
            state.push_nil();
        }

        1
    }

    unsafe fn lua_set_flag(&self, state: &mut State) -> c_int {
        let flag_id = state.to_int(2);
        let flag_val = state.to_bool(3);

        if let (Some(flag_id), Some(flag_val)) = (flag_id, flag_val) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            game_state.set_flag(flag_id.max(0) as usize, flag_val);
        }

        0
    }

    unsafe fn lua_get_skip_flag(&self, state: &mut State) -> c_int {
        if let Some(index) = state.to_int(2) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            state.push(game_state.get_skip_flag(index.max(0) as usize));
        } else {
            state.push_nil();
        }

        1
    }

    unsafe fn lua_set_skip_flag(&self, state: &mut State) -> c_int {
        let flag_id = state.to_int(2);
        let flag_val = state.to_bool(3);

        if let (Some(flag_id), Some(flag_val)) = (flag_id, flag_val) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            game_state.set_skip_flag(flag_id.max(0) as usize, flag_val);
        }

        0
    }

    unsafe fn lua_set_engine_constant(&self, state: &mut State) -> c_int {
        if let Some(constant_id) = state.to_int(2) {
            let game_state = &mut (*(*self.ptr).state_ptr);

            match constant_id {
                0x1000 => {
                    // intro event
                    if let Some(intro_event) = state.to_int(3) {
                        game_state.constants.game.intro_event = intro_event as u16;
                    }
                }
                0x1001 => {
                    // intro stage
                    if let Some(intro_stage) = state.to_int(3) {
                        game_state.constants.game.intro_stage = intro_stage as u16;
                    }
                }
                0x1002 => {
                    // intro pos
                    if let (Some(intro_x), Some(intro_y)) = (state.to_int(3), state.to_int(4)) {
                        game_state.constants.game.intro_player_pos = (intro_x as i16, intro_y as i16);
                    }
                }
                0x1003 => {
                    // ng event
                    if let Some(ng_event) = state.to_int(3) {
                        game_state.constants.game.new_game_event = ng_event as u16;
                    }
                }
                0x1004 => {
                    // ng stage
                    if let Some(ng_stage) = state.to_int(3) {
                        game_state.constants.game.new_game_stage = ng_stage as u16;
                    }
                }
                0x1005 => {
                    // ng pos
                    if let (Some(ng_x), Some(ng_y)) = (state.to_int(3), state.to_int(4)) {
                        game_state.constants.game.new_game_player_pos = (ng_x as i16, ng_y as i16);
                    }
                }
                0x2000 => {
                    // font scale
                    if let Some(font_scale) = state.to_float(3) {
                        if font_scale > 0.0 {
                            game_state.constants.font_scale = font_scale;
                        }
                    }
                }
                _ => {}
            }
        }

        0
    }

    unsafe fn lua_npc_command(&self, state: &mut State) -> c_int {
        if (*self.ptr).game_scene.is_null() {
            state.push_nil();
            return 1;
        }

        if let (Some(npc_id), Some(param_type)) = (state.to_int(2), state.to_int(3)) {
            let game_scene = &mut *(*self.ptr).game_scene;

            let npc = match game_scene.npc_list.get_npc(npc_id as usize) {
                Some(npc) => npc,
                None => {
                    state.push_nil();
                    return 1;
                }
            };

            match param_type {
                0x0e => state.push(npc.cond.0 as i32),
                0x0f => state.push(npc.flags.0),
                0x10 => state.push(npc.x as f32 / 512.0),
                0x11 => state.push(npc.y as f32 / 512.0),
                0x12 => state.push(npc.vel_x as f32 / 512.0),
                0x13 => state.push(npc.vel_y as f32 / 512.0),
                0x14 => state.push(npc.vel_x2 as f32 / 512.0),
                0x15 => state.push(npc.vel_y2 as f32 / 512.0),
                0x16 => state.push(npc.action_num as i32),
                0x17 => state.push(npc.anim_num as i32),
                0x18 => state.push(npc.action_counter as i32),
                0x19 => state.push(npc.action_counter2 as i32),
                0x1a => state.push(npc.action_counter3 as i32),
                0x1b => state.push(npc.anim_counter as i32),
                0x1c => state.push(npc.parent_id as i32),
                0x1d => state.push(npc.npc_type as i32),
                0x1e => state.push(npc.life as i32),
                0x1f => state.push(npc.flag_num as i32),
                0x20 => state.push(npc.event_num as i32),
                0x21 => state.push(npc.direction as i32),
                0x22 => state.push(npc.tsc_direction as i32),
                0x10e => {
                    if let Some(v) = state.to_uint(4) {
                        npc.cond.0 = v as u16;
                    }
                }
                0x10f => {
                    if let Some(v) = state.to_uint(4) {
                        npc.flags.0 = v;
                    }
                }
                0x110 => {
                    if let Some(v) = state.to_float(4) {
                        // set x
                        npc.x = (v * 512.0) as i32;
                    }
                }
                0x111 => {
                    if let Some(v) = state.to_float(4) {
                        // set y
                        npc.y = (v * 512.0) as i32;
                    }
                }
                0x112 => {
                    if let Some(v) = state.to_float(4) {
                        // set vel x
                        npc.vel_x = (v * 512.0) as i32;
                    }
                }
                0x113 => {
                    if let Some(v) = state.to_float(4) {
                        // set vel y
                        npc.vel_y = (v * 512.0) as i32;
                    }
                }
                0x114 => {
                    if let Some(v) = state.to_float(4) {
                        // set vel x 2
                        npc.vel_x2 = (v * 512.0) as i32;
                    }
                }
                0x115 => {
                    if let Some(v) = state.to_float(4) {
                        // set vel y 2
                        npc.vel_y2 = (v * 512.0) as i32;
                    }
                }
                0x116 => {
                    if let Some(v) = state.to_int(4) {
                        npc.action_num = v as u16;
                    }
                }
                0x117 => {
                    if let Some(v) = state.to_int(4) {
                        npc.anim_num = v as u16;
                    }
                }
                0x118 => {
                    if let Some(v) = state.to_int(4) {
                        npc.action_counter = v as u16;
                    }
                }
                0x119 => {
                    if let Some(v) = state.to_int(4) {
                        npc.action_counter2 = v as u16;
                    }
                }
                0x11a => {
                    if let Some(v) = state.to_int(4) {
                        npc.action_counter3 = v as u16;
                    }
                }
                0x11b => {
                    if let Some(v) = state.to_int(4) {
                        npc.anim_counter = v as u16;
                    }
                }
                0x11c => {
                    if let Some(v) = state.to_int(4) {
                        npc.parent_id = v as u16;
                    }
                }
                0x11d => {
                    if let Some(v) = state.to_int(4) {
                        npc.npc_type = v as u16;
                    }
                }
                0x11e => {
                    if let Some(v) = state.to_int(4) {
                        npc.life = v as u16;
                    }
                }
                0x11f => {
                    if let Some(v) = state.to_int(4) {
                        npc.flag_num = v as u16;
                    }
                }
                0x120 => {
                    if let Some(v) = state.to_int(4) {
                        npc.event_num = v as u16;
                    }
                }
                0x121 | 0x122 => {
                    if let Some(v) = state.to_int(4) {
                        npc.direction = Direction::from_int_facing(v as _).unwrap_or(Direction::Left);
                        npc.tsc_direction = v as _;
                    }
                }
                0x200 => {
                    // get player idx
                    let index = npc.get_closest_player_idx_mut(&[&mut game_scene.player1, &mut game_scene.player2]);
                    state.push(index as i32);
                }
                0x201 => {
                    // random
                    if let (Some(min), Some(max)) = (state.to_int(4), state.to_int(5)) {
                        if max < min {
                            state.error("max < min");
                        } else {
                            state.push(npc.rng.range(min..max));
                        }
                    } else {
                        state.error("Invalid parameters supplied.");
                    }
                }
                0x202 => {
                    // get anim rect
                    state.push(npc.anim_rect.left as i32);
                    state.push(npc.anim_rect.top as i32);
                    state.push(npc.anim_rect.right as i32);
                    state.push(npc.anim_rect.bottom as i32);
                }
                0x203 => {
                    // set anim rect
                    if let (Some(l), Some(t), Some(r), Some(b)) =
                        (state.to_int(4), state.to_int(5), state.to_int(6), state.to_int(7))
                    {
                        npc.anim_rect = Rect { left: l as u16, top: t as u16, right: r as u16, bottom: b as u16 };
                    } else {
                        state.error("Invalid parameters supplied.");
                    }
                }
                _ => state.push_nil(),
            }
        } else {
            state.push_nil()
        }

        1
    }

    unsafe fn lua_player_command(&self, state: &mut State) -> c_int {
        if (*self.ptr).game_scene.is_null() {
            state.push_nil();
            return 1;
        }

        if let (Some(player_id), Some(param_type)) = (state.to_int(2), state.to_int(3)) {
            let game_scene = &mut *(*self.ptr).game_scene;

            let player = match player_id {
                0 => &mut game_scene.player1,
                1 => &mut game_scene.player2,
                _ => {
                    state.push_nil();
                    return 1;
                }
            };

            match param_type {
                0x0e => state.push(player.cond.0 as u32),
                0x0f => state.push(player.flags.0),
                0x10 => state.push(player.x as f32 / 512.0),
                0x11 => state.push(player.y as f32 / 512.0),
                0x12 => state.push(player.vel_x as f32 / 512.0),
                0x13 => state.push(player.vel_y as f32 / 512.0),
                0x110 => {
                    if let Some(v) = state.to_float(4) {
                        // set x
                        player.x = (v * 512.0) as i32;
                    }
                }
                0x111 => {
                    if let Some(v) = state.to_float(4) {
                        // set y
                        player.y = (v * 512.0) as i32;
                    }
                }
                0x112 => {
                    if let Some(v) = state.to_float(4) {
                        // set vel x
                        player.vel_x = (v * 512.0) as i32;
                    }
                }
                0x113 => {
                    if let Some(v) = state.to_float(4) {
                        // set vel y
                        player.vel_y = (v * 512.0) as i32;
                    }
                }
                0x200 => {
                    if let Some(points) = state.to_int(4) {
                        player.damage(points, &mut (*(*self.ptr).state_ptr), &game_scene.npc_list);
                    }

                    state.push_nil();
                }
                _ => state.push_nil(),
            }
        } else {
            state.push_nil()
        }

        1
    }

    unsafe fn lua_stage_command(&self, state: &mut State) -> c_int {
        if (*self.ptr).game_scene.is_null() {
            state.push_nil();
            return 1;
        }

        if let Some(param_type) = state.to_int(2) {
            let game_scene = &mut *(*self.ptr).game_scene;
            let game_state = &mut *(*self.ptr).state_ptr;

            match param_type {
                0x01 => state.push(match game_scene.lighting_mode {
                    LightingMode::None => "none",
                    LightingMode::BackgroundOnly => "backgroundOnly",
                    LightingMode::Ambient => "ambient",
                }),
                0x02 => state.push(game_state.settings.shader_effects),
                0x03 => state.push(game_scene.stage_id as u32),
                0x101 => {
                    if let Some(v) = state.to_str(3) {
                        game_scene.lighting_mode = match v {
                            "none" => LightingMode::None,
                            "backgroundOnly" => LightingMode::BackgroundOnly,
                            "ambient" => LightingMode::Ambient,
                            _ => game_scene.lighting_mode,
                        };
                    }

                    state.push_nil();
                }
                _ => state.push_nil(),
            }
        } else {
            state.push_nil()
        }

        1
    }

    unsafe fn lua_load_script(&mut self, state: &mut State) -> c_int {
        let lua_state = &mut (*self.ptr);

        if let Some(name) = state.to_str(2) {
            let name = name.to_string();

            let ctx = &mut (*(*self.ptr).ctx_ptr);

            let path = format!("/Scripts/{}.lua", name);
            let lua_vfs_path = format!("@/Scripts/{}.lua", name);

            fn raise_error(name: &str, state: &mut State, err: &str) {
                let error_msg = format!("module '{}' not found: {}", name, err);
                state.error(&error_msg);
            }

            match filesystem::open(ctx, &path) {
                Ok(mut file) => {
                    let mut buf = Vec::new();
                    if let Err(res) = file.read_to_end(&mut buf) {
                        raise_error(&name, state, &res.to_string());
                        return 0;
                    }

                    let res = state.load_buffer(&buf, &lua_vfs_path);
                    if let Err(err) = check_status(res, state) {
                        raise_error(&name, state, &err.to_string());
                        return 0;
                    }

                    return match state.pcall(0, 1, 0) {
                        Ok(_) => 1,
                        Err((_, err)) => {
                            raise_error(&name, state, &err);
                            0
                        }
                    };
                }
                Err(err) => {
                    raise_error(&name, state, &err.to_string());
                }
            }
        }

        0
    }
}

impl LuaObject for Doukutsu {
    fn name() -> *const i8 {
        c_str!("doukutsu-rs-internal")
    }

    fn lua_fns() -> Vec<luaL_Reg> {
        vec![
            lua_method!("playSfx", Doukutsu, Doukutsu::lua_play_sfx),
            lua_method!("playSong", Doukutsu, Doukutsu::lua_play_song),
            lua_method!("getFlag", Doukutsu, Doukutsu::lua_get_flag),
            lua_method!("setFlag", Doukutsu, Doukutsu::lua_set_flag),
            lua_method!("getSkipFlag", Doukutsu, Doukutsu::lua_get_skip_flag),
            lua_method!("setSkipFlag", Doukutsu, Doukutsu::lua_set_skip_flag),
            lua_method!("setEngineConstant", Doukutsu, Doukutsu::lua_set_engine_constant),
            lua_method!("playerCommand", Doukutsu, Doukutsu::lua_player_command),
            lua_method!("npcCommand", Doukutsu, Doukutsu::lua_npc_command),
            lua_method!("stageCommand", Doukutsu, Doukutsu::lua_stage_command),
            lua_method!("loadScript", Doukutsu, Doukutsu::lua_load_script),
        ]
    }
}

impl LuaScriptingState {
    pub fn try_run_npc_hook(&mut self, npc_id: u16, npc_type: u16) -> bool {
        let mut result = false;

        if let Some(state) = &mut self.state {
            state.get_global(DRS_RUNTIME_GLOBAL);
            state.get_field(-1, "_tryNPCHook");

            state.push(npc_id as i32);
            state.push(npc_type as i32);

            if let Err((_, err)) = state.pcall(2, 1, 0) {
                log::error!("npc_hook error: {}", err);
            }

            if let Some(val) = state.to_bool(-1) {
                result = val;
            }

            state.pop(2);
        }

        result
    }
}
