package main

import (
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"sort"
	"text/template"

	"code.justin.tv/rhys/nursery/internal/_vendor/trace"
)

// Tell all of the unique call stacks that appear in the trace
//
// {{range $stack := $.Stacks}}{{printf "Stack %d\n" .ID}}{{range $frame := .Frames}}{{printf "  %#x %-40s %s:%d\n" .PC .Fn .File .Line}}{{end}}{{end}}

// Tell me the goroutine IDs that exist in the trace.
//
// {{range $g := $.Goroutines}}{{printf "Goroutine %d\n" .ID}}{{end}}

// For a particular goroutine, show me when it started and stopped running, including stacks, event names, and timestamps
//
// {{range $g := $.Goroutines}}{{printf "Goroutine %d\n" .ID}}{{range $ev := .Events}}{{printf "  % 10d %s %s\n" .Ts .P .Type}}{{range $frame := .Stack.Frames}}{{printf "  %20s%s\n" "" .Fn}}{{end}}{{end}}{{end}}

// {{range $g := $.Goroutines}}{{printf "Goroutine %d\n" .ID}}{{range $ev := .Events}}{{printf "  % 10d %s %s" .Ts .P .Type}}{{range $i, $arg := .Type.Desc.Args}}{{printf " %s=%d" $arg (index $ev.Args $i)}}{{end}}{{range $i, $arg := .Type.Desc.SArgs}}{{printf " %s=%q" $arg (index $ev.SArgs $i)}}{{end}}{{if $ev.Link}}{{printf " link=%d" $ev.Link.Ts}}{{end}}{{printf "\n"}}{{range $frame := .Stack.Frames}}{{printf "  %20s%s\n" "" .Fn}}{{end}}{{end}}{{end}}

func main() {
	tmpl := flag.String("f", "", "Text template to use when formatting output")
	tmplFile := flag.String("run", "", "Filename containing template to run")
	flag.Parse()
	if len(flag.Args()) != 1 {
		log.Fatalf("must list execution trace file")
	}

	if *tmpl == "" && *tmplFile != "" {
		b, err := os.ReadFile(*tmplFile)
		if err != nil {
			log.Fatalf("Could not read template file %q: %v", *tmplFile, err)
		}
		*tmpl = string(b)
	}

	t := template.New("")

	t, err := t.Parse(*tmpl)
	if err != nil {
		log.Fatalf("Could not parse template: %v", err)
	}

	filename := flag.Arg(0)
	file, err := os.Open(filename)
	if err != nil {
		log.Fatalf("Could not open execution trace file %q: %v", filename, err)
	}
	defer file.Close()

	result, err := trace.Parse(file, "")
	if err != nil {
		log.Fatalf("Could not parse execution trace: %v", err)
	}

	data, err := convert(result)

	// When we execute the template, write (almost) directly to stdout. When
	// there's a problem with the template, that lets the user see partial
	// results and have a better chance of debugging their template.
	//
	// We also want to end with a newline so we don't offset the next terminal
	// prompt. Track if the final write of the template is a newline. Logging an
	// error will end in a newline, and printing zero bytes is also fine.
	// Otherwise, print out a newline if the result didn't end with one.
	end := lastByteWriter('\n')
	err = t.Execute(io.MultiWriter(os.Stdout, &end), data)
	if err != nil {
		log.Fatalf("Could not execute template: %v", err)
	}
	if byte(end) != '\n' {
		fmt.Printf("\n")
	}
}

type lastByteWriter byte

func (w *lastByteWriter) Write(p []byte) (int, error) {
	if len(p) > 0 {
		*(*byte)(w) = p[len(p)-1]
	}
	return len(p), nil
}

type templateInput struct {
	Stacks     []stack
	Events     eventList
	Goroutines []*goroutine
}

type stack struct {
	ID     uint64
	Frames []*frame
}

type frame struct {
	PC   uint64
	Fn   string
	File string
	Line int
}

type goroutine struct {
	ID     uint64
	Events eventList
}

type event struct {
	Off   int
	Type  eventType
	Ts    int64
	P     proc
	G     uint64
	Stack stack
	Args  []uint64
	SArgs []string
	Link  *event
}

type eventList []*event

func (l eventList) FindRegions(name string, start, end matcher) regionList {
	// .Events.FindRegions "internal/mutex" (match "GoBlockSync" "sync...Mutex..Lock") (match "GoStart")
	panic("oops")
}

type regionList []*region

type region struct {
	Name  string
	Start *event
	End   *event
}

type matcher struct {
}

type eventDesc struct {
	Name  string
	Args  []string
	SArgs []string
}

type eventType uint8

func (et eventType) String() string {
	return trace.EventDescriptions[uint8(et)].Name
}

func (et eventType) Desc() eventDesc {
	desc := trace.EventDescriptions[uint8(et)]
	ed := eventDesc{
		Name:  desc.Name,
		Args:  desc.Args,
		SArgs: desc.SArgs,
	}
	return ed
}

type proc int

func (p proc) String() string {
	switch p := int(p); p {
	default:
		return fmt.Sprintf("P%d", p)
	case trace.FakeP:
		return "FakeP"
	case trace.TimerP:
		return "TimerP"
	case trace.NetpollP:
		return "NetpollP"
	case trace.SyscallP:
		return "SyscallP"
	case trace.GCP:
		return "GCP"
	}
}

func convert(in trace.ParseResult) (*templateInput, error) {
	stacks := make(map[uint64]stack)
	for i, s := range in.Stacks {
		var stk stack
		stk.ID = i
		for _, f := range s {
			stk.Frames = append(stk.Frames, &frame{
				PC:   f.PC,
				Fn:   f.Fn,
				File: f.File,
				Line: f.Line,
			})
		}
		stacks[stk.ID] = stk
	}

	eventMap := make(map[*trace.Event]*event)
	for _, ev := range in.Events {
		eventMap[ev] = &event{
			Off:   ev.Off,
			Type:  eventType(ev.Type),
			Ts:    ev.Ts,
			P:     proc(ev.P),
			G:     ev.G,
			Stack: stacks[ev.StkID],
			Args:  ev.Args[0:len(trace.EventDescriptions[ev.Type].Args)],
			SArgs: ev.SArgs,
		}
	}

	for _, ev := range in.Events {
		link, ok := eventMap[ev.Link]
		if ev.Link != nil && !ok {
			panic("missing")
		}
		eventMap[ev].Link = link
	}

	var data templateInput
	for _, ev := range eventMap {
		data.Events = append(data.Events, ev)
	}
	sort.Slice(data.Events, func(i, j int) bool { return data.Events[i].Ts < data.Events[j].Ts })

	for _, s := range stacks {
		data.Stacks = append(data.Stacks, s)
	}
	sort.Slice(data.Stacks, func(i, j int) bool { return data.Stacks[i].ID < data.Stacks[j].ID })

	gs := make(map[uint64][]*event)
	for _, ev := range data.Events {
		gs[ev.G] = append(gs[ev.G], ev)
	}
	for id, evs := range gs {
		g := &goroutine{
			ID:     id,
			Events: evs,
		}
		data.Goroutines = append(data.Goroutines, g)
	}
	sort.Slice(data.Goroutines, func(i, j int) bool { return data.Goroutines[i].ID < data.Goroutines[j].ID })

	return &data, nil
}
