package driver

import (
	"strings"

	"github.com/google/pprof/internal/measurement"
	"github.com/google/pprof/profile"
)

// addLabelNodes adds pseudo stack frames "label:value" to each Sample with
// labels matching the supplied keys.
//
// rootKeys adds frames at the root of the callgraph (first key becomes new root).
// leafKeys adds frames at the leaf of the callgraph (last key becomes new leaf).
//
// Returns whether there were matches found for the label keys.
func addLabelNodes(p *profile.Profile, rootKeys, leafKeys []string, outputUnit string) (rootm, leafm bool) {
	// Find where to insert the new locations and functions at the end of
	// their ID spaces.
	var maxLocID uint64
	var maxFunctionID uint64
	for _, loc := range p.Location {
		if loc.ID > maxLocID {
			maxLocID = loc.ID
		}
	}
	for _, f := range p.Function {
		if f.ID > maxFunctionID {
			maxFunctionID = f.ID
		}
	}
	nextLocID := maxLocID + 1
	nextFuncID := maxFunctionID + 1

	// Intern the new locations and functions we are generating.
	type locKey struct {
		functionName, fileName string
	}
	locs := map[locKey]*profile.Location{}

	internLoc := func(locKey locKey) *profile.Location {
		loc, found := locs[locKey]
		if found {
			return loc
		}

		function := &profile.Function{
			ID:       nextFuncID,
			Name:     locKey.functionName,
			Filename: locKey.fileName,
		}
		nextFuncID++
		p.Function = append(p.Function, function)

		loc = &profile.Location{
			ID: nextLocID,
			Line: []profile.Line{
				{
					Function: function,
				},
			},
		}
		nextLocID++
		p.Location = append(p.Location, loc)
		locs[locKey] = loc
		return loc
	}

	makeLabelLocs := func(s *profile.Sample, keys []string) ([]*profile.Location, bool) {
		var locs []*profile.Location
		var match bool
		for i := range keys {
			// Loop backwards, ensuring the first tag is closest to the root,
			// and the last tag is closest to the leaves.
			k := keys[len(keys)-1-i]
			values := formatLabelValues(s, k, outputUnit)
			if len(values) > 0 {
				match = true
			}
			locKey := locKey{
				functionName: strings.Join(values, ","),
				fileName:     k,
			}
			loc := internLoc(locKey)
			locs = append(locs, loc)
		}
		return locs, match
	}

	for _, s := range p.Sample {
		rootsToAdd, sampleMatchedRoot := makeLabelLocs(s, rootKeys)
		if sampleMatchedRoot {
			rootm = true
		}
		leavesToAdd, sampleMatchedLeaf := makeLabelLocs(s, leafKeys)
		if sampleMatchedLeaf {
			leafm = true
		}

		var newLocs []*profile.Location
		newLocs = append(newLocs, leavesToAdd...)
		newLocs = append(newLocs, s.Location...)
		newLocs = append(newLocs, rootsToAdd...)
		s.Location = newLocs
	}
	return
}

// formatLabelValues returns all the string and numeric labels in Sample, with
// the numeric labels formatted according to outputUnit.
func formatLabelValues(s *profile.Sample, k string, outputUnit string) []string {
	var values []string
	values = append(values, s.Label[k]...)
	numLabels := s.NumLabel[k]
	numUnits := s.NumUnit[k]
	if len(numLabels) != len(numUnits) {
		return values
	}
	for i, numLabel := range numLabels {
		unit := numUnits[i]
		values = append(values, measurement.ScaledLabel(numLabel, unit, outputUnit))
	}
	return values
}
