package bbgo

import (
	"github.com/sirupsen/logrus"

	"github.com/c9s/bbgo/pkg/envvar"
	"github.com/c9s/bbgo/pkg/indicator"
	"github.com/c9s/bbgo/pkg/types"
)

var (
	debugBOLL = false
)

func init() {
	// when using --dotenv option, the dotenv is loaded from command.PersistentPreRunE, not init.
	// hence here the env var won't enable the debug flag
	envvar.SetBool("DEBUG_BOLL", &debugBOLL)
}

type MACDConfig struct {
	types.IntervalWindow
}

type StandardIndicatorSet struct {
	Symbol string

	// Standard indicators
	// interval -> window
	iwbIndicators  map[types.IntervalWindowBandWidth]*indicator.BOLL
	iwIndicators   map[indicatorKey]indicator.KLinePusher
	macdIndicators map[indicator.MACDConfig]*indicator.MACDLegacy

	stream types.Stream
	store  *types.MarketDataStore
}

type indicatorKey struct {
	iw types.IntervalWindow
	id string
}

func NewStandardIndicatorSet(symbol string, stream types.Stream, store *types.MarketDataStore) *StandardIndicatorSet {
	return &StandardIndicatorSet{
		Symbol:         symbol,
		store:          store,
		stream:         stream,
		iwIndicators:   make(map[indicatorKey]indicator.KLinePusher),
		iwbIndicators:  make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
		macdIndicators: make(map[indicator.MACDConfig]*indicator.MACDLegacy),
	}
}

func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, interval types.Interval) {
	if klines, ok := s.store.KLinesOfInterval(interval); ok {
		for _, k := range *klines {
			inc.PushK(k)
		}
	}

	s.stream.OnKLineClosed(types.KLineWith(s.Symbol, interval, inc.PushK))
}

func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.KLinePusher, iw types.IntervalWindow, id string) indicator.KLinePusher {
	k := indicatorKey{
		iw: iw,
		id: id,
	}
	inc, ok := s.iwIndicators[k]
	if ok {
		return inc
	}

	inc = t
	s.initAndBind(inc, iw.Interval)
	s.iwIndicators[k] = inc
	return t
}

// SMA is a helper function that returns the simple moving average indicator of the given interval and the window size.
func (s *StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA {
	inc := s.allocateSimpleIndicator(&indicator.SMA{IntervalWindow: iw}, iw, "sma")
	return inc.(*indicator.SMA)
}

// EWMA is a helper function that returns the exponential weighed moving average indicator of the given interval and the window size.
func (s *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA {
	inc := s.allocateSimpleIndicator(&indicator.EWMA{IntervalWindow: iw}, iw, "ewma")
	return inc.(*indicator.EWMA)
}

// VWMA
func (s *StandardIndicatorSet) VWMA(iw types.IntervalWindow) *indicator.VWMA {
	inc := s.allocateSimpleIndicator(&indicator.VWMA{IntervalWindow: iw}, iw, "vwma")
	return inc.(*indicator.VWMA)
}

func (s *StandardIndicatorSet) PivotHigh(iw types.IntervalWindow) *indicator.PivotHigh {
	inc := s.allocateSimpleIndicator(&indicator.PivotHigh{IntervalWindow: iw}, iw, "pivothigh")
	return inc.(*indicator.PivotHigh)
}

func (s *StandardIndicatorSet) PivotLow(iw types.IntervalWindow) *indicator.PivotLow {
	inc := s.allocateSimpleIndicator(&indicator.PivotLow{IntervalWindow: iw}, iw, "pivotlow")
	return inc.(*indicator.PivotLow)
}

func (s *StandardIndicatorSet) ATR(iw types.IntervalWindow) *indicator.ATR {
	inc := s.allocateSimpleIndicator(&indicator.ATR{IntervalWindow: iw}, iw, "atr")
	return inc.(*indicator.ATR)
}

func (s *StandardIndicatorSet) ATRP(iw types.IntervalWindow) *indicator.ATRP {
	inc := s.allocateSimpleIndicator(&indicator.ATRP{IntervalWindow: iw}, iw, "atrp")
	return inc.(*indicator.ATRP)
}

func (s *StandardIndicatorSet) EMV(iw types.IntervalWindow) *indicator.EMV {
	inc := s.allocateSimpleIndicator(&indicator.EMV{IntervalWindow: iw}, iw, "emv")
	return inc.(*indicator.EMV)
}

func (s *StandardIndicatorSet) CCI(iw types.IntervalWindow) *indicator.CCI {
	inc := s.allocateSimpleIndicator(&indicator.CCI{IntervalWindow: iw}, iw, "cci")
	return inc.(*indicator.CCI)
}

func (s *StandardIndicatorSet) HULL(iw types.IntervalWindow) *indicator.HULL {
	inc := s.allocateSimpleIndicator(&indicator.HULL{IntervalWindow: iw}, iw, "hull")
	return inc.(*indicator.HULL)
}

func (s *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH {
	inc := s.allocateSimpleIndicator(&indicator.STOCH{IntervalWindow: iw}, iw, "stoch")
	return inc.(*indicator.STOCH)
}

// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth
func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL {
	iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
	inc, ok := s.iwbIndicators[iwb]
	if !ok {
		inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth, SMA: &indicator.SMA{IntervalWindow: iw}}
		s.initAndBind(inc, iw.Interval)

		if debugBOLL {
			inc.OnUpdate(func(sma float64, upBand float64, downBand float64) {
				logrus.Infof("%s BOLL %s: sma=%f up=%f down=%f", s.Symbol, iw.String(), sma, upBand, downBand)
			})
		}
		s.iwbIndicators[iwb] = inc
	}

	return inc
}

func (s *StandardIndicatorSet) MACD(iw types.IntervalWindow, shortPeriod, longPeriod int) *indicator.MACDLegacy {
	config := indicator.MACDConfig{IntervalWindow: iw, ShortPeriod: shortPeriod, LongPeriod: longPeriod}

	inc, ok := s.macdIndicators[config]
	if ok {
		return inc
	}

	inc = &indicator.MACDLegacy{MACDConfig: config}
	s.macdIndicators[config] = inc
	s.initAndBind(inc, config.IntervalWindow.Interval)
	return inc
}

func (s *StandardIndicatorSet) RSI(iw types.IntervalWindow) *indicator.RSI {
	inc := s.allocateSimpleIndicator(&indicator.RSI{IntervalWindow: iw}, iw, "rsi")
	return inc.(*indicator.RSI)
}

// GHFilter is a helper function that returns the G-H (alpha beta) digital filter of the given interval and the window size.
func (s *StandardIndicatorSet) GHFilter(iw types.IntervalWindow) *indicator.GHFilter {
	inc := s.allocateSimpleIndicator(&indicator.GHFilter{IntervalWindow: iw}, iw, "ghfilter")
	return inc.(*indicator.GHFilter)
}

// KalmanFilter is a helper function that returns the Kalman digital filter of the given interval and the window size.
// Note that the additional smooth window is set to zero in standard indicator set. Users have to create their own instance and push K-lines if a smoother filter is needed.
func (s *StandardIndicatorSet) KalmanFilter(iw types.IntervalWindow) *indicator.KalmanFilter {
	inc := s.allocateSimpleIndicator(&indicator.KalmanFilter{IntervalWindow: iw, AdditionalSmoothWindow: 0}, iw, "kalmanfilter")
	return inc.(*indicator.KalmanFilter)
}
