package goroutine

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"
)

// Goroutine represents information about a single goroutine, such as its unique
// ID, state, backtrace, creator, and more.
//
// Go's runtime assigns unique IDs to goroutines, also called "goid" in Go
// runtime parlance. These IDs start with 1 for the main goroutine and only
// increase (unless you manage to create 2**64-2 goroutines during the lifetime
// of your tests so that the goids wrap around). Due to runtime-internal
// optimizations, not all IDs might be used, so that there might be gaps. But
// IDs are never reused, so they're fine as unique goroutine identities.
//
// The size of a goidsis always 64bits, even on 32bit architectures (if you
// like, you might want to double-check for yourself in runtime/runtime2.go and
// runtime/proc.go).
//
// A Goroutine's State field starts with one of the following strings:
//   - "idle"
//   - "runnable"
//   - "running"
//   - "syscall"
//   - ("waiting" ... see below)
//   - ("dead" ... these goroutines should never appear in dumps)
//   - "copystack"
//   - "preempted"
//   - ("???" ... something IS severely broken.)
// In case a goroutine is in waiting state, the State field instead starts with
// one of the following strings, never showing a lonely "waiting" string, but
// rather one of the reasons for waiting:
//   - "chan receive"
//   - "chan send"
//   - "select"
//   - "sleep"
//   - "finalizer wait"
//   - ...quite some more waiting states.
//
// The State description may next contain "(scan)", separated by a single blank
// from the preceding goroutine state text.
//
// If a goroutine is blocked from more than at least a minute, then the state
// description next contains the string "X minutes", where X is the number of
// minutes blocked. This text is separated by a "," and a blank from the
// preceding information.
//
// Finally, OS thread-locked goroutines finally contain "locked to thread" in
// their State description, again separated by a "," and a blank from the
// preceding information.
//
// Please note that the State field never contains the opening and closing
// square brackets as used in plain stack dumps.
type Goroutine struct {
	ID              uint64 // unique goroutine ID ("goid" in Go's runtime parlance)
	State           string // goroutine state, such as "running"
	TopFunction     string // topmost function on goroutine's stack
	CreatorFunction string // name of function creating this goroutine, if any
	BornAt          string // location where the goroutine was started from, if any; format "file-path:line-number"
	Backtrace       string // goroutine's backtrace (of the stack)
}

// String returns a short textual description of this goroutine, but without the
// potentially lengthy and ugly backtrace details.
func (g Goroutine) String() string {
	s := fmt.Sprintf("Goroutine ID: %d, state: %s, top function: %s",
		g.ID, g.State, g.TopFunction)
	if g.CreatorFunction == "" {
		return s
	}
	s += fmt.Sprintf(", created by: %s, at: %s",
		g.CreatorFunction, g.BornAt)
	return s
}

// GomegaString returns the Gomega struct representation of a Goroutine, but
// without a potentially rather lengthy backtrace. This Gomega object value
// dumps getting happily truncated as to become more or less useless.
func (g Goroutine) GomegaString() string {
	return fmt.Sprintf(
		"{ID: %d, State: %q, TopFunction: %q, CreatorFunction: %q, BornAt: %q}",
		g.ID, g.State, g.TopFunction, g.CreatorFunction, g.BornAt)
}

// Goroutines returns information about all goroutines.
func Goroutines() []Goroutine {
	return goroutines(true)
}

// Current returns information about the current goroutine in which it is
// called. Please note that the topmost function name will always be
// runtime.Stack.
func Current() Goroutine {
	return goroutines(false)[0]
}

// goroutines is an internal wrapper around dumping either only the stack of the
// current goroutine of the caller or dumping the stacks of all goroutines, and
// then parsing the dump into separate Goroutine descriptions.
func goroutines(all bool) []Goroutine {
	return parseStack(stacks(all))
}

// parseStack parses the stack dump of one or multiple goroutines, as returned
// by runtime.Stack() and then returns a list of Goroutine descriptions based on
// the dump.
func parseStack(stacks []byte) []Goroutine {
	gs := []Goroutine{}
	r := bufio.NewReader(bytes.NewReader(stacks))
	for {
		// We expect a line describing a new "goroutine", everything else is a
		// failure. And yes, if we get an EOF already with this line, bail out.
		line, err := r.ReadString('\n')
		if err == io.EOF {
			break
		}
		g := new(line)
		// Read the rest ... that is, the backtrace for this goroutine.
		g.TopFunction, g.Backtrace = parseGoroutineBacktrace(r)
		if strings.HasSuffix(g.Backtrace, "\n\n") {
			g.Backtrace = g.Backtrace[:len(g.Backtrace)-1]
		}
		g.CreatorFunction, g.BornAt = findCreator(g.Backtrace)
		gs = append(gs, g)
	}
	return gs
}

// new takes a goroutine line from a stack dump and returns a Goroutine object
// based on the information contained in the dump.
func new(s string) Goroutine {
	s = strings.TrimSuffix(s, ":\n")
	fields := strings.SplitN(s, " ", 3)
	if len(fields) != 3 {
		panic(fmt.Sprintf("invalid stack header: %q", s))
	}
	id, err := strconv.ParseUint(fields[1], 10, 64)
	if err != nil {
		panic(fmt.Sprintf("invalid stack header ID: %q, header: %q", fields[1], s))
	}
	state := strings.TrimSuffix(strings.TrimPrefix(fields[2], "["), "]")
	return Goroutine{ID: id, State: state}
}

// Beginning of line indicating the creator of a Goroutine, if any. This
// indication is missing for the main goroutine as it appeared in a big bang or
// something similar.
const backtraceGoroutineCreator = "created by "

// findCreator solves the great mystery of Gokind, answering the question of who
// created this goroutine? Given a backtrace, that is.
func findCreator(backtrace string) (creator, location string) {
	pos := strings.LastIndex(backtrace, backtraceGoroutineCreator)
	if pos < 0 {
		return
	}
	// Split the "created by ..." line from the following line giving us the
	// (indented) file name:line number and the hex offset of the call location
	// within the function.
	details := strings.SplitN(backtrace[pos+len(backtraceGoroutineCreator):], "\n", 3)
	if len(details) < 2 {
		return
	}
	// Split off the call location hex offset which is of no use to us, and only
	// keep the file path and line number information. This will be useful for
	// diagnosis, when dumping leaked goroutines.
	offsetpos := strings.LastIndex(details[1], " +0x")
	if offsetpos < 0 {
		return
	}
	location = strings.TrimSpace(details[1][:offsetpos])
	creator = details[0]
	return
}

// Beginning of header line introducing a (new) goroutine in a backtrace.
const backtraceGoroutineHeader = "goroutine "

// Length of the header line prefix introducing a (new) goroutine in a
// backtrace.
const backtraceGoroutineHeaderLen = len(backtraceGoroutineHeader)

// parseGoroutineBacktrace reads from reader r the backtrace information until
// the end or until the next goroutine header is seen. This next goroutine
// header is NOT consumed so that callers can still read the next header from
// the reader.
func parseGoroutineBacktrace(r *bufio.Reader) (topFn string, backtrace string) {
	bt := bytes.Buffer{}
	// Read backtrace information belonging to this goroutine until we meet
	// another goroutine header.
	for {
		header, err := r.Peek(backtraceGoroutineHeaderLen)
		if string(header) == backtraceGoroutineHeader {
			// next goroutine header is up for read, so we're done with parsing
			// the backtrace of this goroutine.
			break
		}
		if err != nil && err != io.EOF {
			// There is some serious problem with the stack dump, so we
			// decidedly panic now.
			panic("parsing backtrace failed: " + err.Error())
		}
		line, err := r.ReadString('\n')
		if err != nil && err != io.EOF {
			// There is some serious problem with the stack dump, so we
			// decidedly panic now.
			panic("parsing backtrace failed: " + err.Error())
		}
		// The first line after a goroutine header lists the "topmost" function.
		if topFn == "" {
			line := /*sic!*/ strings.TrimSpace(line)
			idx := strings.LastIndex(line, "(")
			if idx <= 0 {
				panic(fmt.Sprintf("invalid function call stack entry: %q", line))
			}
			topFn = line[:idx]
		}
		// Always append the line read to the goroutine's backtrace.
		bt.WriteString(line)
		if err == io.EOF {
			// we're reached the end of the stack dump, so that's it.
			break
		}
	}
	return topFn, bt.String()
}
