use std::sync::Arc;

use iced::Task;
use ql_core::{
    Instance, IntoIoError, IntoJsonError, IntoStringError, JsonFileError, LAUNCHER_DIR, err,
    json::{
        InstanceConfigJson,
        instance_config::{CustomJarConfig, MainClassMode},
    },
    sanitize_instance_name,
};

use crate::{
    config::sidebar::SidebarSelection,
    state::{
        ADD_JAR_NAME, AutoSaveKind, CustomJarState, EditInstanceMessage, LaunchTab, Launcher,
        MainMenuMessage, MenuCreateInstance, MenuEditInstance, MenuLaunch, Message, NONE_JAR_NAME,
        OPEN_FOLDER_JAR_NAME, ProgressBar, REMOVE_JAR_NAME, State, dir_watch, get_entries,
    },
};

macro_rules! iflet_config {
    ($state:expr, $config:ident <- $body:block) => {
        if let State::Launch(MenuLaunch {
            edit_instance: Some(MenuEditInstance {
                config: $config, ..
            }),
            ..
        }) = $state
        $body
    };

    ($state:expr, $field:ident : $pat:pat, $body:block) => {
        if let State::Launch(MenuLaunch {
            edit_instance: Some(MenuEditInstance {
                config: InstanceConfigJson {
                    $field: $pat,
                    ..
                },
                ..
            }),
            ..
        }) = $state
        $body
    };

    ($state:expr, $field:ident, $body:block) => {
        iflet_config!($state, $field : $field, $body);
    };

    ($state:expr, prefix, |$prefix:ident| $body:block) => {
        iflet_config!($state, global_settings: global_settings, {
            let global_settings =
                global_settings.get_or_insert_default();
            let $prefix =
                &mut global_settings.pre_launch_prefix;
            $body
        });
    };
}

impl Launcher {
    pub fn update_edit_instance(
        &mut self,
        message: EditInstanceMessage,
    ) -> Result<Task<Message>, String> {
        match message {
            EditInstanceMessage::ToggleSplitArg(t) => {
                if let State::Launch(MenuLaunch {
                    edit_instance: Some(menu),
                    ..
                }) = &mut self.state
                {
                    menu.arg_split_by_space = t;
                } else if let State::LauncherSettings(menu) = &mut self.state {
                    menu.arg_split_by_space = t;
                }
            }
            EditInstanceMessage::JavaOverride(n) => {
                iflet_config!(&mut self.state, config <- {
                    config.java_override = Some(n);
                    config.java_override_version = None;
                });
            }
            EditInstanceMessage::JavaOverrideVersion(n) => {
                iflet_config!(&mut self.state, config <- {
                    config.java_override_version = Some(n);
                });
            }
            EditInstanceMessage::BrowseJavaOverride => {
                if let Some(file) = rfd::FileDialog::new()
                    .set_title("Select Java Executable (./bin/java)")
                    .pick_file()
                {
                    iflet_config!(&mut self.state, config <- {
                        config.java_override = Some(file.to_string_lossy().to_string());
                    });
                }
            }
            EditInstanceMessage::MemoryChanged(new_slider_value) => {
                if let State::Launch(MenuLaunch {
                    edit_instance: Some(menu),
                    ..
                }) = &mut self.state
                {
                    menu.slider_value = new_slider_value;
                    menu.config.ram_in_mb = 2f32.powf(new_slider_value) as usize;
                    menu.slider_text = format_memory(menu.config.ram_in_mb);
                    menu.memory_input = menu.config.ram_in_mb.to_string();
                }
            }
            EditInstanceMessage::MemoryInputChanged(input) => {
                if let State::Launch(MenuLaunch {
                    edit_instance: Some(menu),
                    ..
                }) = &mut self.state
                {
                    if let Ok(mb) = input.parse::<usize>() {
                        if mb > 0 {
                            menu.config.ram_in_mb = mb;
                            menu.slider_value = f32::log2(mb as f32);
                            menu.slider_text = format_memory(mb);
                        }
                    }
                    menu.memory_input = input;
                }
            }
            EditInstanceMessage::LoggingToggle(t) => iflet_config!(&mut self.state, config <- {
                config.enable_logger = Some(t);
            }),
            EditInstanceMessage::JavaArgsModeChanged(mode) => {
                iflet_config!(&mut self.state, global_java_args_enable, {
                    *global_java_args_enable = Some(mode);
                });
            }
            EditInstanceMessage::JavaArgs(msg) => {
                let split = self.should_split_args();
                iflet_config!(&mut self.state, java_args, {
                    msg.apply(java_args.get_or_insert_default(), split);
                });
            }
            EditInstanceMessage::GameArgs(msg) => {
                let split = self.should_split_args();
                iflet_config!(&mut self.state, game_args, {
                    msg.apply(game_args.get_or_insert_default(), split);
                });
            }
            EditInstanceMessage::PreLaunchPrefix(msg) => {
                let split = self.should_split_args();
                iflet_config!(&mut self.state, prefix, |pre_launch_prefix| {
                    msg.apply(pre_launch_prefix.get_or_insert_default(), split);
                });
            }
            EditInstanceMessage::PreLaunchPrefixModeChanged(mode) => {
                iflet_config!(&mut self.state, pre_launch_prefix_mode, {
                    *pre_launch_prefix_mode = Some(mode);
                });
            }
            EditInstanceMessage::RenameToggle => {
                if let State::Launch(MenuLaunch {
                    edit_instance: Some(menu),
                    ..
                }) = &mut self.state
                {
                    self.selected_instance
                        .as_ref()
                        .unwrap()
                        .get_name()
                        .clone_into(&mut menu.instance_name);
                    menu.is_editing_name = !menu.is_editing_name;
                }
            }
            EditInstanceMessage::RenameEdit(n) => {
                if let State::Launch(MenuLaunch {
                    edit_instance: Some(menu),
                    ..
                }) = &mut self.state
                {
                    menu.instance_name = n;
                }
            }
            EditInstanceMessage::RenameApply => return self.rename_instance(),
            EditInstanceMessage::ConfigSaved(res) => res?,
            EditInstanceMessage::WindowWidthChanged(width) => {
                iflet_config!(&mut self.state, config <- {
                    config.c_global_settings().window_width = width.parse::<u32>().ok();
                });
            }
            EditInstanceMessage::WindowHeightChanged(height) => {
                iflet_config!(&mut self.state, config <- {
                    config.c_global_settings().window_height = height.parse::<u32>().ok();
                });
            }
            EditInstanceMessage::CustomJarPathChanged(path) => {
                if path == ADD_JAR_NAME {
                    return Ok(self.add_custom_jar());
                } else if let State::Launch(MenuLaunch {
                    edit_instance: Some(menu),
                    ..
                }) = &mut self.state
                {
                    if path == REMOVE_JAR_NAME {
                        if let (Some(jar), Some(list)) =
                            (&menu.config.custom_jar, &mut self.custom_jar)
                        {
                            list.choices.retain(|n| *n != jar.name);
                            let name = jar.name.clone();
                            menu.config.custom_jar = None;
                            return Ok(Task::perform(
                                tokio::fs::remove_file(LAUNCHER_DIR.join("custom_jars").join(name)),
                                |_| Message::Nothing,
                            ));
                        }
                    } else if path == NONE_JAR_NAME {
                        menu.config.custom_jar = None;
                    } else if path == OPEN_FOLDER_JAR_NAME {
                        return Ok(Task::done(Message::CoreOpenPath(
                            LAUNCHER_DIR.join("custom_jars"),
                        )));
                    } else {
                        menu.config.custom_jar.get_or_insert_default().name = path;
                    }
                }
            }
            EditInstanceMessage::CustomJarLoaded(items) => match items {
                Ok(items) => {
                    return Ok(self.loaded_custom_jar(items));
                }
                Err(err) => err!("Couldn't load list of custom jars (1)! {err}"),
            },
            EditInstanceMessage::SetMainClass(t, cls) => {
                if let State::Launch(MenuLaunch {
                    edit_instance:
                        Some(MenuEditInstance {
                            config,
                            main_class_mode,
                            ..
                        }),
                    ..
                }) = &mut self.state
                {
                    *main_class_mode = t;
                    let (name, autos) = match t {
                        Some(MainClassMode::Custom) => (cls, false),
                        Some(MainClassMode::SafeFallback) => (None, true),
                        None => (None, false),
                    };
                    config.main_class_override = name;
                    if let Some(c) = &mut config.custom_jar {
                        c.autoset_main_class = autos;
                    }
                }
            }
            EditInstanceMessage::ReinstallLibraries => {
                return Ok(self.instance_redownload_stage(
                    ql_core::DownloadProgress::DownloadingLibraries {
                        progress: 0,
                        out_of: 0,
                    },
                ));
            }
            EditInstanceMessage::UpdateAssets => {
                return Ok(self.instance_redownload_stage(
                    ql_core::DownloadProgress::DownloadingAssets {
                        progress: 0,
                        out_of: 0,
                    },
                ));
            }
        }
        Ok(Task::none())
    }

    pub fn load_edit_instance(&mut self, new_tab: Option<LaunchTab>) {
        fn load_edit_instance_inner(
            edit_instance: &mut Option<MenuEditInstance>,
            selected_instance: &Instance,
        ) -> Result<(), JsonFileError> {
            let config_path = selected_instance.get_instance_path().join("config.json");

            let config_json = std::fs::read_to_string(&config_path).path(config_path)?;
            let config_json: InstanceConfigJson =
                serde_json::from_str(&config_json).json(config_json)?;

            let slider_value = f32::log2(config_json.ram_in_mb as f32);
            let memory_mb = config_json.ram_in_mb;

            // Use this to check for performance impact
            // std::thread::sleep(std::time::Duration::from_millis(500));

            *edit_instance = Some(MenuEditInstance {
                main_class_mode: config_json.get_main_class_mode(),
                config: config_json,
                slider_value,
                instance_name: selected_instance.name.to_string(),
                old_instance_name: selected_instance.name.clone(),
                slider_text: format_memory(memory_mb),
                memory_input: memory_mb.to_string(),
                is_editing_name: false,
                arg_split_by_space: true,
            });
            Ok(())
        }

        if let State::Launch(_) = &self.state {
        } else {
            _ = self.go_to_main_menu(None);
        }

        if let State::Launch(MenuLaunch {
            tab, edit_instance, ..
        }) = &mut self.state
        {
            if let (LaunchTab::Edit, Some(selected_instance)) =
                (new_tab.unwrap_or(*tab), self.selected_instance.as_ref())
            {
                self.autosave.insert(AutoSaveKind::InstanceConfig); // prevent it from saving *right now*
                if let Err(err) = load_edit_instance_inner(edit_instance, selected_instance) {
                    err!("Could not open edit instance menu: {err}");
                    *edit_instance = None;
                }
            } else {
                *edit_instance = None;
            }
            if let Some(new_tab) = new_tab {
                *tab = new_tab;
            }
        }
    }

    fn instance_redownload_stage(&mut self, stage: ql_core::DownloadProgress) -> Task<Message> {
        let (sender, receiver) = std::sync::mpsc::channel();
        let bar = ProgressBar::with_recv(receiver);
        self.state = State::Create(MenuCreateInstance::DownloadingInstance(bar));

        Task::perform(
            ql_instances::repeat_stage(self.instance().clone(), stage, Some(sender)),
            |t| {
                if let Err(err) = t {
                    Message::Error(err)
                } else {
                    MainMenuMessage::ChangeTab(LaunchTab::Edit).into()
                }
            },
        )
    }

    fn loaded_custom_jar(&mut self, choices: Vec<String>) -> Task<Message> {
        // If the currently selected jar got deleted/renamed
        // then unselect it
        if let State::Launch(MenuLaunch {
            edit_instance: Some(menu),
            ..
        }) = &mut self.state
        {
            if let Some(jar) = &menu.config.custom_jar {
                if !choices.contains(&jar.name) {
                    self.autosave.remove(&AutoSaveKind::InstanceConfig);
                    menu.config.custom_jar = None;
                }
            }
        }

        if let Some(cx) = &mut self.custom_jar {
            cx.choices = choices;
        } else {
            let watcher = match dir_watch(LAUNCHER_DIR.join("custom_jars")) {
                Ok(n) => n,
                Err(err) => {
                    err!("Couldn't load list of custom jars (2)! {err}");
                    return Task::none();
                }
            };
            self.custom_jar = Some(CustomJarState { choices, watcher });
        }

        Task::none()
    }

    fn add_custom_jar(&mut self) -> Task<Message> {
        if let (
            Some(custom_jars),
            State::Launch(MenuLaunch {
                edit_instance: Some(menu),
                ..
            }),
            Some((path, file_name)),
        ) = (
            &mut self.custom_jar,
            &mut self.state,
            rfd::FileDialog::new()
                .set_title("Select Custom Minecraft JAR")
                .add_filter("Java Archive", &["jar"])
                .pick_file()
                .and_then(|n| n.file_name().map(|f| (n.clone(), f.to_owned()))),
        ) {
            let file_name = file_name.to_string_lossy().to_string();
            if !custom_jars.choices.contains(&file_name) {
                custom_jars.choices.insert(1, file_name.clone());
            }

            *menu.config.custom_jar.get_or_insert_default() =
                CustomJarConfig::new(file_name.clone());

            Task::perform(
                tokio::fs::copy(path, LAUNCHER_DIR.join("custom_jars").join(file_name)),
                |_| Message::Nothing,
            )
        } else {
            Task::none()
        }
    }

    fn rename_instance(&mut self) -> Result<Task<Message>, String> {
        let State::Launch(MenuLaunch {
            edit_instance: Some(menu),
            ..
        }) = &mut self.state
        else {
            return Ok(Task::none());
        };

        let sanitized_name = sanitize_instance_name(menu.instance_name.clone());
        if sanitized_name.is_empty() {
            err!("New name is empty or invalid");
            return Ok(Task::none());
        }

        if *menu.old_instance_name == sanitized_name
            || *menu.old_instance_name == menu.instance_name
        {
            // Don't waste time talking to OS
            // and "renaming" instance if nothing has changed.
            return Ok(Task::none());
        }

        let instances_dir =
            LAUNCHER_DIR.join(if self.selected_instance.as_ref().unwrap().is_server() {
                "servers"
            } else {
                "instances"
            });

        let old_path = instances_dir.join(&*menu.old_instance_name);
        let new_path = instances_dir.join(&sanitized_name);

        if new_path.parent().is_none_or(|n| n != instances_dir) {
            err!("New instance path is outside instance dir!");
            return Ok(Task::none());
        }

        let old_name = menu.old_instance_name.clone();
        menu.old_instance_name = Arc::from(sanitized_name.as_str());
        std::fs::rename(&old_path, &new_path)
            .path(&old_path)
            .strerr()?;

        let mut instance = self.selected_instance.clone().unwrap();
        instance.name = Arc::from(sanitized_name.as_str());

        if let Some(s) = &mut self.config.sidebar {
            s.rename(
                &SidebarSelection::Instance(old_name, instance.kind),
                &sanitized_name,
            );
        }

        Ok(Task::perform(get_entries(self.instance().kind), move |n| {
            Message::Multiple(vec![
                Message::CoreListLoaded(n),
                MainMenuMessage::InstanceSelected(instance.clone()).into(),
            ])
        }))
    }
}

impl EditInstanceMessage {
    pub fn edits_config(&self) -> bool {
        match self {
            EditInstanceMessage::ReinstallLibraries |
            EditInstanceMessage::UpdateAssets |
            EditInstanceMessage::RenameToggle |
            EditInstanceMessage::ToggleSplitArg(_) |
            EditInstanceMessage::RenameEdit(_) |
            EditInstanceMessage::RenameApply | // ?
            EditInstanceMessage::CustomJarLoaded(_) |
            EditInstanceMessage::ConfigSaved(_) => false,

            EditInstanceMessage::MemoryChanged(_) |
            EditInstanceMessage::MemoryInputChanged(_) |
            EditInstanceMessage::LoggingToggle(_) |
            EditInstanceMessage::SetMainClass(_, _) |
            EditInstanceMessage::JavaArgs(_) |
            EditInstanceMessage::JavaArgsModeChanged(_) |
            EditInstanceMessage::GameArgs(_) |
            EditInstanceMessage::PreLaunchPrefix(_) |
            EditInstanceMessage::PreLaunchPrefixModeChanged(_) |
            EditInstanceMessage::JavaOverride(_) |
            EditInstanceMessage::JavaOverrideVersion(_) |
            EditInstanceMessage::WindowWidthChanged(_) |
            EditInstanceMessage::WindowHeightChanged(_) |
            EditInstanceMessage::CustomJarPathChanged(_) |
            EditInstanceMessage::BrowseJavaOverride => true,
        }
    }
}

fn format_memory(memory_bytes: usize) -> String {
    const MB_TO_GB: usize = 1024;

    if memory_bytes >= MB_TO_GB {
        format!("{:.2} GB", memory_bytes as f64 / MB_TO_GB as f64)
    } else {
        format!("{memory_bytes} MB")
    }
}
