// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

// Package stack analyzes stack dump of Go processes and simplifies it.
//
// It is mostly useful on servers will large number of identical goroutines,
// making the crash dump harder to read than strictly necessary.
package stack

import (
	"errors"
	"fmt"
	"net/url"
	"os"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"
)

// Func is a function call in a goroutine stack trace.
type Func struct {
	// Complete is the complete reference. It can be ambiguous in case where a
	// path contains dots.
	Complete string
	// ImportPath is the directory name for this function reference, or "main" if
	// it was in package main. The package name may not match.
	ImportPath string
	// DirName is the directory name containing the package in which the function
	// is. Normally this matches the package name, but sometimes there's smartass
	// folks that use a different directory name than the package name.
	DirName string
	// Name is the function name or fully quality method name.
	Name string
	// IsExported is true if the function is exported.
	IsExported bool
	// IsPkgMain is true if it is in the main package.
	IsPkgMain bool

	// Disallow initialization with unnamed parameters.
	_ struct{}
}

// Init parses the raw function call line from a goroutine stack trace.
//
// Go stack traces print a mangled function call, this wrapper unmangle the
// string before printing and adds other filtering methods.
//
// The main caveat is that for calls in package main, the package import URL is
// left out.
func (f *Func) Init(raw string) error {
	// Format can be:
	//  - gopkg.in/yaml%2ev2.(*Struct).Method  (handling dots is tricky)
	//  - main.func·001  (go statements)
	//  - foo  (C code)
	//
	// The function is optimized to reduce its memory usage.
	endPkg := 0
	if lastSlash := strings.LastIndexByte(raw, '/'); lastSlash != -1 {
		// Cut the path elements.
		r := strings.IndexByte(raw[lastSlash+1:], '.')
		if r == -1 {
			return errors.New("bad function reference: expected to have at least one dot")
		}
		endPkg = lastSlash + r + 1
	} else {
		// It's fine if there's no dot, it happens in C code in go1.4 and lower.
		endPkg = strings.IndexByte(raw, '.')
	}
	// Only the path part is escaped.
	var err error
	if f.Complete, err = url.QueryUnescape(raw); err != nil {
		return fmt.Errorf("bad function reference: %w", err)
	}
	// Update the index in the unescaped string.
	endPkg += len(f.Complete) - len(raw)
	if endPkg != -1 {
		f.ImportPath = f.Complete[:endPkg]
	}
	f.Name = f.Complete[endPkg+1:]
	if idx := strings.LastIndexByte(f.Name, ' '); idx > -1 {
		cut := f.Name[:idx]
		// TODO(go1.20): switch to strings.CutSuffix
		const inGoroutineSuffix = " in goroutine"
		if strings.HasSuffix(cut, inGoroutineSuffix) {
			f.Name = strings.TrimSuffix(cut, inGoroutineSuffix)
		}
	}
	f.DirName = f.ImportPath
	if i := strings.LastIndexByte(f.DirName, '/'); i != -1 {
		f.DirName = f.DirName[i+1:]
	}
	if f.ImportPath == "main" {
		f.IsPkgMain = true
		// Consider main.main to be exported.
		if f.Name == "main" {
			f.IsExported = true
		}
	} else {
		parts := strings.Split(f.Name, ".")
		r, _ := utf8.DecodeRuneInString(parts[len(parts)-1])
		f.IsExported = unicode.ToUpper(r) == r
	}
	return nil
}

// String returns Complete.
func (f *Func) String() string {
	return f.Complete
}

// Arg is an argument on a Call.
type Arg struct {
	// IsAggregate is true if the argument is an aggregate type. If true, the
	// argument does not contain a value itself, but contains a set of nested
	// argument fields. If false, the argument contains a single scalar value.
	IsAggregate bool

	// The following are set if IsAggregate == false.

	// Name is a pseudo name given to the argument.
	Name string
	// Value is the raw value as found in the stack trace
	Value uint64
	// IsPtr is true if we guess it's a pointer. It's only a guess, it can be
	// easily confused by a bitmask.
	IsPtr bool
	// IsOffsetTooLarge is true if the argument's frame offset was too large,
	// preventing the argument from being printed in the stack trace.
	IsOffsetTooLarge bool

	// IsInaccurate determines if Value is inaccurate. Stacks could have inaccurate values
	// for arguments passed in registers. Go 1.18 prints a ? for these values.
	IsInaccurate bool

	// The following are set if IsAggregate == true.

	// Fields are the fields/elements of aggregate-typed arguments.
	Fields Args

	// Disallow initialization with unnamed parameters.
	_ struct{}
}

const zeroToNine = "0123456789"

// String prints the argument as the name if present, otherwise as the value.
func (a *Arg) String() string {
	if a.Name != "" {
		return a.Name
	}
	if a.IsOffsetTooLarge {
		return "_"
	}
	if a.IsAggregate {
		return "{" + a.Fields.String() + "}"
	}
	if a.Value < uint64(len(zeroToNine)) {
		return zeroToNine[a.Value : a.Value+1]
	}
	return fmt.Sprintf("0x%x", a.Value)
}

const (
	// The pointer floor can be below 1MiB (!) on Windows.
	// Assumes all values are above 512KiB and positive are pointers; assuming
	// that above half the memory is kernel memory.
	//
	// This is not always true but this should be good enough to help
	// implementing AnyPointer.
	pointerFloor = 512 * 1024
	// Assume the stack was generated with the same bitness (32 vs 64) than the
	// code processing it.
	pointerCeiling = uint64((^uint(0)) >> 1)
)

// equal returns true only if both arguments are exactly equal.
func (a *Arg) equal(r *Arg) bool {
	return a.similar(r, ExactFlags)
}

// similar returns true if the two Arg are equal or almost but not quite equal.
func (a *Arg) similar(r *Arg, similar Similarity) bool {
	if a.IsAggregate != r.IsAggregate {
		return false
	}
	if a.IsAggregate {
		return a.Fields.similar(&r.Fields, similar)
	}
	switch similar {
	case ExactFlags, ExactLines:
		if a.Name != r.Name {
			return false
		}
		if a.IsOffsetTooLarge != r.IsOffsetTooLarge {
			return false
		}
		if a.IsPtr != r.IsPtr {
			return false
		}
		return a.Value == r.Value
	case AnyValue:
		return true
	case AnyPointer:
		if a.IsOffsetTooLarge != r.IsOffsetTooLarge {
			return false
		}
		if a.IsPtr != r.IsPtr {
			return false
		}
		return a.IsPtr || a.Value == r.Value
	default:
		return false
	}
}

// Args is a series of function call arguments.
type Args struct {
	// Values is the arguments as shown on the stack trace. They are mangled via
	// simplification.
	Values []Arg
	// Processed is the arguments generated from processing the source files. It
	// can have a length lower than Values.
	Processed []string
	// Elided when set means there was a trailing ", ...".
	Elided bool

	// Disallow initialization with unnamed parameters.
	_ struct{}
}

func (a *Args) String() string {
	var v []string
	if len(a.Processed) != 0 {
		v = a.Processed
	} else {
		v = make([]string, 0, len(a.Values))
		for _, item := range a.Values {
			v = append(v, item.String())
		}
	}
	if a.Elided {
		v = append(v, "...")
	}
	return strings.Join(v, ", ")
}

// equal returns true only if both arguments are exactly equal.
func (a *Args) equal(r *Args) bool {
	if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
		return false
	}
	for i, l := range a.Values {
		if !l.equal(&r.Values[i]) {
			return false
		}
	}
	return true
}

// similar returns true if the two Args are equal or almost but not quite
// equal.
func (a *Args) similar(r *Args, similar Similarity) bool {
	if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
		return false
	}
	for i, l := range a.Values {
		if !l.similar(&r.Values[i], similar) {
			return false
		}
	}
	return true
}

// merge merges two similar Args, zapping out differences.
func (a *Args) merge(r *Args) Args {
	out := Args{
		Values: make([]Arg, len(a.Values)),
		Elided: a.Elided,
	}
	for i, l := range a.Values {
		rv := &r.Values[i]
		if l.IsAggregate {
			out.Values[i].IsAggregate = true
			out.Values[i].Fields = l.Fields.merge(&rv.Fields)
		} else if !l.equal(rv) {
			out.Values[i].Name = "*"
			out.Values[i].Value = l.Value
			out.Values[i].IsPtr = l.IsPtr
		} else {
			out.Values[i] = l
		}
	}
	return out
}

// walk traverses all non-aggregate arguments in the Args struct, calling the
// provided visitor function with each Arg.
func (a *Args) walk(visitor func(arg *Arg)) {
	for i := range a.Values {
		arg := &a.Values[i]
		if arg.IsAggregate {
			arg.Fields.walk(visitor)
		} else {
			visitor(arg)
		}
	}
}

// Location is the source location, if determined.
type Location int

const (
	// LocationUnknown is the default value when Opts.GuessPaths was false.
	LocationUnknown Location = iota
	// GoMod is a go module, it is outside $GOPATH and is inside a directory
	// containing a go.mod file. This is considered a local copy.
	GoMod
	// GOPATH is in $GOPATH/src. This is either a dependency fetched via
	// GO111MODULE=off or intentionally fetched this way. There is no guaranteed
	// that the local copy is pristine.
	GOPATH
	// GoPkg is in $GOPATH/pkg/mod. This is a dependency fetched via go module.
	// It is considered to be an unmodified external dependency.
	GoPkg
	// Stdlib is when it is a Go standard library function. This includes the 'go
	// test' generated main executable.
	Stdlib

	lastLocation
)

// Call is an item in the stack trace.
//
// All paths in this struct are in POSIX format, using "/" as path separator.
type Call struct {
	// The following are initialized on the first line of the call stack.

	// Func is the fully qualified function name (encoded).
	Func Func
	// Args is the call arguments.
	Args Args

	// The following are initialized on the second line of the call stack.

	// RemoteSrcPath is the full path name of the source file as seen in the
	// trace.
	RemoteSrcPath string
	// Line is the line number.
	Line int
	// SrcName is the base file name of the source file.
	SrcName string
	// DirSrc is one directory plus the file name of the source file. It is a
	// subset of RemoteSrcPath.
	DirSrc string

	// The following are only set if Opts.GuessPaths was set.

	// LocalSrcPath is the full path name of the source file as seen in the host,
	// if found.
	LocalSrcPath string
	// RelSrcPath is the relative path to GOROOT, GOPATH or LocalGoMods.
	RelSrcPath string
	// ImportPath is the fully qualified import path as found on disk (when
	// Opts.GuessPaths was set). Defaults to Func.ImportPath otherwise.
	//
	// In the case of package "main", it returns the underlying path to the main
	// package instead of "main" if Opts.GuessPaths was set.
	ImportPath string
	// Location is the source location, if determined.
	Location Location

	// Disallow initialization with unnamed parameters.
	_ struct{}
}

// Init initializes RemoteSrcPath, SrcName, DirName and Line.
//
// For test main, it initializes Location only with Stdlib.
//
// It does its best educated guess for ImportPath.
func (c *Call) init(srcPath string, line int) {
	c.Line = line
	if srcPath != "" {
		c.RemoteSrcPath = srcPath
		if i := strings.LastIndexByte(c.RemoteSrcPath, '/'); i != -1 {
			c.SrcName = c.RemoteSrcPath[i+1:]
			if i = strings.LastIndexByte(c.RemoteSrcPath[:i], '/'); i != -1 {
				c.DirSrc = c.RemoteSrcPath[i+1:]
			}
		}
		if c.DirSrc == testMainSrc {
			// Consider _test/_testmain.go as stdlib since it's injected by "go test".
			c.Location = Stdlib
		}
	}
	c.ImportPath = c.Func.ImportPath
}

const testMainSrc = "_test" + string(os.PathSeparator) + "_testmain.go"

// updateLocations initializes LocalSrcPath, RelSrcPath, Location and ImportPath.
//
// goroot, localgoroot, localgomod, gomodImportPath and gopaths are expected to
// be in "/" format even on Windows. They must not have a trailing "/".
//
// Returns true if a match was found.
func (c *Call) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool {
	// TODO(maruel): Reduce memory allocations.
	if c.RemoteSrcPath == "" {
		return false
	}
	// Check GOROOT first.
	if goroot != "" {
		if prefix := goroot + "/src/"; strings.HasPrefix(c.RemoteSrcPath, prefix) {
			// Replace remote GOROOT with local GOROOT.
			c.RelSrcPath = c.RemoteSrcPath[len(prefix):]
			c.LocalSrcPath = pathJoin(localgoroot, "src", c.RelSrcPath)
			if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
				c.ImportPath = c.RelSrcPath[:i]
			}
			if c.Location == LocationUnknown {
				c.Location = Stdlib
			}
			return true
		}
	}
	// Check GOPATH.
	// TODO(maruel): Sort for deterministic behavior?
	for prefix, dest := range gopaths {
		if p := prefix + "/src/"; strings.HasPrefix(c.RemoteSrcPath, p) {
			c.RelSrcPath = c.RemoteSrcPath[len(p):]
			c.LocalSrcPath = pathJoin(dest, "src", c.RelSrcPath)
			if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
				c.ImportPath = c.RelSrcPath[:i]
			}
			if c.Location == LocationUnknown {
				c.Location = GOPATH
			}
			return true
		}
		// For modules, the path has to be altered, as it contains the version.
		if p := prefix + "/pkg/mod/"; strings.HasPrefix(c.RemoteSrcPath, p) {
			c.RelSrcPath = c.RemoteSrcPath[len(p):]
			c.LocalSrcPath = pathJoin(dest, "pkg/mod", c.RelSrcPath)
			if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
				c.ImportPath = c.RelSrcPath[:i]
			}
			if c.Location == LocationUnknown {
				c.Location = GoPkg
			}
			return true
		}
	}
	// Check Go modules.
	// Go module path detection only works with stack traces created on the local
	// file system.
	for prefix, pkg := range localgomods {
		if strings.HasPrefix(c.RemoteSrcPath, prefix+"/") {
			c.RelSrcPath = c.RemoteSrcPath[len(prefix)+1:]
			c.LocalSrcPath = c.RemoteSrcPath
			if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
				c.ImportPath = pkg + "/" + c.RelSrcPath[:i]
			} else {
				c.ImportPath = pkg
			}
			if c.Location == LocationUnknown {
				c.Location = GoMod
			}
			return true
		}
	}
	// Maybe the path is just absolute and exists?
	return false
}

// equal returns true only if both calls are exactly equal.
func (c *Call) equal(r *Call) bool {
	return c.Line == r.Line && c.Func.Complete == r.Func.Complete && c.RemoteSrcPath == r.RemoteSrcPath && c.Args.equal(&r.Args)
}

// similar returns true if the two Call are equal or almost but not quite
// equal.
func (c *Call) similar(r *Call, similar Similarity) bool {
	return c.Line == r.Line && c.Func.Complete == r.Func.Complete && c.RemoteSrcPath == r.RemoteSrcPath && c.Args.similar(&r.Args, similar)
}

// merge merges two similar Call, zapping out differences.
func (c *Call) merge(r *Call) Call {
	return Call{
		Func:          c.Func,
		Args:          c.Args.merge(&r.Args),
		RemoteSrcPath: c.RemoteSrcPath,
		Line:          c.Line,
		SrcName:       c.SrcName,
		DirSrc:        c.DirSrc,
		LocalSrcPath:  c.LocalSrcPath,
		RelSrcPath:    c.RelSrcPath,
		ImportPath:    c.ImportPath,
		Location:      c.Location,
	}
}

// Stack is a call stack.
type Stack struct {
	// Calls is the call stack. First is original function, last is leaf
	// function.
	Calls []Call
	// Elided is set when there's >100 items in Stack, currently hardcoded in
	// package runtime.
	Elided bool

	// Disallow initialization with unnamed parameters.
	_ struct{}
}

// equal returns true on if both call stacks are exactly equal.
func (s *Stack) equal(r *Stack) bool {
	if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
		return false
	}
	for i := range s.Calls {
		if !s.Calls[i].equal(&r.Calls[i]) {
			return false
		}
	}
	return true
}

// similar returns true if the two Stack are equal or almost but not quite
// equal.
func (s *Stack) similar(r *Stack, similar Similarity) bool {
	if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
		return false
	}
	for i := range s.Calls {
		if !s.Calls[i].similar(&r.Calls[i], similar) {
			return false
		}
	}
	return true
}

// merge merges two similar Stack, zapping out differences.
func (s *Stack) merge(r *Stack) *Stack {
	// Assumes similar stacks have the same length.
	out := &Stack{
		Calls:  make([]Call, len(s.Calls)),
		Elided: s.Elided,
	}
	for i := range s.Calls {
		out.Calls[i] = s.Calls[i].merge(&r.Calls[i])
	}
	return out
}

// less compares two Stack, where the ones that are less are more
// important, so they come up front.
//
// A Stack with more private functions is 'less' so it is at the top.
// Inversely, a Stack with only public functions is 'more' so it is at the
// bottom.
func (s *Stack) less(r *Stack) bool {
	lLoc := [lastLocation]int{}
	rLoc := [lastLocation]int{}
	lMain := 0
	rMain := 0
	for _, c := range s.Calls {
		lLoc[c.Location]++
		if c.Func.IsPkgMain {
			lMain++
		}
	}
	for _, s := range r.Calls {
		rLoc[s.Location]++
		if s.Func.IsPkgMain {
			rMain++
		}
	}
	if lMain > rMain {
		return true
	}
	if lMain < rMain {
		return false
	}
	for i := 1; i < int(lastLocation); i++ {
		if lLoc[i] > rLoc[i] {
			return true
		}
		if lLoc[i] < rLoc[i] {
			return false
		}
	}
	// Check unknown code type last.
	if lLoc[LocationUnknown] > rLoc[LocationUnknown] {
		return true
	}
	if lLoc[LocationUnknown] < rLoc[LocationUnknown] {
		return false
	}

	// Stack lengths are the same and they are mostly of the same kind of location.
	for x := range s.Calls {
		if s.Calls[x].Func.Complete < r.Calls[x].Func.Complete {
			return true
		}
		if s.Calls[x].Func.Complete > r.Calls[x].Func.Complete {
			return false
		}
		if s.Calls[x].DirSrc < r.Calls[x].DirSrc {
			return true
		}
		if s.Calls[x].DirSrc > r.Calls[x].DirSrc {
			return false
		}
		if s.Calls[x].Line < r.Calls[x].Line {
			return true
		}
		if s.Calls[x].Line > r.Calls[x].Line {
			return false
		}
	}
	// Stacks are the same.
	return false
}

// updateLocations calls updateLocations on each call frame and returns true if
// they were all resolved.
func (s *Stack) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool {
	// If there were none, it was "resolved".
	r := true
	for i := range s.Calls {
		r = s.Calls[i].updateLocations(goroot, localgoroot, localgomods, gopaths) && r
	}
	return r
}

// Signature represents the signature of one or multiple goroutines.
//
// It is effectively the stack trace plus the goroutine internal bits, like
// it's state, if it is thread locked, which call site created this goroutine,
// etc.
type Signature struct {
	// State is the goroutine state at the time of the snapshot.
	//
	// Use git grep 'gopark(|unlock)\(' to find them all plus everything listed
	// in runtime/traceback.go. Valid values includes:
	//     - chan send, chan receive, select
	//     - finalizer wait, mark wait (idle),
	//     - Concurrent GC wait, GC sweep wait, force gc (idle)
	//     - IO wait, panicwait
	//     - semacquire, semarelease
	//     - sleep, timer goroutine (idle)
	//     - trace reader (blocked)
	// Stuck cases:
	//     - chan send (nil chan), chan receive (nil chan), select (no cases)
	// Runnable states:
	//    - idle, runnable, running, syscall, waiting, dead, enqueue, copystack,
	// Scan states:
	//    - scan, scanrunnable, scanrunning, scansyscall, scanwaiting, scandead,
	//      scanenqueue
	//
	// When running under the race detector, the values are 'running' or
	// 'finished'.
	State string
	// CreatedBy is the call stack that created this goroutine, if applicable.
	//
	// Normally, the stack is a single Call.
	//
	// When the race detector is enabled, a full stack snapshot is available.
	CreatedBy Stack
	// SleepMin is the wait time in minutes, if applicable.
	//
	// Not set when running under the race detector.
	SleepMin int
	// SleepMax is the wait time in minutes, if applicable.
	//
	// Not set when running under the race detector.
	SleepMax int
	// Stack is the call stack.
	Stack Stack
	// Locked is set if the goroutine was locked to an OS thread.
	//
	// Not set when running under the race detector.
	Locked bool

	// Disallow initialization with unnamed parameters.
	_ struct{}
}

// equal returns true only if both signatures are exactly equal.
func (s *Signature) equal(r *Signature) bool {
	if s.State != r.State || !s.CreatedBy.equal(&r.CreatedBy) || s.Locked != r.Locked || s.SleepMin != r.SleepMin || s.SleepMax != r.SleepMax {
		return false
	}
	return s.Stack.equal(&r.Stack)
}

// similar returns true if the two Signature are equal or almost but not quite
// equal.
func (s *Signature) similar(r *Signature, similar Similarity) bool {
	if s.State != r.State || !s.CreatedBy.similar(&r.CreatedBy, similar) {
		return false
	}
	if similar == ExactFlags && s.Locked != r.Locked {
		return false
	}
	return s.Stack.similar(&r.Stack, similar)
}

// merge merges two similar Signature, zapping out differences.
func (s *Signature) merge(r *Signature) *Signature {
	min := s.SleepMin
	if r.SleepMin < min {
		min = r.SleepMin
	}
	max := s.SleepMax
	if r.SleepMax > max {
		max = r.SleepMax
	}
	return &Signature{
		State:     s.State,     // Drop right side.
		CreatedBy: s.CreatedBy, // Drop right side.
		SleepMin:  min,
		SleepMax:  max,
		Stack:     *s.Stack.merge(&r.Stack),
		Locked:    s.Locked || r.Locked, // TODO(maruel): This is weirdo.
	}
}

// less compares two Signature, where the ones that are less are more
// important, so they come up front. A Signature with more private functions is
// 'less' so it is at the top. Inversely, a Signature with only public
// functions is 'more' so it is at the bottom.
func (s *Signature) less(r *Signature) bool {
	if s.Stack.less(&r.Stack) {
		return true
	}
	if r.Stack.less(&s.Stack) {
		return false
	}
	if s.Locked && !r.Locked {
		return true
	}
	if r.Locked && !s.Locked {
		return false
	}
	if s.State < r.State {
		return true
	}
	if s.State > r.State {
		return false
	}
	return false
}

// SleepString returns a string "N-M minutes" if the goroutine(s) slept for a
// long time.
//
// Returns an empty string otherwise.
func (s *Signature) SleepString() string {
	if s.SleepMax == 0 {
		return ""
	}
	if s.SleepMin != s.SleepMax {
		return fmt.Sprintf("%d~%d minutes", s.SleepMin, s.SleepMax)
	}
	return fmt.Sprintf("%d minutes", s.SleepMax)
}

// updateLocations calls updateLocations on both CreatedBy and Stack and
// returns true if they were both resolved.
func (s *Signature) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool {
	r := s.CreatedBy.updateLocations(goroot, localgoroot, localgomods, gopaths)
	r = s.Stack.updateLocations(goroot, localgoroot, localgomods, gopaths) && r
	return r
}

// Goroutine represents the state of one goroutine, including the stack trace.
type Goroutine struct {
	// Signature is the stack trace, internal bits, state, which call site
	// created it, etc.
	Signature
	// ID is the goroutine id.
	ID int
	// First is the goroutine first printed, normally the one that crashed.
	First bool

	// RaceWrite is true if a race condition was detected, and this goroutine was
	// race on a write operation, otherwise it was a read.
	RaceWrite bool
	// RaceAddr is set to the address when a data race condition was detected.
	// Otherwise it is 0.
	RaceAddr uint64

	// Disallow initialization with unnamed parameters.
	_ struct{}
}

// Private stuff.

// nameArguments is a post-processing step where Args are 'named' with numbers.
func nameArguments(goroutines []*Goroutine) {
	// Set a name for any pointer occurring more than once.
	type object struct {
		args      []*Arg
		inPrimary bool
	}
	objects := map[uint64]object{}
	// Enumerate all the arguments.
	primary := true
	visit := func(arg *Arg) {
		if arg.IsPtr {
			objects[arg.Value] = object{
				args:      append(objects[arg.Value].args, arg),
				inPrimary: objects[arg.Value].inPrimary || primary,
			}
		}
	}
	for i, g := range goroutines {
		primary = i == 0
		for _, c := range g.Stack.Calls {
			c.Args.walk(visit)
		}
		// CreatedBy.Args is never set.
	}
	order := make(uint64Slice, 0, len(objects)/2)
	for k, obj := range objects {
		if len(obj.args) > 1 && obj.inPrimary {
			order = append(order, k)
		}
	}
	sort.Sort(order)
	nextID := 1
	for _, k := range order {
		for _, arg := range objects[k].args {
			arg.Name = fmt.Sprintf("#%d", nextID)
		}
		nextID++
	}

	// Now do the rest. This is done so the output is deterministic.
	order = make(uint64Slice, 0, len(objects))
	for k := range objects {
		order = append(order, k)
	}
	sort.Sort(order)
	for _, k := range order {
		// Process the remaining pointers, they were not referenced by primary
		// thread so will have higher IDs.
		if objects[k].inPrimary {
			continue
		}
		for _, arg := range objects[k].args {
			arg.Name = fmt.Sprintf("#%d", nextID)
		}
		nextID++
	}
}

func pathJoin(s ...string) string {
	return strings.Join(s, "/")
}

type uint64Slice []uint64

func (a uint64Slice) Len() int           { return len(a) }
func (a uint64Slice) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] }
