//! Uses the GOG api as described [here](https://gogapidocs.readthedocs.io/en/latest/).
//!
//! # Example
//! ```
//! use gog::Gog;
//!
//! let mut http_client = Http::new();
//! let mut gog = Gog::new(&mut http_client);
//! gog.login();
//! gog.sync(download_folder);
//! ```

use configfiles::{ConfigFiles, ConfigError};
use http::{Http, HttpError};
use models;
use models::{Token, Content, Data, Config};
use serde_json;
use serde_json::Value;
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::fmt;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Write;
use std::path::Path;

/// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`.
#[derive(Debug)]
pub enum GogError {
    Error(&'static str),
    ConfigError(ConfigError),
    HttpError(HttpError),
    SerdeError(serde_json::Error),
    IOError(io::Error),
}

impl fmt::Display for GogError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            GogError::Error(ref err) => fmt::Display::fmt(err, f),
            GogError::ConfigError(ref err) => fmt::Display::fmt(err, f),
            GogError::HttpError(ref err) => fmt::Display::fmt(err, f),
            GogError::SerdeError(ref err) => fmt::Display::fmt(err, f),
            GogError::IOError(ref err) => fmt::Display::fmt(err, f),
        }
    }
}

impl From<ConfigError> for GogError {
    fn from(e: ConfigError) -> Self {
        GogError::ConfigError(e)
    }
}

impl From<HttpError> for GogError {
    fn from(e: HttpError) -> Self {
        GogError::HttpError(e)
    }
}

impl From<serde_json::Error> for GogError {
    fn from(e: serde_json::Error) -> Self {
        GogError::SerdeError(e)
    }
}

impl From<io::Error> for GogError {
    fn from(e: io::Error) -> Self {
        GogError::IOError(e)
    }
}

/// Wraps the GOG api.
/// At the moment Client ID and Client Secret are hardcoded to the galaxy client.
pub struct Gog<'a> {
    client_id: String,
    client_secret: String,
    redirect_uri: String,
    games_uri: String,
    http_client: &'a mut Http,
}

impl<'a> Gog<'a> {
    /// Create a new instance of `Gog`.
    pub fn new(http_client: &'a mut Http) -> Gog<'a> {
        Gog {
            client_id: String::from("46899977096215655"),
            client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"),
            redirect_uri: String::from("https://embed.gog.com/on_login_success?origin=client"),
            games_uri: String::from("https://embed.gog.com/user/data/games"),
            http_client: http_client,
        }
    }

    /// Logs into a gog account.
    /// If a token was already saved it should work automatically. Otherwise you
    /// will get an url which you have to open in a browser. There you have to
    /// login and copy the code parameter from the next page into the prompt.
    pub fn login(&mut self) -> Result<(), GogError> {
        let config = ConfigFiles::new();
        let mut token: Token = match config.load("token.json") {
            Ok(value) => value,
            Err(_) => {
                let code = self.get_code()?;
                self.get_token(code.as_str())?
            }
        };

        if token.is_expired() {
            token = self.refresh_token(token.refresh_token.as_str())?;
        }

        config.save("token.json", &token)?;

        let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
        self.http_client.add_header(auth_header.as_str())?;

        Ok(())
    }

    /// Syncs the contents of a gog account with a local folder.
    /// Uses a hash to figure out whether something has changed.
    pub fn sync(&mut self,
                storage_path: &str,
                storage_path_movies: &str,
                os_filters: &Vec<String>,
                language_filters: &Vec<String>,
                resolution_filters: &Vec<String>,
                skip_movies: bool,
                skip_games: bool)
                -> Result<(), GogError> {
        let configfiles = ConfigFiles::new();
        let mut config: Config = match configfiles.load("config.json") {
            Ok(value) => value,
            Err(_) => {
                error!("Configuration error, generating new one...");

                Config {
                    game_storage: String::from(storage_path),
                    movie_storage: String::from(storage_path_movies),
                    content: HashMap::new(),
                    data: HashMap::new(),
                    extras: HashMap::new(),
                    os_filters: os_filters.clone(),
                    language_filters: language_filters.clone(),
                    resolution_filters: resolution_filters.clone(),
                    skip_movies: skip_movies,
                    skip_games: skip_games,
                }
            }
        };

        let content_ids = self.get_content_ids()?;

        for content_id in content_ids {
            let content_hash_saved = match config.content.get(&content_id.to_string()) {
                Some(value) => value.clone(),
                None => u64::min_value(),
            };

            let content = match self.get_content(content_id,
                                                 &os_filters,
                                                 &language_filters,
                                                 &resolution_filters) {
                Ok(value) => value,
                Err(error) => {
                    error!("{}: {}", &content_id, error);
                    continue;
                }
            };

            if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) {
                info!("filtering {}", content.title);
                continue;
            }

            let content_hash = models::get_hash(&content);

            if content_hash_saved == content_hash {
                info!("{} already up to date.", &content.title);
                continue;
            }

            let content_root = if content.is_movie {
                Path::new(storage_path_movies).join(&content.title)
            } else {
                Path::new(storage_path).join(&content.title)
            };

            fs::create_dir_all(&content_root)?;

            let key_root = content_root.join("keys");

            if content.cd_keys.len() > 0 {
                fs::create_dir_all(&key_root)?;
            }

            for (key, value) in content.cd_keys {
                let key_path = key_root.join(format!("{}.txt", key));
                let mut key_file = File::create(&key_path)?;
                key_file.write_all(value.as_bytes())?
            }

            for data in content.data {
                let data_hash_saved = match config.data.get(&data.manual_url) {
                    Some(value) => value.clone(),
                    None => u64::min_value(),
                };
                let data_hash = models::get_hash(&data);

                if data_hash_saved == data_hash {
                    info!("{} already up to date.", &data.manual_url);
                    continue;
                }

                let data_root = Path::new(&content_root).join(&data.language);
                fs::create_dir_all(&data_root)?;

                let data_uri = format!("https://embed.gog.com{}", data.manual_url);

                info!("downloading {} for {}...", &data.manual_url, &content.title);
                self.http_client.download(data_uri.as_str(), &data_root)?;

                config.data.insert(data.manual_url, data_hash);
                configfiles.save("config.json", &config)?;
            }

            for extra in content.extras {
                let extra_hash_saved = match config.extras.get(&extra.manual_url) {
                    Some(value) => value.clone(),
                    None => u64::min_value(),
                };

                let extra_hash = models::get_hash(&extra);

                if extra_hash_saved == extra_hash {
                    info!("{} already up to date.", &extra.manual_url);
                    continue;
                }

                let extra_uri = format!("https://embed.gog.com{}", extra.manual_url);

                info!("downloading {} for {}...", &extra.name, &content.title);
                self.http_client.download(extra_uri.as_str(), &content_root)?;

                config.extras.insert(extra.manual_url, extra_hash);
                configfiles.save("config.json", &config)?;
            }

            config.content.insert(content_id.to_string(), content_hash);
            configfiles.save("config.json", &config)?;
        }

        Ok(())
    }

    fn get_code(&self) -> Result<String, GogError> {
        let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str());

        println!("Open the following url in a browser, login to your account and paste the \
                  resulting code parameter.");
        println!("{}", auth_uri);

        let mut code = String::new();

        print!("Code: ");
        io::stdout().flush()?;

        io::stdin()
            .read_line(&mut code)
            .expect("Failed to read line");

        Ok(code)
    }

    fn get_token(&mut self, code: &str) -> Result<Token, GogError> {
        let token_uri = self.token_uri(self.client_id.as_str(),
                                       self.client_secret.as_str(),
                                       code,
                                       self.redirect_uri.as_str());

        let token_response = match self.http_client.get(token_uri.as_str()) {
            Ok(value) => value,
            Err(error) => return Err(GogError::HttpError(error)),
        };

        match serde_json::from_str(&token_response) {
            Ok(value) => Ok(value),
            Err(error) => Err(GogError::SerdeError(error)),
        }
    }

    fn refresh_token(&mut self, refresh_token: &str) -> Result<Token, GogError> {
        let token_refresh_uri = self.token_refresh_uri(self.client_id.as_str(),
                                                       self.client_secret.as_str(),
                                                       refresh_token);

        let token_response = match self.http_client.get(token_refresh_uri.as_str()) {
            Ok(value) => value,
            Err(error) => return Err(GogError::HttpError(error)),
        };

        match serde_json::from_str(&token_response) {
            Ok(value) => Ok(value),
            Err(error) => Err(GogError::SerdeError(error)),
        }
    }

    fn get_content_ids(&mut self) -> Result<Vec<u64>, GogError> {
        let response = self.http_client.get(self.games_uri.as_str())?;
        let content_ids_raw: Value = serde_json::from_str(response.as_str())?;
        let content_ids_serde = &content_ids_raw["owned"];

        let mut content_ids: Vec<u64> = Vec::new();

        if !content_ids_serde.is_array() {
            return Err(GogError::Error("Error parsing content ids."));
        }

        for content_id in content_ids_serde.as_array().unwrap() {
            let content_id_parsed = content_id.as_u64().unwrap_or(0);

            if content_id_parsed == 0 {
                error!("Cant parse content id {}", content_id);
                continue;
            }

            // The numbers in this list are excluded because they refer to
            // favourites, promotions and such.
            if [1, 2, 3, 4, 5].contains(&content_id_parsed) {
                continue;
            }

            content_ids.push(content_id_parsed);
        }

        return Ok(content_ids);
    }

    fn parse_cd_keys(&self, content_title: &str, raw_cd_keys: &str) -> BTreeMap<String, String> {
        let mut cd_keys = BTreeMap::new();

        if raw_cd_keys.contains("<br>") {
            let splitted_keys = raw_cd_keys.split("<br>");

            let mut key_names: Vec<String> = Vec::new();
            let mut key_values: Vec<String> = Vec::new();

            for (token_index, token) in splitted_keys.enumerate() {
                if token_index % 2 == 0 {
                    let mut key_name = token.to_owned();
                    key_name.truncate(token.len() - 1);
                    key_names.push(key_name);
                } else {
                    key_values.push(token.trim().to_owned());
                }
            }

            for (index, key_name) in key_names.iter().enumerate() {
                let key_value = key_values[index].clone();
                cd_keys.insert(key_name.clone(), key_value);
            }
        } else if !raw_cd_keys.is_empty() {
            cd_keys.insert(content_title.to_owned(), raw_cd_keys.trim().to_owned());
        }

        return cd_keys;
    }

    fn get_content(&mut self,
                   content_id: u64,
                   os_filters: &Vec<String>,
                   language_filters: &Vec<String>,
                   resolution_filters: &Vec<String>)
                   -> Result<Content, GogError> {
        let content_uri = self.content_uri(content_id);
        debug!("looking for information at {}...", &content_uri);

        let response = self.http_client.get(content_uri.as_str())?;

        let content_raw: Value = serde_json::from_str(response.as_str())?;
        debug!("found {:?}", &content_raw);

        let mut content: Content = serde_json::from_str(&response)?;

        let downloads = &content_raw["downloads"];

        if content_raw.is_object() && !content_raw.as_object().unwrap().contains_key("forumLink") {
            return Err(GogError::Error("No forumLink property"));
        }

        let is_movie = match content_raw["forumLink"].as_str() {
            Some(value) => value == "https://embed.gog.com/forum/movies",
            None => false,
        };

        content.is_movie = is_movie;

        if content_raw.is_object() && content_raw.as_object().unwrap().contains_key("cdKey") {
            let cd_keys_raw = content_raw["cdKey"].as_str().unwrap();
            content.cd_keys = self.parse_cd_keys(&content.title, cd_keys_raw);
        }

        debug!("processing installer fields: {:?}", &downloads);
        for languages in downloads.as_array() {
            for language in languages {
                if !language.is_array() || language.as_array().unwrap().len() < 2 {
                    error!("Skipping a language for {}", content.title);
                    continue;
                }

                let data_language = match language[0].as_str() {
                    Some(value) => value.to_lowercase(),
                    None => String::default(),
                };

                if data_language.is_empty() {
                    error!("Skipping a language for {}", content.title);
                    continue;
                }

                if !is_movie && !language_filters.is_empty() &&
                   !language_filters.contains(&data_language) {
                    info!("Skipping {} for {}", &data_language, content.title);
                    continue;
                }

                for systems in language[1].as_object() {
                    for system in systems.keys() {
                        if is_movie && !os_filters.is_empty() && !os_filters.contains(system) {
                            info!("Skipping {} {} for {}",
                                  &data_language,
                                  system,
                                  content.title);
                            continue;
                        }

                        for real_downloads in systems.get(system) {
                            for real_download in real_downloads.as_array() {
                                for download in real_download {
                                    if !download.is_object() ||
                                       !download.as_object().unwrap().contains_key("manualUrl") ||
                                       !download.as_object().unwrap().contains_key("name") {
                                        error!("Skipping data for {}", content.title);
                                        continue;
                                    }

                                    let name: &str = download["name"].as_str().unwrap();


                                    if content.is_movie && !resolution_filters.is_empty() {
                                        let mut found_resolution = false;
                                        for resolution_filter in resolution_filters {
                                            let filter = format!("({})", resolution_filter);
                                            if name.ends_with(&filter) {
                                                found_resolution = true;
                                                break;
                                            }
                                        }

                                        if !found_resolution {
                                            info!("Skipping {}: not a suitable resolution.", name);
                                            continue;
                                        }
                                    }

                                    let data = Data {
                                        manual_url: String::from(download["manualUrl"]
                                            .as_str()
                                            .unwrap()),
                                        version: String::from(download["version"]
                                            .as_str()
                                            .unwrap_or("")),
                                        os: system.clone(),
                                        language: data_language.clone(),
                                    };

                                    content.data.push(data);
                                }
                            }
                        }
                    }
                }
            }
        }

        Ok(content)
    }

    fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String {
        format!("https://auth.gog.\
                 com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2",
                client_id = client_id,
                redirect_uri = redirect_uri)
    }

    fn token_uri(&self,
                 client_id: &str,
                 client_secret: &str,
                 code: &str,
                 redirect_uri: &str)
                 -> String {
        format!("https://auth.gog.\
                 com/token?client_id={client_id}&client_secret={client_secret}&grant_type=authorization_code&code={code}&redirect_uri={redirect_uri}",
                client_id = client_id,
                client_secret = client_secret,
                code = code.trim(),
                redirect_uri = redirect_uri)
    }

    fn token_refresh_uri(&self,
                         client_id: &str,
                         client_secret: &str,
                         refresh_token: &str)
                         -> String {
        format!("https://auth.gog.\
                 com/token?client_id={client_id}&client_secret={client_secret}&grant_type=refresh_token&refresh_token={refresh_token}",
                client_id = client_id,
                client_secret = client_secret,
                refresh_token = refresh_token)
    }

    fn content_uri(&self, content_id: u64) -> String {
        format!("https://embed.gog.com/account/gameDetails/{content_id}.json",
                content_id = content_id)
    }
}
