package metrics

import (
	"sort"
	"time"
)

var (
	_ DurationBuckets = (*durationBuckets)(nil)
	_ Buckets         = (*buckets)(nil)
)

const (
	errBucketsCountNeedsGreaterThanZero = "n needs to be > 0"
	errBucketsStartNeedsGreaterThanZero = "start needs to be > 0"
	errBucketsFactorNeedsGreaterThanOne = "factor needs to be > 1"
)

type durationBuckets struct {
	buckets []time.Duration
}

// NewDurationBuckets returns new DurationBuckets implementation
func NewDurationBuckets(bk ...time.Duration) DurationBuckets {
	sort.Slice(bk, func(i, j int) bool {
		return bk[i] < bk[j]
	})
	return durationBuckets{buckets: bk}
}

func (d durationBuckets) Size() int {
	return len(d.buckets)
}

func (d durationBuckets) MapDuration(dv time.Duration) (idx int) {
	for _, bound := range d.buckets {
		if dv < bound {
			break
		}
		idx++
	}
	return
}

func (d durationBuckets) UpperBound(idx int) time.Duration {
	if idx > d.Size()-1 {
		panic("idx is out of bounds")
	}
	return d.buckets[idx]
}

type buckets struct {
	buckets []float64
}

// NewBuckets returns new Buckets implementation
func NewBuckets(bk ...float64) Buckets {
	sort.Slice(bk, func(i, j int) bool {
		return bk[i] < bk[j]
	})
	return buckets{buckets: bk}
}

func (d buckets) Size() int {
	return len(d.buckets)
}

func (d buckets) MapValue(v float64) (idx int) {
	for _, bound := range d.buckets {
		if v < bound {
			break
		}
		idx++
	}
	return
}

func (d buckets) UpperBound(idx int) float64 {
	if idx > d.Size()-1 {
		panic("idx is out of bounds")
	}
	return d.buckets[idx]
}

// MakeLinearBuckets creates a set of linear value buckets.
func MakeLinearBuckets(start, width float64, n int) Buckets {
	if n <= 0 {
		panic(errBucketsCountNeedsGreaterThanZero)
	}
	bounds := make([]float64, n)
	for i := range bounds {
		bounds[i] = start + (float64(i) * width)
	}
	return NewBuckets(bounds...)
}

// MakeLinearDurationBuckets creates a set of linear duration buckets.
func MakeLinearDurationBuckets(start, width time.Duration, n int) DurationBuckets {
	if n <= 0 {
		panic(errBucketsCountNeedsGreaterThanZero)
	}
	buckets := make([]time.Duration, n)
	for i := range buckets {
		buckets[i] = start + (time.Duration(i) * width)
	}
	return NewDurationBuckets(buckets...)
}

// MakeExponentialBuckets creates a set of exponential value buckets.
func MakeExponentialBuckets(start, factor float64, n int) Buckets {
	if n <= 0 {
		panic(errBucketsCountNeedsGreaterThanZero)
	}
	if start <= 0 {
		panic(errBucketsStartNeedsGreaterThanZero)
	}
	if factor <= 1 {
		panic(errBucketsFactorNeedsGreaterThanOne)
	}
	buckets := make([]float64, n)
	curr := start
	for i := range buckets {
		buckets[i] = curr
		curr *= factor
	}
	return NewBuckets(buckets...)
}

// MakeExponentialDurationBuckets creates a set of exponential duration buckets.
func MakeExponentialDurationBuckets(start time.Duration, factor float64, n int) DurationBuckets {
	if n <= 0 {
		panic(errBucketsCountNeedsGreaterThanZero)
	}
	if start <= 0 {
		panic(errBucketsStartNeedsGreaterThanZero)
	}
	if factor <= 1 {
		panic(errBucketsFactorNeedsGreaterThanOne)
	}
	buckets := make([]time.Duration, n)
	curr := start
	for i := range buckets {
		buckets[i] = curr
		curr = time.Duration(float64(curr) * factor)
	}
	return NewDurationBuckets(buckets...)
}
