Documentation
use std::collections::HashMap;
use std::path::PathBuf;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, Runtime};
use wvb::remote;

pub use wvb::remote::HttpConfig as Http;

type DynamicDirFn<R> = fn(app: &AppHandle<R>) -> Result<PathBuf, Box<dyn std::error::Error>>;

#[derive(Clone)]
pub(crate) enum Dir<R: Runtime> {
  Static(String),
  Dynamic(DynamicDirFn<R>),
}

impl<R: Runtime> Dir<R> {
  pub fn resolve(&self, app: &AppHandle<R>) -> Result<PathBuf, Box<dyn std::error::Error>> {
    match self {
      Self::Static(dir) => Ok(PathBuf::from(dir)),
      Self::Dynamic(f) => {
        let dir = f(app)?;
        Ok(dir)
      }
    }
  }
}

#[derive(Clone, Default)]
pub struct Source<R: Runtime> {
  pub(crate) builtin_dir: Option<Dir<R>>,
  pub(crate) remote_dir: Option<Dir<R>>,
}

impl<R: Runtime> Source<R> {
  pub fn new() -> Self {
    Self {
      builtin_dir: None,
      remote_dir: None,
    }
  }

  pub fn builtin_dir<T: Into<String>>(mut self, dir: T) -> Self {
    self.builtin_dir = Some(Dir::Static(dir.into()));
    self
  }

  pub fn builtin_dir_fn(mut self, dir: DynamicDirFn<R>) -> Self {
    self.builtin_dir = Some(Dir::Dynamic(dir));
    self
  }

  pub fn remote_dir<T: Into<String>>(mut self, dir: T) -> Self {
    self.remote_dir = Some(Dir::Static(dir.into()));
    self
  }

  pub fn remote_dir_fn(mut self, dir: DynamicDirFn<R>) -> Self {
    self.remote_dir = Some(Dir::Dynamic(dir));
    self
  }

  pub(crate) fn resolve_builtin_dir(&self, app: &AppHandle<R>) -> crate::Result<PathBuf> {
    let dir = match self.builtin_dir {
      Some(ref builtin_dir) => {
        let dir = builtin_dir
          .resolve(app)
          .map_err(|e| crate::Error::FailToResolveDirectory(e.to_string()))?;
        dir
      }
      None => {
        let dir = app.path().resolve("bundles", BaseDirectory::Resource)?;
        dir
      }
    };
    Ok(dir)
  }

  pub(crate) fn resolve_remote_dir(&self, app: &AppHandle<R>) -> crate::Result<PathBuf> {
    let dir = match self.remote_dir {
      Some(ref remote_dir) => {
        let dir = remote_dir
          .resolve(app)
          .map_err(|e| crate::Error::FailToResolveDirectory(e.to_string()))?;
        dir
      }
      None => {
        let dir = app.path().resolve("bundles", BaseDirectory::Resource)?;
        dir
      }
    };
    Ok(dir)
  }
}

#[derive(Clone, Default)]
pub struct Remote {
  builder: remote::RemoteBuilder,
}

impl Remote {
  pub fn new(endpoint: impl Into<String>) -> Self {
    let builder = remote::Remote::builder().endpoint(endpoint);
    Self { builder }
  }

  pub fn http(mut self, http: Http) -> Self {
    self.builder = self.builder.http(http);
    self
  }

  pub fn on_download<F>(mut self, on_download: F) -> Self
  where
    F: Fn(u64, u64, String) + Send + Sync + 'static,
  {
    self.builder = self.builder.on_download(on_download);
    self
  }

  pub(crate) fn build(self) -> crate::Result<remote::Remote> {
    let remote = self.builder.build()?;
    Ok(remote)
  }
}

#[derive(Clone)]
pub struct BundleProtocolConfig {
  scheme: String,
}

impl BundleProtocolConfig {
  pub fn new<S: Into<String>>(scheme: S) -> Self {
    Self {
      scheme: scheme.into(),
    }
  }
}

#[derive(Clone)]
pub struct LocalProtocolConfig {
  scheme: String,
  pub(crate) hosts: HashMap<String, String>,
}

impl LocalProtocolConfig {
  pub fn new<S: Into<String>>(scheme: S) -> Self {
    Self {
      scheme: scheme.into(),
      hosts: HashMap::new(),
    }
  }

  pub fn new_with_hosts<T: Into<HashMap<String, String>>>(scheme: String, hosts: T) -> Self {
    Self {
      scheme,
      hosts: hosts.into(),
    }
  }

  pub fn host<T: Into<String>, U: Into<String>>(mut self, host: T, url: U) -> Self {
    self.hosts.insert(host.into(), url.into());
    self
  }

  pub fn hosts<T: Into<HashMap<String, String>>>(mut self, hosts: T) -> Self {
    self.hosts = hosts.into();
    self
  }
}

#[derive(Clone)]
pub enum Protocol {
  Bundle(BundleProtocolConfig),
  Local(LocalProtocolConfig),
}

impl Protocol {
  pub fn bundle<S: Into<String>>(scheme: S) -> BundleProtocolConfig {
    BundleProtocolConfig::new(scheme)
  }

  pub fn local<S: Into<String>>(scheme: S) -> LocalProtocolConfig {
    LocalProtocolConfig::new(scheme)
  }

  pub fn scheme(&self) -> &str {
    match self {
      Protocol::Bundle(x) => &x.scheme,
      Protocol::Local(x) => &x.scheme,
    }
  }
}

impl From<BundleProtocolConfig> for Protocol {
  fn from(value: BundleProtocolConfig) -> Self {
    Protocol::Bundle(value)
  }
}

impl From<LocalProtocolConfig> for Protocol {
  fn from(value: LocalProtocolConfig) -> Self {
    Protocol::Local(value)
  }
}

#[derive(Clone, Default)]
pub struct Config<R: Runtime> {
  pub(crate) source: Source<R>,
  pub(crate) protocols: Vec<Protocol>,
  pub(crate) remote: Option<Remote>,
}

impl<R: Runtime> Config<R> {
  pub fn new() -> Self {
    Self {
      source: Source::new(),
      protocols: vec![],
      remote: Default::default(),
    }
  }

  pub fn source(mut self, source: Source<R>) -> Self {
    self.source = source;
    self
  }

  pub fn protocol<P: Into<Protocol>>(mut self, protocol: P) -> Self {
    self.protocols.push(protocol.into());
    self
  }

  pub fn remote(mut self, remote: Remote) -> Self {
    self.remote = Some(remote);
    self
  }

  pub(crate) fn build_remote(&self) -> crate::Result<Option<remote::Remote>> {
    if let Some(ref remote_config) = self.remote {
      let remote = remote_config.clone().build()?;
      Ok(Some(remote))
    } else {
      Ok(None)
    }
  }
}