// Copyright (c) 2021 Advanced Micro Devices, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

use anyhow::{Context, Result};
use deqp_runner::{parallel_deqp, PiglitCommand, RunnerResults, TestCase};
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::time::Duration;
use structopt::StructOpt;

use deqp_runner::parse_piglit::{parse_piglit_xml_testlist, read_profile_file};

#[derive(Debug, StructOpt)]
#[structopt(
    author = "Eric Anholt <eric@anholt.net>",
    about = "Runs piglit in parallel"
)]
struct Opts {
    #[structopt(subcommand)]
    subcmd: SubCommand,
}

#[derive(Debug, StructOpt)]
enum SubCommand {
    #[structopt(name = "run")]
    Run(Run),
}

#[derive(Debug, StructOpt)]
pub struct Run {
    #[structopt(long, help = "path to piglit folder")]
    piglit_folder: PathBuf,

    #[structopt(long, help = "piglit profile to run (such as quick_gl)")]
    profile: String,

    #[structopt(long = "output", help = "path to output directory")]
    output_dir: PathBuf,

    #[structopt(
        long,
        help = "path to baseline results (such as output/failures.csv from another run)"
    )]
    baseline: Option<PathBuf>,

    #[structopt(
        long,
        help = "path to file of regexes of tests to skip running (for runtime or stability reasons)"
    )]
    skips: Option<PathBuf>,

    #[structopt(
        long,
        help = "path to file of regexes of tests to assume any failures in those tests are flaky results (but still run them, for long-term status tracking)"
    )]
    flakes: Option<PathBuf>,

    #[structopt(
        long,
        default_value = "60.0",
        help = "per-test timeout in floating point seconds"
    )]
    timeout: f32,

    #[structopt(
        short = "j",
        long,
        default_value = "0",
        help = "Number of piglit processes to invoke in parallel (default 0 = number of CPUs in system)"
    )]
    jobs: usize,

    #[structopt(
        long,
        default_value = "25",
        help = "Number of fails or flakes to print in the summary line (0 = no limit)"
    )]
    summary_limit: usize,

    #[structopt(long, default_value = "1", help = "Runs 1 out of every N tests.")]
    fraction: usize,

    #[structopt(
        long,
        default_value = "1",
        help = "Skips the first N-1 tests in the test list before applying --fraction (useful for running N/M fraciton of the test list across multiple devices)."
    )]
    fraction_start: usize,

    #[structopt(long = "process-isolation")]
    process_isolation: bool,

    #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
    verbose: usize,
}

fn parse_piglit_xml(
    piglit_folder: &std::path::Path,
    profile: &str,
    process_isolation: bool,
) -> Result<Vec<TestCase>> {
    let test_folder = piglit_folder.join("tests");
    let text = read_profile_file(&test_folder, profile, process_isolation)?;
    parse_piglit_xml_testlist(&test_folder, &text, process_isolation)
}

fn read_lines<P>(filename: P) -> Result<io::Lines<io::BufReader<File>>>
where
    P: AsRef<Path>,
{
    let file = File::open(&filename)
        .with_context(|| format!("opening filename: {}", filename.as_ref().display()))?;
    Ok(BufReader::new(file).lines())
}

fn main() -> Result<()> {
    let opts = Opts::from_args();

    match opts.subcmd {
        SubCommand::Run(run) => {
            stderrlog::new()
                .module(module_path!())
                .module("deqp_runner")
                .module("runner_results")
                .module("parse_piglit")
                .verbosity(run.verbose)
                .init()
                .unwrap();

            if run.jobs > 0 {
                rayon::ThreadPoolBuilder::new()
                    .num_threads(run.jobs)
                    .build_global()
                    .unwrap();
            }
            println!("Running piglit on {} threads", rayon::current_num_threads());

            // Load all the test names from the caselist file
            if run.fraction < 1 {
                eprintln!("--fraction must be >= 1.");
                std::process::exit(1);
            }
            if run.fraction_start < 1 {
                eprintln!("--fraction_start must be >= 1.");
                std::process::exit(1);
            }

            let tests: Vec<TestCase> =
                parse_piglit_xml(&run.piglit_folder, &run.profile, run.process_isolation)
                    .with_context(|| format!("reading piglit profile '{}'", run.profile))?
                    .into_iter()
                    .skip(run.fraction_start - 1)
                    .step_by(run.fraction)
                    .collect();

            fs::create_dir_all(&run.output_dir).context("creating output directory")?;

            let skips = match run.skips {
                Some(skips) => {
                    let lines = read_lines(skips)?
                        .map(|x| x.context("reading skips list"))
                        .collect::<Result<Vec<String>>>()?;
                    Some(deqp_runner::parse_regex_set(lines)?)
                }
                None => None,
            };

            let flakes = match run.flakes {
                Some(flakes) => {
                    let lines = read_lines(flakes)?
                        .map(|x| x.context("reading flakes list"))
                        .collect::<Result<Vec<String>>>()?;
                    Some(deqp_runner::parse_regex_set(lines)?)
                }
                None => None,
            };

            let baseline = match run.baseline {
                Some(path) => {
                    let mut file = File::open(path).context("Reading baseline")?;
                    RunnerResults::from_csv(&mut file)?
                }
                None => RunnerResults::new(),
            };

            let deqp = PiglitCommand {
                piglit_folder: run.piglit_folder,
                profile: run.profile,
                output_dir: run.output_dir.clone(),
                skips,
                flakes,
                baseline,
                timeout: Duration::from_secs_f32(run.timeout),
            };
            let results = parallel_deqp(&deqp, tests)?;
            results.write_results(&mut File::create(&run.output_dir.join("results.csv"))?)?;
            results.write_failures(&mut File::create(&run.output_dir.join("failures.csv"))?)?;

            results.print_summary(if run.summary_limit == 0 {
                std::usize::MAX
            } else {
                run.summary_limit
            });

            if !results.is_success() {
                std::process::exit(1);
            }
        }
    }

    Ok(())
}
