/*
 * This file is part of espanso.
 *
 * Copyright (C) 2019-2021 Federico Terzi
 *
 * espanso is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * espanso is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with espanso.  If not, see .
 */
use super::{resolve::ResolvedConfig, Config, ConfigStore, ConfigStoreError};
use anyhow::Result;
use log::{debug, error};
use std::{collections::HashSet, path::Path};
pub(crate) struct DefaultConfigStore {
  default: Box,
  customs: Vec>,
}
impl ConfigStore for DefaultConfigStore {
  fn default(&self) -> &dyn super::Config {
    self.default.as_ref()
  }
  fn active<'a>(&'a self, app: &super::AppProperties) -> &'a dyn super::Config {
    // Find a custom config that matches or fallback to the default one
    for custom in self.customs.iter() {
      if custom.is_match(app) {
        return custom.as_ref();
      }
    }
    self.default.as_ref()
  }
  fn configs(&self) -> Vec<&dyn Config> {
    let mut configs = Vec::new();
    configs.push(self.default.as_ref());
    for custom in self.customs.iter() {
      configs.push(custom.as_ref());
    }
    configs
  }
  // TODO: test
  fn get_all_match_paths(&self) -> HashSet {
    let mut paths = HashSet::new();
    paths.extend(self.default().match_paths().iter().cloned());
    for custom in self.customs.iter() {
      paths.extend(custom.match_paths().iter().cloned());
    }
    paths
  }
}
impl DefaultConfigStore {
  // TODO: test
  pub fn load(config_dir: &Path) -> Result {
    if !config_dir.is_dir() {
      return Err(ConfigStoreError::InvalidConfigDir().into());
    }
    // First get the default.yml file
    let default_file = config_dir.join("default.yml");
    if !default_file.exists() || !default_file.is_file() {
      return Err(ConfigStoreError::MissingDefault().into());
    }
    let default = ResolvedConfig::load(&default_file, None)?;
    debug!("loaded default config at path: {:?}", default_file);
    // Then the others
    let mut customs: Vec> = Vec::new();
    for entry in std::fs::read_dir(config_dir).map_err(ConfigStoreError::IOError)? {
      let entry = entry?;
      let config_file = entry.path();
      let extension = config_file
        .extension()
        .unwrap_or_default()
        .to_string_lossy()
        .to_lowercase();
      // Additional config files are loaded best-effort
      if config_file.is_file()
        && config_file != default_file
        && (extension == "yml" || extension == "yaml")
      {
        match ResolvedConfig::load(&config_file, Some(&default)) {
          Ok(config) => {
            customs.push(Box::new(config));
            debug!("loaded config at path: {:?}", config_file);
          }
          Err(err) => {
            error!(
              "unable to load config at path: {:?}, with error: {}",
              config_file, err
            );
          }
        }
      }
    }
    Ok(Self {
      default: Box::new(default),
      customs,
    })
  }
  pub fn from_configs(default: Box, customs: Vec>) -> Result {
    Ok(Self { default, customs })
  }
}
#[cfg(test)]
mod tests {
  use super::*;
  struct MockConfig {
    label: String,
    is_match: bool,
  }
  impl MockConfig {
    pub fn new(label: &str, is_match: bool) -> Self {
      Self {
        label: label.to_string(),
        is_match,
      }
    }
  }
  impl Config for MockConfig {
    fn id(&self) -> i32 {
      0
    }
    fn label(&self) -> &str {
      &self.label
    }
    fn match_paths(&self) -> &[String] {
      unimplemented!()
    }
    fn is_match(&self, _: &crate::config::AppProperties) -> bool {
      self.is_match
    }
    fn backend(&self) -> crate::config::Backend {
      unimplemented!()
    }
  }
  #[test]
  fn config_store_selects_correctly() {
    let default = MockConfig::new("default", false);
    let custom1 = MockConfig::new("custom1", false);
    let custom2 = MockConfig::new("custom2", true);
    let store = DefaultConfigStore {
      default: Box::new(default),
      customs: vec![Box::new(custom1), Box::new(custom2)],
    };
    assert_eq!(store.default().label(), "default");
    assert_eq!(
      store
        .active(&crate::config::AppProperties {
          title: None,
          class: None,
          exec: None,
        })
        .label(),
      "custom2"
    );
  }
  #[test]
  fn config_store_active_fallback_to_default_if_no_match() {
    let default = MockConfig::new("default", false);
    let custom1 = MockConfig::new("custom1", false);
    let custom2 = MockConfig::new("custom2", false);
    let store = DefaultConfigStore {
      default: Box::new(default),
      customs: vec![Box::new(custom1), Box::new(custom2)],
    };
    assert_eq!(store.default().label(), "default");
    assert_eq!(
      store
        .active(&crate::config::AppProperties {
          title: None,
          class: None,
          exec: None,
        })
        .label(),
      "default"
    );
  }
}