dprint 0.15.1

Binary for dprint code formatter—a pluggable and configurable code formatting platform.
use std::sync::Arc;
use std::collections::HashMap;
use std::time::Instant;
use std::path::Path;
use parking_lot::{Mutex, RwLock};

use dprint_core::types::ErrBox;

use crate::environment::Environment;
use crate::utils::{get_lowercase_file_extension, get_lowercase_file_name};
use crate::utils::ErrorCountLogger;
use super::{Plugin, InitializedPlugin, output_plugin_config_diagnostics};

/// This is necessary because of a circular reference where
/// PluginPools hold plugins and the plugins hold a PluginPools.
pub struct PluginsDropper<TEnvironment: Environment> {
    pools: Arc<PluginPools<TEnvironment>>,
}

impl<TEnvironment: Environment> Drop for PluginsDropper<TEnvironment> {
    fn drop(&mut self) {
        self.pools.drop_plugins();
    }
}

impl<TEnvironment: Environment> PluginsDropper<TEnvironment> {
    pub fn new(pools: Arc<PluginPools<TEnvironment>>) -> Self {
        PluginsDropper { pools }
    }
}

struct PluginNameResolutionMaps {
    extension_to_plugin_name_map: HashMap<String, String>,
    file_name_to_plugin_name_map: HashMap<String, String>,
}

pub struct PluginPools<TEnvironment : Environment> {
    environment: TEnvironment,
    pools: Mutex<HashMap<String, Arc<InitializedPluginPool<TEnvironment>>>>,
    plugin_name_maps: RwLock<PluginNameResolutionMaps>,
    /// Plugins may format using other plugins. If so, they should have a locally
    /// owned plugin instance that will be created on demand.
    plugins_for_plugins: Mutex<HashMap<String, HashMap<String, Vec<Box<dyn InitializedPlugin>>>>>,
}

impl<TEnvironment : Environment> PluginPools<TEnvironment> {
    pub fn new(environment: TEnvironment) -> Self {
        PluginPools {
            environment,
            pools: Mutex::new(HashMap::new()),
            plugin_name_maps: RwLock::new(PluginNameResolutionMaps {
                extension_to_plugin_name_map: HashMap::new(),
                file_name_to_plugin_name_map: HashMap::new(),
            }),
            plugins_for_plugins: Mutex::new(HashMap::new()),
        }
    }

    pub fn drop_plugins(&self) {
        {
            let mut pools = self.pools.lock();
            for pool in pools.values() {
                pool.drop_plugins();
            }
            pools.clear();
        }
        {
            let mut plugins_for_plugins = self.plugins_for_plugins.lock();
            plugins_for_plugins.clear();
        }
    }

    pub fn set_plugins(&self, plugins: Vec<Box<dyn Plugin>>) {
        let mut pools = self.pools.lock();
        let mut plugin_name_maps = self.plugin_name_maps.write();
        for plugin in plugins {
            let plugin_name = String::from(plugin.name());
            let plugin_extensions = plugin.file_extensions().clone();
            let plugin_file_names = plugin.file_names().clone();
            pools.insert(plugin_name.clone(), Arc::new(InitializedPluginPool::new(plugin, self.environment.clone())));
            for extension in plugin_extensions.iter() {
                // first added plugin takes precedence
                plugin_name_maps.extension_to_plugin_name_map.entry(extension.to_owned()).or_insert(plugin_name.clone());
            }
            for file_name in plugin_file_names.iter() {
                // first added plugin takes precedence
                plugin_name_maps.file_name_to_plugin_name_map.entry(file_name.to_owned()).or_insert(plugin_name.clone());
            }
        }
    }

    pub fn get_pool(&self, plugin_name: &str) -> Option<Arc<InitializedPluginPool<TEnvironment>>> {
        self.pools.lock().get(plugin_name).map(|p| p.clone())
    }

    pub fn take_instance_for_plugin(&self, parent_plugin_name: &str, sub_plugin_name: &str) -> Result<Box<dyn InitializedPlugin>, ErrBox> {
        let plugin = self.with_plugins_for_parent_and_sub_plugin(parent_plugin_name, sub_plugin_name, |plugins| plugins.pop());

        if let Some(plugin) = plugin {
            Ok(plugin)
        } else {
            let pool = self.get_pool(sub_plugin_name).expect("Expected the plugin to exist in the pool.");
            if let Some(plugin) = pool.take_if_available() {
                Ok(plugin)
            } else {
                pool.create_instance()
            }
        }
    }

    pub fn release_instance_for_plugin(&self, parent_plugin_name: &str, sub_plugin_name: &str, plugin: Box<dyn InitializedPlugin>) {
        // There is a chance the data in plugins_for_plugins was already cleared by another thread.
        // If that occurs, ensure it is recreated to allow this plugin to be released into the
        // main pool once the `release` method is called by the worker.
        self.with_plugins_for_parent_and_sub_plugin(parent_plugin_name, sub_plugin_name, |plugins| {
            plugins.push(plugin);
        });
    }

    fn with_plugins_for_parent_and_sub_plugin<TResult>(
        &self,
        parent_plugin_name: &str,
        sub_plugin_name: &str,
        with_plugins: impl FnOnce(&mut Vec<Box<dyn InitializedPlugin>>) -> TResult,
    ) -> TResult {
        let mut plugins_for_plugins = self.plugins_for_plugins.lock();
        let plugins_for_plugin = if let Some(plugins_for_plugin) = plugins_for_plugins.get_mut(parent_plugin_name) {
            plugins_for_plugin
        } else {
            plugins_for_plugins.insert(parent_plugin_name.to_string(), HashMap::new());
            plugins_for_plugins.get_mut(parent_plugin_name).unwrap()
        };
        let mut plugins = if let Some(plugins) = plugins_for_plugin.get_mut(sub_plugin_name) {
            plugins
        } else {
            plugins_for_plugin.insert(sub_plugin_name.to_string(), Vec::new());
            plugins_for_plugin.get_mut(sub_plugin_name).unwrap()
        };

        with_plugins(&mut plugins)
    }

    pub fn get_plugin_name_from_file_name(&self, file_name: &Path) -> Option<String> {
        let plugin_name_maps = self.plugin_name_maps.read();
        get_lowercase_file_name(file_name)
            .map(|file_name| plugin_name_maps.file_name_to_plugin_name_map.get(&file_name))
            .flatten()
            .or_else(|| get_lowercase_file_extension(file_name)
                .map(|ext| plugin_name_maps.extension_to_plugin_name_map.get(&ext))
                .flatten()
            )
            .map(|name| name.to_owned())
    }

    pub fn release(&self, parent_plugin_name: &str) {
        let plugins_for_plugin = self.plugins_for_plugins.lock().remove(parent_plugin_name);
        if let Some(plugins_for_plugin) = plugins_for_plugin {
            for (sub_plugin_name, initialized_plugins) in plugins_for_plugin.into_iter() {
                if let Some(pool) = self.get_pool(&sub_plugin_name) {
                    pool.release_all(initialized_plugins);
                }
            }
        }
    }

    /// Gets a hash to be used for the "incremental" feature to tell if any plugins have changed.
    pub fn get_plugins_hash(&self) -> u64 {
        use std::num::Wrapping;
        // yeah, I know adding hashes isn't right, but the chance of this not working
        // in order to tell when a plugin has changed is super low.
        let pools = self.pools.lock();
        let mut hash_sum = Wrapping(0);
        for (_, pool) in pools.iter() {
            hash_sum += Wrapping(pool.plugin.get_hash());
        }
        hash_sum.0
    }
}

pub struct PoolTimeSnapshot {
    pub startup_time: u64,
    pub average_format_time: u64,
    pub has_plugin_available: bool,
}

struct PluginTimeStats {
    startup_time: u64,
    total_format_time: u64,
    format_count: u64,
}

pub enum TakePluginResult {
    HadDiagnostics,
    Success(Box<dyn InitializedPlugin>),
}

pub struct InitializedPluginPool<TEnvironment : Environment> {
    environment: TEnvironment,
    name: String,
    plugin: Box<dyn Plugin>,
    items: Mutex<Vec<Box<dyn InitializedPlugin>>>, // todo: RwLock
    time_stats: RwLock<PluginTimeStats>,
    checked_diagnostics: Mutex<Option<bool>>,
}

impl<TEnvironment : Environment> InitializedPluginPool<TEnvironment> {
    pub fn new(plugin: Box<dyn Plugin>, environment: TEnvironment) -> InitializedPluginPool<TEnvironment> {
        InitializedPluginPool {
            environment,
            name: plugin.name().to_string(),
            plugin: plugin,
            items: Mutex::new(Vec::new()),
            time_stats: RwLock::new(PluginTimeStats {
                // assume this if never created
                startup_time: 250,
                // give each plugin an average format time to start
                total_format_time: 50,
                format_count: 1,
            }),
            checked_diagnostics: Mutex::new(None),
        }
    }

    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    pub fn drop_plugins(&self) {
        let mut items = self.items.lock();
        items.clear();
    }

    pub fn take_or_create_checking_config_diagnostics(
        &self,
        error_logger: &ErrorCountLogger<TEnvironment>
    ) -> Result<TakePluginResult, ErrBox> {
        if let Some(plugin) = self.take_if_available() {
            Ok(TakePluginResult::Success(plugin))
        } else {
            let instance = self.create_instance()?;

            // only allow one thread to ever check and output the diagnostics (we don't want the messages being spammed)
            let mut has_checked_diagnostics = self.checked_diagnostics.lock();
            match *has_checked_diagnostics {
                Some(was_success) => if !was_success {
                    return Ok(TakePluginResult::HadDiagnostics);
                },
                None => {
                    let result = output_plugin_config_diagnostics(self.name(), &instance, &error_logger);
                    *has_checked_diagnostics = Some(result.is_ok());
                    if let Err(err) = result {
                        self.environment.log_error(&err.to_string());
                        return Ok(TakePluginResult::HadDiagnostics);
                    }
                }
            }

            Ok(TakePluginResult::Success(instance))
        }
    }

    pub fn take_if_available(&self) -> Option<Box<dyn InitializedPlugin>> {
        let mut items = self.items.lock();
        items.pop()
    }

    pub fn release(&self, plugin: Box<dyn InitializedPlugin>) {
        let mut items = self.items.lock();
        items.push(plugin);
    }

    pub fn release_all(&self, plugins: Vec<Box<dyn InitializedPlugin>>) {
        let mut items = self.items.lock();
        items.extend(plugins);
    }

    pub fn get_time_snapshot(&self) -> PoolTimeSnapshot {
        let has_plugin_available = !self.items.lock().is_empty();
        let time_stats = self.time_stats.read();
        let average_format_time = (time_stats.total_format_time as f64 / time_stats.format_count as f64) as u64;
        PoolTimeSnapshot {
            startup_time: time_stats.startup_time,
            average_format_time,
            has_plugin_available,
        }
    }

    fn create_instance(&self) -> Result<Box<dyn InitializedPlugin>, ErrBox> {
        let start_instant = Instant::now();
        log_verbose!(self.environment, "Creating instance of {}", self.plugin.name());
        let plugin = self.plugin.initialize()?;
        let startup_duration = start_instant.elapsed().as_millis() as u64;
        log_verbose!(self.environment, "Created instance of {} in {}ms", self.plugin.name(), startup_duration);
        self.time_stats.write().startup_time = startup_duration; // store the latest duration
        Ok(plugin)
    }

    pub fn format_measuring_time<TResult>(&self, mut action: impl FnMut() -> TResult) -> TResult {
        let start_instant = Instant::now();
        let result = action();
        let elapsed_time = start_instant.elapsed();
        let mut time_stats = self.time_stats.write();
        time_stats.total_format_time += elapsed_time.as_millis() as u64;
        time_stats.format_count += 1;
        result
    }
}