package timing

import (
	"sync"
	"sync/atomic"
	"time"

	"github.com/lbryio/ytsync/v5/metrics"
	"github.com/sirupsen/logrus"
)

type Timing struct {
	component    string
	milliseconds int64
	min          int64
	max          int64
	invocations  int32
}

var timings *sync.Map

func TimedComponent(component string) *Timing {
	if timings == nil {
		timings = &sync.Map{}
	}
	stored, _ := timings.LoadOrStore(component, &Timing{
		component:    component,
		milliseconds: 0,
		min:          int64(99999999),
	})
	t, _ := stored.(*Timing)
	return t
}

func ClearTimings() {
	if timings == nil {
		return
	}
	timings.Range(func(key interface{}, value interface{}) bool {
		timings.Delete(key)
		return true
	})
}

func Report() {
	var totalTime time.Duration
	timings.Range(func(key interface{}, value interface{}) bool {
		totalTime += value.(*Timing).Get()
		return true
	})
	timings.Range(func(key interface{}, value interface{}) bool {
		component := key
		componentRuntime := value.(*Timing).Get().String()
		percentTime := float64(value.(*Timing).Get()) / float64(totalTime) * 100
		invocations := value.(*Timing).Invocations()
		avgTime := (time.Duration(int64(float64(value.(*Timing).Get()) / float64(value.(*Timing).Invocations())))).String()
		minRuntime := value.(*Timing).Min().String()
		maxRuntime := value.(*Timing).Max().String()
		logrus.Printf("component %s ran for %s (%.2f%% of the total time) - invoked %d times with an average of %s per call, a minimum of %s and a maximum of %s",
			component,
			componentRuntime,
			percentTime,
			invocations,
			avgTime,
			minRuntime,
			maxRuntime,
		)
		return true
	})
}

func (t *Timing) Add(d time.Duration) {
	metrics.Durations.WithLabelValues(t.component).Observe(d.Seconds())
	atomic.AddInt64(&t.milliseconds, d.Milliseconds())
	for {
		oldMin := atomic.LoadInt64(&t.min)
		if d.Milliseconds() < oldMin {
			if atomic.CompareAndSwapInt64(&t.min, oldMin, d.Milliseconds()) {
				break
			}
		} else {
			break
		}
	}
	for {
		oldMax := atomic.LoadInt64(&t.max)
		if d.Milliseconds() > oldMax {
			if atomic.CompareAndSwapInt64(&t.max, oldMax, d.Milliseconds()) {
				break
			}
		} else {
			break
		}
	}
	atomic.AddInt32(&t.invocations, 1)
}

func (t *Timing) Get() time.Duration {
	ms := atomic.LoadInt64(&t.milliseconds)
	return time.Duration(ms) * time.Millisecond
}

func (t *Timing) Invocations() int32 {
	return atomic.LoadInt32(&t.invocations)
}

func (t *Timing) Min() time.Duration {
	ms := atomic.LoadInt64(&t.min)
	return time.Duration(ms) * time.Millisecond
}
func (t *Timing) Max() time.Duration {
	ms := atomic.LoadInt64(&t.max)
	return time.Duration(ms) * time.Millisecond
}