package chain

import (
	"encoding/json"
	"sync"
	"time"

	"github.com/aergoio/aergo/v2/types"
)

//go:generate stringer -type=statIndex
type statIndex int

const (
	// Warning: Each statIndex constant has a String method, which is
	// automatically generated by 'stringer' with 'go generate' command. For the
	// detail, check https://blog.golang.org/generate

	// ReorgStat is a constant representing a stat about reorganization.
	ReorgStat statIndex = iota
	// MaxStat is a constant representing a value less than which all the
	// constants corresponding chain stats must be.
	MaxStat
)

var (
	// To add a new one to chain stats, implements statItem interface and add
	// its constructor here. Additionally, you need to add a constant
	// corresponding to its index like statReorg above.
	statItemCtors = map[statIndex]func() statItem{
		ReorgStat: newStReorg,
	}
)

type stats []*stat

func newStats() stats {
	s := make(stats, MaxStat)
	for i := statIndex(0); i < MaxStat; i++ {
		s[i] = newStat(statItemCtors[i]())
	}
	return s
}

func (s stats) JSON() string {
	r := make(map[string]json.RawMessage)
	for i := statIndex(0); i < MaxStat; i++ {
		if b, err := json.Marshal(s.clone(i)); err == nil {
			r[i.String()] = json.RawMessage(b)
		}
	}
	if m, err := json.Marshal(r); err == nil {
		return string(m)
	}
	return ""
}

func (s stats) get(idx statIndex) *stat {
	return []*stat(s)[idx]
}

func (s stats) clone(idx statIndex) interface{} {
	i := s.get(idx)
	i.RLock()
	defer i.RUnlock()
	return i.clone()
}

func (s stats) updateEvent(idx statIndex, args ...interface{}) {
	i := s.get(idx)
	i.Lock()
	defer i.Unlock()

	i.updateEvent(args...)
}

type stat struct {
	sync.RWMutex
	statItem
}

func newStat(i statItem) *stat {
	return &stat{statItem: i}
}

type statItem interface {
	updateEvent(args ...interface{})
	clone() interface{}
}

type stReorg struct {
	totalElapsed   time.Duration
	Count          int64
	AverageElapsed float64  `json:"Average Elapsed Time,omitempty"`
	Latest         *evReorg `json:",omitempty"`
}

func newStReorg() statItem {
	return &stReorg{}
}

type evReorg struct {
	OldBest *blockInfo `json:"Old Best,omitempty"`
	Fork    *blockInfo `json:"Fork At,omitempty"`
	NewBest *blockInfo `json:"New Best,omitempty"`
	Time    time.Time
}

type blockInfo struct {
	Hash   string
	Height types.BlockNo
}

func (sr *stReorg) getCount() int64 {
	return sr.Count
}

func (sr *stReorg) getLatestEvent() interface{} {
	return sr.Latest
}

func (sr *stReorg) updateEvent(args ...interface{}) {
	if len(args) != 4 {
		logger.Info().Int("len", len(args)).Msg("invalid # of arguments for the reorg stat update")
		return
	}

	et := args[0].(time.Duration)

	bi := make([]*blockInfo, len(args))
	for i, a := range args[1:] {
		var block *types.Block
		ok := false
		if block, ok = a.(*types.Block); !ok {
			logger.Info().Int("arg idx", i).Msg("invalid type of argument")
			return
		}
		bi[i] = &blockInfo{Hash: block.ID(), Height: block.BlockNo()}
	}

	sr.Latest = &evReorg{
		OldBest: bi[0],
		NewBest: bi[1],
		Fork:    bi[2],
		Time:    time.Now(),
	}

	sr.totalElapsed += et
	sr.Count++
	sr.AverageElapsed = (sr.totalElapsed / time.Duration(sr.Count)).Seconds()
}

func (sr *stReorg) clone() interface{} {
	c := *sr
	if sr.Latest != nil {
		l := *sr.Latest
		c.Latest = &l
	}

	return &c
}
