package pprof

import (
	"archive/tar"
	"bytes"
	"context"
	"fmt"
	"io"
	"path"
	"runtime"
	"runtime/pprof"
	"runtime/trace"
	"time"
)

func SetGlobalProfiling(enabled bool) {
	if enabled {
		// Copy the rates used in 1.x.
		runtime.MemProfileRate = 4096
		runtime.SetBlockProfileRate(int(1 * time.Second))
		runtime.SetMutexProfileFraction(1)
	} else {
		runtime.MemProfileRate = 0
		runtime.SetBlockProfileRate(0)
		runtime.SetMutexProfileFraction(0)
	}
}

// collectAllProfiles generates a tarball containing:
//   - goroutine profile
//   - blocking profile
//   - mutex profile
//   - heap profile
//   - allocations profile
//   - (optionally) trace profile
//   - (optionally) CPU profile
//
// All information is added to a tar archive and then compressed, before being
// returned to the requester as an archive file. Where profiles support debug
// parameters, the profile is collected with debug=1.
func collectAllProfiles(ctx context.Context, traceDuration time.Duration, cpuDuration time.Duration) (io.Reader, error) {
	// prof describes a profile name and a debug value, or in the case of a CPU
	// profile, the number of seconds to collect the profile for.
	type prof struct {
		Name     string        // name of profile
		Duration time.Duration // duration of profile if applicable. currently only used by cpu and trace
	}

	var profiles = []prof{
		{Name: "goroutine"},
		{Name: "block"},
		{Name: "mutex"},
		{Name: "heap"},
		{Name: "allocs"},
		{Name: "threadcreate"},
	}
	if traceDuration > 0 {
		profiles = append(profiles, prof{"trace", traceDuration})
	}
	if cpuDuration > 0 {
		// We want to gather CPU profiles first, if enabled.
		profiles = append([]prof{{"cpu", cpuDuration}}, profiles...)
	}

	tarball := &bytes.Buffer{}
	buf := &bytes.Buffer{} // Temporary buffer for each profile/query result.

	tw := tar.NewWriter(tarball)
	// Collect and write out profiles.
	for _, profile := range profiles {
		switch profile.Name {
		case "cpu":
			if err := pprof.StartCPUProfile(buf); err != nil {
				return nil, err
			}
			sleep(ctx, profile.Duration)
			pprof.StopCPUProfile()

		case "trace":
			if err := trace.Start(buf); err != nil {
				return nil, err
			}
			sleep(ctx, profile.Duration)
			trace.Stop()

		default:
			prof := pprof.Lookup(profile.Name)
			if prof == nil {
				return nil, fmt.Errorf("unable to find profile %q", profile.Name)
			}

			if err := prof.WriteTo(buf, 0); err != nil {
				return nil, err
			}
		}

		// Write the profile file's header.
		if err := tw.WriteHeader(&tar.Header{
			Name: path.Join("profiles", profile.Name+".pb.gz"),
			Mode: 0600,
			Size: int64(buf.Len()),
		}); err != nil {
			return nil, err
		}

		// Write the profile file's data.
		if _, err := tw.Write(buf.Bytes()); err != nil {
			return nil, err
		}

		// Reset the buffer for the next profile.
		buf.Reset()
	}

	// Close the tar writer.
	if err := tw.Close(); err != nil {
		return nil, err
	}

	return tarball, nil
}

// Adapted from net/http/pprof/pprof.go
func sleep(ctx context.Context, d time.Duration) {
	select {
	case <-time.After(d):
	case <-ctx.Done():
	}
}
