// Copyright 2019 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 settings

import (
	"fmt"
	"maps"
	"path/filepath"
	"reflect"
	"strings"
	"time"

	"golang.org/x/tools/gopls/internal/file"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/protocol/semtok"
	"golang.org/x/tools/gopls/internal/telemetry"
	"golang.org/x/tools/gopls/internal/util/frob"
)

// An Annotation is a category of Go compiler optimization diagnostic.
type Annotation string

const (
	// Nil controls nil checks.
	Nil Annotation = "nil"

	// Escape controls diagnostics about escape choices.
	Escape Annotation = "escape"

	// Inline controls diagnostics about inlining choices.
	Inline Annotation = "inline"

	// Bounds controls bounds checking diagnostics.
	Bounds Annotation = "bounds"
)

// Options holds various configuration that affects Gopls execution, organized
// by the nature or origin of the settings.
//
// Options must be comparable with reflect.DeepEqual, and serializable with
// [frob.Codec].
//
// This type defines both the logic of LSP-supplied option parsing
// (see [SetOptions]), and the public documentation of options in
// ../../doc/settings.md (generated by gopls/doc/generate).
//
// Each exported field of each embedded type such as "ClientOptions"
// contributes a user-visible option setting. The option name is the
// field name rendered in camelCase. Unlike most Go doc comments,
// these fields should be documented using GitHub markdown.
type Options struct {
	ClientOptions
	ServerOptions
	UserOptions
	InternalOptions
}

// Debug returns a list of "name = value" strings for each Options field.
func (o *Options) Debug() []string {
	var res []string

	var visitStruct func(v reflect.Value, path []string)
	visitStruct = func(v reflect.Value, path []string) {
		for i := range v.NumField() {
			f := v.Field(i)
			ftyp := v.Type().Field(i)
			path := append(path, ftyp.Name)
			if ftyp.Type.Kind() == reflect.Struct {
				visitStruct(f, path)
			} else {
				res = append(res, fmt.Sprintf("%s = %#v",
					strings.Join(path, "."),
					f.Interface()))
			}
		}
	}
	visitStruct(reflect.ValueOf(o).Elem(), nil)

	return res
}

// ClientOptions holds LSP-specific configuration that is provided by the
// client.
//
// ClientOptions must be comparable with reflect.DeepEqual.
type ClientOptions struct {
	ClientInfo                                 protocol.ClientInfo
	InsertTextFormat                           protocol.InsertTextFormat
	InsertReplaceSupported                     bool
	ConfigurationSupported                     bool
	DynamicConfigurationSupported              bool
	DynamicRegistrationSemanticTokensSupported bool
	DynamicWatchedFilesSupported               bool
	RelativePatternsSupported                  bool
	PreferredContentFormat                     protocol.MarkupKind
	LineFoldingOnly                            bool
	HierarchicalDocumentSymbolSupport          bool
	ImportsSource                              ImportsSourceEnum `status:"experimental"`
	SemanticTypes                              []string
	SemanticMods                               []string
	RelatedInformationSupported                bool
	CompletionTags                             bool
	CompletionDeprecated                       bool
	SupportedResourceOperations                []protocol.ResourceOperationKind
	CodeActionResolveOptions                   []string
	ShowDocumentSupported                      bool
	// SupportedWorkDoneProgressFormats specifies the formats supported by the
	// client for handling workdone progress metadata.
	SupportedWorkDoneProgressFormats map[WorkDoneProgressStyle]bool
}

// ServerOptions holds LSP-specific configuration that is provided by the
// server.
//
// ServerOptions must be comparable with reflect.DeepEqual.
type ServerOptions struct {
	SupportedCodeActions map[file.Kind]map[protocol.CodeActionKind]bool
	SupportedCommands    []string
}

// Note: BuildOptions must be comparable with reflect.DeepEqual.
type BuildOptions struct {
	// BuildFlags is the set of flags passed on to the build system when invoked.
	// It is applied to queries like `go list`, which is used when discovering files.
	// The most common use is to set `-tags`.
	BuildFlags []string

	// Env adds environment variables to external commands run by `gopls`, most notably `go list`.
	Env map[string]string

	// DirectoryFilters can be used to exclude unwanted directories from the
	// workspace. By default, all directories are included. Filters are an
	// operator, `+` to include and `-` to exclude, followed by a path prefix
	// relative to the workspace folder. They are evaluated in order, and
	// the last filter that applies to a path controls whether it is included.
	// The path prefix can be empty, so an initial `-` excludes everything.
	//
	// DirectoryFilters also supports the `**` operator to match 0 or more directories.
	//
	// Examples:
	//
	// Exclude node_modules at current depth: `-node_modules`
	//
	// Exclude node_modules at any depth: `-**/node_modules`
	//
	// Include only project_a: `-` (exclude everything), `+project_a`
	//
	// Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`
	DirectoryFilters []string

	// TemplateExtensions gives the extensions of file names that are treated
	// as template files. (The extension
	// is the part of the file name after the final dot.)
	TemplateExtensions []string

	// obsolete, no effect
	MemoryMode string `status:"experimental"`

	// ExpandWorkspaceToModule determines which packages are considered
	// "workspace packages" when the workspace is using modules.
	//
	// Workspace packages affect the scope of workspace-wide operations. Notably,
	// gopls diagnoses all packages considered to be part of the workspace after
	// every keystroke, so by setting "ExpandWorkspaceToModule" to false, and
	// opening a nested workspace directory, you can reduce the amount of work
	// gopls has to do to keep your workspace up to date.
	ExpandWorkspaceToModule bool `status:"experimental"`

	// StandaloneTags specifies a set of build constraints that identify
	// individual Go source files that make up the entire main package of an
	// executable.
	//
	// A common example of standalone main files is the convention of using the
	// directive `//go:build ignore` to denote files that are not intended to be
	// included in any package, for example because they are invoked directly by
	// the developer using `go run`.
	//
	// Gopls considers a file to be a standalone main file if and only if it has
	// package name "main" and has a build directive of the exact form
	// "//go:build tag" or "// +build tag", where tag is among the list of tags
	// configured by this setting. Notably, if the build constraint is more
	// complicated than a simple tag (such as the composite constraint
	// `//go:build tag && go1.18`), the file is not considered to be a standalone
	// main file.
	//
	// This setting is only supported when gopls is built with Go 1.16 or later.
	StandaloneTags []string

	// WorkspaceFiles configures the set of globs that match files defining the
	// logical build of the current workspace. Any on-disk changes to any files
	// matching a glob specified here will trigger a reload of the workspace.
	//
	// This setting need only be customized in environments with a custom
	// GOPACKAGESDRIVER.
	WorkspaceFiles []string
}

// Note: UIOptions must be comparable with reflect.DeepEqual.
type UIOptions struct {
	DocumentationOptions
	CompletionOptions
	NavigationOptions
	DiagnosticOptions
	InlayHintOptions

	// Codelenses overrides the enabled/disabled state of each of gopls'
	// sources of [Code Lenses](codelenses.md).
	//
	// Example Usage:
	//
	// ```json5
	// "gopls": {
	// ...
	//   "codelenses": {
	//     "generate": false,  // Don't show the `go generate` lens.
	//   }
	// ...
	// }
	// ```
	Codelenses map[CodeLensSource]bool

	// SemanticTokens controls whether the LSP server will send
	// semantic tokens to the client.
	SemanticTokens bool `status:"experimental"`

	// NoSemanticString turns off the sending of the semantic token 'string'
	//
	// Deprecated: Use SemanticTokenTypes["string"] = false instead. See
	// golang/vscode-go#3632
	NoSemanticString bool `status:"experimental"`

	// NoSemanticNumber turns off the sending of the semantic token 'number'
	//
	// Deprecated: Use SemanticTokenTypes["number"] = false instead. See
	// golang/vscode-go#3632.
	NoSemanticNumber bool `status:"experimental"`

	// SemanticTokenTypes configures the semantic token types. It allows
	// disabling types by setting each value to false.
	// By default, all types are enabled.
	SemanticTokenTypes map[string]bool `status:"experimental"`

	// SemanticTokenModifiers configures the semantic token modifiers. It allows
	// disabling modifiers by setting each value to false.
	// By default, all modifiers are enabled.
	SemanticTokenModifiers map[string]bool `status:"experimental"`
}

// A CodeLensSource identifies an (algorithmic) source of code lenses.
type CodeLensSource string

// CodeLens sources
//
// These identifiers appear in the "codelenses" configuration setting,
// and in the user documentation thereof, which is generated by
// gopls/doc/generate/generate.go parsing this file.
//
// Doc comments should use GitHub Markdown.
// The first line becomes the title.
//
// (For historical reasons, each code lens source identifier typically
// matches the name of one of the command.Commands returned by it,
// but that isn't essential.)
const (
	// Run `go generate`
	//
	// This codelens source annotates any `//go:generate` comments
	// with commands to run `go generate` in this directory, on
	// all directories recursively beneath this one.
	//
	// See [Generating code](https://go.dev/blog/generate) for
	// more details.
	CodeLensGenerate CodeLensSource = "generate"

	// Re-generate cgo declarations
	//
	// This codelens source annotates an `import "C"` declaration
	// with a command to re-run the [cgo
	// command](https://pkg.go.dev/cmd/cgo) to regenerate the
	// corresponding Go declarations.
	//
	// Use this after editing the C code in comments attached to
	// the import, or in C header files included by it.
	CodeLensRegenerateCgo CodeLensSource = "regenerate_cgo"

	// Run govulncheck
	//
	// This codelens source annotates the `module` directive in a go.mod file
	// with a command to run govulncheck synchronously.
	//
	// [Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that
	// computes the set of functions reachable within your application, including
	// dependencies; queries a database of known security vulnerabilities; and
	// reports any potential problems it finds.
	//
	//gopls:status experimental
	CodeLensVulncheck CodeLensSource = "vulncheck"

	// Run govulncheck (legacy)
	//
	// This codelens source annotates the `module` directive in a go.mod file
	// with a command to run Govulncheck asynchronously.
	//
	// [Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that
	// computes the set of functions reachable within your application, including
	// dependencies; queries a database of known security vulnerabilities; and
	// reports any potential problems it finds.
	//
	//gopls:status experimental
	CodeLensRunGovulncheck CodeLensSource = "run_govulncheck"

	// Run tests and benchmarks
	//
	// This codelens source annotates each `Test` and `Benchmark`
	// function in a `*_test.go` file with a command to run it.
	//
	// This source is off by default because VS Code has
	// a client-side custom UI for testing, and because progress
	// notifications are not a great UX for streamed test output.
	// See:
	// - golang/go#67400 for a discussion of this feature.
	// - https://github.com/joaotavora/eglot/discussions/1402
	//   for an alternative approach.
	CodeLensTest CodeLensSource = "test"

	// Tidy go.mod file
	//
	// This codelens source annotates the `module` directive in a
	// go.mod file with a command to run [`go mod
	// tidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures
	// that the go.mod file matches the source code in the module.
	CodeLensTidy CodeLensSource = "tidy"

	// Update dependencies
	//
	// This codelens source annotates the `module` directive in a
	// go.mod file with commands to:
	//
	// - check for available upgrades,
	// - upgrade direct dependencies, and
	// - upgrade all dependencies transitively.
	CodeLensUpgradeDependency CodeLensSource = "upgrade_dependency"

	// Update vendor directory
	//
	// This codelens source annotates the `module` directive in a
	// go.mod file with a command to run [`go mod
	// vendor`](https://go.dev/ref/mod#go-mod-vendor), which
	// creates or updates the directory named `vendor` in the
	// module root so that it contains an up-to-date copy of all
	// necessary package dependencies.
	CodeLensVendor CodeLensSource = "vendor"
)

// Note: CompletionOptions must be comparable with reflect.DeepEqual.
type CompletionOptions struct {
	// Placeholders enables placeholders for function parameters or struct
	// fields in completion responses.
	UsePlaceholders bool

	// CompletionBudget is the soft latency goal for completion requests. Most
	// requests finish in a couple milliseconds, but in some cases deep
	// completions can take much longer. As we use up our budget we
	// dynamically reduce the search scope to ensure we return timely
	// results. Zero means unlimited.
	CompletionBudget time.Duration `status:"debug"`

	// Matcher sets the algorithm that is used when calculating completion
	// candidates.
	Matcher Matcher `status:"advanced"`

	// ExperimentalPostfixCompletions enables artificial method snippets
	// such as "someSlice.sort!".
	ExperimentalPostfixCompletions bool `status:"experimental"`

	// CompleteFunctionCalls enables function call completion.
	//
	// When completing a statement, or when a function return type matches the
	// expected of the expression being completed, completion may suggest call
	// expressions (i.e. may include parentheses).
	CompleteFunctionCalls bool
}

// Note: DocumentationOptions must be comparable with reflect.DeepEqual.
type DocumentationOptions struct {
	// HoverKind controls the information that appears in the hover text.
	// SingleLine is intended for use only by authors of editor plugins.
	HoverKind HoverKind

	// LinkTarget is the base URL for links to Go package
	// documentation returned by LSP operations such as Hover and
	// DocumentLinks and in the CodeDescription field of each
	// Diagnostic.
	//
	// It might be one of:
	//
	// * `"godoc.org"`
	// * `"pkg.go.dev"`
	//
	// If company chooses to use its own `godoc.org`, its address can be used as well.
	//
	// Modules matching the GOPRIVATE environment variable will not have
	// documentation links in hover.
	LinkTarget string

	// LinksInHover controls the presence of documentation links in hover markdown.
	LinksInHover LinksInHoverEnum
}

// LinksInHoverEnum has legal values:
//
// - `false`, for no links;
// - `true`, for links to the `linkTarget` domain; or
// - `"gopls"`, for links to gopls' internal documentation viewer.
//
// Note: this type has special logic in loadEnums in generate.go.
// Be sure to reflect enum and doc changes there!
type LinksInHoverEnum int

const (
	LinksInHover_None LinksInHoverEnum = iota
	LinksInHover_LinkTarget
	LinksInHover_Gopls
)

// MarshalJSON implements the json.Marshaler interface, so that the default
// values are formatted correctly in documentation. (See [Options.setOne] for
// the flexible custom unmarshalling behavior).
func (l LinksInHoverEnum) MarshalJSON() ([]byte, error) {
	switch l {
	case LinksInHover_None:
		return []byte("false"), nil
	case LinksInHover_LinkTarget:
		return []byte("true"), nil
	case LinksInHover_Gopls:
		return []byte(`"gopls"`), nil
	default:
		return nil, fmt.Errorf("invalid LinksInHover value %d", l)
	}
}

// Note: FormattingOptions must be comparable with reflect.DeepEqual.
type FormattingOptions struct {
	// Local is the equivalent of the `goimports -local` flag, which puts
	// imports beginning with this string after third-party packages. It should
	// be the prefix of the import path whose imports should be grouped
	// separately.
	//
	// It is used when tidying imports (during an LSP Organize
	// Imports request) or when inserting new ones (for example,
	// during completion); an LSP Formatting request merely sorts the
	// existing imports.
	Local string

	// Gofumpt indicates if we should run gofumpt formatting.
	Gofumpt bool
}

// Note: DiagnosticOptions must be comparable with reflect.DeepEqual,
// and frob-encodable (no interfaces).
type DiagnosticOptions struct {
	// Analyses specify analyses that the user would like to enable or disable.
	// A map of the names of analysis passes that should be enabled/disabled.
	// A full list of analyzers that gopls uses can be found in
	// [analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).
	//
	// Example Usage:
	//
	// ```json5
	// ...
	// "analyses": {
	//   "unreachable": false, // Disable the unreachable analyzer.
	//   "unusedvariable": true  // Enable the unusedvariable analyzer.
	// }
	// ...
	// ```
	Analyses map[string]bool

	// Staticcheck configures the default set of analyses staticcheck.io.
	// These analyses are documented on
	// [Staticcheck's website](https://staticcheck.io/docs/checks/).
	//
	// The "staticcheck" option has three values:
	// - false: disable all staticcheck analyzers
	// - true: enable all staticcheck analyzers
	// - unset: enable a subset of staticcheck analyzers
	//   selected by gopls maintainers for runtime efficiency
	//   and analytic precision.
	//
	// Regardless of this setting, individual analyzers can be
	// selectively enabled or disabled using the `analyses` setting.
	Staticcheck         bool `status:"experimental"`
	StaticcheckProvided bool `status:"experimental"` // = "staticcheck" was explicitly provided

	// Annotations specifies the various kinds of compiler
	// optimization details that should be reported as diagnostics
	// when enabled for a package by the "Toggle compiler
	// optimization details" (`gopls.gc_details`) command.
	//
	// (Some users care only about one kind of annotation in their
	// profiling efforts. More importantly, in large packages, the
	// number of annotations can sometimes overwhelm the user
	// interface and exceed the per-file diagnostic limit.)
	//
	// TODO(adonovan): rename this field to CompilerOptDetail.
	Annotations map[Annotation]bool

	// Vulncheck enables vulnerability scanning.
	Vulncheck VulncheckMode `status:"experimental"`

	// DiagnosticsDelay controls the amount of time that gopls waits
	// after the most recent file modification before computing deep diagnostics.
	// Simple diagnostics (parsing and type-checking) are always run immediately
	// on recently modified packages.
	//
	// This option must be set to a valid duration string, for example `"250ms"`.
	DiagnosticsDelay time.Duration `status:"advanced"`

	// DiagnosticsTrigger controls when to run diagnostics.
	DiagnosticsTrigger DiagnosticsTrigger `status:"experimental"`

	// AnalysisProgressReporting controls whether gopls sends progress
	// notifications when construction of its index of analysis facts is taking a
	// long time. Cancelling these notifications will cancel the indexing task,
	// though it will restart after the next change in the workspace.
	//
	// When a package is opened for the first time and heavyweight analyses such as
	// staticcheck are enabled, it can take a while to construct the index of
	// analysis facts for all its dependencies. The index is cached in the
	// filesystem, so subsequent analysis should be faster.
	AnalysisProgressReporting bool
}

type InlayHintOptions struct {
	// Hints specify inlay hints that users want to see. A full list of hints
	// that gopls uses can be found in
	// [inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).
	Hints map[InlayHint]bool `status:"experimental"`
}

// An InlayHint identifies a category of hint that may be
// independently requested through the "hints" setting.
type InlayHint string

// This is the source from which gopls/doc/inlayHints.md is generated.
const (
	// ParameterNames controls inlay hints for parameter names:
	// ```go
	// 	parseInt(/* str: */ "123", /* radix: */ 8)
	// ```
	ParameterNames InlayHint = "parameterNames"

	// AssignVariableTypes controls inlay hints for variable types in assign statements:
	// ```go
	// 	i/* int*/, j/* int*/ := 0, len(r)-1
	// ```
	AssignVariableTypes InlayHint = "assignVariableTypes"

	// ConstantValues controls inlay hints for constant values:
	// ```go
	// 	const (
	// 		KindNone   Kind = iota/* = 0*/
	// 		KindPrint/*  = 1*/
	// 		KindPrintf/* = 2*/
	// 		KindErrorf/* = 3*/
	// 	)
	// ```
	ConstantValues InlayHint = "constantValues"

	// RangeVariableTypes controls inlay hints for variable types in range statements:
	// ```go
	// 	for k/* int*/, v/* string*/ := range []string{} {
	// 		fmt.Println(k, v)
	// 	}
	// ```
	RangeVariableTypes InlayHint = "rangeVariableTypes"

	// CompositeLiteralTypes controls inlay hints for composite literal types:
	// ```go
	// 	for _, c := range []struct {
	// 		in, want string
	// 	}{
	// 		/*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"},
	// 	}
	// ```
	CompositeLiteralTypes InlayHint = "compositeLiteralTypes"

	// CompositeLiteralFieldNames inlay hints for composite literal field names:
	// ```go
	// 	{/*in: */"Hello, world", /*want: */"dlrow ,olleH"}
	// ```
	CompositeLiteralFieldNames InlayHint = "compositeLiteralFields"

	// FunctionTypeParameters inlay hints for implicit type parameters on generic functions:
	// ```go
	// 	myFoo/*[int, string]*/(1, "hello")
	// ```
	FunctionTypeParameters InlayHint = "functionTypeParameters"
)

type NavigationOptions struct {
	// ImportShortcut specifies whether import statements should link to
	// documentation or go to definitions.
	ImportShortcut ImportShortcut

	// SymbolMatcher sets the algorithm that is used when finding workspace symbols.
	SymbolMatcher SymbolMatcher `status:"advanced"`

	// SymbolStyle controls how symbols are qualified in symbol responses.
	//
	// Example Usage:
	//
	// ```json5
	// "gopls": {
	// ...
	//   "symbolStyle": "Dynamic",
	// ...
	// }
	// ```
	SymbolStyle SymbolStyle `status:"advanced"`

	// SymbolScope controls which packages are searched for workspace/symbol
	// requests. When the scope is "workspace", gopls searches only workspace
	// packages. When the scope is "all", gopls searches all loaded packages,
	// including dependencies and the standard library.
	SymbolScope SymbolScope
}

// UserOptions holds custom Gopls configuration (not part of the LSP) that is
// modified by the client.
//
// UserOptions must be comparable with reflect.DeepEqual.
type UserOptions struct {
	BuildOptions
	UIOptions
	FormattingOptions

	// VerboseOutput enables additional debug logging.
	VerboseOutput bool `status:"debug"`
}

// EnvSlice returns Env as a slice of k=v strings.
func (u *UserOptions) EnvSlice() []string {
	var result []string
	for k, v := range u.Env {
		result = append(result, fmt.Sprintf("%v=%v", k, v))
	}
	return result
}

// SetEnvSlice sets Env from a slice of k=v strings.
func (u *UserOptions) SetEnvSlice(env []string) {
	u.Env = map[string]string{}
	for _, kv := range env {
		split := strings.SplitN(kv, "=", 2)
		if len(split) != 2 {
			continue
		}
		u.Env[split[0]] = split[1]
	}
}

type WorkDoneProgressStyle string

const WorkDoneProgressStyleLog WorkDoneProgressStyle = "log"

// InternalOptions contains settings that are not intended for use by the
// average user. These may be settings used by tests or outdated settings that
// will soon be deprecated. Some of these settings may not even be configurable
// by the user.
//
// TODO(rfindley): even though these settings are not intended for
// modification, some of them should be surfaced in our documentation.
type InternalOptions struct {
	// VerboseWorkDoneProgress controls whether the LSP server should send
	// progress reports for all work done outside the scope of an RPC.
	// Used by the regression tests.
	VerboseWorkDoneProgress bool

	// The following options were previously available to users, but they
	// really shouldn't be configured by anyone other than "power users".

	// CompletionDocumentation enables documentation with completion results.
	CompletionDocumentation bool

	// CompleteUnimported enables completion for packages that you do not
	// currently import.
	CompleteUnimported bool

	// DeepCompletion enables the ability to return completions from deep
	// inside relevant entities, rather than just the locally accessible ones.
	//
	// Consider this example:
	//
	// ```go
	// package main
	//
	// import "fmt"
	//
	// type wrapString struct {
	//     str string
	// }
	//
	// func main() {
	//     x := wrapString{"hello world"}
	//     fmt.Printf(<>)
	// }
	// ```
	//
	// At the location of the `<>` in this program, deep completion would suggest
	// the result `x.str`.
	DeepCompletion bool

	// ShowBugReports causes a message to be shown when the first bug is reported
	// on the server.
	// This option applies only during initialization.
	ShowBugReports bool

	// SubdirWatchPatterns configures the file watching glob patterns registered
	// by gopls.
	//
	// Some clients (namely VS Code) do not send workspace/didChangeWatchedFile
	// notifications for files contained in a directory when that directory is
	// deleted:
	// https://github.com/microsoft/vscode/issues/109754
	//
	// In this case, gopls would miss important notifications about deleted
	// packages. To work around this, gopls registers a watch pattern for each
	// directory containing Go files.
	//
	// Unfortunately, other clients experience performance problems with this
	// many watch patterns, so there is no single behavior that works well for
	// all clients.
	//
	// The "subdirWatchPatterns" setting allows configuring this behavior. Its
	// default value of "auto" attempts to guess the correct behavior based on
	// the client name. We'd love to avoid this specialization, but as described
	// above there is no single value that works for all clients.
	//
	// If any LSP client does not behave well with the default value (for
	// example, if like VS Code it drops file notifications), please file an
	// issue.
	SubdirWatchPatterns SubdirWatchPatterns

	// ReportAnalysisProgressAfter sets the duration for gopls to wait before starting
	// progress reporting for ongoing go/analysis passes.
	//
	// It is intended to be used for testing only.
	ReportAnalysisProgressAfter time.Duration

	// TelemetryPrompt controls whether gopls prompts about enabling Go telemetry.
	//
	// Once the prompt is answered, gopls doesn't ask again, but TelemetryPrompt
	// can prevent the question from ever being asked in the first place.
	TelemetryPrompt bool

	// LinkifyShowMessage controls whether the client wants gopls
	// to linkify links in showMessage. e.g. [go.dev](https://go.dev).
	LinkifyShowMessage bool

	// IncludeReplaceInWorkspace controls whether locally replaced modules in a
	// go.mod file are treated like workspace modules.
	// Or in other words, if a go.mod file with local replaces behaves like a
	// go.work file.
	IncludeReplaceInWorkspace bool

	// ZeroConfig enables the zero-config algorithm for workspace layout,
	// dynamically creating build configurations for different modules,
	// directories, and GOOS/GOARCH combinations to cover open files.
	ZeroConfig bool

	// PullDiagnostics enables support for pull diagnostics.
	//
	// TODO(rfindley): make pull diagnostics robust, and remove this option,
	// allowing pull diagnostics by default.
	PullDiagnostics bool
}

type SubdirWatchPatterns string

const (
	SubdirWatchPatternsOn   SubdirWatchPatterns = "on"
	SubdirWatchPatternsOff  SubdirWatchPatterns = "off"
	SubdirWatchPatternsAuto SubdirWatchPatterns = "auto"
)

type ImportShortcut string

const (
	BothShortcuts      ImportShortcut = "Both"
	LinkShortcut       ImportShortcut = "Link"
	DefinitionShortcut ImportShortcut = "Definition"
)

func (s ImportShortcut) ShowLinks() bool {
	return s == BothShortcuts || s == LinkShortcut
}

func (s ImportShortcut) ShowDefinition() bool {
	return s == BothShortcuts || s == DefinitionShortcut
}

// ImportsSourceEnum has legal values:
//
// - `off` to disable searching the file system for imports
// - `gopls` to use the metadata graph and module cache index
// - `goimports` for the old behavior, to be deprecated
type ImportsSourceEnum string

const (
	ImportsSourceOff       ImportsSourceEnum = "off"
	ImportsSourceGopls     ImportsSourceEnum = "gopls"
	ImportsSourceGoimports ImportsSourceEnum = "goimports"
)

type Matcher string

const (
	Fuzzy           Matcher = "Fuzzy"
	CaseInsensitive Matcher = "CaseInsensitive"
	CaseSensitive   Matcher = "CaseSensitive"
)

// A SymbolMatcher controls the matching of symbols for workspace/symbol
// requests.
type SymbolMatcher string

const (
	SymbolFuzzy           SymbolMatcher = "Fuzzy"
	SymbolFastFuzzy       SymbolMatcher = "FastFuzzy"
	SymbolCaseInsensitive SymbolMatcher = "CaseInsensitive"
	SymbolCaseSensitive   SymbolMatcher = "CaseSensitive"
)

// A SymbolStyle controls the formatting of symbols in workspace/symbol results.
type SymbolStyle string

const (
	// PackageQualifiedSymbols is package qualified symbols i.e.
	// "pkg.Foo.Field".
	PackageQualifiedSymbols SymbolStyle = "Package"
	// FullyQualifiedSymbols is fully qualified symbols, i.e.
	// "path/to/pkg.Foo.Field".
	FullyQualifiedSymbols SymbolStyle = "Full"
	// DynamicSymbols uses whichever qualifier results in the highest scoring
	// match for the given symbol query. Here a "qualifier" is any "/" or "."
	// delimited suffix of the fully qualified symbol. i.e. "to/pkg.Foo.Field" or
	// just "Foo.Field".
	DynamicSymbols SymbolStyle = "Dynamic"
)

// A SymbolScope controls the search scope for workspace/symbol requests.
type SymbolScope string

const (
	// WorkspaceSymbolScope matches symbols in workspace packages only.
	WorkspaceSymbolScope SymbolScope = "workspace"
	// AllSymbolScope matches symbols in any loaded package, including
	// dependencies.
	AllSymbolScope SymbolScope = "all"
)

type HoverKind string

const (
	SingleLine            HoverKind = "SingleLine"
	NoDocumentation       HoverKind = "NoDocumentation"
	SynopsisDocumentation HoverKind = "SynopsisDocumentation"
	FullDocumentation     HoverKind = "FullDocumentation"

	// Structured is a misguided experimental setting that returns a JSON
	// hover format. This setting should not be used, as it will be removed in a
	// future release of gopls.
	Structured HoverKind = "Structured"
)

type VulncheckMode string

const (
	// Disable vulnerability analysis.
	ModeVulncheckOff VulncheckMode = "Off"
	// In Imports mode, `gopls` will report vulnerabilities that affect packages
	// directly and indirectly used by the analyzed main module.
	ModeVulncheckImports VulncheckMode = "Imports"

	// TODO: VulncheckRequire, VulncheckCallgraph
)

type DiagnosticsTrigger string

const (
	// Trigger diagnostics on file edit and save. (default)
	DiagnosticsOnEdit DiagnosticsTrigger = "Edit"
	// Trigger diagnostics only on file save. Events like initial workspace load
	// or configuration change will still trigger diagnostics.
	DiagnosticsOnSave DiagnosticsTrigger = "Save"
	// TODO: support "Manual"?
)

type CounterPath = telemetry.CounterPath

// Set updates *Options based on the provided JSON value:
// null, bool, string, number, array, or object.
//
// The applied result describes settings that were applied. Each CounterPath
// contains at least the name of the setting, but may also include sub-setting
// names for settings that are themselves maps, and/or a non-empty bucket name
// when bucketing is desirable.
//
// On failure, it returns one or more non-nil errors.
func (o *Options) Set(value any) (applied []CounterPath, errs []error) {
	switch value := value.(type) {
	case nil:
	case map[string]any:
		seen := make(map[string]struct{})
		for name, value := range value {
			// Use only the last segment of a dotted name such as
			// ui.navigation.symbolMatcher. The other segments
			// are discarded, even without validation (!).
			// (They are supported to enable hierarchical names
			// in the VS Code graphical configuration UI.)
			split := strings.Split(name, ".")
			name = split[len(split)-1]

			if _, ok := seen[name]; ok {
				errs = append(errs, fmt.Errorf("duplicate value for %s", name))
			}
			seen[name] = struct{}{}

			paths, err := o.setOne(name, value)
			if err != nil {
				err := fmt.Errorf("setting option %q: %w", name, err)
				errs = append(errs, err)
			}
			_, soft := err.(*SoftError)
			if err == nil || soft {
				if len(paths) == 0 {
					path := CounterPath{name, ""}
					applied = append(applied, path)
				} else {
					for _, subpath := range paths {
						path := append(CounterPath{name}, subpath...)
						applied = append(applied, path)
					}
				}
			}
		}
	default:
		errs = append(errs, fmt.Errorf("invalid options type %T (want JSON null or object)", value))
	}
	return applied, errs
}

func (o *Options) ForClientCapabilities(clientInfo *protocol.ClientInfo, caps protocol.ClientCapabilities) {
	if clientInfo != nil {
		o.ClientInfo = *clientInfo
	}
	if caps.Workspace.WorkspaceEdit != nil {
		o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations
	}
	// Check if the client supports snippets in completion items.
	if c := caps.TextDocument.Completion; c.CompletionItem.SnippetSupport {
		o.InsertTextFormat = protocol.SnippetTextFormat
	}
	o.InsertReplaceSupported = caps.TextDocument.Completion.CompletionItem.InsertReplaceSupport
	if caps.Window.ShowDocument != nil {
		o.ShowDocumentSupported = caps.Window.ShowDocument.Support
	}
	// Check if the client supports configuration messages.
	o.ConfigurationSupported = caps.Workspace.Configuration
	o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
	o.DynamicRegistrationSemanticTokensSupported = caps.TextDocument.SemanticTokens.DynamicRegistration
	o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration
	o.RelativePatternsSupported = caps.Workspace.DidChangeWatchedFiles.RelativePatternSupport

	// Check which types of content format are supported by this client.
	if hover := caps.TextDocument.Hover; hover != nil && len(hover.ContentFormat) > 0 {
		o.PreferredContentFormat = hover.ContentFormat[0]
	}
	// Check if the client supports only line folding.

	if fr := caps.TextDocument.FoldingRange; fr != nil {
		o.LineFoldingOnly = fr.LineFoldingOnly
	}
	// Check if the client supports hierarchical document symbols.
	o.HierarchicalDocumentSymbolSupport = caps.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport

	// Client's semantic tokens
	o.SemanticTypes = caps.TextDocument.SemanticTokens.TokenTypes
	o.SemanticMods = caps.TextDocument.SemanticTokens.TokenModifiers
	// we don't need Requests, as we support full functionality
	// we don't need Formats, as there is only one, for now

	// Check if the client supports diagnostic related information.
	o.RelatedInformationSupported = caps.TextDocument.PublishDiagnostics.RelatedInformation
	// Check if the client completion support includes tags (preferred) or deprecation
	if caps.TextDocument.Completion.CompletionItem.TagSupport != nil &&
		caps.TextDocument.Completion.CompletionItem.TagSupport.ValueSet != nil {
		o.CompletionTags = true
	} else if caps.TextDocument.Completion.CompletionItem.DeprecatedSupport {
		o.CompletionDeprecated = true
	}

	// Check if the client supports code actions resolving.
	if caps.TextDocument.CodeAction.DataSupport && caps.TextDocument.CodeAction.ResolveSupport != nil {
		o.CodeActionResolveOptions = caps.TextDocument.CodeAction.ResolveSupport.Properties
	}

	// Client experimental capabilities.
	if experimental, ok := caps.Experimental.(map[string]any); ok {
		if formats, ok := experimental["progressMessageStyles"].([]any); ok {
			o.SupportedWorkDoneProgressFormats = make(map[WorkDoneProgressStyle]bool, len(formats))
			for _, f := range formats {
				o.SupportedWorkDoneProgressFormats[WorkDoneProgressStyle(f.(string))] = true
			}
		}
	}
}

var codec = frob.CodecFor[*Options]()

func (o *Options) Clone() *Options {
	data := codec.Encode(o)
	var clone *Options
	codec.Decode(data, &clone)
	return clone
}

// validateDirectoryFilter validates if the filter string
// - is not empty
// - start with either + or -
// - doesn't contain currently unsupported glob operators: *, ?
func validateDirectoryFilter(ifilter string) (string, error) {
	filter := fmt.Sprint(ifilter)
	if filter == "" || (filter[0] != '+' && filter[0] != '-') {
		return "", fmt.Errorf("invalid filter %v, must start with + or -", filter)
	}
	segs := strings.Split(filter[1:], "/")
	unsupportedOps := [...]string{"?", "*"}
	for _, seg := range segs {
		if seg != "**" {
			for _, op := range unsupportedOps {
				if strings.Contains(seg, op) {
					return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue", filter, op)
				}
			}
		}
	}

	return strings.TrimRight(filepath.FromSlash(filter), "/"), nil
}

// setOne updates a field of o based on the name and value.
//
// The applied result describes the counter values to be updated as a result of
// the applied setting. If the result is nil, the default counter for this
// setting should be updated.
//
// For example, if the setting name is "foo",
//   - If applied is nil, update the count for "foo".
//   - If applied is []CounterPath{{"bucket"}}, update the count for
//     foo:bucket.
//   - If applied is []CounterPath{{"a","b"}, {"c","d"}}, update foo/a:b and
//     foo/c:d.
//
// It returns an error if the value was invalid or duplicate.
// It is the caller's responsibility to augment the error with 'name'.
func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error) {
	switch name {
	case "env":
		env, ok := value.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("invalid type %T (want JSON object)", value)
		}
		if o.Env == nil {
			o.Env = make(map[string]string)
		}
		for k, v := range env {
			// For historic compatibility, we accept int too (e.g. CGO_ENABLED=1).
			switch v.(type) {
			case string, int:
				o.Env[k] = fmt.Sprint(v)
			default:
				return nil, fmt.Errorf("invalid map value %T (want string)", v)
			}
		}
		return nil, nil

	case "buildFlags":
		return nil, setStringSlice(&o.BuildFlags, value)

	case "directoryFilters":
		filterStrings, err := asStringSlice(value)
		if err != nil {
			return nil, err
		}
		var filters []string
		for _, filterStr := range filterStrings {
			filter, err := validateDirectoryFilter(filterStr)
			if err != nil {
				return nil, err
			}
			filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/"))
		}
		o.DirectoryFilters = filters
		return nil, nil

	case "workspaceFiles":
		return nil, setStringSlice(&o.WorkspaceFiles, value)
	case "completionDocumentation":
		return setBool(&o.CompletionDocumentation, value)
	case "usePlaceholders":
		return setBool(&o.UsePlaceholders, value)
	case "deepCompletion":
		return setBool(&o.DeepCompletion, value)
	case "completeUnimported":
		return setBool(&o.CompleteUnimported, value)
	case "completionBudget":
		return nil, setDuration(&o.CompletionBudget, value)
	case "importsSource":
		return setEnum(&o.ImportsSource, value,
			ImportsSourceOff,
			ImportsSourceGopls,
			ImportsSourceGoimports)
	case "matcher":
		return setEnum(&o.Matcher, value,
			Fuzzy,
			CaseSensitive,
			CaseInsensitive)

	case "symbolMatcher":
		return setEnum(&o.SymbolMatcher, value,
			SymbolFuzzy,
			SymbolFastFuzzy,
			SymbolCaseInsensitive,
			SymbolCaseSensitive)

	case "symbolStyle":
		return setEnum(&o.SymbolStyle, value,
			FullyQualifiedSymbols,
			PackageQualifiedSymbols,
			DynamicSymbols)

	case "symbolScope":
		return setEnum(&o.SymbolScope, value,
			WorkspaceSymbolScope,
			AllSymbolScope)

	case "hoverKind":
		// TODO(rfindley): reinstate the deprecation of Structured hover by making
		// it a warning in gopls v0.N+1, and removing it in gopls v0.N+2.
		return setEnum(&o.HoverKind, value,
			NoDocumentation,
			SingleLine,
			SynopsisDocumentation,
			FullDocumentation,
			Structured,
		)

	case "linkTarget":
		return nil, setString(&o.LinkTarget, value)

	case "linksInHover":
		switch value {
		case false:
			o.LinksInHover = LinksInHover_None
		case true:
			o.LinksInHover = LinksInHover_LinkTarget
		case "gopls":
			o.LinksInHover = LinksInHover_Gopls
		default:
			return nil, fmt.Errorf(`invalid value %s; expect false, true, or "gopls"`, value)
		}
		return nil, nil

	case "importShortcut":
		return setEnum(&o.ImportShortcut, value,
			BothShortcuts,
			LinkShortcut,
			DefinitionShortcut)

	case "analyses":
		counts, err := setBoolMap(&o.Analyses, value)
		if err != nil {
			return nil, err
		}
		if o.Analyses["fieldalignment"] {
			return counts, &SoftError{"the 'fieldalignment' analyzer was removed in gopls/v0.17.0; instead, hover over struct fields to see size/offset information (https://go.dev/issue/66861)"}
		}
		return counts, nil

	case "hints":
		return setBoolMap(&o.Hints, value)

	case "annotations":
		return setAnnotationMap(&o.Annotations, value)

	case "vulncheck":
		return setEnum(&o.Vulncheck, value,
			ModeVulncheckOff,
			ModeVulncheckImports)

	case "codelenses", "codelens":
		lensOverrides, err := asBoolMap[CodeLensSource](value)
		if err != nil {
			return nil, err
		}
		if o.Codelenses == nil {
			o.Codelenses = make(map[CodeLensSource]bool)
		}
		o.Codelenses = maps.Clone(o.Codelenses)
		maps.Copy(o.Codelenses, lensOverrides)

		var counts []CounterPath
		for k, v := range lensOverrides {
			counts = append(counts, CounterPath{string(k), fmt.Sprint(v)})
		}

		if name == "codelens" {
			return counts, deprecatedError("codelenses")
		}
		return counts, nil

	case "staticcheck":
		o.StaticcheckProvided = true
		return setBool(&o.Staticcheck, value)

	case "local":
		return nil, setString(&o.Local, value)

	case "verboseOutput":
		return setBool(&o.VerboseOutput, value)

	case "verboseWorkDoneProgress":
		return setBool(&o.VerboseWorkDoneProgress, value)

	case "showBugReports":
		return setBool(&o.ShowBugReports, value)

	case "gofumpt":
		return setBool(&o.Gofumpt, value)

	case "completeFunctionCalls":
		return setBool(&o.CompleteFunctionCalls, value)

	case "semanticTokens":
		return setBool(&o.SemanticTokens, value)

	// TODO(hxjiang): deprecate noSemanticString and noSemanticNumber.
	case "noSemanticString":
		counts, err := setBool(&o.NoSemanticString, value)
		if err != nil {
			return nil, err
		}
		return counts, &SoftError{"noSemanticString setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being)."}

	case "noSemanticNumber":
		counts, err := setBool(&o.NoSemanticNumber, value)
		if err != nil {
			return nil, err
		}
		return counts, &SoftError{"noSemanticNumber setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being)."}

	case "semanticTokenTypes":
		return setBoolMap(&o.SemanticTokenTypes, value)

	case "semanticTokenModifiers":
		return setBoolMap(&o.SemanticTokenModifiers, value)

	case "expandWorkspaceToModule":
		// See golang/go#63536: we can consider deprecating
		// expandWorkspaceToModule, but probably need to change the default
		// behavior in that case to *not* expand to the module.
		return setBool(&o.ExpandWorkspaceToModule, value)

	case "experimentalPostfixCompletions":
		return setBool(&o.ExperimentalPostfixCompletions, value)

	case "templateExtensions":
		switch value := value.(type) {
		case []any:
			return nil, setStringSlice(&o.TemplateExtensions, value)
		case nil:
			o.TemplateExtensions = nil
		default:
			return nil, fmt.Errorf("unexpected type %T (want JSON array of string)", value)
		}
		return nil, nil

	case "diagnosticsDelay":
		return nil, setDuration(&o.DiagnosticsDelay, value)

	case "diagnosticsTrigger":
		return setEnum(&o.DiagnosticsTrigger, value,
			DiagnosticsOnEdit,
			DiagnosticsOnSave)

	case "analysisProgressReporting":
		return setBool(&o.AnalysisProgressReporting, value)

	case "standaloneTags":
		return nil, setStringSlice(&o.StandaloneTags, value)

	case "subdirWatchPatterns":
		return setEnum(&o.SubdirWatchPatterns, value,
			SubdirWatchPatternsOn,
			SubdirWatchPatternsOff,
			SubdirWatchPatternsAuto)

	case "reportAnalysisProgressAfter":
		return nil, setDuration(&o.ReportAnalysisProgressAfter, value)

	case "telemetryPrompt":
		return setBool(&o.TelemetryPrompt, value)

	case "linkifyShowMessage":
		return setBool(&o.LinkifyShowMessage, value)

	case "includeReplaceInWorkspace":
		return setBool(&o.IncludeReplaceInWorkspace, value)

	case "zeroConfig":
		return setBool(&o.ZeroConfig, value)

	case "pullDiagnostics":
		return setBool(&o.PullDiagnostics, value)

	// deprecated and renamed settings
	//
	// These should never be deleted: there is essentially no cost
	// to providing a better error message indefinitely; it's not
	// as if we would ever want to recycle the name of a setting.

	// renamed
	case "experimentalDisabledAnalyses":
		return nil, deprecatedError("analyses")

	case "disableDeepCompletion":
		return nil, deprecatedError("deepCompletion")

	case "disableFuzzyMatching":
		return nil, deprecatedError("fuzzyMatching")

	case "wantCompletionDocumentation":
		return nil, deprecatedError("completionDocumentation")

	case "wantUnimportedCompletions":
		return nil, deprecatedError("completeUnimported")

	case "fuzzyMatching":
		return nil, deprecatedError("matcher")

	case "caseSensitiveCompletion":
		return nil, deprecatedError("matcher")

	case "experimentalDiagnosticsDelay":
		return nil, deprecatedError("diagnosticsDelay")

	// deprecated

	case "allowImplicitNetworkAccess":
		return nil, deprecatedError("")

	case "memoryMode":
		return nil, deprecatedError("")

	case "tempModFile":
		return nil, deprecatedError("")

	case "experimentalWorkspaceModule":
		return nil, deprecatedError("")

	case "experimentalTemplateSupport":
		return nil, deprecatedError("")

	case "experimentalWatchedFileDelay":
		return nil, deprecatedError("")

	case "experimentalPackageCacheKey":
		return nil, deprecatedError("")

	case "allowModfileModifications":
		return nil, deprecatedError("")

	case "allExperiments":
		// golang/go#65548: this setting is a no-op, but we fail don't report it as
		// deprecated, since the nightly VS Code injects it.
		//
		// If, in the future, VS Code stops injecting this, we could theoretically
		// report an error here, but it also seems harmless to keep ignoring this
		// setting forever.
		return nil, nil

	case "experimentalUseInvalidMetadata":
		return nil, deprecatedError("")

	case "newDiff":
		return nil, deprecatedError("")

	case "wantSuggestedFixes":
		return nil, deprecatedError("")

	case "noIncrementalSync":
		return nil, deprecatedError("")

	case "watchFileChanges":
		return nil, deprecatedError("")

	case "go-diff":
		return nil, deprecatedError("")

	default:
		return nil, fmt.Errorf("unexpected setting")
	}
}

// EnabledSemanticTokenModifiers returns a map of modifiers to boolean.
func (o *Options) EnabledSemanticTokenModifiers() map[semtok.Modifier]bool {
	copy := make(map[semtok.Modifier]bool, len(o.SemanticTokenModifiers))
	for k, v := range o.SemanticTokenModifiers {
		copy[semtok.Modifier(k)] = v
	}
	return copy
}

// EnabledSemanticTokenTypes returns a map of types to boolean.
func (o *Options) EnabledSemanticTokenTypes() map[semtok.Type]bool {
	copy := make(map[semtok.Type]bool, len(o.SemanticTokenTypes))
	for k, v := range o.SemanticTokenTypes {
		copy[semtok.Type(k)] = v
	}
	if o.NoSemanticString {
		copy[semtok.TokString] = false
	}
	if o.NoSemanticNumber {
		copy[semtok.TokNumber] = false
	}
	return copy
}

// A SoftError is an error that does not affect the functionality of gopls.
type SoftError struct {
	msg string
}

func (e *SoftError) Error() string {
	return e.msg
}

// deprecatedError reports the current setting as deprecated.
// The optional replacement is suggested to the user.
func deprecatedError(replacement string) error {
	msg := "this setting is deprecated"
	if replacement != "" {
		msg = fmt.Sprintf("%s, use %q instead", msg, replacement)
	}
	return &SoftError{msg}
}

// setT() and asT() helpers: the setT forms write to the 'dest *T'
// variable only on success, to reduce boilerplate in Option.set.

func setBool(dest *bool, value any) ([]CounterPath, error) {
	b, err := asBool(value)
	if err != nil {
		return nil, err
	}
	*dest = b
	return []CounterPath{{fmt.Sprint(b)}}, nil
}

func asBool(value any) (bool, error) {
	b, ok := value.(bool)
	if !ok {
		return false, fmt.Errorf("invalid type %T (want bool)", value)
	}
	return b, nil
}

func setDuration(dest *time.Duration, value any) error {
	str, err := asString(value)
	if err != nil {
		return err
	}
	parsed, err := time.ParseDuration(str)
	if err != nil {
		return err
	}
	*dest = parsed
	return nil
}

func setAnnotationMap(dest *map[Annotation]bool, value any) ([]CounterPath, error) {
	all, err := asBoolMap[string](value)
	if err != nil {
		return nil, err
	}
	var counters []CounterPath
	// Default to everything enabled by default.
	m := make(map[Annotation]bool)
	for k, enabled := range all {
		var a Annotation
		cnts, err := setEnum(&a, k,
			Nil,
			Escape,
			Inline,
			Bounds)
		if err != nil {
			// In case of an error, process any legacy values.
			switch k {
			case "noEscape":
				m[Escape] = false
				return nil, fmt.Errorf(`"noEscape" is deprecated, set "Escape: false" instead`)

			case "noNilcheck":
				m[Nil] = false
				return nil, fmt.Errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`)

			case "noInline":
				m[Inline] = false
				return nil, fmt.Errorf(`"noInline" is deprecated, set "Inline: false" instead`)

			case "noBounds":
				m[Bounds] = false
				return nil, fmt.Errorf(`"noBounds" is deprecated, set "Bounds: false" instead`)

			default:
				return nil, err
			}
		}
		counters = append(counters, cnts...)
		m[a] = enabled
	}
	*dest = m
	return counters, nil
}

func setBoolMap[K ~string](dest *map[K]bool, value any) ([]CounterPath, error) {
	m, err := asBoolMap[K](value)
	if err != nil {
		return nil, err
	}
	*dest = m
	var counts []CounterPath
	for k, v := range m {
		counts = append(counts, CounterPath{string(k), fmt.Sprint(v)})
	}
	return counts, nil
}

func asBoolMap[K ~string](value any) (map[K]bool, error) {
	all, ok := value.(map[string]any)
	if !ok {
		return nil, fmt.Errorf("invalid type %T (want JSON object)", value)
	}
	m := make(map[K]bool)
	for a, enabled := range all {
		b, ok := enabled.(bool)
		if !ok {
			return nil, fmt.Errorf("invalid type %T for object field %q", enabled, a)
		}
		m[K(a)] = b
	}
	return m, nil
}

func setString(dest *string, value any) error {
	str, err := asString(value)
	if err != nil {
		return err
	}
	*dest = str
	return nil
}

func asString(value any) (string, error) {
	str, ok := value.(string)
	if !ok {
		return "", fmt.Errorf("invalid type %T (want string)", value)
	}
	return str, nil
}

func setStringSlice(dest *[]string, value any) error {
	slice, err := asStringSlice(value)
	if err != nil {
		return err
	}
	*dest = slice
	return nil
}

func asStringSlice(value any) ([]string, error) {
	array, ok := value.([]any)
	if !ok {
		return nil, fmt.Errorf("invalid type %T (want JSON array of string)", value)
	}
	var slice []string
	for _, elem := range array {
		str, ok := elem.(string)
		if !ok {
			return nil, fmt.Errorf("invalid array element type %T (want string)", elem)
		}
		slice = append(slice, str)
	}
	return slice, nil
}

func setEnum[S ~string](dest *S, value any, options ...S) ([]CounterPath, error) {
	enum, err := asEnum(value, options...)
	if err != nil {
		return nil, err
	}
	*dest = enum
	return []CounterPath{{string(enum)}}, nil
}

func asEnum[S ~string](value any, options ...S) (S, error) {
	str, err := asString(value)
	if err != nil {
		return "", err
	}
	for _, opt := range options {
		if strings.EqualFold(str, string(opt)) {
			return opt, nil
		}
	}
	return "", fmt.Errorf("invalid option %q for enum", str)
}
