// 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 cmd

import (
	"context"
	"flag"
	"fmt"

	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/settings"
)

// codelens implements the codelens verb for gopls.
type codelens struct {
	EditFlags
	app *application

	Exec bool `flag:"exec" help:"execute the first matching code lens"`
}

func (r *codelens) Name() string      { return "codelens" }
func (r *codelens) Parent() string    { return r.app.Name() }
func (r *codelens) Usage() string     { return "[codelens-flags] file[:line[:col]] [title]" }
func (r *codelens) ShortHelp() string { return "List or execute code lenses for a file" }
func (r *codelens) DetailedHelp(f *flag.FlagSet) {
	fmt.Fprint(f.Output(), `
The codelens command lists or executes code lenses for the specified
file, or line within a file. A code lens is a command associated with
a position in the code.

With an optional title argument, only code lenses matching that
title are considered.

By default, the codelens command lists the available lenses for the
specified file or line within a file, including the title and
title of the command. With the -exec flag, the first matching command
is executed, and its output is printed to stdout.

Example:

	$ gopls codelens a_test.go                     # list code lenses in a file
	$ gopls codelens a_test.go:10                  # list code lenses on line 10
	$ gopls codelens a_test.go "run test"          # list gopls.run_tests commands
	$ gopls codelens -exec a_test.go:10 "run test" # run a specific test

codelens-flags:
`)
	printFlagDefaults(f)
}

func (r *codelens) Run(ctx context.Context, args ...string) error {
	var filename, title string
	switch len(args) {
	case 0:
		return commandLineErrorf("codelens requires a file name")
	case 2:
		title = args[1]
		fallthrough
	case 1:
		filename = args[0]
	default:
		return commandLineErrorf("codelens expects at most two arguments")
	}

	r.app.editFlags = &r.EditFlags // in case a codelens perform an edit

	// Override the default setting for codelenses["test"], which is
	// off by default because VS Code has a superior client-side
	// implementation. But this client is not VS Code.
	// See golang.LensFuncs().
	origOptions := r.app.options
	r.app.options = func(opts *settings.Options) {
		if origOptions != nil {
			origOptions(opts)
		}
		if opts.Codelenses == nil {
			opts.Codelenses = make(map[settings.CodeLensSource]bool)
		}
		opts.Codelenses[settings.CodeLensTest] = true
	}

	cli, _, err := r.app.connect(ctx)
	if err != nil {
		return err
	}
	defer cli.terminate(ctx)

	filespan := parseSpan(filename)
	file, err := cli.openFile(ctx, filespan.URI())
	if err != nil {
		return err
	}
	loc, err := file.spanLocation(filespan)
	if err != nil {
		return err
	}

	p := protocol.CodeLensParams{
		TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
	}
	lenses, err := cli.server.CodeLens(ctx, &p)
	if err != nil {
		return err
	}

	for _, lens := range lenses {
		sp, err := file.rangeSpan(lens.Range)
		if err != nil {
			return nil
		}

		if title != "" && lens.Command.Title != title {
			continue // title was specified but does not match
		}
		if filespan.HasPosition() && !protocol.Intersect(loc.Range, lens.Range) {
			continue // position was specified but does not match
		}

		// -exec: run the first matching code lens.
		if r.Exec {
			_, err := executeCommand(ctx, cli.server, lens.Command)
			return err
		}

		// No -exec: list matching code lenses.
		fmt.Printf("%v: %q [%s]\n", sp, lens.Command.Title, lens.Command.Command)
	}

	if r.Exec {
		return fmt.Errorf("no code lens at %s with title %q", filespan, title)
	}
	return nil
}
