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

package algo

import (
	"fmt"
	"log"
	"strconv"
	"strings"
	"sync"
	"time"

	"git.dotya.ml/wanderer/math-optim/report"
	"git.dotya.ml/wanderer/math-optim/stats"
	"git.dotya.ml/wanderer/math-optim/util"
	"gonum.org/v1/gonum/floats"
	"gonum.org/v1/plot"
	"gonum.org/v1/plot/plotter"
	"gonum.org/v1/plot/plotutil"
	"gonum.org/v1/plot/vg"
)

const (
	preferredFont      = "Mono"
	titlePreferredFont = "Sans"
	yAxisLabel         = "f(x)"
	xAxisLabel         = "Generations"
	xAxisLabelAlt      = "FES"
)

// PlotMeanValsMulti creates plots for every member of 'stats.AlgoMeanVals' it
// is handed and saves them as 'result.Pic's results into a package-global
// slice.
func PlotMeanValsMulti(
	w *sync.WaitGroup,
	dimens, iterations int,
	bench, fPrefix, fExt string,
	algoMeanVals ...stats.AlgoMeanVals,
) {
	defer w.Done()

	// track time.
	start := time.Now()

	pWidth := 13 * vg.Centimeter
	pHeight := 13 * vg.Centimeter

	plotter.DefaultFont.Typeface = preferredFont
	plotter.DefaultLineStyle.Width = vg.Points(2.0)

	p := plot.New()
	pic := report.NewPic()

	p.Title.Text = fmt.Sprintf(
		"Comparison of Means (%dI) -\n%s (%dD)",
		iterations, bench, dimens,
	)

	// p.X.Label.Text = "Objective func. evaluations"
	p.X.Label.Text = xAxisLabelAlt
	p.X.Label.TextStyle.Font.Variant = preferredFont
	p.X.Label.TextStyle.Font.Weight = 1 // Medium
	p.X.Tick.Label.Font.Variant = preferredFont
	p.Y.Label.Text = yAxisLabel
	p.Y.Label.TextStyle.Font.Variant = preferredFont
	p.Y.Label.TextStyle.Font.Weight = 1 // Medium
	p.Y.Tick.Label.Font.Variant = preferredFont

	p.Title.TextStyle.Font.Size = 14.5
	p.Title.TextStyle.Font.Variant = titlePreferredFont
	p.Title.TextStyle.Font.Weight = 2 // SemiBold
	p.Title.Padding = 3 * vg.Millimeter

	p.Legend.TextStyle.Font.Variant = preferredFont
	p.Legend.TextStyle.Font.Size = 10
	p.Legend.Top = true
	p.Legend.Padding = 2 * vg.Millimeter

	lines := make([]interface{}, 0)

	for _, v := range algoMeanVals {
		// mark the end of the X axis with the greatest of len(v)
		if greatest := float64(len(v.MeanVals)); greatest > p.X.Max {
			p.X.Max = greatest
		}

		if min := floats.Min(v.MeanVals); min < p.Y.Min {
			p.Y.Min = min
		}

		if max := floats.Max(v.MeanVals); max > p.Y.Max {
			p.Y.Max = max
		}

		pts := make(plotter.XYs, len(v.MeanVals))

		// fill the plotter with datapoints.
		for k, res := range v.MeanVals {
			pts[k].X = float64(k)
			pts[k].Y = res
		}

		lines = append(lines, v.Title, pts)
	}

	err := plotutil.AddLines(
		p,
		lines...,
	)
	if err != nil {
		log.Panic(err)
	}

	filename := util.SanitiseFName(
		fmt.Sprintf("%s%scomparison-of-means-%dI-%s-%02dD",
			report.GetPicsDir(),
			fPrefix,
			iterations,
			bench,
			dimens,
		),
	)

	// set pic file path and caption.
	pic.FilePath = filename
	// pic.Caption = strings.ReplaceAll(filename, " ", "~")
	pic.Caption = strings.ReplaceAll(
		fmt.Sprintf("Comparison of Means (%dI) - %s (%dD)",
			iterations, bench, dimens,
		),
		" ", "~")
	pic.Bench = bench

	elapsed := time.Since(start)
	info := fmt.Sprintf("saving img to file: %s%s [generated in %s]",
		filename, fExt, elapsed,
	)
	log.Println(info)

	// Save the plot to a file using the above-constructed 'filename'.
	if err := p.Save(
		pWidth,
		pHeight,
		filename+fExt,
	); err != nil {
		panic(err)
	}

	mCoMPL.Lock()
	comparisonOfMeansPicList.Pics = append(comparisonOfMeansPicList.Pics, *pic)
	mCoMPL.Unlock()
}

func plotMeanVals(meanVals []float64, title, algo string, fes int) *plot.Plot {
	plotter.DefaultFont.Typeface = preferredFont
	plotter.DefaultLineStyle.Width = vg.Points(2.0)

	if fes != len(meanVals) {
		log.Fatalf("meanVals - FES mismatch: %d vs %d , bailing\n", fes, len(meanVals))
	}

	p := plot.New()
	// pic := report.NewPic()

	p.Title.Text = "Mean - " + title

	if strings.Contains(algo, "DE") || strings.Contains(algo, "SOMA") {
		p.X.Label.Text = xAxisLabelAlt
	} else {
		p.X.Label.Text = xAxisLabel
	}

	// p.X.Label.Padding = 8 * vg.Millimeter
	p.X.Label.TextStyle.Font.Variant = preferredFont
	p.X.Label.TextStyle.Font.Weight = 1 // Medium
	p.X.Tick.Label.Font.Variant = preferredFont
	// p.X.Padding = 2 * vg.Millimeter
	p.Y.Label.Text = yAxisLabel
	// p.Y.Label.Padding = 2 * vg.Millimeter
	p.Y.Label.TextStyle.Font.Variant = preferredFont
	p.Y.Label.TextStyle.Font.Weight = 1 // Medium
	p.Y.Tick.Label.Font.Variant = preferredFont
	// p.Y.Padding = 1 * vg.Millimeter

	p.Title.TextStyle.Font.Size = 14.5
	p.Title.TextStyle.Font.Variant = titlePreferredFont
	p.Title.TextStyle.Font.Weight = 2 // SemiBold
	// p.Title.Padding = 5 * vg.Millimeter
	p.Title.Padding = 3 * vg.Millimeter

	// mark the end of the X axis with len(meanVals).
	p.X.Max = float64(len(meanVals))
	p.Y.Min = floats.Min(meanVals)
	p.Y.Max = floats.Max(meanVals)

	pts := make(plotter.XYs, len(meanVals))

	// fill the plotter with datapoints.
	for k, res := range meanVals {
		pts[k].X = float64(k)
		pts[k].Y = res
	}

	lines := make([]interface{}, 0)
	lines = append(lines, pts)

	err := plotutil.AddLines(
		p,
		lines...,
	)
	if err != nil {
		log.Panic(err)
	}

	return p
}

// violating gocognit 30, TODO(me): still split this up. fPrefix is always "plot" according to unparam.
// nolint: gocognit,unparam
func plotAllDims(algoStats []stats.Stats, fPrefix, fExt string, ch chan report.PicList, chMean chan report.PicList) {
	start := time.Now()

	picsDir := report.GetPicsDir()

	// create picsDir (if not exists), fail on error, no point in going on
	// parsing algoStats and computing images if we cannot save them.
	if err := util.CreatePath(picsDir); err != nil {
		log.Fatalln("went to create picsDir, there was an issue: ", err)
	}

	pL := report.NewPicList()
	pics := make([]report.Pic, 0)
	pLMean := report.NewPicList()
	picsMean := make([]report.Pic, 0)

	// since the algoStats only contains results of a single algo, it's safe to
	// set the value like this.
	pL.Algo = algoStats[0].Algo
	pLMean.Algo = algoStats[0].Algo

	pWidth := 13 * vg.Centimeter
	pHeight := 13 * vg.Centimeter

	plotter.DefaultFont.Typeface = preferredFont
	plotter.DefaultLineStyle.Width = vg.Points(1.5)

	for _, s := range algoStats {
		p := plot.New()
		pic := report.NewPic()

		p.Title.Text = fmt.Sprintf("D: %d, G: %d, I: %d",
			s.Dimens,
			s.Generations,
			s.Iterations,
		)

		// For differential evolution, add params to title.
		if strings.Contains(s.Algo, "DE") {
			p.Title.Text += fmt.Sprintf(",\nNP: %d, F: ", s.NP) +
				strconv.FormatFloat(s.F, 'f', -1, 64) + ", CR: " +
				strconv.FormatFloat(s.CR, 'f', -1, 64)
		}

		// this is latex-rendered.
		pic.Caption = strings.ReplaceAll(p.Title.Text, " ", "~")

		// since a single stat slice of algoStats only contains results of a
		// single bench func, it's safe to set the value like this.
		pL.Bench = s.BenchFuncStats[0].BenchName
		pLMean.Bench = s.BenchFuncStats[0].BenchName

		if strings.Contains(s.Algo, "DE") {
			p.X.Label.Text = xAxisLabelAlt
		} else {
			p.X.Label.Text = xAxisLabel
		}

		p.X.Label.TextStyle.Font.Variant = preferredFont
		p.X.Label.TextStyle.Font.Weight = 1 // Medium
		p.X.Tick.Label.Font.Variant = preferredFont
		p.Y.Label.Text = yAxisLabel
		p.Y.Label.TextStyle.Font.Variant = preferredFont
		p.Y.Label.TextStyle.Font.Weight = 1 // Medium
		p.Y.Tick.Label.Font.Variant = preferredFont
		// p.Y.Padding = 1 * vg.Millimeter

		p.Title.TextStyle.Font.Size = 14.5
		p.Title.TextStyle.Font.Variant = titlePreferredFont
		p.Title.TextStyle.Font.Weight = 2 // SemiBold
		// p.Title.Padding = 5 * vg.Millimeter
		p.Title.Padding = 3 * vg.Millimeter

		// p.Legend.TextStyle.Font.Variant = preferredFontStyle
		// p.Legend.TextStyle.Font.Size = 8
		// p.Legend.Top = true
		// p.Legend.Padding = 0 * vg.Centimeter
		// p.Add(plotter.NewGrid())

		for _, dim := range s.BenchFuncStats {
			// infinite thanks to this SO comment for the interface "hack":
			//   https://stackoverflow.com/a/44872993
			lines := make([]interface{}, 0)

			for _, iter := range dim.BenchResults {
				// mark the end of the X axis with len(iter.Results).
				p.X.Max = float64(len(iter.Results))

				if floats.Min(iter.Results) < p.Y.Min {
					p.Y.Min = floats.Min(iter.Results)
				}

				if floats.Max(iter.Results) > p.Y.Max {
					p.Y.Max = floats.Max(iter.Results)
				}

				pts := make(plotter.XYs, len(iter.Results))

				// fill the plotter with datapoints.
				for k, res := range iter.Results {
					pts[k].X = float64(k)
					pts[k].Y = res
				}

				// lines = append(lines, "#"+fmt.Sprint(j), pts)
				lines = append(lines, pts)
			}

			err := plotutil.AddLines(
				p,
				lines...,
			)
			if err != nil {
				log.Printf("issue constructing a plot, bench: %s", dim.BenchName)

				// panic (don't panic, I know) instead of a hard exit.
				log.Panic(err)
			}

			// TODO(me): add Neighbourhood param
			// TODO(me): add search space percent param
			filename := fmt.Sprintf("%s%s-%s-%s-%dD-%dG-%dI",
				picsDir,
				fPrefix,
				util.SanitiseFName(s.Algo),
				util.SanitiseFName(dim.BenchName),
				s.Dimens,
				s.Generations,
				len(dim.BenchResults),
			)
			filenameMean := filename + util.SanitiseFName(" Mean")

			// NEVER EVER ATTEMPT TO INITIALISE THIS WITH `pic`!
			picMean := report.NewPic()
			meanTitle := fmt.Sprintf("D: %d, G: %d, I: %d",
				s.Dimens,
				s.Generations,
				s.Iterations,
			)

			// this is latex-rendered.
			picMean.Caption = "Mean - " + strings.ReplaceAll(meanTitle, " ", "~")

			// set pic file path (later used in tmpl generation)
			pic.FilePath = filename
			picMean.FilePath = filenameMean

			// get the *mean* plot.
			pMean := plotMeanVals(dim.MeanVals, meanTitle, s.Algo, s.Generations)

			elapsed := time.Since(start)
			info := fmt.Sprintf("saving img to file: %s(-Mean)%s [generated in %s]",
				filename, fExt, elapsed,
			)

			switch {
			case s.Algo == "Random Search":
				printRandomSearch(info)

			case strings.Contains(s.Algo, "Stochastic Hill Climbing"):
				printSHC(info)

			default:
				log.Println(info)
			}

			// Save the plot to a file using the above-constructed 'filename'.
			if err := p.Save(
				pWidth,
				pHeight,
				filename+fExt,
			); err != nil {
				panic(err)
			}
			// log.Println(filename + fExt)

			// save pic.
			pics = append(pics, *pic)

			// save plot of pMean.
			if err := pMean.Save(
				pWidth,
				pHeight,
				filenameMean+fExt,
			); err != nil {
				panic(err)
			}

			// save mean pic.
			picsMean = append(picsMean, *picMean)
		}
	}

	pL.Pics = pics
	pLMean.Pics = picsMean

	ch <- *pL
	chMean <- *pLMean
}
