// Package stack implements utilities to capture, manipulate, and format call
// stacks.
package stack

import (
	"fmt"
	"path/filepath"
	"runtime"
	"strings"
)

// Call records a single function invocation from a goroutine stack. It is a
// wrapper for the program counter values returned by runtime.Caller and
// runtime.Callers and consumed by runtime.FuncForPC.
type Call uintptr

// Format implements fmt.Formatter with support for the following verbs.
//
//    %s    source file
//    %d    line number
//    %n    function name
//    %v    equivalent to %s:%d
//
// It accepts the '+' and '#' flags for most of the verbs as follows.
//
//    %+s   path of source file relative to the compile time GOPATH
//    %#s   full path of source file
//    %+n   import path qualified function name
//    %+v   equivalent to %+s:%d
//    %#v   equivalent to %#s:%d
func (pc Call) Format(s fmt.State, c rune) {
	// BUG(ChrisHines): Subtracting one from pc is a work around for
	// https://code.google.com/p/go/issues/detail?id=7690. The idea for this
	// work around comes from rsc's initial patch at
	// https://codereview.appspot.com/84100043/#ps20001, but as noted in the
	// issue discussion, it is not a complete fix since it doesn't handle some
	// cases involving signals. Just the same, it handles all of the other
	// cases I have tested.
	pcFix := uintptr(pc) - 1
	fn := runtime.FuncForPC(pcFix)
	if fn == nil {
		fmt.Fprintf(s, "%%!%c(NOFUNC)", c)
		return
	}

	switch c {
	case 's', 'v':
		file, line := fn.FileLine(pcFix)
		switch {
		case s.Flag('#'):
			// done
		case s.Flag('+'):
			// Here we want to get the source file path relative to the
			// compile time GOPATH. As of Go 1.3.x there is no direct way to
			// know the compiled GOPATH at runtime, but we can infer the
			// number of path segments in the GOPATH. We note that fn.Name()
			// returns the function name qualified by the import path, which
			// does not include the GOPATH. Thus we can trim segments from the
			// beginning of the file path until the number of path separators
			// remaining is one more than the number of path separators in the
			// function name. For example, given:
			//
			//    GOPATH     /home/user
			//    file       /home/user/src/pkg/sub/file.go
			//    fn.Name()  pkg/sub.Type.Method
			//
			// We want to produce:
			//
			//    pkg/sub/file.go
			//
			// From this we can easily see that fn.Name() has one less path
			// separator than our desired output.
			const sep = "/"
			impCnt := strings.Count(fn.Name(), sep) + 1
			pathCnt := strings.Count(file, sep)
			for pathCnt > impCnt {
				i := strings.Index(file, sep)
				if i == -1 {
					break
				}
				file = file[i+len(sep):]
				pathCnt--
			}
		default:
			const sep = "/"
			if i := strings.LastIndex(file, sep); i != -1 {
				file = file[i+len(sep):]
			}
		}
		fmt.Fprint(s, file)
		if c == 'v' {
			fmt.Fprint(s, ":", line)
		}

	case 'd':
		_, line := fn.FileLine(pcFix)
		fmt.Fprint(s, line)

	case 'n':
		name := fn.Name()
		if !s.Flag('+') {
			const pathSep = "/"
			if i := strings.LastIndex(name, pathSep); i != -1 {
				name = name[i+len(pathSep):]
			}
			const pkgSep = "."
			if i := strings.Index(name, pkgSep); i != -1 {
				name = name[i+len(pkgSep):]
			}
		}
		fmt.Fprint(s, name)
	}
}

// Callers returns a Trace for the current goroutine with element 0
// identifying the calling function.
func Callers() Trace {
	pcs := poolBuf()
	pcs = pcs[:cap(pcs)]
	n := runtime.Callers(2, pcs)
	cs := make([]Call, n)
	for i, pc := range pcs[:n] {
		cs[i] = Call(pc)
	}
	putPoolBuf(pcs)
	return cs
}

// name returns the import path qualified name of the function containing the
// call.
func (pc Call) name() string {
	pcFix := uintptr(pc) - 1 // work around for go issue #7690
	fn := runtime.FuncForPC(pcFix)
	if fn == nil {
		return "???"
	}
	return fn.Name()
}

func (pc Call) file() string {
	pcFix := uintptr(pc) - 1 // work around for go issue #7690
	fn := runtime.FuncForPC(pcFix)
	if fn == nil {
		return "???"
	}
	file, _ := fn.FileLine(pcFix)
	return file
}

// Trace records a sequence of function invocations from a goroutine stack.
type Trace []Call

// Format implements fmt.Formatter by printing the Trace as square brackes ([,
// ]) surrounding a space separated list of Calls each formatted with the
// supplied verb and options.
func (pcs Trace) Format(s fmt.State, c rune) {
	s.Write([]byte("["))
	for i, pc := range pcs {
		if i > 0 {
			s.Write([]byte(" "))
		}
		pc.Format(s, c)
	}
	s.Write([]byte("]"))
}

// TrimBelow returns a slice of the Trace with all entries below pc removed.
func (pcs Trace) TrimBelow(pc Call) Trace {
	for len(pcs) > 0 && pcs[0] != pc {
		pcs = pcs[1:]
	}
	return pcs
}

// TrimAbove returns a slice of the Trace with all entries above pc removed.
func (pcs Trace) TrimAbove(pc Call) Trace {
	for len(pcs) > 0 && pcs[len(pcs)-1] != pc {
		pcs = pcs[:len(pcs)-1]
	}
	return pcs
}

// TrimBelowName returns a slice of the Trace with all entries below the
// lowest with function name name removed.
func (pcs Trace) TrimBelowName(name string) Trace {
	for len(pcs) > 0 && pcs[0].name() != name {
		pcs = pcs[1:]
	}
	return pcs
}

// TrimAboveName returns a slice of the Trace with all entries above the
// highest with function name name removed.
func (pcs Trace) TrimAboveName(name string) Trace {
	for len(pcs) > 0 && pcs[len(pcs)-1].name() != name {
		pcs = pcs[:len(pcs)-1]
	}
	return pcs
}

var goroot string

func init() {
	goroot = filepath.ToSlash(runtime.GOROOT())
	if runtime.GOOS == "windows" {
		goroot = strings.ToLower(goroot)
	}
}

func inGoroot(path string) bool {
	if runtime.GOOS == "windows" {
		path = strings.ToLower(path)
	}
	return strings.HasPrefix(path, goroot)
}

// TrimRuntime returns a slice of the Trace with the topmost entries from the
// go runtime removed. It considers any calls originating from files under
// GOROOT as part of the runtime.
func (pcs Trace) TrimRuntime() Trace {
	for len(pcs) > 0 && inGoroot(pcs[len(pcs)-1].file()) {
		pcs = pcs[:len(pcs)-1]
	}
	return pcs
}
