use std::{
    collections::BTreeMap as Map,
    fmt,
    marker::PhantomData,
    path::{Path, PathBuf},
};
use inner_future::stream::StreamExt;
use serde::{
    de::{self, SeqAccess, Visitor},
    Deserialize, Deserializer, Serialize,
};
use super::VersionType;
use crate::{
    package::PackageName,
    prelude::*,
    semver::MinecraftVersion,
    utils::{get_full_path, NATIVE_ARCH_LAZY},
};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct OSRule {
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub version: String,
    #[serde(default)]
    pub arch: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct ApplyRule {
    pub action: String,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub os: Option<OSRule>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub features: Option<Map<String, bool>>,
}
pub trait Allowed {
    fn is_allowed(&self) -> bool;
}
impl Allowed for [ApplyRule] {
    fn is_allowed(&self) -> bool {
        if self.is_empty() {
            true
        } else {
            let mut should_push = false;
            for rule in self {
                if rule.action == "disallow" {
                    if let Some(os) = &rule.os {
                        if !os.name.is_empty()
                            && os.name != crate::utils::TARGET_OS
                            && !os.arch.is_empty()
                            && os.arch != NATIVE_ARCH_LAZY.as_ref()
                        {
                            continue;
                        } else {
                            break;
                        }
                    } else {
                        continue;
                    }
                } else if rule.action == "allow" {
                    if let Some(os) = &rule.os {
                        if (!os.name.is_empty() && os.name != crate::utils::TARGET_OS)
                            || (!os.arch.is_empty() && os.arch != NATIVE_ARCH_LAZY.as_ref())
                        {
                            continue;
                        } else {
                            should_push = true;
                            break;
                        }
                    } else {
                        should_push = true; continue;
                    }
                }
            }
            should_push
        }
    }
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct SpecificalArgument {
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub rules: Vec<ApplyRule>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    #[serde(deserialize_with = "string_or_seq")]
    pub value: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum Argument {
    Common(String),
    Specify(SpecificalArgument),
}
#[test]
fn argument_test() {
    let text = serde_json::from_str::<Argument>(r#""test""#).unwrap();
    assert_eq!(text, Argument::Common("test".into()));
    let specify = serde_json::from_str::<Argument>(r#"{"rules":[],"value":"test"}"#).unwrap();
    assert_eq!(
        specify,
        Argument::Specify(SpecificalArgument {
            rules: vec![],
            value: vec!["test".into()]
        })
    );
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(default)]
pub struct Arguments {
    pub game: Vec<Argument>,
    pub jvm: Vec<Argument>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct AssetIndex {
    pub id: String,
    pub sha1: String,
    pub size: u32,
    #[serde(rename = "totalSize")]
    pub total_size: u32,
    pub url: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct DownloadItem {
    #[serde(default)]
    pub path: String,
    pub sha1: String,
    pub size: usize,
    pub url: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LibraryDownload {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub artifact: Option<DownloadItem>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub classifiers: Option<Map<String, DownloadItem>>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Library {
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub rules: Vec<ApplyRule>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub downloads: Option<LibraryDownload>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub natives: Option<Map<String, String>>,
    pub name: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LoggingFile {
    pub id: String,
    pub sha1: String,
    pub size: u32,
    pub url: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LoggingConfig {
    pub argument: String,
    #[serde(rename = "type")]
    pub logger_type: String,
    pub file: LoggingFile,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(default)]
pub struct SCLLaunchConfig {
    pub max_mem: Option<usize>,
    pub java_path: String,
    pub game_independent: bool,
    pub window_title: String,
    pub jvm_args: String,
    pub game_args: String,
    pub wrapper_path: String,
    pub wrapper_args: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Logging {
    pub client: Option<LoggingConfig>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct JavaVersion {
    pub component: String,
    #[serde(rename = "majorVersion")]
    pub major_version: u8,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct VersionMeta {
    #[serde(default)]
    #[serde(rename = "inheritsFrom")]
    pub inherits_from: String,
    #[serde(default)]
    #[serde(rename = "clientVersion")]
    pub client_version: String,
    #[serde(default)]
    #[serde(rename = "javaVersion")]
    pub java_version: Option<JavaVersion>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub arguments: Option<Arguments>,
    #[serde(default)]
    #[serde(rename = "minecraftArguments")]
    pub minecraft_arguments: String,
    #[serde(rename = "assetIndex")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub asset_index: Option<AssetIndex>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub downloads: Option<Map<String, DownloadItem>>,
    #[serde(default)]
    pub libraries: Vec<Library>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub logging: Option<Logging>,
    #[serde(rename = "mainClass")]
    pub main_class: String,
    #[serde(skip)]
    pub main_jars: Vec<String>,
}
impl VersionMeta {
    pub(crate) fn fix_libraries(&mut self) {
        for library in &mut self.libraries {
            if library.rules.is_allowed()
                && library.downloads.is_none()
                && library.natives.is_none()
            {
                if let Ok(p) = library.name.parse::<PackageName>() {
                    let p = p.to_maven_jar_path("");
                    library.downloads = Some(LibraryDownload {
                        artifact: Some(DownloadItem {
                            path: p,
                            sha1: "".into(),
                            size: 0,
                            url: "".into(),
                        }),
                        classifiers: None,
                    })
                }
            }
        }
    }
    pub fn required_java_version(&self) -> u8 {
        if let Some(java_version) = &self.java_version {
            java_version.major_version
        } else if let Some(assets) = &self.asset_index {
            if let Ok((_, ver)) = crate::semver::parse_version(&assets.id) {
                ver.required_java_version()
            } else {
                8
            }
        } else if !self.inherits_from.is_empty() {
            if let Ok((_, ver)) = crate::semver::parse_version(&self.inherits_from) {
                ver.required_java_version()
            } else {
                8
            }
        } else {
            8
        }
    }
}
impl std::ops::AddAssign for VersionMeta {
    fn add_assign(&mut self, data: VersionMeta) {
        self.main_class = data.main_class.to_owned();
        self.minecraft_arguments = data.minecraft_arguments;
        self.libraries.extend_from_slice(&data.libraries);
        self.main_jars.extend_from_slice(&data.main_jars);
        if let Some(downloads) = &mut data.downloads.to_owned() {
            if let Some(self_downloads) = &mut self.downloads {
                self_downloads.append(downloads);
            } else {
                self.downloads = Some(downloads.to_owned());
            }
        }
        if let Some(arguments) = &data.arguments {
            if let Some(self_arguments) = &mut self.arguments {
                for a in arguments.jvm.iter() {
                    self_arguments.jvm.push(a.to_owned());
                }
                for a in arguments.game.iter() {
                    self_arguments.game.push(a.to_owned());
                }
            } else {
                self.arguments = Some(arguments.to_owned())
            }
        }
        if let Some(logging) = &data.logging {
            self.logging = Some(logging.to_owned());
        }
    }
}
#[derive(Debug, Clone, Default)]
pub struct VersionInfo {
    pub version_base: String,
    pub version: String,
    pub meta: Option<VersionMeta>,
    pub scl_launch_config: Option<SCLLaunchConfig>,
    pub version_type: VersionType,
    pub minecraft_version: MinecraftVersion,
    pub required_java: u8,
}
impl VersionInfo {
    pub async fn load(&mut self) -> DynResult {
        let version_base_path = Path::new(&self.version_base);
        if version_base_path.is_dir() {
            let jar_path = version_base_path
                .join(&self.version)
                .join(format!("{}.jar", &self.version));
            let meta_path = version_base_path
                .join(&self.version)
                .join(format!("{}.json", &self.version));
            let scl_config_path = version_base_path.join(&self.version).join(".scl.json");
            if !meta_path.is_file() {
                anyhow::bail!(
                    "该版本 {} (游戏文件夹:{}) 缺失元数据文件",
                    &self.version,
                    &self.version_base
                )
            } else {
                if scl_config_path.is_file() {
                    let data = inner_future::fs::read_to_string(scl_config_path).await?;
                    let scl_config = serde_json::from_str(data.trim_start_matches('\u{feff}'))?; self.scl_launch_config = Some(scl_config);
                }
                let data = inner_future::fs::read_to_string(meta_path).await?;
                let mut meta: VersionMeta =
                    serde_json::from_str(data.trim_start_matches('\u{feff}'))?; if jar_path.is_file() {
                    meta.main_jars.push(get_full_path(jar_path));
                }
                self.required_java = meta.required_java_version();
                if let Some(assets) = &meta.asset_index {
                    if let Ok((_, ver)) = crate::semver::parse_version(&assets.id) {
                        self.minecraft_version = ver;
                    }
                } else if !meta.inherits_from.is_empty() {
                    if let Ok((_, ver)) = crate::semver::parse_version(&meta.inherits_from) {
                        self.minecraft_version = ver;
                    }
                }
                self.meta = Some(meta);
                self.version_type = self.guess_version_type();
                Ok(())
            }
        } else {
            anyhow::bail!("游戏文件夹 {} 不是正确的文件夹", &self.version_base)
        }
    }
    pub async fn delete(self) {
        let version_base_path = Path::new(&self.version_base);
        if version_base_path.is_dir() {
            let version_path = version_base_path.join(&self.version);
            let _ = inner_future::fs::remove_dir_all(version_path).await;
        }
    }
    pub async fn rename_version(&mut self, new_version_name: &str) -> DynResult {
        let version_base_path = Path::new(&self.version_base);
        if version_base_path.is_dir() {
            let version_path = version_base_path.join(&self.version);
            let version_jar_path = version_path.join(format!("{}.jar", self.version));
            let version_json_path = version_path.join(format!("{}.json", self.version));
            let new_version_path = version_base_path.join(new_version_name);
            let new_version_jar_path = version_path.join(format!("{new_version_name}.jar"));
            let new_version_json_path = version_path.join(format!("{new_version_name}.json"));
            if new_version_path.is_dir() {
                anyhow::bail!("目标版本名称已存在")
            } else {
                if version_jar_path.is_file() {
                    inner_future::fs::rename(version_jar_path, new_version_jar_path).await?;
                }
                if version_json_path.is_file() {
                    inner_future::fs::rename(version_json_path, new_version_json_path).await?
                };
                inner_future::fs::rename(version_path, new_version_path).await?;
                self.version = new_version_name.to_owned();
                Ok(())
            }
        } else {
            anyhow::bail!("文件夹不存在")
        }
    }
    pub async fn save(&self) -> DynResult {
        let version_base_path = Path::new(&self.version_base);
        if version_base_path.is_dir() {
            let meta_path = version_base_path
                .join(&self.version)
                .join(format!("{}.json", &self.version));
            let scl_config_path = version_base_path.join(&self.version).join(".scl.json");
            if !meta_path.is_file() {
                anyhow::bail!("版本 JSON 元数据文件缺失")
            } else {
                if let Some(meta) = &self.meta {
                    let file = std::fs::OpenOptions::new()
                        .truncate(true)
                        .write(true)
                        .open(meta_path);
                    if let Ok(file) = file {
                        match serde_json::to_writer(file, meta) {
                            Ok(_) => {}
                            Err(err) => {
                                anyhow::bail!("元数据解析失败:{}", err)
                            }
                        }
                    } else {
                        anyhow::bail!("无法打开版本元数据文件")
                    }
                }
                if let Some(scl_config) = &self.scl_launch_config {
                    let file = std::fs::OpenOptions::new()
                        .truncate(true)
                        .create(true)
                        .write(true)
                        .open(scl_config_path);
                    if let Ok(file) = file {
                        match serde_json::to_writer(file, scl_config) {
                            Ok(_) => {}
                            Err(err) => {
                                anyhow::bail!("SCL 配置文件写入失败:{}", err)
                            }
                        }
                    } else {
                        anyhow::bail!("无法打开 SCL 配置文件")
                    }
                } else if scl_config_path.is_file() {
                    inner_future::fs::remove_file(scl_config_path).await?;
                }
                Ok(())
            }
        } else {
            anyhow::bail!("版本文件夹未找到")
        }
    }
    pub fn guess_version_type(&self) -> VersionType {
        let mut has_optifine = false;
        let mut has_fabric = false;
        if let Some(meta) = &self.meta {
            for lib in &meta.libraries {
                if lib.name.starts_with("net.fabricmc:") {
                    has_fabric = true;
                } else if lib.name.starts_with("net.minecraftforge:") {
                    return VersionType::Forge;
                } else if lib.name.starts_with("org.quiltmc:") {
                    return VersionType::QuiltMC;
                } else if lib.name.starts_with("optifine:") {
                    has_optifine = true;
                }
            }
            if has_fabric {
                VersionType::Fabric
            } else if has_optifine {
                VersionType::Optifine
            } else {
                VersionType::Vanilla
            }
        } else {
            VersionType::Unknown
        }
    }
    pub fn version_path(&self) -> PathBuf {
        if self
            .scl_launch_config
            .as_ref()
            .map(|x| x.game_independent)
            .unwrap_or(false)
        {
            let mut result = PathBuf::new();
            result.push(&self.version_base);
            result.push(&self.version);
            result
        } else {
            let mut result = PathBuf::new();
            result.push(&self.version_base);
            result.pop();
            result
        }
    }
    pub async fn get_mods(&self) -> DynResult<Vec<super::mods::Mod>> {
        let mods_path = self.version_path().join("mods");
        if !mods_path.is_dir() {
            return Ok(vec![]);
        }
        let mut files = inner_future::fs::read_dir(mods_path).await?;
        let mut results = vec![];
        while let Some(file) = files.try_next().await? {
            if file.path().is_file()
                && file
                    .path()
                    .file_name()
                    .map(|x| x.to_string_lossy().ends_with(".jar"))
                    == Some(true)
                || file
                    .path()
                    .file_name()
                    .map(|x| x.to_string_lossy().ends_with(".jar.disabled"))
                    == Some(true)
            {
                results.push(super::mods::Mod::from_path(file.path()).await?);
            }
        }
        Ok(results)
    }
    pub async fn get_automated_maxium_memory(&self) -> u64 {
        let mem_status = crate::utils::get_mem_status();
        let mut free = mem_status.free as i64;
        let mods = self.get_mods().await.unwrap_or_default();
        let (mem_min, mem_t1, mem_t2, mem_t3) = if !mods.is_empty() {
            (
                400 + mods.len() as i64 * 7,
                1500 + mods.len() as i64 * 10,
                3000 + mods.len() as i64 * 17,
                6000 + mods.len() as i64 * 34,
            )
        } else {
            (300, 1500, 2500, 4000)
        };
        let mut result = 0;
        let mem_delta = mem_t1;
        free = (free - 100).max(0);
        result += free.min(mem_delta);
        free -= mem_delta + 100;
        if free < 100 {
            return result.max(mem_min) as _;
        }
        let mem_delta = mem_t2 - mem_t1;
        free = (free - 100).max(0);
        result += ((free as f64 * 0.8) as i64).min(mem_delta);
        free -= ((mem_delta as f64 / 0.8) as i64) + 100;
        if free < 100 {
            return result.max(mem_min) as _;
        }
        let mem_delta = mem_t2 - mem_t1;
        free = (free - 200).max(0);
        result += ((free as f64 * 0.6) as i64).min(mem_delta);
        free -= ((mem_delta as f64 / 0.6) as i64) + 200;
        if free < 100 {
            return result.max(mem_min) as _;
        }
        let mem_delta = mem_t3;
        free = (free - 300).max(0);
        result += ((free as f64 * 0.4) as i64).min(mem_delta);
        free -= ((mem_delta as f64 / 0.4) as i64) + 300;
        if free < 100 {
            return result.max(mem_min) as _;
        }
        result.max(mem_min) as _
    }
    pub async fn get_saves(&self) -> DynResult<Vec<WorldSave>> {
        let saves_path = self.version_path().join("saves");
        let mut result = Vec::new();
        let mut files = inner_future::fs::read_dir(&saves_path).await?;
        while let Some(_file) = files.try_next().await? {
            result.push(WorldSave {})
        }
        Ok(result)
    }
    pub async fn get_resources_packs(&self) -> DynResult<Vec<ResourcesPack>> {
        let resourcepacks_path = self.version_path().join("resourcepacks");
        let texturepacks_path = self.version_path().join("texturepacks");
        let mut result = Vec::new();
        if resourcepacks_path.is_dir() {
            let mut files = inner_future::fs::read_dir(&resourcepacks_path).await?;
            while let Some(_file) = files.try_next().await? {
                result.push(ResourcesPack {})
            }
        }
        if texturepacks_path.is_dir() {
            let mut files = inner_future::fs::read_dir(&texturepacks_path).await?;
            while let Some(_file) = files.try_next().await? {
                result.push(ResourcesPack {})
            }
        }
        Ok(result)
    }
}
#[derive(Debug)]
pub struct WorldSave {
    }
#[derive(Debug)]
pub struct ResourcesPack {
    }
fn string_or_seq<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
    D: Deserializer<'de>,
{
    struct StringOrVec(PhantomData<Vec<String>>);
    impl<'de> Visitor<'de> for StringOrVec {
        type Value = Vec<String>;
        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("string or list of strings")
        }
        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            Ok(vec![value.into()])
        }
        fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
        where
            S: SeqAccess<'de>,
        {
            Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
        }
    }
    deserializer.deserialize_any(StringOrVec(PhantomData))
}