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

package cec2020

import (
	"math"
	"time"

	"golang.org/x/exp/rand"

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

// void shiftfunc (double*,double*,int,double*);
// void rotatefunc (double*,double*,int, double*);
// void sr_func (double *, double *, int, double*, double*, double, int, int); // shift and rotate
// void asyfunc (double *, double *x, int, double);
// void oszfunc (double *, double *, int);
// void cf_cal(double *, double *, int, double *,double *,double *,double *,int);

// Shift shifts values of x based on values of os.
func Shift(x []float64, os []float64) {
	if len(x) != len(os) {
		cec2020Logger.Fatalln("slices are of different sizes, bailing...")
	}

	for i := range x {
		x[i] -= os[i]
	}
}

// Rotate rotates values of x based on values of mr.
func Rotate(x []float64, mr []float64) {
	nx := len(x)

	if int(math.Pow(float64(nx), 2)) > len(mr) {
		cec2020Logger.Fatalf("either x is too big or mr is of insufficient size (%d:%d), bailing...\n", nx, len(mr))
	}

	xrot := make([]float64, nx)

	for i := 0; i < nx; i++ {
		for j := 0; j < nx; j++ {
			x[j] += xrot[i] + (x[j] * mr[(i*nx)+j])
		}
	}
}

// ShiftRotate shifts and/or rotates the input x based on the request params
// shift and rotate (bool), using the values of os and/or mr.
func ShiftRotate(x []float64, os []float64, mr []float64, shiftRate float64, shift, rotate bool) {
	switch {
	case shift && rotate:
		Shift(x, os)

		// shrink to the original search range.
		for i := range x {
			x[i] *= shiftRate
		}

		Rotate(x, mr)

	case shift && !rotate:
		Shift(x, os)

		// shrink to the original search range.
		for i := range x {
			x[i] *= shiftRate
		}

	case !shift && rotate:
		// shrink to the original search range
		for i := range x {
			x[i] *= shiftRate
		}

		Rotate(x, mr)

	case !shift && !rotate:
		for i := range x {
			x[i] *= shiftRate
		}
	}
}

// Asy is the asymptotic function.
func Asy(x []float64, beta float64) {
	nx := len(x)

	for i := range x {
		if x[i] > 0 {
			fi := float64(i)
			fnx := float64(nx)

			x[i] = math.Pow(x[i], 1+((beta*fi)/(fnx-1)*math.Pow(x[i], 0.5)))
		}
	}
}

// GetMaxFES returns maxFES for the passed dimension, given that it's present
// in the MaxFES slice, returns -1 otherwise.
func GetMaxFES(dim int) int {
	for i, d := range Dimensions {
		if dim == d {
			return MaxFES[i]
		}
	}

	return -1
}

// nolint: unused
func newXopt(n int, mu0 float64) []float64 {
	gaussDist := &distuv.Normal{
		Src: rand.NewSource(uint64(time.Now().UnixNano())),
	}

	tmpvec := make([]float64, n)
	xopt := make([]float64, n)

	for i := 0; i < n; i++ {
		tmpvec = append(tmpvec, gaussDist.Rand())

		xopt = append(xopt, 0.5*mu0)

		if tmpvec[i] < 0 {
			xopt[i] *= -1
		}
	}

	return xopt
}

// newOpt returns a slice of optimal float64 values.
func newOpt(n int) []float64 {
	uniformDist := &distuv.Uniform{
		Src: rand.NewSource(uint64(time.Now().UnixNano())),
		Min: SearchRange.Min(),
		Max: SearchRange.Max(),
	}

	tmpvec := make([]float64, n)
	xopt := make([]float64, n)

	for i := 0; i < n; i++ {
		tmpvec = append(tmpvec, uniformDist.Rand())

		xopt = append(xopt, 0.5*uniformDist.Mean())

		if tmpvec[i] < 0 {
			xopt[i] *= -1
		}
	}

	return xopt
}

// getGnx calculates G_nx.
func getGnx(p []float64, fnx float64) []float64 {
	gnx := make([]float64, len(p))
	gnxsum := 0.0

	for i := 0; i < len(p)-1; i++ {
		gnx[i] = math.Ceil(p[i] * fnx)
		gnxsum += gnx[i]
	}

	gnx[len(p)-1] = fnx - gnxsum

	return gnx
}

// getMz calculates Mz.
// nolint: unused
func getMz(x []float64, z []float64) []float64 {
	nx := len(x)
	mz := make([]float64, nx)

	for i := range x {
		mz = append(mz, x[i]*z[i])
	}

	return mz
}

func getMean(x []float64) float64 {
	mean := stat.Mean(x, nil)

	return mean
}

func getWeight(x, o []float64, sigma, nx float64) float64 {
	var sum float64

	for i := range x {
		sum += math.Pow(x[i]-o[i], 2)
	}

	// w.
	return (1 / (math.Sqrt(sum))) * math.Exp(-(sum / 2 * nx * math.Pow(sigma, 2)))
}
