use std::time::Duration;
use std::time::UNIX_EPOCH;

use openpgp_cert_d as cert_d;

use cert_d::CertD;
use cert_d::Tag;

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let args = std::env::args().collect::<Vec<String>>();

    let certd = if args.len() == 1 {
        CertD::new()?
    } else if args.len() == 2 {
        CertD::with_base_dir(&args[1])?
    } else {
        eprintln!("Usage: {} [CERTD]", args[0]);
        return Err("Invalid arguments".into());
    };

    let mut size_one_count = [0; 64];
    let mut secs_one_count = [0; 64];
    let mut nanos_one_count = [0; 64];
    let mut tag_one_count = [0; 64];
    let mut count = 0;

    let count_ones = |one_count: &mut [usize; 64], mut value: u64| {
        for i in 0..64 {
            if value % 2 == 1 {
                one_count[i] += 1;
            }
            value >>= 1;
        }
    };

    for item in certd.iter_files() {
        let Ok((fpr, file)) = item else { continue };

        let m = match file.metadata() {
            Ok(m) => m,
            Err(err) => {
                eprintln!("Failed to stat {}: {}", fpr, err);
                continue;
            }
        };
        let tag = match Tag::try_from(&m) {
            Ok(m) => m,
            Err(err) => {
                eprintln!("Failed to compute tag for {}: {}", fpr, err);
                continue;
            }
        };

        count_ones(&mut size_one_count, m.len());

        let mtime =  m.modified()?
            .duration_since(UNIX_EPOCH)
            .unwrap_or_else(|_| Duration::new(0, 0));
        count_ones(&mut secs_one_count, mtime.as_secs());
        count_ones(&mut nanos_one_count, mtime.subsec_nanos() as u64);

        count_ones(&mut tag_one_count, tag.0);

        count += 1;
    }

    // Estimate the entropy using a Maximum Likelihood estimator
    // (i.e., shannon entropy).
    //
    // https://en.wikipedia.org/wiki/Entropy_estimation
    // https://strimmerlab.github.io/publications/lecture-notes/MATH20802/from-entropy-to-maximum-likelihood.html
    let mut size_entropy = 0f64;
    let mut secs_entropy = 0f64;
    let mut nanos_entropy = 0f64;
    let mut tag_entropy = 0f64;
    let mut max_entropy = 0f64;

    // Entropy of the probability (`p` = 0..1) of a binary event.
    let entropy = |p: f64| -> f64 {
        assert!(0. <= p);
        assert!(p <= 1.);

        // log(0) is NaN, so is 0 * log(0).  But we need 0 * log(0) to
        // be 0.
        if p < 0.0001 || p > 0.9999 {
            0.
        } else {
            -p * p.log2() - (1. - p) * (1. - p).log2()
        }
    };

    let p = |one_count: usize| -> f64 {
        f64::from(one_count as u32) / f64::from(count)
    };

    for i in 0..64 {
        let size_prob = p(size_one_count[i]);
        let secs_prob = p(secs_one_count[i]);
        let nanos_prob = p(nanos_one_count[i]);
        let tag_prob = p(tag_one_count[i]);

        size_entropy += entropy(size_prob);
        secs_entropy += entropy(secs_prob);
        nanos_entropy += entropy(nanos_prob);
        tag_entropy += entropy(tag_prob);
        // Sanity check.
        max_entropy += entropy(0.5);

        eprintln!("{:2}: size: {:3.0}% ({:5}); secs: {:3.0}% ({:5}); \
                   nanos: {:3.0}% ({:5}); tag: {:3.0}% ({:5})",
                  i,
                  size_prob * 100., size_one_count[i],
                  secs_prob * 100., secs_one_count[i],
                  nanos_prob * 100., nanos_one_count[i],
                  tag_prob * 100., tag_one_count[i]);
    }

    eprintln!("Maximum-likelihood estimate of entropy (max: {} bits):",
              max_entropy);
    eprintln!("  size: {:.2} bits", size_entropy);
    eprintln!("  secs: {:.2} bits", secs_entropy);
    eprintln!("  nanos: {:.2} bits", nanos_entropy);
    eprintln!("  max empirical entropy: {:.2} bits",
              size_entropy + secs_entropy + nanos_entropy);
    eprintln!("  tag: {:.2} bits", tag_entropy);

    Ok(())
}
