#![allow(clippy::module_name_repetitions)]
use std::{env, path::PathBuf};

use color_eyre::Result;
use directories::ProjectDirs;
pub use keybindings::{parse_key, Binding, KeyBindings};
use lazy_static::lazy_static;
use previewers::PreviewersConfig;
use serde::Deserialize;
use shell_integration::ShellIntegrationConfig;
use styles::Styles;
pub use themes::Theme;
use tracing::{debug, warn};
use ui::UiConfig;

mod keybindings;
mod previewers;
mod shell_integration;
mod styles;
mod themes;
mod ui;

const CONFIG: &str = include_str!("../../.config/config.toml");

#[allow(dead_code, clippy::module_name_repetitions)]
#[derive(Clone, Debug, Deserialize, Default)]
pub struct AppConfig {
    #[serde(default)]
    pub data_dir: PathBuf,
    #[serde(default)]
    pub config_dir: PathBuf,
    #[serde(default = "default_frame_rate")]
    pub frame_rate: f64,
    #[serde(default = "default_tick_rate")]
    pub tick_rate: f64,
}

#[allow(dead_code)]
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Config {
    #[allow(clippy::struct_field_names)]
    #[serde(default, flatten)]
    pub config: AppConfig,
    #[serde(default)]
    pub keybindings: KeyBindings,
    #[serde(default)]
    pub styles: Styles,
    #[serde(default)]
    pub ui: UiConfig,
    #[serde(default)]
    pub previewers: PreviewersConfig,
    #[serde(default)]
    pub shell_integration: ShellIntegrationConfig,
}

lazy_static! {
    pub static ref PROJECT_NAME: String = String::from("television");
    pub static ref PROJECT_NAME_UPPER: String = PROJECT_NAME.to_uppercase();
    pub static ref DATA_FOLDER: Option<PathBuf> =
        // if `TELEVISION_DATA` is set, use that as the data directory
        env::var_os(format!("{}_DATA", PROJECT_NAME_UPPER.clone())).map(PathBuf::from).or_else(|| {
            // otherwise, use the XDG data directory
            env::var_os("XDG_DATA_HOME").map(PathBuf::from).map(|p| p.join(PROJECT_NAME.as_str())).filter(|p| p.is_absolute())
        });
    pub static ref CONFIG_FOLDER: Option<PathBuf> =
        // if `TELEVISION_CONFIG` is set, use that as the television config directory
        env::var_os(format!("{}_CONFIG", PROJECT_NAME_UPPER.clone())).map(PathBuf::from).or_else(|| {
            // otherwise, use the XDG config directory + 'television'
            env::var_os("XDG_CONFIG_HOME").map(PathBuf::from).map(|p| p.join(PROJECT_NAME.as_str())).filter(|p| p.is_absolute())
        });
}

const CONFIG_FILE_NAME: &str = "config.toml";

impl Config {
    // FIXME: default management is a bit of a mess right now
    #[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
    pub fn new() -> Result<Self> {
        // Load the default_config values as base defaults
        let default_config: Config =
            toml::from_str(CONFIG).expect("default config should be valid");

        // initialize the config builder
        let data_dir = get_data_dir();
        let config_dir = get_config_dir();
        std::fs::create_dir_all(&config_dir)
            .expect("Failed creating configuration directory");
        std::fs::create_dir_all(&data_dir)
            .expect("Failed creating data directory");

        let mut builder = config::Config::builder()
            .set_default("data_dir", data_dir.to_str().unwrap())?
            .set_default("config_dir", config_dir.to_str().unwrap())?
            .set_default("frame_rate", default_config.config.frame_rate)?
            .set_default("tick_rate", default_config.config.tick_rate)?
            .set_default("ui", default_config.ui.clone())?
            .set_default("previewers", default_config.previewers.clone())?
            .set_default("theme", default_config.ui.theme.clone())?
            .set_default(
                "shell_integration",
                default_config.shell_integration.clone(),
            )?;

        // Load the user's config file
        let source = config::File::from(config_dir.join(CONFIG_FILE_NAME))
            .format(config::FileFormat::Toml)
            .required(false);
        builder = builder.add_source(source);

        if config_dir.join(CONFIG_FILE_NAME).is_file() {
            debug!("Found config file at {:?}", config_dir);
            let mut cfg: Self = builder.build()?.try_deserialize().unwrap();
            //.with_context(|| {
            //    format!(
            //        "Error parsing config file at {:?}",
            //        config_dir.join(CONFIG_FILE_NAME)
            //    )
            //})?;

            for (mode, default_bindings) in default_config.keybindings.iter() {
                let user_bindings = cfg.keybindings.entry(*mode).or_default();
                for (command, key) in default_bindings {
                    user_bindings
                        .entry(command.clone())
                        .or_insert_with(|| key.clone());
                }
            }

            for (mode, default_styles) in default_config.styles.iter() {
                let user_styles = cfg.styles.entry(*mode).or_default();
                for (style_key, style) in default_styles {
                    user_styles.entry(style_key.clone()).or_insert(*style);
                }
            }

            debug!("Config: {:?}", cfg);
            Ok(cfg)
        } else {
            warn!("No config file found at {:?}, creating default configuration file at that location.", config_dir);
            // create the default configuration file in the user's config directory
            std::fs::write(config_dir.join(CONFIG_FILE_NAME), CONFIG)?;
            Ok(default_config)
        }
    }
}

pub fn get_data_dir() -> PathBuf {
    let directory = if let Some(s) = DATA_FOLDER.clone() {
        debug!("Using data directory: {:?}", s);
        s
    } else if let Some(proj_dirs) = project_directory() {
        debug!("Falling back to default data dir");
        proj_dirs.data_local_dir().to_path_buf()
    } else {
        PathBuf::from("../../../../..").join(".data")
    };
    directory
}

pub fn get_config_dir() -> PathBuf {
    let directory = if let Some(s) = CONFIG_FOLDER.clone() {
        debug!("Config directory: {:?}", s);
        s
    } else if cfg!(unix) {
        // default to ~/.config/television for unix systems
        if let Some(base_dirs) = directories::BaseDirs::new() {
            let cfg_dir =
                base_dirs.home_dir().join(".config").join("television");
            debug!("Config directory: {:?}", cfg_dir);
            cfg_dir
        } else {
            PathBuf::from("../../../../..").join(".config")
        }
    } else if let Some(proj_dirs) = project_directory() {
        debug!("Falling back to default config dir");
        proj_dirs.config_local_dir().to_path_buf()
    } else {
        PathBuf::from("../../../../..").join("../../../../../.config")
    };
    directory
}

fn project_directory() -> Option<ProjectDirs> {
    ProjectDirs::from("com", "", env!("CARGO_PKG_NAME"))
}

fn default_frame_rate() -> f64 {
    60.0
}

fn default_tick_rate() -> f64 {
    50.0
}
