use std::{
    io::{BufRead, BufReader},
    path::PathBuf,
    process::{Child, Command, Stdio},
    sync::{Arc, Mutex},
};

use downloader::progress::Reporter;
use log::{debug, info};
use tokio::{
    fs::{create_dir_all, remove_file, File, OpenOptions},
    io::AsyncWriteExt,
};
use wincompatlib::prelude::*;

use crate::{
    components::{
        component_downloader::ComponentDownloader, dxvk_component::DXVKComponent,
        game_component::GameComponent, wine_component::WineComponent,
    },
    game_config::GameConfig,
    game_patcher,
    utils::{get_game_name, get_game_name_with_executable, github_requester::GithubRequester},
};

pub struct EnvironmentVariable {
    name: String,
    value: String,
}

impl EnvironmentVariable {
    pub fn parse(variable: &str) -> Self {
        let split: Vec<&str> = variable.split('=').collect();

        Self {
            name: split[0].to_string(),
            value: split[1].to_string(),
        }
    }
}

pub struct GameManager;

impl GameManager {
    pub async fn install_wine<P>(
        config_dir: PathBuf,
        release_index: usize,
        progress: Option<Arc<P>>,
    ) -> anyhow::Result<WineComponent>
    where
        P: Reporter + 'static,
    {
        let mut wine_component = WineComponent::new(config_dir);
        wine_component.set_github_release_index(release_index);

        wine_component.install(progress).await?;

        let mut config = GameConfig::get_config().await;
        config.is_wine_installed = true;
        GameConfig::save_config(config).await?;

        Ok(wine_component)
    }

    pub async fn install_dxvk<P>(
        wine: &Wine,
        config_dir: PathBuf,
        release_index: usize,
        progress: Option<Arc<P>>,
    ) -> anyhow::Result<()>
    where
        P: Reporter + 'static,
    {
        let mut dxvk_component = DXVKComponent::from_wine(wine, config_dir);
        dxvk_component.set_github_release_index(release_index);

        dxvk_component.install(progress).await?;

        let mut config = GameConfig::get_config().await;
        config.is_dxvk_installed = true;
        GameConfig::save_config(config).await?;

        Ok(())
    }

    pub async fn install_font<P>(wine: &Wine, progress: Option<Arc<P>>) -> anyhow::Result<()>
    where
        P: Reporter + 'static,
    {
        let max = 1;

        if let Some(p) = &progress {
            p.setup(Some(max), "");
        }

        notify_fonts_progress(0, max, &progress);

        wine.install_font(Font::Arial)?;
        notify_fonts_progress(1, max, &progress);

        let mut config = GameConfig::get_config().await;
        config.is_font_installed = true;
        GameConfig::save_config(config).await?;

        Ok(())
    }

    pub async fn install_dependencies(wine: &Wine) -> anyhow::Result<()> {
        let winetricks = Winetricks::from_wine("/bin/winetricks", wine);
        //winetricks.install("corefonts")?;
        let mut child = winetricks.install("vcrun2022")?;

        child
            .wait()
            .expect("Something failed when waiting for the installation");

        let mut config = GameConfig::get_config().await;
        config.is_dependecies_installed = true;
        GameConfig::save_config(config).await?;

        Ok(())
    }

    pub async fn install_game<P>(game_dir: PathBuf, progress: Arc<P>) -> anyhow::Result<()>
    where
        P: Reporter + 'static,
    {
        let _ = create_dir_all(game_dir.clone()).await;

        let game_component = GameComponent::new(game_dir);
        game_component.install(Some(progress)).await?;

        let mut config = GameConfig::get_config().await;
        config.is_game_installed = true;
        GameConfig::save_config(config).await?;

        Ok(())
    }

    // this function just pass is_game_installed and is_game_patched to false,
    // so the launcher on the next iteration download the new file and delete the old one with the check process in the installation process
    pub async fn update_game() -> anyhow::Result<()> {
        let mut config = GameConfig::get_config().await;
        config.is_game_installed = false;
        config.is_game_patched = false;
        GameConfig::save_config(config).await?;

        Ok(())
    }

    pub async fn patch_game(game_dir: PathBuf) -> anyhow::Result<()> {
        game_patcher::patch_game(game_dir).await?;

        Ok(())
    }

    pub async fn start_game(
        wine: &Wine,
        game_dir: PathBuf,
        options: Option<String>,
        env_variables: Vec<EnvironmentVariable>,
        show_logs: bool,
    ) -> anyhow::Result<()> {
        let wine_version = wine.version()?;
        let binary_path = game_dir
            .join(get_game_name())
            .join(get_game_name_with_executable());

        debug!("wine version : {:?}", wine_version);

        let mut child = if let Some(custom_command) = options {
            Self::run(wine, binary_path, Some(custom_command), env_variables).await?
        } else {
            if let Some(custom_command) = GameConfig::get_launch_options().await.unwrap() {
                Self::run(wine, binary_path, Some(custom_command), env_variables).await?
            } else {
                Self::run(wine, binary_path, None, env_variables).await?
            }
        }?;

        let log_stdout = Arc::new(Mutex::new(None));
        let log_stderr = Arc::new(Mutex::new(None));

        if show_logs {
            let stderr = child.stderr.take().unwrap();
            let stdout = child.stdout.take().unwrap();

            let log_stdout_ref = log_stdout.clone();
            let log_stderr_ref = log_stderr.clone();

            tokio::task::spawn(async move {
                let bufread = BufReader::new(stdout);
                let mut stdout_save = String::new();
                let _: Vec<_> = bufread
                    .lines()
                    .inspect(|s| {
                        if let Ok(str) = s {
                            info!("[wine] > {}", str);
                            stdout_save.push_str(str);
                        }
                    })
                    .collect();
                *log_stdout_ref.lock().unwrap() = Some(stdout_save);
            });
            tokio::task::spawn(async move {
                let bufread = BufReader::new(stderr);
                let mut stderr_save = String::new();
                let _: Vec<_> = bufread
                    .lines()
                    .inspect(|s| {
                        if let Ok(str) = s {
                            info!("[wine] > {}", str);
                            stderr_save.push_str(str);
                        }
                    })
                    .collect();
                *log_stderr_ref.lock().unwrap() = Some(stderr_save);
            });
        }

        let output = child
            .wait_with_output()
            .expect("Failed to wait for the process");

        let log_file_path = GameConfig::get_config_directory().await.join("game.log");
        if log_file_path.exists() {
            remove_file(log_file_path.clone())
                .await
                .expect("Failed to remove old log file");
        }
        File::create(log_file_path.clone())
            .await
            .expect("Failed to create build file");

        let mut log_file = OpenOptions::new()
            .write(true)
            .append(true)
            .open(log_file_path)
            .await
            .expect("Failed to open the log file");

        log_file
            .write_all("--- stdout ---\n".as_bytes())
            .await
            .expect("Failed to write the output to the log file");

        let log_stdout_value = log_stdout.lock().unwrap();
        let v;
        let to_write = if log_stdout_value.is_some() {
            v = log_stdout_value.clone().unwrap();
            v.as_bytes()
        } else {
            &output.stdout
        };

        log_file
            .write_all(to_write)
            .await
            .expect("Failed to write the output to the log file");

        log_file
            .write_all("--- stderr ---\n".as_bytes())
            .await
            .expect("Failed to write the output to the log file");

        let log_stderr_value = log_stderr.lock().unwrap();
        let v;
        let to_write = if log_stderr_value.is_some() {
            debug!("test");
            v = log_stderr_value.clone().unwrap();
            v.as_bytes()
        } else {
            &output.stderr
        };

        log_file
            .write_all(to_write)
            .await
            .expect("Failed to write the output to the log file");

        Ok(())
    }

    async fn run(
        wine: &Wine,
        binary_path: PathBuf,
        custom_command: Option<String>,
        env_variables: Vec<EnvironmentVariable>,
    ) -> anyhow::Result<Result<Child, std::io::Error>> {
        let mut command: Vec<&str> = vec![];

        let wine_path = GameConfig::get_config_directory()
            .await
            .join("wine")
            .join("bin")
            .join("wine");
        let mut wine_path = wine_path.to_str().unwrap();

        if let Some(distro) = whatadistro::identify() {
            if distro.is_similar("nixos") {
                wine_path = "wine";

                info!("Nixos detected, using system Wine...");
            };
        };

        //command.push(wine.python.to_str().unwrap());
        command.push(wine_path);
        //command.push("run");
        command.push(binary_path.to_str().unwrap());

        let launch_option;
        if custom_command.is_some() {
            launch_option = custom_command.unwrap();
            debug!("Starting game with --options -> {}", launch_option.clone());
            let tokens: Vec<&str> = launch_option.split_whitespace().collect();

            // position of the %command%
            let index = if let Some(v) = tokens.iter().position(|&s| s == "%command%") {
                v
            } else {
                anyhow::bail!("You forget to put %command% in your custom launch command");
            };

            let start_command = tokens[0..index].to_vec();
            let mut end_command = tokens[(index + 1)..tokens.len()].to_vec();

            start_command.iter().rev().for_each(|str| {
                command.insert(0, str);
            });
            command.append(&mut end_command);
        } else {
            debug!("Starting game without --options");
        }

        debug!("Command preview -> {}", command.join(" "));
        debug!(
            "Command envs -> {:?}",
            wine.get_envs()["WINEPREFIX"].clone()
        );

        Ok(Command::new(command[0])
            .args(&command[1..command.len()])
            .env("WINE_PREFIX", wine.get_envs()["WINEPREFIX"].clone())
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn())
    }
}

fn notify_fonts_progress<P>(nbr: u64, max: u64, progress: &Option<Arc<P>>)
where
    P: Reporter + 'static,
{
    info!("{}", format!("{}/{} font installed", nbr, max));
    if let Some(p) = progress {
        p.progress(nbr);
    }
}
