// Copyright 2019, OpenCensus Authors
//
// Licensed 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 metric

import (
	"sync"
	"time"

	"go.opencensus.io/internal/tagencoding"

	"go.opencensus.io/metric/metricdata"
)

// baseMetric is common representation for gauge and cumulative metrics.
//
// baseMetric maintains a value for each combination of label values passed to
// Set, Add, or Inc method.
//
// baseMetric should not be used directly, use metric specific type such as
// Float64Gauge or Int64Gauge.
type baseMetric struct {
	vals             sync.Map
	desc             metricdata.Descriptor
	start            time.Time
	keys             []metricdata.LabelKey
	constLabelValues []metricdata.LabelValue
	bmType           baseMetricType
}

type baseMetricType int

const (
	gaugeInt64 baseMetricType = iota
	gaugeFloat64
	derivedGaugeInt64
	derivedGaugeFloat64
	cumulativeInt64
	cumulativeFloat64
	derivedCumulativeInt64
	derivedCumulativeFloat64
)

type baseEntry interface {
	read(t time.Time) metricdata.Point
}

func (bm *baseMetric) startTime() *time.Time {
	switch bm.bmType {
	case cumulativeInt64, cumulativeFloat64, derivedCumulativeInt64, derivedCumulativeFloat64:
		return &bm.start
	default:
		// gauges don't have start time.
		return nil
	}
}

// Read returns the current values of the baseMetric as a metric for export.
func (bm *baseMetric) read() *metricdata.Metric {
	now := time.Now()
	startTime := bm.startTime()
	if startTime == nil {
		startTime = &now
	}
	m := &metricdata.Metric{
		Descriptor: bm.desc,
	}
	bm.vals.Range(func(k, v interface{}) bool {
		entry := v.(baseEntry)
		key := k.(string)
		labelVals := bm.decodeLabelVals(key)
		m.TimeSeries = append(m.TimeSeries, &metricdata.TimeSeries{
			StartTime:   *startTime,
			LabelValues: labelVals,
			Points: []metricdata.Point{
				entry.read(now),
			},
		})
		return true
	})
	return m
}

func (bm *baseMetric) encodeLabelVals(labelVals []metricdata.LabelValue) string {
	vb := &tagencoding.Values{}
	for _, v := range labelVals {
		b := make([]byte, 1, len(v.Value)+1)
		if v.Present {
			b[0] = 1
			b = append(b, []byte(v.Value)...)
		}
		vb.WriteValue(b)
	}
	return string(vb.Bytes())
}

func (bm *baseMetric) decodeLabelVals(s string) []metricdata.LabelValue {
	vals := make([]metricdata.LabelValue, 0, len(bm.keys))
	vb := &tagencoding.Values{Buffer: []byte(s)}
	for range bm.keys {
		v := vb.ReadValue()
		if v[0] == 0 {
			vals = append(vals, metricdata.LabelValue{})
		} else {
			vals = append(vals, metricdata.NewLabelValue(string(v[1:])))
		}
	}
	return vals
}

func (bm *baseMetric) entryForValues(labelVals []metricdata.LabelValue, newEntry func() baseEntry) (interface{}, error) {
	labelVals = append(bm.constLabelValues, labelVals...)
	if len(labelVals) != len(bm.keys) {
		return nil, errKeyValueMismatch
	}
	mapKey := bm.encodeLabelVals(labelVals)
	if entry, ok := bm.vals.Load(mapKey); ok {
		return entry, nil
	}
	entry, _ := bm.vals.LoadOrStore(mapKey, newEntry())
	return entry, nil
}

func (bm *baseMetric) upsertEntry(labelVals []metricdata.LabelValue, newEntry func() baseEntry) error {
	labelVals = append(bm.constLabelValues, labelVals...)
	if len(labelVals) != len(bm.keys) {
		return errKeyValueMismatch
	}
	mapKey := bm.encodeLabelVals(labelVals)
	bm.vals.Delete(mapKey)
	bm.vals.Store(mapKey, newEntry())
	return nil
}
