package math

import (
	"errors"
	"math"
)

var (
	errZeroValue               = errors.New("cannot calculate average of no values")
	errNegativeValueOutOfRange = errors.New("received negative number less than -1")
	errGeometricNegative       = errors.New("cannot calculate a geometric mean with negative values")
	errCalmarHighest           = errors.New("cannot calculate calmar ratio with highest price of 0")
	errCAGRNoIntervals         = errors.New("cannot calculate CAGR with no intervals")
	errCAGRZeroOpenValue       = errors.New("cannot calculate CAGR with an open value of 0")
	errInformationBadLength    = errors.New("benchmark rates length does not match returns rates")
)

// CalculateAmountWithFee returns a calculated fee included amount on fee
func CalculateAmountWithFee(amount, fee float64) float64 {
	return amount + CalculateFee(amount, fee)
}

// CalculateFee returns a simple fee on amount
func CalculateFee(amount, fee float64) float64 {
	return amount * (fee / 100)
}

// CalculatePercentageGainOrLoss returns the percentage rise over a certain
// period
func CalculatePercentageGainOrLoss(priceNow, priceThen float64) float64 {
	return (priceNow - priceThen) / priceThen * 100
}

// CalculatePercentageDifference returns the percentage of difference between
// multiple time periods
func CalculatePercentageDifference(amount, secondAmount float64) float64 {
	return (amount - secondAmount) / ((amount + secondAmount) / 2) * 100
}

// CalculateNetProfit returns net profit
func CalculateNetProfit(amount, priceThen, priceNow, costs float64) float64 {
	return (priceNow * amount) - (priceThen * amount) - costs
}

// RoundFloat rounds your floating point number to the desired decimal place
func RoundFloat(x float64, prec int) float64 {
	pow := math.Pow(10, float64(prec))
	return math.Round(x*pow) / pow
}

// CompoundAnnualGrowthRate Calculates CAGR.
// Using years, intervals per year would be 1 and number of intervals would be the number of years
// Using days, intervals per year would be 365 and number of intervals would be the number of days
func CompoundAnnualGrowthRate(openValue, closeValue, intervalsPerYear, numberOfIntervals float64) (float64, error) {
	if numberOfIntervals == 0 {
		return 0, errCAGRNoIntervals
	}
	if openValue == 0 {
		return 0, errCAGRZeroOpenValue
	}
	k := math.Pow(closeValue/openValue, intervalsPerYear/numberOfIntervals) - 1
	return k * 100, nil
}

// CalmarRatio is a function of the average compounded annual rate of return versus its maximum drawdown.
// The higher the Calmar ratio, the better it performed on a risk-adjusted basis during the given time frame, which is mostly commonly set at 36 months
func CalmarRatio(highestPrice, lowestPrice, average, riskFreeRateForPeriod float64) (float64, error) {
	if highestPrice == 0 {
		return 0, errCalmarHighest
	}
	drawdownDiff := (highestPrice - lowestPrice) / highestPrice
	if drawdownDiff == 0 {
		return 0, nil
	}
	return (average - riskFreeRateForPeriod) / drawdownDiff, nil
}

// InformationRatio The information ratio (IR) is a measurement of portfolio returns beyond the returns of a benchmark,
// usually an index, compared to the volatility of those returns.
// The benchmark used is typically an index that represents the market or a particular sector or industry.
func InformationRatio(returnsRates, benchmarkRates []float64, averageValues, averageComparison float64) (float64, error) {
	if len(benchmarkRates) != len(returnsRates) {
		return 0, errInformationBadLength
	}
	var diffs []float64
	for i := range returnsRates {
		diffs = append(diffs, returnsRates[i]-benchmarkRates[i])
	}
	stdDev, err := PopulationStandardDeviation(diffs)
	if err != nil {
		return 0, err
	}
	if stdDev == 0 {
		return 0, nil
	}
	return (averageValues - averageComparison) / stdDev, nil
}

// PopulationStandardDeviation calculates standard deviation using population based calculation
func PopulationStandardDeviation(values []float64) (float64, error) {
	if len(values) < 2 {
		return 0, nil
	}
	valAvg, err := ArithmeticMean(values)
	if err != nil {
		return 0, err
	}
	diffs := make([]float64, len(values))
	for x := range values {
		diffs[x] = math.Pow(values[x]-valAvg, 2)
	}
	var diffAvg float64
	diffAvg, err = ArithmeticMean(diffs)
	if err != nil {
		return 0, err
	}
	return math.Sqrt(diffAvg), nil
}

// SampleStandardDeviation standard deviation is a statistic that
// measures the dispersion of a dataset relative to its mean and
// is calculated as the square root of the variance
func SampleStandardDeviation(values []float64) (float64, error) {
	if len(values) < 2 {
		return 0, nil
	}
	mean, err := ArithmeticMean(values)
	if err != nil {
		return 0, err
	}
	var superMean []float64
	var combined float64
	for i := range values {
		result := math.Pow(values[i]-mean, 2)
		superMean = append(superMean, result)
		combined += result
	}
	avg := combined / (float64(len(superMean)) - 1)
	return math.Sqrt(avg), nil
}

// GeometricMean is an average which indicates the central tendency or
// typical value of a set of numbers by using the product of their values
// The geometric average can only process positive numbers
func GeometricMean(values []float64) (float64, error) {
	if len(values) == 0 {
		return 0, errZeroValue
	}
	product := 1.0
	for i := range values {
		if values[i] <= 0 {
			// cannot use negative or zero values in geometric calculation
			return 0, errGeometricNegative
		}
		product *= values[i]
	}
	geometricPower := math.Pow(product, 1/float64(len(values)))
	return geometricPower, nil
}

// FinancialGeometricMean is a modified geometric average to assess
// the negative returns of investments. It accepts It adds +1 to each
// This does impact the final figures as it is modifying values
// It is still ultimately calculating a geometric average
// which should only be compared to other financial geometric averages
func FinancialGeometricMean(values []float64) (float64, error) {
	if len(values) == 0 {
		return 0, errZeroValue
	}
	product := 1.0
	for i := range values {
		if values[i] < -1 {
			// cannot lose more than 100%, figures are incorrect
			// losing exactly 100% will return a 0 value, but is not an error
			return 0, errNegativeValueOutOfRange
		}
		// as we cannot have negative or zero value geometric numbers
		// adding a 1 to the percentage movements allows for differentiation between
		// negative numbers (eg -0.1 translates to 0.9) and positive numbers (eg 0.1 becomes 1.1)
		modVal := values[i] + 1
		product *= modVal
	}
	geometricPower := math.Pow(product, 1/float64(len(values)))
	if geometricPower > 0 {
		// we minus 1 because we manipulated the values to be non-zero/negative
		geometricPower--
	}
	return geometricPower, nil
}

// ArithmeticMean is the basic form of calculating an average.
// Divide the sum of all values by the length of values
func ArithmeticMean(values []float64) (float64, error) {
	if len(values) == 0 {
		return 0, errZeroValue
	}
	var sumOfValues float64
	for x := range values {
		sumOfValues += values[x]
	}
	return sumOfValues / float64(len(values)), nil
}

// SortinoRatio returns sortino ratio of backtest compared to risk-free
func SortinoRatio(movementPerCandle []float64, riskFreeRatePerInterval, average float64) (float64, error) {
	totalIntervals := float64(len(movementPerCandle))
	if totalIntervals == 0 {
		return 0, errZeroValue
	}
	totalNegativeResultsSquared := 0.0
	for x := range movementPerCandle {
		if movementPerCandle[x]-riskFreeRatePerInterval < 0 {
			totalNegativeResultsSquared += math.Pow(movementPerCandle[x]-riskFreeRatePerInterval, 2)
		}
	}
	averageDownsideDeviation := math.Sqrt(totalNegativeResultsSquared / float64(len(movementPerCandle)))

	return (average - riskFreeRatePerInterval) / averageDownsideDeviation, nil
}

// SharpeRatio returns sharpe ratio of backtest compared to risk-free
func SharpeRatio(movementPerCandle []float64, riskFreeRatePerInterval, average float64) (float64, error) {
	totalIntervals := float64(len(movementPerCandle))
	if totalIntervals == 0 {
		return 0, errZeroValue
	}
	var excessReturns []float64
	for i := range movementPerCandle {
		excessReturns = append(excessReturns, movementPerCandle[i]-riskFreeRatePerInterval)
	}
	standardDeviation, err := PopulationStandardDeviation(excessReturns)
	if err != nil {
		return 0, err
	}
	if standardDeviation == 0 {
		return 0, nil
	}

	return (average - riskFreeRatePerInterval) / standardDeviation, nil
}
