// Copyright 2022 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// tint-bench repeatedly emits a WGSL file from a template, then times how long
// it takes to execute the tint executable with that WGSL file.
package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"time"

	"dawn.googlesource.com/dawn/tools/src/fileutils"
	"dawn.googlesource.com/dawn/tools/src/oswrapper"
	"dawn.googlesource.com/dawn/tools/src/template"
)

func main() {
	if err := run(oswrapper.GetRealOSWrapper()); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

// [from .. to]
type Range struct {
	from int
	to   int
}

// TODO(crbug.com/344014313): Add unittest coverage once writeWGSLFile is
// converted to use dependency injection.
func run(osWrapper oswrapper.OSWrapper) error {
	alphaRange := Range{}

	iterations := 0
	tmplPath := ""
	flag.StringVar(&tmplPath, "tmpl", "tint-bench.tmpl", `the WGSL template to benchmark.
Searches in order: absolute, relative to CWD, then relative to `+fileutils.ThisDir())
	flag.IntVar(&alphaRange.from, "alpha-from", 0, "the start value for 'Alpha'")
	flag.IntVar(&alphaRange.to, "alpha-to", 10, "the end value for 'Alpha'")
	flag.IntVar(&iterations, "iterations", 10, "number of times to benchmark tint")
	flag.Usage = func() {
		fmt.Println("tint-bench repeatedly emits a WGSL file from a template, then times how long")
		fmt.Println("it takes to execute the tint executable with that WGSL file.")
		fmt.Println("")
		fmt.Println("usage:")
		fmt.Println("  tint-bench <bench-flags> [tint-exe] <tint-flags>")
		fmt.Println("")
		fmt.Println("bench-flags:")
		flag.PrintDefaults()
		os.Exit(1)
	}

	flag.Parse()

	if tmplPath == "" {
		return fmt.Errorf("missing template path")
	}

	tmpl, err := template.FromFile(tmplPath, osWrapper)
	if err != nil {
		if !filepath.IsAbs(tmplPath) {
			// Try relative to this .go file
			tmplPath = filepath.Join(fileutils.ThisDir(), tmplPath)
			tmpl, err = template.FromFile(tmplPath, osWrapper)
		}
	}
	if err != nil {
		return fmt.Errorf("failed to load template: %v", err)
	}

	args := flag.Args()
	if len(args) < 1 {
		flag.Usage()
	}
	tintExe := args[0]

	fmt.Println(" alpha | Time (μs)")
	fmt.Println("-------+-----------------")

	for alpha := alphaRange.from; alpha < alphaRange.to; alpha++ {
		alpha := alpha
		funcs := template.Functions{
			"Alpha": func() int { return alpha },
		}
		wgslPath, err := writeWGSLFile(tmpl, funcs)
		if err != nil {
			return err
		}

		tintArgs := []string{wgslPath}
		tintArgs = append(tintArgs, args[1:]...)

		durations := []time.Duration{}
		for i := 0; i < iterations; i++ {
			tint := exec.Command(tintExe, tintArgs...)
			start := time.Now()
			if out, err := tint.CombinedOutput(); err != nil {
				return fmt.Errorf("tint failed with error: %w\n%v\n\nwith: Alpha=%v", err, string(out), alpha)
			}
			duration := time.Since(start)
			durations = append(durations, duration)
		}
		sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] })

		median := durations[len(durations)/2]
		fmt.Printf("%6.v | %v\n", alpha, median.Microseconds())
	}
	return nil
}

func writeWGSLFile(tmpl *template.Template, funcs template.Functions) (string, error) {
	const path = "tint-bench.wgsl"
	wgslFile, err := os.Create(path)
	if err != nil {
		return "", fmt.Errorf("failed to create benchmark WGSL test file: %w", err)
	}
	defer wgslFile.Close()
	if err := tmpl.Run(wgslFile, nil, funcs); err != nil {
		return "", fmt.Errorf("template error:\n%w", err)
	}
	return path, nil
}
