package proc

import (
	"time"

	seq "github.com/ncabatoff/go-seq/seq"
	common "github.com/ncabatoff/process-exporter"
)

type (
	// Grouper is the top-level interface to the process metrics.  All tracked
	// procs sharing the same group name are aggregated.
	Grouper struct {
		// groupAccum records the historical accumulation of a group so that
		// we can avoid ever decreasing the counts we return.
		groupAccum        map[string]Counts
		tracker           *Tracker
		threadAccum       map[string]map[string]Threads
		debug             bool
		removeEmptyGroups bool
	}

	// GroupByName maps group name to group metrics.
	GroupByName map[string]Group

	// Threads collects metrics for threads in a group sharing a thread name.
	Threads struct {
		Name       string
		NumThreads int
		Counts
	}

	// Group describes the metrics of a single group.
	Group struct {
		Counts
		States
		Wchans map[string]int
		Procs  int
		Memory
		OldestStartTime time.Time
		OpenFDs         uint64
		WorstFDratio    float64
		NumThreads      uint64
		Threads         []Threads
	}
)

// Returns true if x < y.  Test designers should ensure they always have
// a unique name/numthreads combination for each group.
func lessThreads(x, y Threads) bool { return seq.Compare(x, y) < 0 }

// NewGrouper creates a grouper.
func NewGrouper(namer common.MatchNamer, trackChildren, trackThreads, recheck bool, recheckTimeLimit time.Duration, debug bool, removeEmptyGroups bool) *Grouper {
	g := Grouper{
		groupAccum:        make(map[string]Counts),
		threadAccum:       make(map[string]map[string]Threads),
		tracker:           NewTracker(namer, trackChildren, recheck, recheckTimeLimit, debug),
		debug:             debug,
		removeEmptyGroups: removeEmptyGroups,
	}
	return &g
}

func groupadd(grp Group, ts Update) Group {
	var zeroTime time.Time

	grp.Procs++
	grp.Memory.ResidentBytes += ts.Memory.ResidentBytes
	grp.Memory.VirtualBytes += ts.Memory.VirtualBytes
	grp.Memory.VmSwapBytes += ts.Memory.VmSwapBytes
	grp.Memory.ProportionalBytes += ts.Memory.ProportionalBytes
	grp.Memory.ProportionalSwapBytes += ts.Memory.ProportionalSwapBytes
	if ts.Filedesc.Open != -1 {
		grp.OpenFDs += uint64(ts.Filedesc.Open)
	}
	openratio := float64(ts.Filedesc.Open) / float64(ts.Filedesc.Limit)
	if grp.WorstFDratio < openratio {
		grp.WorstFDratio = openratio
	}
	grp.NumThreads += ts.NumThreads
	grp.Counts.Add(ts.Latest)
	grp.States.Add(ts.States)
	if grp.OldestStartTime == zeroTime || ts.Start.Before(grp.OldestStartTime) {
		grp.OldestStartTime = ts.Start
	}

	if grp.Wchans == nil {
		grp.Wchans = make(map[string]int)
	}
	for wchan, count := range ts.Wchans {
		grp.Wchans[wchan] += count
	}

	return grp
}

// Update asks the tracker to report on each tracked process by name.
// These are aggregated by groupname, augmented by accumulated counts
// from the past, and returned.  Note that while the Tracker reports
// only what counts have changed since last cycle, Grouper.Update
// returns counts that never decrease.  If removeEmptyGroups is false,
// then even once the last process with name X disappears, name X will
// still appear in the results with the same counts as before; of course,
// all non-count metrics will be zero.
func (g *Grouper) Update(iter Iter) (CollectErrors, GroupByName, error) {
	cerrs, tracked, err := g.tracker.Update(iter)
	if err != nil {
		return cerrs, nil, err
	}
	return cerrs, g.groups(tracked), nil
}

// Translate the updates into a new GroupByName and update internal history.
func (g *Grouper) groups(tracked []Update) GroupByName {
	groups := make(GroupByName)
	threadsByGroup := make(map[string][]ThreadUpdate)

	for _, update := range tracked {
		groups[update.GroupName] = groupadd(groups[update.GroupName], update)
		if update.Threads != nil {
			threadsByGroup[update.GroupName] =
				append(threadsByGroup[update.GroupName], update.Threads...)
		}
	}

	// Add any accumulated counts to what was just observed,
	// and update the accumulators.
	for gname, group := range groups {
		if oldcounts, ok := g.groupAccum[gname]; ok {
			group.Counts.Add(Delta(oldcounts))
		}
		g.groupAccum[gname] = group.Counts
		group.Threads = g.threads(gname, threadsByGroup[gname])
		groups[gname] = group
	}

	// Now add any groups that were observed in the past but aren't running now (or delete them, if removeEmptyGroups is true).
	for gname, gcounts := range g.groupAccum {
		if _, ok := groups[gname]; !ok {
			if g.removeEmptyGroups {
				delete(g.groupAccum, gname)
				delete(g.threadAccum, gname)
			} else {
				groups[gname] = Group{Counts: gcounts}
			}
		}
	}

	return groups
}

func (g *Grouper) threads(gname string, tracked []ThreadUpdate) []Threads {
	if len(tracked) == 0 {
		delete(g.threadAccum, gname)
		return nil
	}

	ret := make([]Threads, 0, len(tracked))
	threads := make(map[string]Threads)

	// First aggregate the thread metrics by thread name.
	for _, nc := range tracked {
		curthr := threads[nc.ThreadName]
		curthr.NumThreads++
		curthr.Counts.Add(nc.Latest)
		curthr.Name = nc.ThreadName
		threads[nc.ThreadName] = curthr
	}

	// Add any accumulated counts to what was just observed,
	// and update the accumulators.
	if history := g.threadAccum[gname]; history != nil {
		for tname := range threads {
			if oldcounts, ok := history[tname]; ok {
				counts := threads[tname]
				counts.Add(Delta(oldcounts.Counts))
				threads[tname] = counts
			}
		}
	}

	g.threadAccum[gname] = threads

	for _, thr := range threads {
		ret = append(ret, thr)
	}
	return ret
}
