// Copyright ©2014 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package fd

import (
	"math"
	"runtime"
)

// A Point is a stencil location in a finite difference formula.
type Point struct {
	Loc   float64
	Coeff float64
}

// Formula represents a finite difference formula on a regularly spaced grid
// that approximates the derivative of order k of a function f at x as
//
//	d^k f(x) ≈ (1 / Step^k) * \sum_i Coeff_i * f(x + Step * Loc_i).
//
// Step must be positive, or the finite difference formula will panic.
type Formula struct {
	// Stencil is the set of sampling Points which are used to estimate the
	// derivative. The locations will be scaled by Step and are relative to x.
	Stencil    []Point
	Derivative int     // The order of the approximated derivative.
	Step       float64 // Default step size for the formula.
}

func (f Formula) isZero() bool {
	return f.Stencil == nil && f.Derivative == 0 && f.Step == 0
}

// Settings is the settings structure for computing finite differences.
type Settings struct {
	// Formula is the finite difference formula used
	// for approximating the derivative.
	// Zero value indicates a default formula.
	Formula Formula
	// Step is the distance between points of the stencil.
	// If equal to 0, formula's default step will be used.
	Step float64

	OriginKnown bool    // Flag that the value at the origin x is known.
	OriginValue float64 // Value at the origin (only used if OriginKnown is true).

	Concurrent bool // Should the function calls be executed concurrently.
}

// Forward represents a first-order accurate forward approximation
// to the first derivative.
var Forward = Formula{
	Stencil:    []Point{{Loc: 0, Coeff: -1}, {Loc: 1, Coeff: 1}},
	Derivative: 1,
	Step:       2e-8,
}

// Forward2nd represents a first-order accurate forward approximation
// to the second derivative.
var Forward2nd = Formula{
	Stencil:    []Point{{Loc: 0, Coeff: 1}, {Loc: 1, Coeff: -2}, {Loc: 2, Coeff: 1}},
	Derivative: 2,
	Step:       1e-4,
}

// Backward represents a first-order accurate backward approximation
// to the first derivative.
var Backward = Formula{
	Stencil:    []Point{{Loc: -1, Coeff: -1}, {Loc: 0, Coeff: 1}},
	Derivative: 1,
	Step:       2e-8,
}

// Backward2nd represents a first-order accurate forward approximation
// to the second derivative.
var Backward2nd = Formula{
	Stencil:    []Point{{Loc: 0, Coeff: 1}, {Loc: -1, Coeff: -2}, {Loc: -2, Coeff: 1}},
	Derivative: 2,
	Step:       1e-4,
}

// Central represents a second-order accurate centered approximation
// to the first derivative.
var Central = Formula{
	Stencil:    []Point{{Loc: -1, Coeff: -0.5}, {Loc: 1, Coeff: 0.5}},
	Derivative: 1,
	Step:       6e-6,
}

// Central2nd represents a second-order accurate centered approximation
// to the second derivative.
var Central2nd = Formula{
	Stencil:    []Point{{Loc: -1, Coeff: 1}, {Loc: 0, Coeff: -2}, {Loc: 1, Coeff: 1}},
	Derivative: 2,
	Step:       1e-4,
}

var negativeStep = "fd: negative step"

// checkFormula checks if the formula is valid, and panics otherwise.
func checkFormula(formula Formula) {
	if formula.Derivative == 0 || formula.Stencil == nil || formula.Step <= 0 {
		panic("fd: bad formula")
	}
}

// computeWorkers returns the desired number of workers given the concurrency
// level and number of evaluations.
func computeWorkers(concurrent bool, evals int) int {
	if !concurrent {
		return 1
	}
	nWorkers := runtime.GOMAXPROCS(0)
	if nWorkers > evals {
		nWorkers = evals
	}
	return nWorkers
}

// usesOrigin returns whether the stencil uses the origin, which is true iff
// one of the locations in the stencil equals 0.
func usesOrigin(stencil []Point) bool {
	for _, pt := range stencil {
		if pt.Loc == 0 {
			return true
		}
	}
	return false
}

// getOrigin returns the value at the origin. It returns originValue if originKnown
// is true. It returns the value returned by f if stencil contains a point with
// zero location, and NaN otherwise.
func getOrigin(originKnown bool, originValue float64, f func() float64, stencil []Point) float64 {
	if originKnown {
		return originValue
	}
	for _, pt := range stencil {
		if pt.Loc == 0 {
			return f()
		}
	}
	return math.NaN()
}

const (
	badDerivOrder = "fd: invalid derivative order"
)
