// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package main

// This server runs along side the karma tests and listens for POST requests
// when any test case reports it has output for Perf. See perfReporter.js
// for the browser side part.

// Unlike the gold ingester, the perf ingester allows multiple reports
// of the same benchmark and will output the average of these results
// on a call to dump

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
	"sync"

	"github.com/google/uuid"
	"go.skia.org/infra/perf/go/ingest/format"
)

// upload_nano_results looks for anything*.json
// We add the random UUID to avoid name clashes when uploading to
// the perf bucket (which uploads to folders based on Month/Day/Hour, which can
// easily have duplication if multiple perf tasks run in an hour.)
var JSON_FILENAME = fmt.Sprintf("%s_browser_bench.json", uuid.New().String())

var (
	outDir = flag.String("out_dir", "/OUT/", "location to dump the Perf JSON")
	port   = flag.String("port", "8081", "Port to listen on.")

	botId            = flag.String("bot_id", "", "swarming bot id")
	browser          = flag.String("browser", "Chrome", "Browser Key")
	buildBucketID    = flag.Int64("buildbucket_build_id", 0, "Buildbucket build id key")
	builder          = flag.String("builder", "", "Swarming task name")
	compiledLanguage = flag.String("compiled_language", "wasm", "wasm or asm.js")
	config           = flag.String("config", "Release", "Configuration (e.g. Debug/Release) key")
	gitHash          = flag.String("git_hash", "-", "The git commit hash of the version being tested")
	hostOS           = flag.String("host_os", "Debian9", "OS Key")
	issue            = flag.Int64("issue", 0, "issue (if tryjob)")
	patch_storage    = flag.String("patch_storage", "", "patch storage (if tryjob)")
	patchset         = flag.Int64("patchset", 0, "patchset (if tryjob)")
	taskId           = flag.String("task_id", "", "swarming task id")
	sourceType       = flag.String("source_type", "pathkit", "Gold Source type, like pathkit,canvaskit")
)

// Received from the JS side.
type reportBody struct {
	// a name describing the benchmark. Should be unique enough to allow use of grep.
	BenchName string `json:"bench_name"`
	// The number of microseconds of the task.
	TimeMicroSeconds float64 `json:"time_us"`
}

// The keys to be used at the top level for all Results.
var defaultKeys map[string]string

// contains all the results reported in through report_perf_data
var results map[string][]reportBody
var resultsMutex sync.Mutex

type BenchData struct {
	Hash         string                         `json:"gitHash"`
	Issue        string                         `json:"issue"`
	PatchSet     string                         `json:"patchset"`
	Key          map[string]string              `json:"key"`
	Options      map[string]string              `json:"options,omitempty"`
	Results      map[string]format.BenchResults `json:"results"`
	PatchStorage string                         `json:"patch_storage,omitempty"`

	SwarmingTaskID string `json:"swarming_task_id,omitempty"`
	SwarmingBotID  string `json:"swarming_bot_id,omitempty"`
}

func main() {
	flag.Parse()

	cpuGPU := "CPU"
	if strings.Index(*builder, "-GPU-") != -1 {
		cpuGPU = "GPU"
	}
	defaultKeys = map[string]string{
		"arch":              "WASM",
		"browser":           *browser,
		"compiled_language": *compiledLanguage,
		"compiler":          "emsdk",
		"configuration":     *config,
		"cpu_or_gpu":        cpuGPU,
		"cpu_or_gpu_value":  "Browser",
		"os":                *hostOS,
		"source_type":       *sourceType,
	}

	results = make(map[string][]reportBody)

	http.HandleFunc("/report_perf_data", reporter)
	http.HandleFunc("/dump_json", dumpJSON)

	fmt.Printf("Waiting for perf ingestion on port %s\n", *port)

	log.Fatal(http.ListenAndServe(":"+*port, nil))
}

// reporter handles when the client reports a test has a benchmark.
func reporter(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "Only POST accepted", 400)
		return
	}
	defer r.Body.Close()

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Malformed body", 400)
		return
	}

	benchOutput := reportBody{}
	if err := json.Unmarshal(body, &benchOutput); err != nil {
		fmt.Println(err)
		http.Error(w, "Could not unmarshal JSON", 400)
		return
	}

	if _, err := w.Write([]byte("Accepted")); err != nil {
		fmt.Printf("Could not write response: %s\n", err)
		return
	}
	resultsMutex.Lock()
	defer resultsMutex.Unlock()
	results[benchOutput.BenchName] = append(results[benchOutput.BenchName], benchOutput)
}

// createOutputFile creates a file and set permissions correctly.
func createOutputFile(p string) (*os.File, error) {
	outputFile, err := os.Create(p)
	if err != nil {
		return nil, fmt.Errorf("Could not open file %s on disk: %s", p, err)
	}
	// Make this accessible (and deletable) by all users
	if err = outputFile.Chmod(0666); err != nil {
		return nil, fmt.Errorf("Could not change permissions of file %s: %s", p, err)
	}
	return outputFile, nil
}

// dumpJSON writes out a JSON file with all the results, typically at the end of
// all the tests. If there is more than one result per benchmark, we report the average.
func dumpJSON(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "Only POST accepted", 400)
		return
	}

	p := path.Join(*outDir, JSON_FILENAME)
	outputFile, err := createOutputFile(p)
	defer outputFile.Close()
	if err != nil {
		fmt.Println(err)
		http.Error(w, "Could not open json file on disk", 500)
		return
	}

	benchData := BenchData{
		Hash:           *gitHash,
		Issue:          strconv.FormatInt(*issue, 10),
		PatchStorage:   *patch_storage,
		PatchSet:       strconv.FormatInt(*patchset, 10),
		Key:            defaultKeys,
		SwarmingBotID:  *botId,
		SwarmingTaskID: *taskId,
	}

	allResults := make(map[string]format.BenchResults)
	for name, benches := range results {
		samples := []float64{}
		total := float64(0)
		for _, t := range benches {
			samples = append(samples, t.TimeMicroSeconds)
			total += t.TimeMicroSeconds
		}
		allResults[name] = map[string]format.BenchResult{
			"default": map[string]interface{}{
				"average_us": total / float64(len(benches)),
				"samples":    samples,
			},
		}
	}
	benchData.Results = allResults

	enc := json.NewEncoder(outputFile)
	enc.SetIndent("", "  ") // Make it human readable.
	if err := enc.Encode(&benchData); err != nil {
		fmt.Println(err)
		http.Error(w, "Could not write json to disk", 500)
		return
	}
	fmt.Println("JSON Written")
}
