package indicator

import (
	"time"

	"github.com/c9s/bbgo/pkg/datatype/floats"
	"github.com/c9s/bbgo/pkg/types"
)

/*
boll implements the bollinger indicator:

The Basics of Bollinger Bands
- https://www.investopedia.com/articles/technical/102201.asp

Bollinger Bands
- https://www.investopedia.com/terms/b/bollingerbands.asp

Bollinger Bands Technical indicator guide:
- https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/bollinger-bands
*/

//go:generate callbackgen -type BOLL
type BOLL struct {
	types.IntervalWindow

	// K is the multiplier of Std, generally it's 2
	K float64

	SMA    *SMA
	StdDev *StdDev

	UpBand   floats.Slice
	DownBand floats.Slice

	EndTime time.Time

	updateCallbacks []func(sma, upBand, downBand float64)
}

type BandType int

func (inc *BOLL) GetUpBand() types.SeriesExtend {
	return types.NewSeries(&inc.UpBand)
}

func (inc *BOLL) GetDownBand() types.SeriesExtend {
	return types.NewSeries(&inc.DownBand)
}

func (inc *BOLL) GetSMA() types.SeriesExtend {
	return types.NewSeries(inc.SMA)
}

func (inc *BOLL) GetStdDev() types.SeriesExtend {
	return inc.StdDev
}

func (inc *BOLL) LastUpBand() float64 {
	if len(inc.UpBand) == 0 {
		return 0.0
	}

	return inc.UpBand[len(inc.UpBand)-1]
}

func (inc *BOLL) LastDownBand() float64 {
	if len(inc.DownBand) == 0 {
		return 0.0
	}

	return inc.DownBand[len(inc.DownBand)-1]
}

func (inc *BOLL) Update(value float64) {
	if inc.SMA == nil {
		inc.SMA = &SMA{IntervalWindow: inc.IntervalWindow}
	}

	if inc.StdDev == nil {
		inc.StdDev = &StdDev{IntervalWindow: inc.IntervalWindow}
	}

	inc.SMA.Update(value)
	inc.StdDev.Update(value)

	var sma = inc.SMA.Last(0)
	var stdDev = inc.StdDev.Last(0)
	var band = inc.K * stdDev

	var upBand = sma + band
	var downBand = sma - band

	inc.UpBand.Push(upBand)
	inc.DownBand.Push(downBand)
}

func (inc *BOLL) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) {
	target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK))
}

func (inc *BOLL) PushK(k types.KLine) {
	if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
		return
	}
	inc.Update(k.Close.Float64())
	inc.EndTime = k.EndTime.Time()
	inc.EmitUpdate(inc.SMA.Last(0), inc.UpBand.Last(0), inc.DownBand.Last(0))
}

func (inc *BOLL) LoadK(allKLines []types.KLine) {
	for _, k := range allKLines {
		inc.PushK(k)
	}

	inc.EmitUpdate(inc.SMA.Last(0), inc.UpBand.Last(0), inc.DownBand.Last(0))
}

func (inc *BOLL) CalculateAndUpdate(allKLines []types.KLine) {
	if inc.SMA == nil {
		inc.LoadK(allKLines)
		return
	}

	var last = allKLines[len(allKLines)-1]
	inc.PushK(last)
}

func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
	if inc.Interval != interval {
		return
	}

	if inc.EndTime != zeroTime && inc.EndTime.Before(inc.EndTime) {
		return
	}

	inc.CalculateAndUpdate(window)
}

func (inc *BOLL) Bind(updater KLineWindowUpdater) {
	updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
