use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use oxhttp::model::{Method, Request, Status};
use oxigraph::io::RdfFormat;
use oxigraph::model::{GraphName, GraphNameRef};
use oxigraph::sparql::{Query, QueryResults, Update};
use oxigraph::store::Store;
use rand::random;
use std::env::temp_dir;
use std::fs::{remove_dir_all, File};
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};

fn store_load(c: &mut Criterion) {
    {
        let mut data = Vec::new();
        read_data("explore-1000.nt.zst")
            .read_to_end(&mut data)
            .unwrap();

        let mut group = c.benchmark_group("store load");
        group.throughput(Throughput::Bytes(data.len() as u64));
        group.sample_size(10);
        group.bench_function("load BSBM explore 1000 in memory", |b| {
            b.iter(|| {
                let store = Store::new().unwrap();
                do_load(&store, &data);
            })
        });
        group.bench_function("load BSBM explore 1000 in on disk", |b| {
            b.iter(|| {
                let path = TempDir::default();
                let store = Store::open(&path).unwrap();
                do_load(&store, &data);
            })
        });
        group.bench_function("load BSBM explore 1000 in on disk with bulk load", |b| {
            b.iter(|| {
                let path = TempDir::default();
                let store = Store::open(&path).unwrap();
                do_bulk_load(&store, &data);
            })
        });
    }

    {
        let mut data = Vec::new();
        read_data("explore-10000.nt.zst")
            .read_to_end(&mut data)
            .unwrap();

        let mut group = c.benchmark_group("store load large");
        group.throughput(Throughput::Bytes(data.len() as u64));
        group.sample_size(10);
        group.bench_function("load BSBM explore 10000 in on disk with bulk load", |b| {
            b.iter(|| {
                let path = TempDir::default();
                let store = Store::open(&path).unwrap();
                do_bulk_load(&store, &data);
            })
        });
    }
}

fn do_load(store: &Store, data: &[u8]) {
    store
        .load_graph(data, RdfFormat::NTriples, GraphName::DefaultGraph, None)
        .unwrap();
    store.optimize().unwrap();
}

fn do_bulk_load(store: &Store, data: &[u8]) {
    store
        .bulk_loader()
        .load_graph(data, RdfFormat::NTriples, GraphNameRef::DefaultGraph, None)
        .unwrap();
    store.optimize().unwrap();
}

fn store_query_and_update(c: &mut Criterion) {
    let mut data = Vec::new();
    read_data("explore-1000.nt.zst")
        .read_to_end(&mut data)
        .unwrap();

    let operations = BufReader::new(read_data("mix-exploreAndUpdate-1000.tsv.zst"))
        .lines()
        .map(|l| {
            let l = l.unwrap();
            let mut parts = l.trim().split('\t');
            let kind = parts.next().unwrap();
            let operation = parts.next().unwrap();
            match kind {
                "query" => Operation::Query(Query::parse(operation, None).unwrap()),
                "update" => Operation::Update(Update::parse(operation, None).unwrap()),
                _ => panic!("Unexpected operation kind {kind}"),
            }
        })
        .collect::<Vec<_>>();
    let query_operations = operations
        .iter()
        .filter(|o| matches!(o, Operation::Query(_)))
        .cloned()
        .collect::<Vec<_>>();

    let mut group = c.benchmark_group("store operations");
    group.throughput(Throughput::Elements(operations.len() as u64));
    group.sample_size(10);

    {
        let memory_store = Store::new().unwrap();
        do_bulk_load(&memory_store, &data);
        group.bench_function("BSBM explore 1000 query in memory", |b| {
            b.iter(|| run_operation(&memory_store, &query_operations))
        });
        group.bench_function("BSBM explore 1000 queryAndUpdate in memory", |b| {
            b.iter(|| run_operation(&memory_store, &operations))
        });
    }

    {
        let path = TempDir::default();
        let disk_store = Store::open(&path).unwrap();
        do_bulk_load(&disk_store, &data);
        group.bench_function("BSBM explore 1000 query on disk", |b| {
            b.iter(|| run_operation(&disk_store, &query_operations))
        });
        group.bench_function("BSBM explore 1000 queryAndUpdate on disk", |b| {
            b.iter(|| run_operation(&disk_store, &operations))
        });
    }
}

fn run_operation(store: &Store, operations: &[Operation]) {
    for operation in operations {
        match operation {
            Operation::Query(q) => match store.query(q.clone()).unwrap() {
                QueryResults::Boolean(_) => (),
                QueryResults::Solutions(s) => {
                    for s in s {
                        s.unwrap();
                    }
                }
                QueryResults::Graph(g) => {
                    for t in g {
                        t.unwrap();
                    }
                }
            },
            Operation::Update(u) => store.update(u.clone()).unwrap(),
        }
    }
}

fn sparql_parsing(c: &mut Criterion) {
    let mut data = Vec::new();
    read_data("explore-1000.nt.zst")
        .read_to_end(&mut data)
        .unwrap();

    let operations = BufReader::new(read_data("mix-exploreAndUpdate-1000.tsv.zst"))
        .lines()
        .map(|l| {
            let l = l.unwrap();
            let mut parts = l.trim().split('\t');
            let kind = parts.next().unwrap();
            let operation = parts.next().unwrap();
            match kind {
                "query" => RawOperation::Query(operation.to_owned()),
                "update" => RawOperation::Update(operation.to_owned()),
                _ => panic!("Unexpected operation kind {kind}"),
            }
        })
        .collect::<Vec<_>>();

    let mut group = c.benchmark_group("sparql parsing");
    group.sample_size(10);
    group.throughput(Throughput::Bytes(
        operations
            .iter()
            .map(|o| match o {
                RawOperation::Query(q) => q.len(),
                RawOperation::Update(u) => u.len(),
            })
            .sum::<usize>() as u64,
    ));
    group.bench_function("BSBM query and update set", |b| {
        b.iter(|| {
            for operation in &operations {
                match operation {
                    RawOperation::Query(q) => {
                        Query::parse(q, None).unwrap();
                    }
                    RawOperation::Update(u) => {
                        Update::parse(u, None).unwrap();
                    }
                }
            }
        })
    });
}

criterion_group!(store, sparql_parsing, store_query_and_update, store_load);

criterion_main!(store);

fn read_data(file: &str) -> impl Read {
    if !Path::new(file).exists() {
        let mut client = oxhttp::Client::new();
        client.set_redirection_limit(5);
        let url = format!("https://github.com/Tpt/bsbm-tools/releases/download/v0.2/{file}");
        let request = Request::builder(Method::GET, url.parse().unwrap()).build();
        let response = client.request(request).unwrap();
        assert_eq!(
            response.status(),
            Status::OK,
            "{}",
            response.into_body().to_string().unwrap()
        );
        std::io::copy(&mut response.into_body(), &mut File::create(file).unwrap()).unwrap();
    }
    zstd::Decoder::new(File::open(file).unwrap()).unwrap()
}

#[derive(Clone)]
enum RawOperation {
    Query(String),
    Update(String),
}

#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
enum Operation {
    Query(Query),
    Update(Update),
}

struct TempDir(PathBuf);

impl Default for TempDir {
    fn default() -> Self {
        Self(temp_dir().join(format!("oxigraph-bench-{}", random::<u128>())))
    }
}

impl AsRef<Path> for TempDir {
    fn as_ref(&self) -> &Path {
        &self.0
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        remove_dir_all(&self.0).unwrap()
    }
}