package kapacitor


import (
	"fmt"
	"time"
	"reflect"

	"github.com/influxdata/influxdb/influxql"
	"github.com/influxdata/kapacitor/models"
	"github.com/influxdata/kapacitor/edge"
	"github.com/influxdata/kapacitor/pipeline"
)

{{/* Define typed Aggregate/Emit types */}}
{{range .}}

func convert{{.Name}}Point(
	name string,
	p edge.FieldsTagsTimeGetter,
	field string,
	isSimpleSelector bool,
	topBottomInfo *pipeline.TopBottomCallInfo,
) (*influxql.{{.Name}}Point, error) {
	value, ok := p.Fields()[field]
	if !ok {
		return nil, fmt.Errorf("field %s missing from point cannot aggregate", field)
	}
	typed, ok := value.({{.Type}})
	if !ok {
		return nil, fmt.Errorf("field %s has wrong type: got %T exp {{.Type}}", field, value)
	}
	ap := &influxql.{{.Name}}Point{
		Name:  name,
		Tags:  influxql.NewTags(p.Tags()),
		Time:  p.Time().UnixNano(),
		Value: typed,
	}
	if topBottomInfo != nil {
		// We need to populate the Aux fields
		{{.name}}PopulateAuxFieldsAndTags(ap, topBottomInfo.FieldsAndTags, p.Fields(), p.Tags())
	}

	if isSimpleSelector {
		ap.Aux = []interface{}{ p.Tags(), p.Fields() }
	}

	return ap, nil
}

type {{.name}}PointAggregator struct {
	field         string
	topBottomInfo *pipeline.TopBottomCallInfo
	isSimpleSelector bool
	aggregator influxql.{{.Name}}PointAggregator
}

func {{.name}}PopulateAuxFieldsAndTags(ap *influxql.{{.Name}}Point, fieldsAndTags []string, fields models.Fields, tags models.Tags) {
	ap.Aux = make([]interface{}, len(fieldsAndTags))
	for i, name := range fieldsAndTags {
		if f, ok := fields[name]; ok {
			ap.Aux[i] = f
		} else {
			ap.Aux[i] = tags[name]
		}
	}
}

func (a *{{.name}}PointAggregator) AggregatePoint(name string, p edge.FieldsTagsTimeGetter) error {
	ap, err := convert{{.Name}}Point(name, p, a.field, a.isSimpleSelector, a.topBottomInfo)
	if err != nil {
		return nil
	}
	a.aggregator.Aggregate{{.Name}}(ap)
	return nil
}

type {{.name}}PointEmitter struct {
	baseReduceContext
	emitter influxql.{{.Name}}PointEmitter
	isSimpleSelector bool
    byName bool
}

func (e *{{.name}}PointEmitter) EmitPoint() (edge.PointMessage, error) {
	slice := e.emitter.Emit()
	if len(slice) != 1 {
		return nil, nil
	}
	ap := slice[0]
	var t time.Time
	if e.pointTimes {
		if ap.Time == influxql.ZeroTime {
			t = e.time
		} else {
			t = time.Unix(0, ap.Time).UTC()
		}
	} else {
		t = e.time
	}

	var fields models.Fields
	var tags models.Tags
	if e.isSimpleSelector {
		tags = ap.Aux[0].(models.Tags)
		fields = ap.Aux[1].(models.Fields)
		if e.as != e.field {
			fields = fields.Copy()
			fields[e.as] = fields[e.field]
			delete(fields, e.field)
		}
	} else {
		tags = e.groupInfo.Tags
		fields = map[string]interface{}{e.as: ap.Value}
	}

	return edge.NewPointMessage(
		e.name, "", "",
		e.groupInfo.Dimensions,
		fields,
		tags,
		t,
	), nil
}

func (e *{{.name}}PointEmitter) EmitBatch() edge.BufferedBatchMessage {
	slice := e.emitter.Emit()
	begin := edge.NewBeginBatchMessage(
		e.name,
		e.groupInfo.Tags,
		e.groupInfo.Dimensions.ByName,
		e.time,
		len(slice),
	)
	points := make([]edge.BatchPointMessage, len(slice))
	var t time.Time
	for i, ap := range slice {
		if e.pointTimes {
			if ap.Time == influxql.ZeroTime {
				t = e.time
			} else {
				t = time.Unix(0, ap.Time).UTC()
			}
		} else {
			t = e.time
		}
		var tags models.Tags
		if l := len(ap.Tags.KeyValues()); l > 0 {
			// Merge batch and point specific tags
			tags = make(models.Tags, len(e.groupInfo.Tags)+l)
			for k, v := range e.groupInfo.Tags {
				tags[k] = v
			}
			for k, v := range ap.Tags.KeyValues() {
				tags[k] = v
			}
		} else {
			tags = e.groupInfo.Tags
		}
		points[i] = edge.NewBatchPointMessage(
			models.Fields{e.as: ap.Value},
			tags,
			t,
		)
		if t.After(begin.Time()) {
			begin.SetTime(t)
		}
	}
	return edge.NewBufferedBatchMessage(
		begin,
		points,
		edge.NewEndBatchMessage(),
	)
}

{{end}}

{{/* Define composite types for reduceContext */}}
{{with $types := .}}
{{range $a := $types}}
{{range $e := $types}}

// {{$a.name}}{{if ne $a.Name $e.Name}}{{$e.Name}}{{end}}ReduceContext uses composition to implement the reduceContext interface
type {{$a.name}}{{if ne $a.Name $e.Name}}{{$e.Name}}{{end}}ReduceContext struct {
    {{$a.name}}PointAggregator
    {{$e.name}}PointEmitter
}

{{end}}{{end}}


{{/* Define switch cases for reduceContext contruction */}}

func determineReduceContextCreateFn(method string, kind reflect.Kind, rc pipeline.ReduceCreater)  (fn createReduceContextFunc, err error) {
	switch kind {
{{range $a := $types}}
	case {{.Kind}}:
		switch {
{{range $e := $types}}
		case rc.Create{{$a.Name}}{{if ne $a.Name $e.Name}}{{$e.Name}}{{end}}Reducer != nil:
			 fn = func(c baseReduceContext) reduceContext {
				a, e := rc.Create{{$a.Name}}{{if ne $a.Name $e.Name}}{{$e.Name}}{{end}}Reducer()
				return &{{$a.name}}{{if ne $a.Name $e.Name}}{{$e.Name}}{{end}}ReduceContext{
					{{$a.name}}PointAggregator: {{$a.name}}PointAggregator{
						field:      c.field,
						topBottomInfo: rc.TopBottomCallInfo,
						isSimpleSelector: rc.IsSimpleSelector,
						aggregator: a,
					},
					{{$e.name}}PointEmitter: {{$e.name}}PointEmitter{
						baseReduceContext: c,
						emitter:           e,
						isSimpleSelector: rc.IsSimpleSelector,
					},
				}
			}
{{end}}
		default:
			err = fmt.Errorf("cannot apply %s to {{$a.Type}} field", method)
		}
{{end}}
	default:
		err = fmt.Errorf("invalid field kind: %v", kind)
	}
	return
}
{{end}}
