// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"cmp"
	"log"
	"math"
	"net/http"
	"slices"
	"strconv"
	"time"

	"internal/trace"
	"internal/trace/traceviewer"
)

func JSONTraceHandler(parsed *parsedTrace) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		opts := defaultGenOpts()

		switch r.FormValue("view") {
		case "thread":
			opts.mode = traceviewer.ModeThreadOriented
		}
		if goids := r.FormValue("goid"); goids != "" {
			// Render trace focused on a particular goroutine.

			id, err := strconv.ParseUint(goids, 10, 64)
			if err != nil {
				log.Printf("failed to parse goid parameter %q: %v", goids, err)
				return
			}
			goid := trace.GoID(id)
			g, ok := parsed.summary.Goroutines[goid]
			if !ok {
				log.Printf("failed to find goroutine %d", goid)
				return
			}
			opts.mode = traceviewer.ModeGoroutineOriented
			if g.StartTime != 0 {
				opts.startTime = g.StartTime.Sub(parsed.startTime())
			} else {
				opts.startTime = 0
			}
			if g.EndTime != 0 {
				opts.endTime = g.EndTime.Sub(parsed.startTime())
			} else { // The goroutine didn't end.
				opts.endTime = parsed.endTime().Sub(parsed.startTime())
			}
			opts.focusGoroutine = goid
			opts.goroutines = trace.RelatedGoroutinesV2(parsed.events, goid)
		} else if taskids := r.FormValue("focustask"); taskids != "" {
			taskid, err := strconv.ParseUint(taskids, 10, 64)
			if err != nil {
				log.Printf("failed to parse focustask parameter %q: %v", taskids, err)
				return
			}
			task, ok := parsed.summary.Tasks[trace.TaskID(taskid)]
			if !ok || (task.Start == nil && task.End == nil) {
				log.Printf("failed to find task with id %d", taskid)
				return
			}
			opts.setTask(parsed, task)
		} else if taskids := r.FormValue("taskid"); taskids != "" {
			taskid, err := strconv.ParseUint(taskids, 10, 64)
			if err != nil {
				log.Printf("failed to parse taskid parameter %q: %v", taskids, err)
				return
			}
			task, ok := parsed.summary.Tasks[trace.TaskID(taskid)]
			if !ok {
				log.Printf("failed to find task with id %d", taskid)
				return
			}
			// This mode is goroutine-oriented.
			opts.mode = traceviewer.ModeGoroutineOriented
			opts.setTask(parsed, task)

			// Pick the goroutine to orient ourselves around by just
			// trying to pick the earliest event in the task that makes
			// any sense. Though, we always want the start if that's there.
			var firstEv *trace.Event
			if task.Start != nil {
				firstEv = task.Start
			} else {
				for _, logEv := range task.Logs {
					if firstEv == nil || logEv.Time() < firstEv.Time() {
						firstEv = logEv
					}
				}
				if task.End != nil && (firstEv == nil || task.End.Time() < firstEv.Time()) {
					firstEv = task.End
				}
			}
			if firstEv == nil || firstEv.Goroutine() == trace.NoGoroutine {
				log.Printf("failed to find task with id %d", taskid)
				return
			}

			// Set the goroutine filtering options.
			goid := firstEv.Goroutine()
			opts.focusGoroutine = goid
			goroutines := make(map[trace.GoID]struct{})
			for _, task := range opts.tasks {
				// Find only directly involved goroutines.
				for id := range task.Goroutines {
					goroutines[id] = struct{}{}
				}
			}
			opts.goroutines = goroutines
		}

		// Parse start and end options. Both or none must be present.
		start := int64(0)
		end := int64(math.MaxInt64)
		if startStr, endStr := r.FormValue("start"), r.FormValue("end"); startStr != "" && endStr != "" {
			var err error
			start, err = strconv.ParseInt(startStr, 10, 64)
			if err != nil {
				log.Printf("failed to parse start parameter %q: %v", startStr, err)
				return
			}

			end, err = strconv.ParseInt(endStr, 10, 64)
			if err != nil {
				log.Printf("failed to parse end parameter %q: %v", endStr, err)
				return
			}
		}

		c := traceviewer.ViewerDataTraceConsumer(w, start, end)
		if err := generateTrace(parsed, opts, c); err != nil {
			log.Printf("failed to generate trace: %v", err)
		}
	})
}

// traceContext is a wrapper around a traceviewer.Emitter with some additional
// information that's useful to most parts of trace viewer JSON emission.
type traceContext struct {
	*traceviewer.Emitter
	startTime trace.Time
	endTime   trace.Time
}

// elapsed returns the elapsed time between the trace time and the start time
// of the trace.
func (ctx *traceContext) elapsed(now trace.Time) time.Duration {
	return now.Sub(ctx.startTime)
}

type genOpts struct {
	mode      traceviewer.Mode
	startTime time.Duration
	endTime   time.Duration

	// Used if mode != 0.
	focusGoroutine trace.GoID
	goroutines     map[trace.GoID]struct{} // Goroutines to be displayed for goroutine-oriented or task-oriented view. goroutines[0] is the main goroutine.
	tasks          []*trace.UserTaskSummary
}

// setTask sets a task to focus on.
func (opts *genOpts) setTask(parsed *parsedTrace, task *trace.UserTaskSummary) {
	opts.mode |= traceviewer.ModeTaskOriented
	if task.Start != nil {
		opts.startTime = task.Start.Time().Sub(parsed.startTime())
	} else { // The task started before the trace did.
		opts.startTime = 0
	}
	if task.End != nil {
		opts.endTime = task.End.Time().Sub(parsed.startTime())
	} else { // The task didn't end.
		opts.endTime = parsed.endTime().Sub(parsed.startTime())
	}
	opts.tasks = task.Descendents()
	slices.SortStableFunc(opts.tasks, func(a, b *trace.UserTaskSummary) int {
		aStart, bStart := parsed.startTime(), parsed.startTime()
		if a.Start != nil {
			aStart = a.Start.Time()
		}
		if b.Start != nil {
			bStart = b.Start.Time()
		}
		if a.Start != b.Start {
			return cmp.Compare(aStart, bStart)
		}
		// Break ties with the end time.
		aEnd, bEnd := parsed.endTime(), parsed.endTime()
		if a.End != nil {
			aEnd = a.End.Time()
		}
		if b.End != nil {
			bEnd = b.End.Time()
		}
		return cmp.Compare(aEnd, bEnd)
	})
}

func defaultGenOpts() *genOpts {
	return &genOpts{
		startTime: time.Duration(0),
		endTime:   time.Duration(math.MaxInt64),
	}
}

func generateTrace(parsed *parsedTrace, opts *genOpts, c traceviewer.TraceConsumer) error {
	ctx := &traceContext{
		Emitter:   traceviewer.NewEmitter(c, opts.startTime, opts.endTime),
		startTime: parsed.events[0].Time(),
		endTime:   parsed.events[len(parsed.events)-1].Time(),
	}
	defer ctx.Flush()

	var g generator
	if opts.mode&traceviewer.ModeGoroutineOriented != 0 {
		g = newGoroutineGenerator(ctx, opts.focusGoroutine, opts.goroutines)
	} else if opts.mode&traceviewer.ModeThreadOriented != 0 {
		g = newThreadGenerator()
	} else {
		g = newProcGenerator()
	}
	runGenerator(ctx, g, parsed, opts)
	return nil
}
