package driver

import (
	"fmt"
	"net/url"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync"
)

// config holds settings for a single named config.
// The JSON tag name for a field is used both for JSON encoding and as
// a named variable.
type config struct {
	// Filename for file-based output formats, stdout by default.
	Output string `json:"-"`

	// Display options.
	CallTree            bool    `json:"call_tree,omitempty"`
	RelativePercentages bool    `json:"relative_percentages,omitempty"`
	Unit                string  `json:"unit,omitempty"`
	CompactLabels       bool    `json:"compact_labels,omitempty"`
	SourcePath          string  `json:"-"`
	TrimPath            string  `json:"-"`
	IntelSyntax         bool    `json:"intel_syntax,omitempty"`
	Mean                bool    `json:"mean,omitempty"`
	SampleIndex         string  `json:"-"`
	DivideBy            float64 `json:"-"`
	Normalize           bool    `json:"normalize,omitempty"`
	Sort                string  `json:"sort,omitempty"`

	// Label pseudo stack frame generation options
	TagRoot string `json:"tagroot,omitempty"`
	TagLeaf string `json:"tagleaf,omitempty"`

	// Filtering options
	DropNegative bool    `json:"drop_negative,omitempty"`
	NodeCount    int     `json:"nodecount,omitempty"`
	NodeFraction float64 `json:"nodefraction,omitempty"`
	EdgeFraction float64 `json:"edgefraction,omitempty"`
	Trim         bool    `json:"trim,omitempty"`
	Focus        string  `json:"focus,omitempty"`
	Ignore       string  `json:"ignore,omitempty"`
	PruneFrom    string  `json:"prune_from,omitempty"`
	Hide         string  `json:"hide,omitempty"`
	Show         string  `json:"show,omitempty"`
	ShowFrom     string  `json:"show_from,omitempty"`
	TagFocus     string  `json:"tagfocus,omitempty"`
	TagIgnore    string  `json:"tagignore,omitempty"`
	TagShow      string  `json:"tagshow,omitempty"`
	TagHide      string  `json:"taghide,omitempty"`
	NoInlines    bool    `json:"noinlines,omitempty"`
	ShowColumns  bool    `json:"showcolumns,omitempty"`

	// Output granularity
	Granularity string `json:"granularity,omitempty"`
}

// defaultConfig returns the default configuration values; it is unaffected by
// flags and interactive assignments.
func defaultConfig() config {
	return config{
		Unit:         "minimum",
		NodeCount:    -1,
		NodeFraction: 0.005,
		EdgeFraction: 0.001,
		Trim:         true,
		DivideBy:     1.0,
		Sort:         "flat",
		Granularity:  "", // Default depends on the display format
	}
}

// currentConfig holds the current configuration values; it is affected by
// flags and interactive assignments.
var currentCfg = defaultConfig()
var currentMu sync.Mutex

func currentConfig() config {
	currentMu.Lock()
	defer currentMu.Unlock()
	return currentCfg
}

func setCurrentConfig(cfg config) {
	currentMu.Lock()
	defer currentMu.Unlock()
	currentCfg = cfg
}

// configField contains metadata for a single configuration field.
type configField struct {
	name         string              // JSON field name/key in variables
	urlparam     string              // URL parameter name
	saved        bool                // Is field saved in settings?
	field        reflect.StructField // Field in config
	choices      []string            // Name Of variables in group
	defaultValue string              // Default value for this field.
}

var (
	configFields []configField // Precomputed metadata per config field

	// configFieldMap holds an entry for every config field as well as an
	// entry for every valid choice for a multi-choice field.
	configFieldMap map[string]configField
)

func init() {
	// Config names for fields that are not saved in settings and therefore
	// do not have a JSON name.
	notSaved := map[string]string{
		// Not saved in settings, but present in URLs.
		"SampleIndex": "sample_index",

		// Following fields are also not placed in URLs.
		"Output":     "output",
		"SourcePath": "source_path",
		"TrimPath":   "trim_path",
		"DivideBy":   "divide_by",
	}

	// choices holds the list of allowed values for config fields that can
	// take on one of a bounded set of values.
	choices := map[string][]string{
		"sort":        {"cum", "flat"},
		"granularity": {"functions", "filefunctions", "files", "lines", "addresses"},
	}

	// urlparam holds the mapping from a config field name to the URL
	// parameter used to hold that config field. If no entry is present for
	// a name, the corresponding field is not saved in URLs.
	urlparam := map[string]string{
		"drop_negative":        "dropneg",
		"call_tree":            "calltree",
		"relative_percentages": "rel",
		"unit":                 "unit",
		"compact_labels":       "compact",
		"intel_syntax":         "intel",
		"nodecount":            "n",
		"nodefraction":         "nf",
		"edgefraction":         "ef",
		"trim":                 "trim",
		"focus":                "f",
		"ignore":               "i",
		"prune_from":           "prunefrom",
		"hide":                 "h",
		"show":                 "s",
		"show_from":            "sf",
		"tagfocus":             "tf",
		"tagignore":            "ti",
		"tagshow":              "ts",
		"taghide":              "th",
		"mean":                 "mean",
		"sample_index":         "si",
		"normalize":            "norm",
		"sort":                 "sort",
		"granularity":          "g",
		"noinlines":            "noinlines",
		"showcolumns":          "showcolumns",
	}

	def := defaultConfig()
	configFieldMap = map[string]configField{}
	t := reflect.TypeFor[config]()
	for i, n := 0, t.NumField(); i < n; i++ {
		field := t.Field(i)
		js := strings.Split(field.Tag.Get("json"), ",")
		if len(js) == 0 {
			continue
		}
		// Get the configuration name for this field.
		name := js[0]
		if name == "-" {
			name = notSaved[field.Name]
			if name == "" {
				// Not a configurable field.
				continue
			}
		}
		f := configField{
			name:     name,
			urlparam: urlparam[name],
			saved:    (name == js[0]),
			field:    field,
			choices:  choices[name],
		}
		f.defaultValue = def.get(f)
		configFields = append(configFields, f)
		configFieldMap[f.name] = f
		for _, choice := range f.choices {
			configFieldMap[choice] = f
		}
	}
}

// fieldPtr returns a pointer to the field identified by f in *cfg.
func (cfg *config) fieldPtr(f configField) interface{} {
	// reflect.ValueOf: converts to reflect.Value
	// Elem: dereferences cfg to make *cfg
	// FieldByIndex: fetches the field
	// Addr: takes address of field
	// Interface: converts back from reflect.Value to a regular value
	return reflect.ValueOf(cfg).Elem().FieldByIndex(f.field.Index).Addr().Interface()
}

// get returns the value of field f in cfg.
func (cfg *config) get(f configField) string {
	switch ptr := cfg.fieldPtr(f).(type) {
	case *string:
		return *ptr
	case *int:
		return fmt.Sprint(*ptr)
	case *float64:
		return fmt.Sprint(*ptr)
	case *bool:
		return fmt.Sprint(*ptr)
	}
	panic(fmt.Sprintf("unsupported config field type %v", f.field.Type))
}

// set sets the value of field f in cfg to value.
func (cfg *config) set(f configField, value string) error {
	switch ptr := cfg.fieldPtr(f).(type) {
	case *string:
		if len(f.choices) > 0 {
			// Verify that value is one of the allowed choices.
			if slices.Contains(f.choices, value) {
				*ptr = value
				return nil
			}
			return fmt.Errorf("invalid %q value %q", f.name, value)
		}
		*ptr = value
	case *int:
		v, err := strconv.Atoi(value)
		if err != nil {
			return err
		}
		*ptr = v
	case *float64:
		v, err := strconv.ParseFloat(value, 64)
		if err != nil {
			return err
		}
		*ptr = v
	case *bool:
		v, err := stringToBool(value)
		if err != nil {
			return err
		}
		*ptr = v
	default:
		panic(fmt.Sprintf("unsupported config field type %v", f.field.Type))
	}
	return nil
}

// isConfigurable returns true if name is either the name of a config field, or
// a valid value for a multi-choice config field.
func isConfigurable(name string) bool {
	_, ok := configFieldMap[name]
	return ok
}

// isBoolConfig returns true if name is either name of a boolean config field,
// or a valid value for a multi-choice config field.
func isBoolConfig(name string) bool {
	f, ok := configFieldMap[name]
	if !ok {
		return false
	}
	if name != f.name {
		return true // name must be one possible value for the field
	}
	var cfg config
	_, ok = cfg.fieldPtr(f).(*bool)
	return ok
}

// completeConfig returns the list of configurable names starting with prefix.
func completeConfig(prefix string) []string {
	var result []string
	for v := range configFieldMap {
		if strings.HasPrefix(v, prefix) {
			result = append(result, v)
		}
	}
	return result
}

// configure stores the name=value mapping into the current config, correctly
// handling the case when name identifies a particular choice in a field.
func configure(name, value string) error {
	currentMu.Lock()
	defer currentMu.Unlock()
	f, ok := configFieldMap[name]
	if !ok {
		return fmt.Errorf("unknown config field %q", name)
	}
	if f.name == name {
		return currentCfg.set(f, value)
	}
	// name must be one of the choices. If value is true, set field-value
	// to name.
	if v, err := strconv.ParseBool(value); v && err == nil {
		return currentCfg.set(f, name)
	}
	return fmt.Errorf("unknown config field %q", name)
}

// resetTransient sets all transient fields in *cfg to their currently
// configured values.
func (cfg *config) resetTransient() {
	current := currentConfig()
	cfg.Output = current.Output
	cfg.SourcePath = current.SourcePath
	cfg.TrimPath = current.TrimPath
	cfg.DivideBy = current.DivideBy
	cfg.SampleIndex = current.SampleIndex
}

// applyURL updates *cfg based on params.
func (cfg *config) applyURL(params url.Values) error {
	for _, f := range configFields {
		var value string
		if f.urlparam != "" {
			value = params.Get(f.urlparam)
		}
		if value == "" {
			continue
		}
		if err := cfg.set(f, value); err != nil {
			return fmt.Errorf("error setting config field %s: %v", f.name, err)
		}
	}
	return nil
}

// makeURL returns a URL based on initialURL that contains the config contents
// as parameters.  The second result is true iff a parameter value was changed.
func (cfg *config) makeURL(initialURL url.URL) (url.URL, bool) {
	q := initialURL.Query()
	changed := false
	for _, f := range configFields {
		if f.urlparam == "" || !f.saved {
			continue
		}
		v := cfg.get(f)
		if v == f.defaultValue {
			v = "" // URL for of default value is the empty string.
		} else if f.field.Type.Kind() == reflect.Bool {
			// Shorten bool values to "f" or "t"
			v = v[:1]
		}
		if q.Get(f.urlparam) == v {
			continue
		}
		changed = true
		if v == "" {
			q.Del(f.urlparam)
		} else {
			q.Set(f.urlparam, v)
		}
	}
	if changed {
		initialURL.RawQuery = q.Encode()
	}
	return initialURL, changed
}
