// Package debugging contains useful methods for debugging the app, safely
// isolated from the rest of the app's packages.
package debugging

import (
	"fmt"
	"runtime"
	"strings"
)

// Configurable variables for the stack tracer functions.
var (
	// StackDepth is the depth that Callers() will crawl up the call stack. This
	// variable is configurable.
	StackDepth = 20

	// StopAt is the function name to stop the tracebacks at. Set to a blank
	// string to not stop and trace all the way up to `runtime.goexit` or
	// wherever.
	StopAt = "main.main"
)

// Minimum depth given to runtime.Caller() so that the call stacks will exclude
// the call to debugging.Caller() itself -- so this debug module won't debug its
// own function calls in the tracebacks.
const minDepth = 2

// Caller returns the filename and line number that called the calling
// function.
func Caller() string {
	if pc, file, no, ok := runtime.Caller(minDepth); ok {
		frames := runtime.CallersFrames([]uintptr{pc})
		frame, _ := frames.Next()
		if frame.Function != "" {
			return fmt.Sprintf("%s#%d: %s()",
				frame.File,
				frame.Line,
				frame.Function,
			)
		}
		return fmt.Sprintf("%s#%d",
			file,
			no,
		)
	}
	return "[no caller information]"
}

// Callers returns an array of all the callers of the current function.
func Callers() []string {
	var (
		callers []string
		pc      = make([]uintptr, StackDepth)
		count   = runtime.Callers(minDepth, pc)
	)
	pc = pc[:count] // only pass valid program counters to CallersFrames
	var frames = runtime.CallersFrames(pc)
	_ = frames

	// Loop to get frames of the call stack.
	for {
		frame, more := frames.Next()

		callers = append(callers, fmt.Sprintf("%s#%d: %s()",
			frame.File,
			frame.Line,
			frame.Function,
		))

		if StopAt != "" && frame.Function == StopAt {
			break
		}

		if !more {
			break
		}
	}

	return callers
}

// StringifyCallers pretty-prints the Callers as a single string with newlines.
func StringifyCallers() string {
	callers := Callers()
	var result []string
	for i, caller := range callers {
		if i == 0 {
			continue // StringifyCallers() would be the first row, skip it.
		}
		result = append(result, fmt.Sprintf("%d: %s", i, caller))
	}
	return strings.Join(result, "\n")
}

// PrintCallers prints the stringified callers directly to STDOUT.
func PrintCallers() {
	fmt.Println("Call stack (most recent/current function first):")
	for i, caller := range Callers() {
		if i == 0 {
			continue // PrintCallers() would be the first row, skip it.
		}
		fmt.Printf("%d: %s\n", i, caller)
	}
}

// Pause until the user hits enter in the console.
func Pause() {
	var x string
	fmt.Print("Press enter to continue . . .")
	fmt.Scanf("%s", &x)
}
