// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package core

import (
	"math"
	"sort"
	"strings"
	"sync/atomic"
	"time"

	"github.com/apache/skywalking-go/plugins/core/reporter"
)

func (t *Tracer) Metrics() interface{} {
	return t
}

func (t *Tracer) initMetricsCollect(meterCollectSecond int) {
	if meterCollectSecond < 0 {
		return
	}
	collectDuration := time.Duration(meterCollectSecond) * time.Second
	go func() {
		for {
			time.Sleep(collectDuration)

			t.reachNotInitMetrics()

			t.sendMetrics()
		}
	}()
}

func (t *Tracer) reachNotInitMetrics() {
	registers, hooks := MetricsObtain()
	if len(registers) == 0 && len(hooks) == 0 {
		return
	}
	for _, meter := range registers {
		switch m := meter.(type) {
		case NoInitCounter:
			counter := newCounter(m.Name(), m.Labels(), m.Get())
			m.ChangeFunctions(counter.Inc, counter.Get)
			t.registerMetrics(m.Name(), m.Labels(), counter)
		case NoInitGauge:
			gauge := newGauge(m.Name(), m.Labels(), m.Getter())
			t.registerMetrics(m.Name(), m.Labels(), gauge)
		case NoInitHistogram:
			histogram := newHistogramFromExistingBuckets(m.Name(), m.Labels(), m.Buckets())
			m.ChangeFunctions(histogram.Observe, histogram.ObserveWithCount)
			t.registerMetrics(m.Name(), m.Labels(), histogram)
		}
	}
	for _, hook := range hooks {
		t.AddCollectHook(hook)
	}
}

func (t *Tracer) sendMetrics() {
	meters := make([]reporter.ReportedMeter, 0)
	// call collect hook
	for _, hook := range t.allMeterCollectListeners() {
		hook()
	}
	t.meterMap.Range(func(key, value interface{}) bool {
		if m, ok := value.(reporter.ReportedMeter); ok {
			meters = append(meters, m)
		} else {
			t.Log.Errorf("unknown meter type: %T", value)
		}
		return true
	})

	t.Reporter.SendMetrics(meters)
}

func (t *Tracer) allMeterCollectListeners() []func() {
	t.meterCollectListenersLock.RLock()
	defer t.meterCollectListenersLock.RUnlock()
	listeners := make([]func(), 0, len(t.meterCollectListeners))
	listeners = append(listeners, t.meterCollectListeners...)
	return listeners
}

func (t *Tracer) NewCounter(name string, opt interface{}) interface{} {
	counter := newCounter(name, nil, 0)
	if o, ok := opt.(meterOpts); ok && o != nil {
		counter.labels = o.GetLabels()
	}
	t.registerMetrics(name, counter.labels, counter)
	return counter
}

func (t *Tracer) NewGauge(name string, getter func() float64, opt interface{}) interface{} {
	gauge := newGauge(name, nil, getter)
	if o, ok := opt.(meterOpts); ok && o != nil {
		gauge.labels = o.GetLabels()
	}
	t.registerMetrics(name, gauge.labels, gauge)
	return gauge
}

func (t *Tracer) NewHistogram(name string, minValue float64, steps []float64, opt interface{}) interface{} {
	histogram := newHistogramFromSteps(name, nil, minValue, steps)
	if o, ok := opt.(meterOpts); ok && o != nil {
		histogram.labels = o.GetLabels()
	}
	t.registerMetrics(name, histogram.labels, histogram)
	return histogram
}

func (t *Tracer) AddCollectHook(f func()) {
	t.meterCollectListenersLock.Lock()
	defer t.meterCollectListenersLock.Unlock()
	t.meterCollectListeners = append(t.meterCollectListeners, f)
}

func (t *Tracer) registerMetrics(name string, labels map[string]string, meter interface{}) {
	var sb strings.Builder
	for k, v := range labels {
		sb.WriteString(k)
		sb.WriteString("=")
		sb.WriteString(v)
		sb.WriteString(",")
	}
	sb.WriteString(name)

	t.meterMap.Store(sb.String(), meter)
}

type meterOpts interface {
	GetLabels() map[string]string
}

type counterImpl struct {
	name   string
	labels map[string]string

	valBits uint64
	valInt  uint64
}

func newCounter(name string, labels map[string]string, val float64) *counterImpl {
	var valBits, valInt uint64
	ival := uint64(val)
	if float64(ival) == val {
		valInt = ival
	} else {
		valBits = math.Float64bits(val)
	}

	return &counterImpl{
		name:    name,
		labels:  labels,
		valBits: valBits,
		valInt:  valInt,
	}
}

func (c *counterImpl) Name() string {
	return c.name
}

func (c *counterImpl) Labels() map[string]string {
	return c.labels
}

func (c *counterImpl) Value() float64 {
	return c.Get()
}

func (c *counterImpl) Get() float64 {
	fval := math.Float64frombits(atomic.LoadUint64(&c.valBits))
	ival := atomic.LoadUint64(&c.valInt)
	return fval + float64(ival)
}

func (c *counterImpl) Inc(val float64) {
	if val < 0 {
		return
	}

	ival := uint64(val)
	if float64(ival) == val {
		atomic.AddUint64(&c.valInt, ival)
		return
	}

	for {
		oldBits := atomic.LoadUint64(&c.valBits)
		newBits := math.Float64bits(math.Float64frombits(oldBits) + val)
		if atomic.CompareAndSwapUint64(&c.valBits, oldBits, newBits) {
			return
		}
	}
}

type gaugeImpl struct {
	name   string
	labels map[string]string
	getter func() float64
}

func newGauge(name string, labels map[string]string, getter func() float64) *gaugeImpl {
	return &gaugeImpl{
		name:   name,
		labels: labels,
		getter: getter,
	}
}

func (g *gaugeImpl) Name() string {
	return g.name
}

func (g *gaugeImpl) Labels() map[string]string {
	return g.labels
}

func (g *gaugeImpl) Get() float64 {
	return g.getter()
}

func (g *gaugeImpl) Value() float64 {
	return g.getter()
}

type histogramImpl struct {
	name   string
	labels map[string]string

	buckets []*histogramBucket
}

func (h *histogramImpl) Name() string {
	return h.name
}

func (h *histogramImpl) Labels() map[string]string {
	return h.labels
}

func (h *histogramImpl) BucketValues() []reporter.ReportedMeterBucketValue {
	var values []reporter.ReportedMeterBucketValue
	for _, b := range h.buckets {
		values = append(values, b)
	}
	return values
}

func (h *histogramImpl) Observe(v float64) {
	if b := h.findBucket(v); b != nil {
		atomic.AddInt64(b.value, 1)
	}
}

func (h *histogramImpl) ObserveWithCount(v float64, c int64) {
	if b := h.findBucket(v); b != nil {
		atomic.AddInt64(b.value, c)
	}
}

func (h *histogramImpl) findBucket(v float64) *histogramBucket {
	var low, high = 0, len(h.buckets) - 1

	for low <= high {
		var mid = (low + high) / 2
		if h.buckets[mid].bucket < v {
			low = mid + 1
		} else if h.buckets[mid].bucket > v {
			high = mid - 1
		} else {
			return h.buckets[mid]
		}
	}

	low--

	if low < len(h.buckets) && low >= 0 {
		return h.buckets[low]
	}
	return nil
}

type histogramBucket struct {
	bucket float64
	value  *int64
}

func newHistogramFromExistingBuckets(name string, labels map[string]string, buckets []interface{}) *histogramImpl {
	result := &histogramImpl{
		name:   name,
		labels: labels,
	}

	result.buckets = make([]*histogramBucket, 0, len(buckets))
	for i, b := range buckets {
		bucket := b.(NoInitHistogramBucket)
		result.buckets[i] = &histogramBucket{
			bucket: bucket.Bucket(),
			value:  bucket.Value(),
		}
	}

	return result
}

func newHistogramFromSteps(name string, labels map[string]string, minVal float64, steps []float64) *histogramImpl {
	result := &histogramImpl{
		name:   name,
		labels: labels,
	}

	// sort steps and check for duplicates
	sort.Float64s(steps)
	prevVal := steps[0]
	for i := 1; i < len(steps); i++ {
		if prevVal == steps[i] {
			panic("duplicate histogram bucket value")
		} else {
			prevVal = steps[i]
		}
	}
	if minVal != steps[0] {
		steps = append([]float64{minVal}, steps...)
	}

	result.buckets = make([]*histogramBucket, len(steps))
	for i, s := range steps {
		result.buckets[i] = &histogramBucket{
			bucket: s,
			value:  new(int64),
		}
	}

	return result
}

func (h *histogramBucket) Bucket() float64 {
	return h.bucket
}

func (h *histogramBucket) Count() int64 {
	return atomic.LoadInt64(h.value)
}

func (h *histogramBucket) IsNegativeInfinity() bool {
	return false
}

type NoInitCounter interface {
	Name() string
	Labels() map[string]string
	Get() float64
	ChangeFunctions(add func(v float64), get func() float64)
}

type NoInitGauge interface {
	Name() string
	Labels() map[string]string
	Getter() func() float64
}

type NoInitHistogram interface {
	Name() string
	Labels() map[string]string
	Buckets() []interface{}
	ChangeFunctions(observe func(v float64), observeWithCount func(v float64, c int64))
}

type NoInitHistogramBucket interface {
	Bucket() float64
	Value() *int64
}
