// 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 optimize

import "math"

const defaultBisectionCurvature = 0.9

var _ Linesearcher = (*Bisection)(nil)

// Bisection is a Linesearcher that uses a bisection to find a point that
// satisfies the strong Wolfe conditions with the given curvature factor and
// a decrease factor of zero.
type Bisection struct {
	// CurvatureFactor is the constant factor in the curvature condition.
	// Smaller values result in a more exact line search.
	// A set value must be in the interval (0, 1), otherwise Init will panic.
	// If it is zero, it will be defaulted to 0.9.
	CurvatureFactor float64

	minStep  float64
	maxStep  float64
	currStep float64

	initF float64
	minF  float64
	maxF  float64
	lastF float64

	initGrad float64

	lastOp Operation
}

func (b *Bisection) Init(f, g float64, step float64) Operation {
	if step <= 0 {
		panic("bisection: bad step size")
	}
	if g >= 0 {
		panic("bisection: initial derivative is non-negative")
	}

	if b.CurvatureFactor == 0 {
		b.CurvatureFactor = defaultBisectionCurvature
	}
	if b.CurvatureFactor <= 0 || b.CurvatureFactor >= 1 {
		panic("bisection: CurvatureFactor not between 0 and 1")
	}

	b.minStep = 0
	b.maxStep = math.Inf(1)
	b.currStep = step

	b.initF = f
	b.minF = f
	b.maxF = math.NaN()

	b.initGrad = g

	// Only evaluate the gradient when necessary.
	b.lastOp = FuncEvaluation
	return b.lastOp
}

func (b *Bisection) Iterate(f, g float64) (Operation, float64, error) {
	if b.lastOp != FuncEvaluation && b.lastOp != GradEvaluation {
		panic("bisection: Init has not been called")
	}
	minF := b.initF
	if b.maxF < minF {
		minF = b.maxF
	}
	if b.minF < minF {
		minF = b.minF
	}
	if b.lastOp == FuncEvaluation {
		// See if the function value is good enough to make progress. If it is,
		// evaluate the gradient. If not, set it to the upper bound if the bound
		// has not yet been found, otherwise iterate toward the minimum location.
		if f <= minF {
			b.lastF = f
			b.lastOp = GradEvaluation
			return b.lastOp, b.currStep, nil
		}
		if math.IsInf(b.maxStep, 1) {
			b.maxStep = b.currStep
			b.maxF = f
			return b.nextStep((b.minStep + b.maxStep) / 2)
		}
		if b.minF <= b.maxF {
			b.maxStep = b.currStep
			b.maxF = f
		} else {
			b.minStep = b.currStep
			b.minF = f
		}
		return b.nextStep((b.minStep + b.maxStep) / 2)
	}
	f = b.lastF
	// The function value was lower. Check if this location is sufficient to
	// converge the linesearch, otherwise iterate.
	if StrongWolfeConditionsMet(f, g, minF, b.initGrad, b.currStep, 0, b.CurvatureFactor) {
		b.lastOp = MajorIteration
		return b.lastOp, b.currStep, nil
	}
	if math.IsInf(b.maxStep, 1) {
		// The function value is lower. If the gradient is positive, an upper bound
		// of the minimum been found. If the gradient is negative, search farther
		// in that direction.
		if g > 0 {
			b.maxStep = b.currStep
			b.maxF = f
			return b.nextStep((b.minStep + b.maxStep) / 2)
		}
		b.minStep = b.currStep
		b.minF = f
		return b.nextStep(b.currStep * 2)
	}
	// The interval has been bounded, and we have found a new lowest value. Use
	// the gradient to decide which direction.
	if g < 0 {
		b.minStep = b.currStep
		b.minF = f
	} else {
		b.maxStep = b.currStep
		b.maxF = f
	}
	return b.nextStep((b.minStep + b.maxStep) / 2)
}

// nextStep checks if the new step is equal to the old step.
// This can happen if min and max are the same, or if the step size is infinity,
// both of which indicate the minimization must stop. If the steps are different,
// it sets the new step size and returns the evaluation type and the step. If the steps
// are the same, it returns an error.
func (b *Bisection) nextStep(step float64) (Operation, float64, error) {
	if b.currStep == step {
		b.lastOp = NoOperation
		return b.lastOp, b.currStep, ErrLinesearcherFailure
	}
	b.currStep = step
	b.lastOp = FuncEvaluation
	return b.lastOp, b.currStep, nil
}
