//! The `lib_bench` module
//!
//! This module runs all the library benchmarks

use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt::Debug;
use std::marker::{Send, Sync};
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;

use anyhow::Result;

use super::format::{LibraryBenchmarkHeader, OutputFormat};
use super::meta::Metadata;
use super::summary::{BaselineKind, BaselineName, BenchmarkKind, BenchmarkSummary, SummaryOutput};
use super::tool::config::ToolConfigs;
use super::tool::path::{ToolOutputPath, ToolOutputPathKind};
use super::tool::run::RunOptions;
use crate::api::{
    EntryPoint, LibraryBenchmarkConfig, LibraryBenchmarkGroups, RawToolArgs, ValgrindTool,
};
use crate::error::Error;
use crate::runner::args;
use crate::runner::common::{
    BaselineDataProcessor, Baselines, BenchmarkDataProcessor, BenchmarkSummaries, CapturedOutput,
    Config, Groups, LoadBaselineDataProcessor, ModulePath, Runner, SaveBaselineDataProcessor,
};

/// Implements [`Benchmark`] to run a [`LibBench`] and compare against an earlier [`BenchmarkKind`]
#[derive(Debug)]
pub struct BaselineBenchmark {
    baseline_kind: BaselineKind,
}

/// A `LibBench` represents a single benchmark under the `#[library_benchmark]` attribute macro
///
/// It needs an implementation of `Benchmark` to be run.
#[derive(Debug, Clone)]
pub struct LibBench {
    /// The index of the `#[bench]` in the `#[library_benchmark]`
    pub bench_index: usize,
    /// The arguments of the `consts` parameter as a single string
    pub consts_display: Option<String>,
    /// The default [`ValgrindTool`]. If not changed it is `Callgrind`.
    pub default_tool: ValgrindTool,
    /// The arguments of `args` attribute as a single string
    pub display: Option<String>,
    /// The name of the annotated function
    pub function_name: String,
    /// The index of the `#[library_benchmark]` in the `library_benchmark_group!`
    pub group_index: usize,
    /// The id of the benchmark as in `#[bench::id]`
    pub id: Option<String>,
    /// The index of the element in the iterator of `#[benches::id(iter = ITERATOR)]` if present
    pub iter_index: Option<usize>,
    /// The [`ModulePath`].
    ///
    /// This is an artificial path for display purposes and does not reflect the real module path
    /// of the benchmark in the benchmark file
    pub module_path: ModulePath,
    /// The [`OutputFormat`]
    pub output_format: OutputFormat,
    /// The [`RunOptions`]
    pub run_options: RunOptions,
    /// The tool configurations for this benchmark run
    pub tools: ToolConfigs,
}

/// Implements [`Benchmark`] to load a [`LibBench`] baseline run and compare against another
/// baseline
///
/// This benchmark runner does not run valgrind or execute anything.
#[derive(Debug)]
pub struct LoadBaselineBenchmark {
    baseline: BaselineName,
    loaded_baseline: BaselineName,
}

/// Implements [`Benchmark`] to save a [`LibBench`] run as baseline. If present compare against a
/// former baseline with the same name
#[derive(Debug)]
pub struct SaveBaselineBenchmark {
    baseline: BaselineName,
}

/// Strategy interface for executing library benchmarks in different baseline modes.
///
/// Despite having the same name, this trait differs from `bin_bench::Benchmark` and is
/// designed to run a `LibBench` only.
pub trait Benchmark: Debug + Send + Sync {
    /// Returns the pair of loaded and active baseline names used for this run.
    fn baselines(&self) -> Baselines;

    /// Creates the post-run data processor for the selected tools.
    ///
    /// The processor uses [`ToolConfigs`], the benchmark `project_root`, and the computed
    /// [`ToolOutputPath`] to parse metrics, evaluate regressions, and produce additional
    /// artifacts.
    fn data_processor(
        &self,
        tools: &ToolConfigs,
        project_root: &Path,
        output_path: &ToolOutputPath,
    ) -> Box<dyn BenchmarkDataProcessor>;

    /// Computes the output location for this benchmark run.
    ///
    /// The path is derived from [`LibBench`], the global [`Config`], the enclosing
    /// `group_module_path`, and whether temporary directories should be used.
    ///
    /// # Errors
    ///
    /// Returns an error if the output path cannot be created or initialized.
    fn default_output_path(
        &self,
        lib_bench: &LibBench,
        config: &Config,
        group_module_path: &ModulePath,
        use_temp_dir: bool,
    ) -> Result<ToolOutputPath>;

    /// Executes a benchmark and returns its populated summary.
    ///
    /// The method consumes [`LibBench`], uses `main_index` to address the benchmark harness entry,
    /// optionally captures output into [`CapturedOutput`], reacts to `force_shutdown`, and writes
    /// artifacts under [`ToolOutputPath`].
    ///
    /// # Errors
    ///
    /// Returns an error if launching, running, or post-processing the benchmark fails.
    fn run(
        &self,
        lib_bench: LibBench,
        config: &Config,
        main_index: usize,
        captured_output: Option<CapturedOutput>,
        force_shutdown: &Arc<AtomicBool>,
        output_path: ToolOutputPath,
    ) -> Result<BenchmarkSummary>;
}

impl Benchmark for BaselineBenchmark {
    fn default_output_path(
        &self,
        lib_bench: &LibBench,
        config: &Config,
        group_module_path: &ModulePath,
        use_temp_dir: bool,
    ) -> Result<ToolOutputPath> {
        let kind = if lib_bench.default_tool.has_output_file() {
            ToolOutputPathKind::Out
        } else {
            ToolOutputPathKind::Log
        };
        let output_path = ToolOutputPath::new(
            kind,
            lib_bench.default_tool,
            &self.baseline_kind,
            &config.meta.target_dir,
            group_module_path,
            &lib_bench.name(),
            use_temp_dir,
        )?;

        if !use_temp_dir {
            output_path.init()?;
        }

        Ok(output_path)
    }

    fn baselines(&self) -> Baselines {
        match &self.baseline_kind {
            BaselineKind::Old => (None, None),
            BaselineKind::Name(name) => (None, Some(name.to_string())),
        }
    }

    fn run(
        &self,
        lib_bench: LibBench,
        config: &Config,
        main_index: usize,
        captured_output: Option<CapturedOutput>,
        force_shutdown: &Arc<AtomicBool>,
        output_path: ToolOutputPath,
    ) -> Result<BenchmarkSummary> {
        let header = LibraryBenchmarkHeader::new(&lib_bench);
        let benchmark_summary = lib_bench.create_benchmark_summary(
            config,
            &output_path,
            &lib_bench.function_name,
            header.description(),
            self.baselines(),
        );

        lib_bench.tools.run(
            benchmark_summary,
            config,
            &config.bench_bin,
            &lib_bench.bench_args(main_index),
            &lib_bench.run_options,
            &output_path,
            &lib_bench.module_path,
            captured_output.as_ref(),
            force_shutdown,
        )
    }

    fn data_processor(
        &self,
        tools: &ToolConfigs,
        project_root: &Path,
        output_path: &ToolOutputPath,
    ) -> Box<dyn BenchmarkDataProcessor> {
        Box::new(BaselineDataProcessor {
            analyzers: tools.analyzers(project_root, output_path),
        })
    }
}

impl LibBench {
    /// Returns whether any configured tool enables fail-fast regression handling.
    pub fn is_fail_fast(&self) -> bool {
        self.tools
            .0
            .iter()
            .any(|c| c.regression_config.is_fail_fast())
    }

    /// Creates a new library benchmark.
    pub fn new(
        id: Option<String>,
        display: Option<String>,
        consts_display: Option<String>,
        module_path: ModulePath,
        function_name: String,
        meta: &Metadata,
        config: LibraryBenchmarkConfig,
        group_index: usize,
        bench_index: usize,
        iter_index: Option<usize>,
        default_tool: ValgrindTool,
    ) -> Result<Option<Self>> {
        let id = if let Some(iter_index) = iter_index {
            id.as_ref().map(|s| format!("{s}_{iter_index}"))
        } else {
            id
        };

        if let Some(filter) = meta.args.filter.as_ref() {
            let is_matched = match id.as_ref() {
                Some(id) => filter.apply(module_path.join(id).as_str()),
                None => filter.apply(module_path.as_str()),
            };
            if !is_matched {
                return Ok(None);
            }
        }

        let mut envs = config.resolve_envs();
        // meta envs are already resolved
        envs.extend(meta.args.envs.iter().flatten().cloned());

        let mut default_args = HashMap::new();

        // The cachegrind client requests are not inserted into the benchmark function if the
        // default tool is not cachegrind, so setting --instr-at-start to `no` is only required if
        // the default tool sent by the benchmark harness (not with command-line arguments) is
        // cachegrind. Also, we only need to set this in library benchmarks, so it's best to use
        // `default_args` to add this command-line argument.
        let default_tool = if let Some(meta_default_tool) = meta.args.default_tool {
            meta_default_tool
        } else {
            if default_tool == ValgrindTool::Cachegrind {
                default_args.insert(
                    ValgrindTool::Cachegrind,
                    RawToolArgs::new(["--instr-at-start=no"]),
                );
            }
            config.default_tool.unwrap_or(default_tool)
        };

        let mut output_format = config
            .output_format
            .map_or_else(OutputFormat::default, Into::into);
        output_format.kind = meta.args.output_format;

        let tool_configs = ToolConfigs::new(
            &mut output_format,
            config.tools,
            &module_path,
            id.as_ref(),
            meta,
            default_tool,
            &EntryPoint::Default,
            &config.valgrind_args,
            &default_args,
        )
        .map_err(|error| {
            Error::ConfigurationError(module_path.clone(), id.clone(), error.to_string())
        })?;

        Ok(Some(Self {
            group_index,
            bench_index,
            iter_index,
            id,
            function_name,
            display,
            consts_display,
            run_options: RunOptions {
                env_clear: meta
                    .args
                    .env_clear
                    .unwrap_or_else(|| config.env_clear.unwrap_or(args::defaults::ENV_CLEAR)),
                envs,
                ..Default::default()
            },
            tools: tool_configs,
            module_path,
            output_format,
            default_tool,
        }))
    }

    /// The name of this `LibBench` consisting of the name of the benchmark function and if present,
    /// the id of the bench attribute (`#[bench::ID(...)]`)
    ///
    /// The name is used to identify a benchmark run within the same [`Group`] and has therefore to
    /// be unique within the same [`Group`]
    ///
    /// [`Group`]: [crate::runner::common::Group]
    fn name(&self) -> String {
        if let Some(bench_id) = &self.id {
            format!("{}.{}", &self.function_name, bench_id)
        } else {
            self.function_name.clone()
        }
    }

    /// The arguments for the `bench_bin` to actually run the benchmark function
    fn bench_args(&self, main_index: usize) -> Vec<OsString> {
        // The string has a fixed length to have an equal argument parser in the benchmark binary
        // for all benchmarks.
        let index_to_string = |index| format!("{index:05}");

        let mut args = vec![
            OsString::from("--gungraun-run".to_owned()),
            OsString::from(index_to_string(main_index)),
            OsString::from(index_to_string(self.group_index)),
            OsString::from(index_to_string(self.bench_index)),
        ];

        if let Some(iter_index) = self.iter_index {
            args.push(OsString::from(index_to_string(iter_index)));
        }

        args
    }

    /// This method creates the initial [`BenchmarkSummary`]
    fn create_benchmark_summary(
        &self,
        config: &Config,
        output_path: &ToolOutputPath,
        function_name: &str,
        description: Option<String>,
        baselines: Baselines,
    ) -> BenchmarkSummary {
        let summary_output = config
            .meta
            .args
            .save_summary
            .map(|format| SummaryOutput::new(format, &output_path.dir));

        BenchmarkSummary::new(
            BenchmarkKind::LibraryBenchmark,
            config.meta.project_root.clone(),
            config.package_dir.clone(),
            config.bench_file.clone(),
            config.bench_bin.clone(),
            &self.module_path,
            function_name,
            self.id.clone(),
            description,
            summary_output,
            baselines,
        )
    }
}

impl Benchmark for LoadBaselineBenchmark {
    fn default_output_path(
        &self,
        lib_bench: &LibBench,
        config: &Config,
        group_module_path: &ModulePath,
        _use_temp_dir: bool,
    ) -> Result<ToolOutputPath> {
        let kind = if lib_bench.default_tool.has_output_file() {
            ToolOutputPathKind::BaseOut(self.loaded_baseline.to_string())
        } else {
            ToolOutputPathKind::BaseLog(self.loaded_baseline.to_string())
        };
        ToolOutputPath::new(
            kind,
            lib_bench.default_tool,
            &BaselineKind::Name(self.baseline.clone()),
            &config.meta.target_dir,
            group_module_path,
            &lib_bench.name(),
            false,
        )
    }

    fn baselines(&self) -> Baselines {
        (
            Some(self.loaded_baseline.to_string()),
            Some(self.baseline.to_string()),
        )
    }

    fn run(
        &self,
        lib_bench: LibBench,
        config: &Config,
        _main_index: usize,
        _captured_output: Option<CapturedOutput>,
        _force_shutdown: &Arc<AtomicBool>,
        output_path: ToolOutputPath,
    ) -> Result<BenchmarkSummary> {
        let header = LibraryBenchmarkHeader::new(&lib_bench);
        Ok(lib_bench.create_benchmark_summary(
            config,
            &output_path,
            &lib_bench.function_name,
            header.description(),
            self.baselines(),
        ))
    }

    fn data_processor(
        &self,
        tools: &ToolConfigs,
        project_root: &Path,
        output_path: &ToolOutputPath,
    ) -> Box<dyn BenchmarkDataProcessor> {
        Box::new(LoadBaselineDataProcessor {
            analyzers: tools.analyzers(project_root, output_path),
        })
    }
}

impl Benchmark for SaveBaselineBenchmark {
    fn default_output_path(
        &self,
        lib_bench: &LibBench,
        config: &Config,
        group_module_path: &ModulePath,
        use_temp_dir: bool,
    ) -> Result<ToolOutputPath> {
        let kind = if lib_bench.default_tool.has_output_file() {
            ToolOutputPathKind::BaseOut(self.baseline.to_string())
        } else {
            ToolOutputPathKind::BaseLog(self.baseline.to_string())
        };

        let output_path = ToolOutputPath::new(
            kind,
            lib_bench.default_tool,
            &BaselineKind::Name(self.baseline.clone()),
            &config.meta.target_dir,
            group_module_path,
            &lib_bench.name(),
            use_temp_dir,
        )?;

        if !use_temp_dir {
            output_path.init()?;
        }

        Ok(output_path)
    }

    fn baselines(&self) -> Baselines {
        (
            Some(self.baseline.to_string()),
            Some(self.baseline.to_string()),
        )
    }

    fn run(
        &self,
        lib_bench: LibBench,
        config: &Config,
        main_index: usize,
        captured_output: Option<CapturedOutput>,
        force_shutdown: &Arc<AtomicBool>,
        output_path: ToolOutputPath,
    ) -> Result<BenchmarkSummary> {
        let header = LibraryBenchmarkHeader::new(&lib_bench);
        let benchmark_summary = lib_bench.create_benchmark_summary(
            config,
            &output_path,
            &lib_bench.function_name,
            header.description(),
            self.baselines(),
        );

        lib_bench.tools.run(
            benchmark_summary,
            config,
            &config.bench_bin,
            &lib_bench.bench_args(main_index),
            &lib_bench.run_options,
            &output_path,
            &lib_bench.module_path,
            captured_output.as_ref(),
            force_shutdown,
        )
    }

    fn data_processor(
        &self,
        tools: &ToolConfigs,
        project_root: &Path,
        output_path: &ToolOutputPath,
    ) -> Box<dyn BenchmarkDataProcessor> {
        Box::new(SaveBaselineDataProcessor {
            analyzers: tools.analyzers(project_root, output_path),
        })
    }
}

/// Creates the library benchmark executor [`Benchmark`] matching the current baseline mode.
///
/// # Panics
///
/// Panics when `--load-baseline` is active but no comparison baseline is configured.
pub fn benchmark_factory(config: &Config) -> Arc<dyn Benchmark> {
    if let Some(baseline_name) = &config.meta.args.save_baseline {
        Arc::new(SaveBaselineBenchmark {
            baseline: baseline_name.clone(),
        })
    } else if let Some(baseline_name) = &config.meta.args.load_baseline {
        Arc::new(LoadBaselineBenchmark {
            loaded_baseline: baseline_name.clone(),
            baseline: config
                .meta
                .args
                .baseline
                .as_ref()
                .expect("A baseline should be present")
                .clone(),
        })
    } else {
        Arc::new(BaselineBenchmark {
            baseline_kind: config
                .meta
                .args
                .baseline
                .as_ref()
                .map_or(BaselineKind::Old, |name| BaselineKind::Name(name.clone())),
        })
    }
}

/// Print a list of all benchmarks with a short summary
pub fn list(benchmark_groups: LibraryBenchmarkGroups, config: &Config) -> Result<()> {
    Groups::from_library_benchmark(&config.module_path, benchmark_groups, &config.meta)
        .map(Groups::list)
}

/// The top-level method which should be used to initiate running all benchmarks
pub fn run(benchmark_groups: LibraryBenchmarkGroups, config: Config) -> Result<BenchmarkSummaries> {
    Runner::from_library_benchmark(benchmark_groups, config).and_then(Runner::run)
}
