// Copyright 2023 wanderer <a_mirre at utb dot cz>
// SPDX-License-Identifier: GPL-3.0-or-later

package stats

import (
	"fmt"
	"sort"

	"git.dotya.ml/wanderer/math-optim/report"
	"gonum.org/v1/gonum/floats"
	"gonum.org/v1/gonum/stat"
)

// getColLayout returns a string slice of Latex table column alignment
// settings.
func getColLayout() []string {
	return []string{"c", "c", "c", "c", "c"}
}

// getColNames returns names of table columns, i.e. statistical features we are
// interested in.
func getColNames() []string {
	// the first column describes specific iteration settings and is therefore
	// dynamically set, hence not present here nor mentioned in
	// `getColLayout()`.
	return []string{"min", "max", "mean", "median", "stddev"}
}

// SaveTable sifts through computed values, organises data in table-like
// structure as defined in the `report` pkg and passes it on to be fed to a
// tmpl, result of which is then saved in a `.tex` file (filename based on the
// algo name string).
func SaveTable(algo string, algoStats [][]Stats) {
	table := report.NewTable()

	table.Algo = algo
	table.Header = getColNames()
	table.ColLayout = getColLayout()

	for _, singleFunc := range algoStats {
		// append/merge(...) if necessary.
		table.Rows = append(table.Rows, parseSingleBenchStats(singleFunc)...)
	}

	report.SaveTableToFile(*table)
}

// parseSingleBenchStats processes results of a particular bench and constructs
// statistics.
func parseSingleBenchStats(benchStats []Stats) []report.Row {
	rows := make([]report.Row, 0)

	for _, s := range benchStats {
		for _, dim := range s.BenchFuncStats {
			row := report.NewRow()

			row.Title = "D=" + fmt.Sprint(s.Dimens) + ", f=" + dim.BenchName +
				", G=" + fmt.Sprint(s.Generations) +
				", I=" + fmt.Sprint(s.Iterations)

			row.Title = makeRowTitle(
				dim.BenchName,
				s.Dimens,
				s.Generations,
				s.Iterations,
			)

			// collect the best.
			var best []float64

			for _, iter := range dim.BenchResults {
				last := s.Generations - 1

				best = append(best, iter.Results[last])
			}

			row.Values = statsFromBest(best)

			rows = append(rows, *row)
		}
	}

	return rows
}

func makeRowTitle(bench string, dimens, generations, iterations int) string {
	return "D=" + fmt.Sprint(dimens) + ", f=" + bench +
		", G=" + fmt.Sprint(generations) +
		", I=" + fmt.Sprint(iterations)
}

// statsFromBest computes the actual statistics upon the slice of best results,
// returns a slice of float64s.
func statsFromBest(best []float64) []float64 {
	s := make([]float64, len(getColNames()))

	s[0] = floats.Min(best)
	s[1] = floats.Max(best)
	s[2] = stat.Mean(best, nil)
	s[3] = median(best)
	s[4] = stat.StdDev(best, nil)

	return s
}

// as per https://gosamples.dev/calculate-median/.
func median(data []float64) float64 {
	dataCopy := make([]float64, len(data))
	copy(dataCopy, data)

	sort.Float64s(dataCopy)

	var median float64

	//nolint: gocritic
	if l := len(dataCopy); l == 0 {
		return 0
	} else if l%2 == 0 {
		median = (dataCopy[l/2-1] + dataCopy[l/2]) / 2
	} else {
		median = dataCopy[l/2]
	}

	return median
}
