//! Estimates CPU (processor) core frequency.
//!
//! Estimates the CPU frequency by measuring the time it takes to run a sequence of
//! instructions in a dependency chain. The instruction sequence is written in assembly like this:
//!
//! ```assembly
//!     inc    %rax
//!     inc    %rax
//!     inc    %rax
//!     inc    %rax
//! ```
//!
//! This method does not use performance counters (other than the wall clock), and
//! the estimated frequency is likely to be the boosted frequency if such a feature is available
//! on the system, subject to temperature and/or other conditions.
//!
//! While this method of frequency estimation works most of the time, there are a number of known
//! example of microarchitectures where this measurement reports half the actual operating frequency
//! (e.g. IBM POWER7 and POWER8).
//!
//! This crate currently supports following platforms:
//!
//! | ISA       | Linux   | macOS | Windows(MSVC) |
//! | --------- | ------- | ----- | ------------- |
//! | aarch64   | ☑       | ☑     | ☑             |
//! | i586      | ☑       |       |               |
//! | i686      | ☑       |       | not yet       |
//! | mips      | not yet |       |               |
//! | mips64    | ☑       |       |               |
//! | powerpc   | not yet |       |               |
//! | powerpc64 | not yet |       |               |
//! | riscv64   | ☑       |       |               |
//! | s390x     | not yet |       |               |
//! | sparc     | not yet |       |               |
//! | sparc64   | not yet |       |               |
//! | x86_64    | ☑       | ☑     | ☑             |
//!
use std::time::{Duration, Instant};
use thiserror::Error;

#[link(name = "calcmhz")]
extern "C" {
    fn incrementSequence(nloops: u64) -> u64;
}

/// The error type for estimation functions.
#[derive(Error, Debug)]
pub enum Error {
    /// Indicates that the estimation process failed to calculate the CPU frequency
    /// because the CPU was so fast that the wall clock did not change after running the
    /// measurement instruction sequence, or loop counter overflow have occurred when trying to
    /// adjust it to meet the specified minimum duration.
    #[error(
        "Wall clock did not change or loop count overflow during the measurement. Too fast CPU?"
    )]
    CpuTooFastError,
}

/// Result of estimation.
pub struct Estimation {
    /// Estimated CPU frequency in MHz.
    pub mhz: f64,
    /// The time it took to run the measurement.
    pub elapsed: Duration,
    /// The number of loops is adjusted to meet the minimum duration requirement specified. This is the final adjusted loop count used in the estimation.
    pub loops: u64,
}

/// Reasonable value for use as the initial number of loops. 10000.
pub const REASONABLE_INITIAL_LOOPS: u64 = 10000u64;
/// Reasonable duration for the estimation. 500ms.
pub const REASONABLE_MIN_DURATION: Duration = Duration::from_millis(500);

/// Runs the measurement instruction sequence and returns the estimated CPU frequency.
/// The initial number of loops is specified in the `loops` argument.
/// If the elapsed time is shorter than `min_duration` the function tries to
/// adjust the number of loops to meet the minimum duration requirement.
///
/// The adjusted loop count is returned in the `loops` field in the returned [`Estimation`].
/// The returned `loops` variable can be passed to subsequent calls to this function as hint.
/// [`REASONABLE_INITIAL_LOOPS`] and [`REASONABLE_MIN_DURATION`] are reasonable defaults for
/// these settings in typical systems.
///
/// The function returns [`Error::CpuTooFastError`] when the wall clock
/// does not change after the measurement or the required loop count exceeds the
/// range of `u64`.
/// The elapsed time is returned in the `elapsed` field in the returned [`Estimation`].
///
/// ```
/// let estimate = calcmhz::estimate_mhz(
///   calcmhz::REASONABLE_INITIAL_LOOPS,
///   calcmhz::REASONABLE_MIN_DURATION
/// ).unwrap();
/// assert!(estimate.mhz > 4.7);
/// println!("{} MHz, took {} seconds to run {} loops",
///   estimate.mhz,
///   estimate.elapsed.as_secs_f64(),
///   estimate.loops
/// );
/// ```
pub fn estimate_mhz(
    initial_loops: u64,
    min_duration: Duration,
) -> Result<Estimation, crate::Error> {
    let mut loops = initial_loops;
    let min_duration_as_secs = min_duration.as_secs_f64();

    loop {
        let start = Instant::now();
        let number_of_insns = unsafe { incrementSequence(loops) };
        let elapsed = start.elapsed();

        if elapsed >= min_duration {
            return Ok(Estimation {
                mhz: 1.0e-6 * (number_of_insns as f64) / elapsed.as_secs_f64(),
                elapsed: elapsed,
                loops: loops,
            });
        }

        // Insufficient elapsed time. Adjust number of loops.
        let newloops = if elapsed.is_zero() {
            loops << 1
        } else {
            let elapsed_secs = elapsed.as_secs_f64();
            (loops as f64 * 1.25 * (min_duration_as_secs / elapsed_secs)) as u64
        };
        // detect overflow
        if newloops <= loops {
            return Err(crate::Error::CpuTooFastError);
        }
        loops = newloops;
    }
}

/// Convenience function that is used when all that is required is the CPU frequency.
/// Uses reasonable defaults for [`estimate_mhz`] and returns just the CPU frequency.
///
/// ```
/// let cpu_frequency_in_mhz = calcmhz::mhz().unwrap();
/// assert!(cpu_frequency_in_mhz > 4.7);
/// println!("{} MHz", cpu_frequency_in_mhz);
/// ```
pub fn mhz() -> Result<f64, crate::Error> {
    match estimate_mhz(REASONABLE_INITIAL_LOOPS, REASONABLE_MIN_DURATION) {
        Ok(estimation) => Ok(estimation.mhz),
        Err(e) => Err(e),
    }
}
