// Copyright 2018 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.

// Output is currently different on 32 bits on go1.17 so skip the examples for
// now. It's a gross hack.

//go:build go1.18 || !386

package stack_test

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime/debug"

	"github.com/maruel/panicparse/v2/stack"
)

// Runs a crashing program and converts it to a dense text format like pp does.
func Example_text() {
	source := `package main

	import "time"

	func main() {
		c := crashy{}
		go func() {
			c.die(42.)
		}()
		select {}
	}

	type crashy struct{}

	func (c crashy) die(f float32) {
		time.Sleep(time.Millisecond)
		panic(int(f))
	}`

	// Skipped error handling to make the example shorter.
	root, _ := ioutil.TempDir("", "stack")
	defer os.RemoveAll(root)
	p := filepath.Join(root, "main.go")
	ioutil.WriteFile(p, []byte(source), 0600)
	// Disable both optimization (-N) and inlining (-l).
	c := exec.Command("go", "run", "-gcflags", "-N -l", p)
	// This is important, otherwise only the panicking goroutine will be printed.
	c.Env = append(os.Environ(), "GOTRACEBACK=1")
	raw, _ := c.CombinedOutput()
	stream := bytes.NewReader(raw)

	s, suffix, err := stack.ScanSnapshot(stream, os.Stdout, stack.DefaultOpts())
	if err != nil && err != io.EOF {
		log.Fatal(err)
	}

	// Find out similar goroutine traces and group them into buckets.
	buckets := s.Aggregate(stack.AnyValue).Buckets

	// Calculate alignment.
	srcLen := 0
	pkgLen := 0
	for _, bucket := range buckets {
		for _, line := range bucket.Signature.Stack.Calls {
			if l := len(fmt.Sprintf("%s:%d", line.SrcName, line.Line)); l > srcLen {
				srcLen = l
			}
			if l := len(filepath.Base(line.Func.ImportPath)); l > pkgLen {
				pkgLen = l
			}
		}
	}

	for _, bucket := range buckets {
		// Print the goroutine header.
		extra := ""
		if s := bucket.SleepString(); s != "" {
			extra += " [" + s + "]"
		}
		if bucket.Locked {
			extra += " [locked]"
		}

		if len(bucket.CreatedBy.Calls) != 0 {
			extra += fmt.Sprintf(" [Created by %s.%s @ %s:%d]", bucket.CreatedBy.Calls[0].Func.DirName, bucket.CreatedBy.Calls[0].Func.Name, bucket.CreatedBy.Calls[0].SrcName, bucket.CreatedBy.Calls[0].Line)
		}
		fmt.Printf("%d: %s%s\n", len(bucket.IDs), bucket.State, extra)

		// Print the stack lines.
		for _, line := range bucket.Stack.Calls {
			fmt.Printf(
				"    %-*s %-*s %s(%s)\n",
				pkgLen, line.Func.DirName, srcLen,
				fmt.Sprintf("%s:%d", line.SrcName, line.Line),
				line.Func.Name, &line.Args)
		}
		if bucket.Stack.Elided {
			io.WriteString(os.Stdout, "    (...)\n")
		}
	}

	// If there was any remaining data in the pipe, dump it now.
	if len(suffix) != 0 {
		os.Stdout.Write(suffix)
	}
	if err == nil {
		io.Copy(os.Stdout, stream)
	}

	// Output:
	// panic: 42
	//
	// 1: running [Created by main.main @ main.go:7]
	//     main main.go:17 crashy.die(42)
	//     main main.go:8  main.func1()
	// 1: select (no cases)
	//     main main.go:10 main()
	// exit status 2
}

// Process multiple consecutive goroutine snapshots.
func Example_stream() {
	// Stream of stack traces:
	var r io.Reader
	var w io.Writer
	opts := stack.DefaultOpts()
	for {
		s, suffix, err := stack.ScanSnapshot(r, w, opts)
		if s != nil {
			// Process the snapshot...
		}

		if err != nil && err != io.EOF {
			if len(suffix) != 0 {
				w.Write(suffix)
			}
			log.Fatal(err)
		}
		// Prepend the suffix that was read to the rest of the input stream to
		// catch the next snapshot signature:
		r = io.MultiReader(bytes.NewReader(suffix), r)
	}
}

// Converts a stack trace from os.Stdin into HTML on os.Stdout, discarding
// everything else.
func Example_hTML() {
	s, _, err := stack.ScanSnapshot(os.Stdin, ioutil.Discard, stack.DefaultOpts())
	if err != nil && err != io.EOF {
		log.Fatal(err)
	}
	if s != nil {
		s.Aggregate(stack.AnyValue).ToHTML(os.Stdout, "")
	}
}

// A sample parseStack function expects a stdlib stacktrace from runtime.Stack or debug.Stack and returns
// the parsed stack object.
func Example_simple() {
	parseStack := func(rawStack []byte) stack.Stack {
		s, _, err := stack.ScanSnapshot(bytes.NewReader(rawStack), ioutil.Discard, stack.DefaultOpts())
		if err != nil && err != io.EOF {
			panic(err)
		}

		if len(s.Goroutines) > 1 {
			panic(errors.New("provided stacktrace had more than one goroutine"))
		}

		return s.Goroutines[0].Signature.Stack
	}

	parsedStack := parseStack(debug.Stack())
	fmt.Printf("parsedStack: %#v", parsedStack)
}
