// Copyright 2019-2020 Georges Racinet <georges.racinet@octobus.net>
//
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2 or any later version.

use std::fs::File;
use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::time::Instant;

use hg::revlog::node::*;
use hg::revlog::nodemap::*;
use hg::revlog::*;
use memmap2::MmapOptions;
use rand::Rng;

mod index;
use index::Index;

fn mmap_index(repo_path: &Path) -> Index {
    let mut path = PathBuf::from(repo_path);
    path.extend([".hg", "store", "00changelog.i"].iter());
    Index::load_mmap(path)
}

fn mmap_nodemap(path: &Path) -> NodeTree {
    let file = File::open(path).unwrap();
    let mmap = unsafe { MmapOptions::new().map(&file).unwrap() };
    let len = mmap.len();
    NodeTree::load_bytes(Box::new(mmap), len)
}

/// Scan the whole index and create the corresponding nodemap file at `path`
fn create(index: &Index, path: &Path) -> io::Result<()> {
    let mut file = File::create(path)?;
    let start = Instant::now();
    let mut nm = NodeTree::default();
    for rev in 0..index.len() {
        let rev = Revision(rev as BaseRevision);
        nm.insert(index, index.node(rev), rev).unwrap();
    }
    eprintln!("Nodemap constructed in RAM in {:?}", start.elapsed());
    file.write_all(&nm.into_readonly_and_added_bytes().1)?;
    eprintln!("Nodemap written to disk");
    Ok(())
}

fn query(index: &Index, nm: &NodeTree, prefix: &str) {
    let start = Instant::now();
    let res = NodePrefix::from_hex(prefix).map(|p| nm.find_bin(index, p));
    println!("Result found in {:?}: {:?}", start.elapsed(), res);
}

fn bench(index: &Index, nm: &NodeTree, queries: usize) {
    let len = index.len() as u32;
    let mut rng = rand::rng();
    let nodes: Vec<Node> = (0..queries)
        .map(|_| {
            *index.node(Revision((rng.random::<u32>() % len) as BaseRevision))
        })
        .collect();
    if queries < 10 {
        let nodes_hex: Vec<String> =
            nodes.iter().map(|n| format!("{:x}", n)).collect();
        println!("Nodes: {:?}", nodes_hex);
    }
    let mut last: Option<Revision> = None;
    let start = Instant::now();
    for node in nodes.iter() {
        last = nm.find_bin(index, node.into()).unwrap();
    }
    let elapsed = start.elapsed();
    println!(
        "Did {} queries in {:?} (mean {:?}), last was {:x} with result {:?}",
        queries,
        elapsed,
        elapsed / (queries as u32),
        nodes.last().unwrap(),
        last
    );
}

fn main() {
    use clap::Parser;
    use clap::Subcommand;

    #[derive(Parser)]
    #[command()]
    /// Nodemap pure Rust example
    struct App {
        // Path to the repository, always necessary for its index
        #[arg(short, long)]
        repository: PathBuf,
        // Path to the nodemap file, independent of REPOSITORY
        #[arg(short, long)]
        nodemap_file: PathBuf,
        #[command(subcommand)]
        command: Command,
    }

    #[derive(Subcommand)]
    enum Command {
        /// Create `NODEMAP_FILE` by scanning repository index
        Create,
        /// Query `NODEMAP_FILE` for `prefix`
        Query { prefix: String },
        /// Perform #`QUERIES` random successful queries on `NODEMAP_FILE`
        Bench { queries: usize },
    }

    let app = App::parse();

    let repo = &app.repository;
    let nm_path = &app.nodemap_file;

    let index = mmap_index(repo);
    let nm = mmap_nodemap(nm_path);

    match &app.command {
        Command::Create => {
            println!(
                "Creating nodemap file {} for repository {}",
                nm_path.display(),
                repo.display()
            );
            create(&index, Path::new(nm_path)).unwrap();
        }
        Command::Bench { queries } => {
            println!(
                "Doing {} random queries in nodemap file {} of repository {}",
                queries,
                nm_path.display(),
                repo.display()
            );
            bench(&index, &nm, *queries);
        }
        Command::Query { prefix } => {
            println!(
                "Querying {} in nodemap file {} of repository {}",
                prefix,
                nm_path.display(),
                repo.display()
            );
            query(&index, &nm, prefix);
        }
    }
}
