// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0

package bench

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"gonum.org/v1/gonum/mat"
	"gonum.org/v1/gonum/stat"
)

type subsResults map[string](*[]float64)

type runResults struct {
	ID             string  `json:"id"`
	Successes      int64   `json:"successes"`
	Failures       int64   `json:"failures"`
	RunTime        float64 `json:"run_time"`
	MsgTimeMin     float64 `json:"msg_time_min"`
	MsgTimeMax     float64 `json:"msg_time_max"`
	MsgTimeMean    float64 `json:"msg_time_mean"`
	MsgTimeStd     float64 `json:"msg_time_std"`
	MsgDelTimeMin  float64 `json:"msg_del_time_min"`
	MsgDelTimeMax  float64 `json:"msg_del_time_max"`
	MsgDelTimeMean float64 `json:"msg_del_time_mean"`
	MsgDelTimeStd  float64 `json:"msg_del_time_std"`
	MsgsPerSec     float64 `json:"msgs_per_sec"`
}

type totalResults struct {
	Ratio             float64 `json:"ratio"`
	Successes         int64   `json:"successes"`
	Failures          int64   `json:"failures"`
	TotalRunTime      float64 `json:"total_run_time"`
	AvgRunTime        float64 `json:"avg_run_time"`
	MsgTimeMin        float64 `json:"msg_time_min"`
	MsgTimeMax        float64 `json:"msg_time_max"`
	MsgDelTimeMin     float64 `json:"msg_del_time_min"`
	MsgDelTimeMax     float64 `json:"msg_del_time_max"`
	MsgTimeMeanAvg    float64 `json:"msg_time_mean_avg"`
	MsgTimeMeanStd    float64 `json:"msg_time_mean_std"`
	MsgDelTimeMeanAvg float64 `json:"msg_del_time_mean_avg"`
	MsgDelTimeMeanStd float64 `json:"msg_del_time_mean_std"`
	TotalMsgsPerSec   float64 `json:"total_msgs_per_sec"`
	AvgMsgsPerSec     float64 `json:"avg_msgs_per_sec"`
}

// JSONResults are used to export results as a JSON document.
type JSONResults struct {
	Runs   []*runResults `json:"runs"`
	Totals *totalResults `json:"totals"`
}

func calcMsgRes(m *message, res *runResults) *float64 {
	if m.Error {
		res.Failures++
		return nil
	}
	res.Successes++
	diff := float64(m.Delivered.Sub(m.Sent).Nanoseconds() / 1000) // in microseconds
	return &diff
}

func calcRes(r *runResults, start time.Time, times []float64) *runResults {
	duration := time.Since(start)
	timeMatrix := mat.NewDense(1, len(times), times)
	r.MsgTimeMin = mat.Min(timeMatrix)
	r.MsgTimeMax = mat.Max(timeMatrix)
	r.MsgTimeMean = stat.Mean(times, nil)
	r.MsgTimeStd = stat.StdDev(times, nil)
	r.RunTime = duration.Seconds()
	r.MsgsPerSec = float64(r.Successes) / duration.Seconds()
	return r
}

func calculateTotalResults(results []*runResults, totalTime time.Duration, sr subsResults) *totalResults {
	if results == nil || len(results) < 1 {
		return nil
	}
	totals := new(totalResults)
	msgTimeMeans := make([]float64, len(results))
	msgTimeMeansDelivered := make([]float64, len(results))
	msgsPerSecs := make([]float64, len(results))
	runTimes := make([]float64, len(results))
	bws := make([]float64, len(results))

	totals.TotalRunTime = totalTime.Seconds()

	totals.MsgTimeMin = results[0].MsgTimeMin
	for i, res := range results {
		totals.Successes += res.Successes
		totals.Failures += res.Failures
		totals.TotalMsgsPerSec += res.MsgsPerSec

		// Don't count those client that sent no messages.
		if res.MsgsPerSec == 0 {
			continue
		}

		if res.MsgTimeMin < totals.MsgTimeMin {
			totals.MsgTimeMin = res.MsgTimeMin
		}

		if res.MsgTimeMax > totals.MsgTimeMax {
			totals.MsgTimeMax = res.MsgTimeMax
		}

		if res.MsgDelTimeMin < totals.MsgDelTimeMin {
			totals.MsgDelTimeMin = res.MsgDelTimeMin
		}

		if res.MsgDelTimeMax > totals.MsgDelTimeMax {
			totals.MsgDelTimeMax = res.MsgDelTimeMax
		}

		msgTimeMeansDelivered[i] = res.MsgDelTimeMean
		msgTimeMeans[i] = res.MsgTimeMean
		msgsPerSecs[i] = res.MsgsPerSec
		runTimes[i] = res.RunTime
		bws[i] = res.MsgsPerSec
	}

	for _, v := range sr {
		times := mat.NewDense(1, len(*v), *v)
		totals.MsgDelTimeMin = mat.Min(times) / 1000
		totals.MsgDelTimeMax = mat.Max(times) / 1000
		totals.MsgDelTimeMeanAvg = stat.Mean(*v, nil) / 1000
		totals.MsgDelTimeMeanStd = stat.StdDev(*v, nil) / 1000
	}

	totals.Ratio = float64(totals.Successes) / float64(totals.Successes+totals.Failures)
	totals.AvgMsgsPerSec = stat.Mean(msgsPerSecs, nil)
	totals.AvgRunTime = stat.Mean(runTimes, nil)
	totals.MsgDelTimeMeanAvg = stat.Mean(msgTimeMeansDelivered, nil)
	totals.MsgDelTimeMeanStd = stat.StdDev(msgTimeMeansDelivered, nil)
	totals.MsgTimeMeanAvg = stat.Mean(msgTimeMeans, nil)
	totals.MsgTimeMeanStd = stat.StdDev(msgTimeMeans, nil)

	return totals
}

func printResults(results []*runResults, totals *totalResults, format string, quiet bool) {
	switch format {
	case "json":
		jr := JSONResults{
			Runs:   results,
			Totals: totals,
		}
		data, err := json.Marshal(jr)
		if err != nil {
			log.Printf("Failed to prepare results for printing - %s\n", err.Error())
		}
		var out bytes.Buffer
		if err = json.Indent(&out, data, "", "\t"); err != nil {
			return
		}

		fmt.Println(out.String())
	default:
		if !quiet {
			for _, res := range results {
				fmt.Printf("======= CLIENT %s =======\n", res.ID)
				fmt.Printf("Ratio:                   %.6f (%d/%d)\n", float64(res.Successes)/float64(res.Successes+res.Failures), res.Successes, res.Successes+res.Failures)
				fmt.Printf("Succeeded:               %d\n", res.Successes)
				fmt.Printf("Failed:                  %d\n", res.Failures)
				fmt.Printf("Runtime (s):             %.3f\n", res.RunTime)
				fmt.Printf("Msg time min (µs):       %.3f\n", res.MsgTimeMin)
				fmt.Printf("Msg time max (µs):       %.3f\n", res.MsgTimeMax)
				fmt.Printf("Msg time mean (µs):      %.3f\n", res.MsgTimeMean)
				fmt.Printf("Msg time std (µs):       %.3f\n\n", res.MsgTimeStd)

				fmt.Printf("Bandwidth (msg/sec):     %.3f\n\n", res.MsgsPerSec)
			}
		}
		fmt.Printf("========= TOTAL (%d) =========\n", len(results))
		fmt.Printf("Total Ratio:                 %.3f (%d/%d)\n", totals.Ratio, totals.Successes, totals.Successes+totals.Failures)
		fmt.Printf("Succeeded:                   %d\n", totals.Successes)
		fmt.Printf("Failed:                      %d\n", totals.Failures)
		fmt.Printf("Total Runtime (sec):         %.3f\n", totals.TotalRunTime)
		fmt.Printf("Average Runtime (sec):       %.3f\n", totals.AvgRunTime)
		fmt.Printf("Msg time min (µs):           %.3f\n", totals.MsgTimeMin)
		fmt.Printf("Msg time max (µs):           %.3f\n", totals.MsgTimeMax)
		fmt.Printf("Msg time mean (µs):          %.3f\n", totals.MsgTimeMeanAvg)
		fmt.Printf("Msg time mean std (µs):      %.3f\n", totals.MsgTimeMeanStd)

		fmt.Printf("Average Bandwidth (msg/sec): %.3f\n", totals.AvgMsgsPerSec)
		fmt.Printf("Total Bandwidth (msg/sec):   %.3f\n", totals.TotalMsgsPerSec)
	}
}
