// Copyright 2014 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 driver

import (
	"fmt"
	"io"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"cmd/pprof/internal/commands"
	"cmd/pprof/internal/plugin"
	"cmd/pprof/internal/profile"
)

var profileFunctionNames = []string{}

// functionCompleter replaces provided substring with a function
// name retrieved from a profile if a single match exists. Otherwise,
// it returns unchanged substring. It defaults to no-op if the profile
// is not specified.
func functionCompleter(substring string) string {
	found := ""
	for _, fName := range profileFunctionNames {
		if strings.Contains(fName, substring) {
			if found != "" {
				return substring
			}
			found = fName
		}
	}
	if found != "" {
		return found
	}
	return substring
}

// updateAutoComplete enhances autocompletion with information that can be
// retrieved from the profile
func updateAutoComplete(p *profile.Profile) {
	profileFunctionNames = nil // remove function names retrieved previously
	for _, fn := range p.Function {
		profileFunctionNames = append(profileFunctionNames, fn.Name)
	}
}

// splitCommand splits the command line input into tokens separated by
// spaces. Takes care to separate commands of the form 'top10' into
// two tokens: 'top' and '10'
func splitCommand(input string) []string {
	fields := strings.Fields(input)
	if num := strings.IndexAny(fields[0], "0123456789"); num != -1 {
		inputNumber := fields[0][num:]
		fields[0] = fields[0][:num]
		fields = append([]string{fields[0], inputNumber}, fields[1:]...)
	}
	return fields
}

// interactive displays a prompt and reads commands for profile
// manipulation/visualization.
func interactive(p *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error {
	updateAutoComplete(p)

	// Enter command processing loop.
	ui.Print("Entering interactive mode (type \"help\" for commands)")
	ui.SetAutoComplete(commands.NewCompleter(f.commands))

	for {
		input, err := readCommand(p, ui, f)
		if err != nil {
			if err != io.EOF {
				return err
			}
			if input == "" {
				return nil
			}
		}
		// Process simple commands.
		switch input {
		case "":
			continue
		case ":":
			f.flagFocus = newString("")
			f.flagIgnore = newString("")
			f.flagTagFocus = newString("")
			f.flagTagIgnore = newString("")
			f.flagHide = newString("")
			continue
		}

		fields := splitCommand(input)
		// Process report generation commands.
		if _, ok := f.commands[fields[0]]; ok {
			if err := generateReport(p, fields, obj, ui, f); err != nil {
				if err == io.EOF {
					return nil
				}
				ui.PrintErr(err)
			}
			continue
		}

		switch cmd := fields[0]; cmd {
		case "help":
			commandHelp(fields, ui, f)
			continue
		case "exit", "quit":
			return nil
		}

		// Process option settings.
		if of, err := optFlags(p, input, f); err == nil {
			f = of
		} else {
			ui.PrintErr("Error: ", err.Error())
		}
	}
}

func generateReport(p *profile.Profile, cmd []string, obj plugin.ObjTool, ui plugin.UI, f *flags) error {
	prof := p.Copy()

	cf, err := cmdFlags(prof, cmd, ui, f)
	if err != nil {
		return err
	}

	return generate(true, prof, obj, ui, cf)
}

// validateRegex checks if a string is a valid regular expression.
func validateRegex(v string) error {
	_, err := regexp.Compile(v)
	return err
}

// readCommand prompts for and reads the next command.
func readCommand(p *profile.Profile, ui plugin.UI, f *flags) (string, error) {
	//ui.Print("Options:\n", f.String(p))
	s, err := ui.ReadLine()
	return strings.TrimSpace(s), err
}

func commandHelp(_ []string, ui plugin.UI, f *flags) error {
	help := `
 Commands:
   cmd [n] [--cum] [focus_regex]* [-ignore_regex]*
       Produce a text report with the top n entries.
       Include samples matching focus_regex, and exclude ignore_regex.
       Add --cum to sort using cumulative data.
       Available commands:
`
	var commands []string
	for name, cmd := range f.commands {
		commands = append(commands, fmt.Sprintf("         %-12s %s", name, cmd.Usage))
	}
	sort.Strings(commands)

	help = help + strings.Join(commands, "\n") + `
   peek func_regex
       Display callers and callees of functions matching func_regex.

   dot [n] [focus_regex]* [-ignore_regex]* [>file]
       Produce an annotated callgraph with the top n entries.
       Include samples matching focus_regex, and exclude ignore_regex.
       For other outputs, replace dot with:
       - Graphic formats: dot, svg, pdf, ps, gif, png (use > to name output file)
       - Graph viewer:    gv, web, evince, eog

   callgrind [n] [focus_regex]* [-ignore_regex]* [>file]
       Produce a file in callgrind-compatible format.
       Include samples matching focus_regex, and exclude ignore_regex.

   weblist func_regex [-ignore_regex]*
       Show annotated source with interspersed assembly in a web browser.

   list func_regex [-ignore_regex]*
       Print source for routines matching func_regex, and exclude ignore_regex.

   disasm func_regex [-ignore_regex]*
       Disassemble routines matching func_regex, and exclude ignore_regex.

   tags tag_regex [-ignore_regex]*
       List tags with key:value matching tag_regex and exclude ignore_regex.

   quit/exit/^D
 	     Exit pprof.

   option=value
       The following options can be set individually:
           cum/flat:           Sort entries based on cumulative or flat data
           call_tree:          Build context-sensitive call trees
           nodecount:          Max number of entries to display
           nodefraction:       Min frequency ratio of nodes to display
           edgefraction:       Min frequency ratio of edges to display
           focus/ignore:       Regexp to include/exclude samples by name/file
           tagfocus/tagignore: Regexp or value range to filter samples by tag
                               eg "1mb", "1mb:2mb", ":64kb"

           functions:          Level of aggregation for sample data
           files:
           lines:
           addresses:

           unit:               Measurement unit to use on reports

           Sample value selection by index:
            sample_index:      Index of sample value to display
            mean:              Average sample value over first value

           Sample value selection by name:
            alloc_space        for heap profiles
            alloc_objects
            inuse_space
            inuse_objects

            total_delay        for contention profiles
            mean_delay
            contentions

   :   Clear focus/ignore/hide/tagfocus/tagignore`

	ui.Print(help)
	return nil
}

// cmdFlags parses the options of an interactive command and returns
// an updated flags object.
func cmdFlags(prof *profile.Profile, input []string, ui plugin.UI, f *flags) (*flags, error) {
	cf := *f

	var focus, ignore string
	output := *cf.flagOutput
	nodeCount := *cf.flagNodeCount
	cmd := input[0]

	// Update output flags based on parameters.
	tokens := input[1:]
	for p := 0; p < len(tokens); p++ {
		t := tokens[p]
		if t == "" {
			continue
		}
		if c, err := strconv.ParseInt(t, 10, 32); err == nil {
			nodeCount = int(c)
			continue
		}
		switch t[0] {
		case '>':
			if len(t) > 1 {
				output = t[1:]
				continue
			}
			// find next token
			for p++; p < len(tokens); p++ {
				if tokens[p] != "" {
					output = tokens[p]
					break
				}
			}
		case '-':
			if t == "--cum" || t == "-cum" {
				cf.flagCum = newBool(true)
				continue
			}
			ignore = catRegex(ignore, t[1:])
		default:
			focus = catRegex(focus, t)
		}
	}

	pcmd, ok := f.commands[cmd]
	if !ok {
		return nil, fmt.Errorf("Unexpected parse failure: %v", input)
	}
	// Reset flags
	cf.flagCommands = make(map[string]*bool)
	cf.flagParamCommands = make(map[string]*string)

	if !pcmd.HasParam {
		cf.flagCommands[cmd] = newBool(true)

		switch cmd {
		case "tags":
			cf.flagTagFocus = newString(focus)
			cf.flagTagIgnore = newString(ignore)
		default:
			cf.flagFocus = newString(catRegex(*cf.flagFocus, focus))
			cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore))
		}
	} else {
		if focus == "" {
			focus = "."
		}
		cf.flagParamCommands[cmd] = newString(focus)
		cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore))
	}

	if nodeCount < 0 {
		switch cmd {
		case "text", "top":
			// Default text/top to 10 nodes on interactive mode
			nodeCount = 10
		default:
			nodeCount = 80
		}
	}

	cf.flagNodeCount = newInt(nodeCount)
	cf.flagOutput = newString(output)

	// Do regular flags processing
	if err := processFlags(prof, ui, &cf); err != nil {
		cf.usage(ui)
		return nil, err
	}

	return &cf, nil
}

func catRegex(a, b string) string {
	if a == "" {
		return b
	}
	if b == "" {
		return a
	}
	return a + "|" + b
}

// optFlags parses an interactive option setting and returns
// an updated flags object.
func optFlags(p *profile.Profile, input string, f *flags) (*flags, error) {
	inputs := strings.SplitN(input, "=", 2)
	option := strings.ToLower(strings.TrimSpace(inputs[0]))
	var value string
	if len(inputs) == 2 {
		value = strings.TrimSpace(inputs[1])
	}

	of := *f

	var err error
	var bv bool
	var uv uint64
	var fv float64

	switch option {
	case "cum":
		if bv, err = parseBool(value); err != nil {
			return nil, err
		}
		of.flagCum = newBool(bv)
	case "flat":
		if bv, err = parseBool(value); err != nil {
			return nil, err
		}
		of.flagCum = newBool(!bv)
	case "call_tree":
		if bv, err = parseBool(value); err != nil {
			return nil, err
		}
		of.flagCallTree = newBool(bv)
	case "unit":
		of.flagDisplayUnit = newString(value)
	case "sample_index":
		if uv, err = strconv.ParseUint(value, 10, 32); err != nil {
			return nil, err
		}
		if ix := int(uv); ix < 0 || ix >= len(p.SampleType) {
			return nil, fmt.Errorf("sample_index out of range [0..%d]", len(p.SampleType)-1)
		}
		of.flagSampleIndex = newInt(int(uv))
	case "mean":
		if bv, err = parseBool(value); err != nil {
			return nil, err
		}
		of.flagMean = newBool(bv)
	case "nodecount":
		if uv, err = strconv.ParseUint(value, 10, 32); err != nil {
			return nil, err
		}
		of.flagNodeCount = newInt(int(uv))
	case "nodefraction":
		if fv, err = strconv.ParseFloat(value, 64); err != nil {
			return nil, err
		}
		of.flagNodeFraction = newFloat64(fv)
	case "edgefraction":
		if fv, err = strconv.ParseFloat(value, 64); err != nil {
			return nil, err
		}
		of.flagEdgeFraction = newFloat64(fv)
	case "focus":
		if err = validateRegex(value); err != nil {
			return nil, err
		}
		of.flagFocus = newString(value)
	case "ignore":
		if err = validateRegex(value); err != nil {
			return nil, err
		}
		of.flagIgnore = newString(value)
	case "tagfocus":
		if err = validateRegex(value); err != nil {
			return nil, err
		}
		of.flagTagFocus = newString(value)
	case "tagignore":
		if err = validateRegex(value); err != nil {
			return nil, err
		}
		of.flagTagIgnore = newString(value)
	case "hide":
		if err = validateRegex(value); err != nil {
			return nil, err
		}
		of.flagHide = newString(value)
	case "addresses", "files", "lines", "functions":
		if bv, err = parseBool(value); err != nil {
			return nil, err
		}
		if !bv {
			return nil, fmt.Errorf("select one of addresses/files/lines/functions")
		}
		setGranularityToggle(option, &of)
	default:
		if ix := findSampleIndex(p, "", option); ix >= 0 {
			of.flagSampleIndex = newInt(ix)
		} else if ix := findSampleIndex(p, "total_", option); ix >= 0 {
			of.flagSampleIndex = newInt(ix)
			of.flagMean = newBool(false)
		} else if ix := findSampleIndex(p, "mean_", option); ix >= 1 {
			of.flagSampleIndex = newInt(ix)
			of.flagMean = newBool(true)
		} else {
			return nil, fmt.Errorf("unrecognized command: %s", input)
		}
	}
	return &of, nil
}

// parseBool parses a string as a boolean value.
func parseBool(v string) (bool, error) {
	switch strings.ToLower(v) {
	case "true", "t", "yes", "y", "1", "":
		return true, nil
	case "false", "f", "no", "n", "0":
		return false, nil
	}
	return false, fmt.Errorf(`illegal input "%s" for bool value`, v)
}

func findSampleIndex(p *profile.Profile, prefix, sampleType string) int {
	if !strings.HasPrefix(sampleType, prefix) {
		return -1
	}
	sampleType = strings.TrimPrefix(sampleType, prefix)
	for i, r := range p.SampleType {
		if r.Type == sampleType {
			return i
		}
	}
	return -1
}

// setGranularityToggle manages the set of granularity options. These
// operate as a toggle; turning one on turns the others off.
func setGranularityToggle(o string, fl *flags) {
	t, f := newBool(true), newBool(false)
	fl.flagFunctions = f
	fl.flagFiles = f
	fl.flagLines = f
	fl.flagAddresses = f
	switch o {
	case "functions":
		fl.flagFunctions = t
	case "files":
		fl.flagFiles = t
	case "lines":
		fl.flagLines = t
	case "addresses":
		fl.flagAddresses = t
	default:
		panic(fmt.Errorf("unexpected option %s", o))
	}
}