package rpc2

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/go-delve/delve/service"
	"github.com/go-delve/delve/service/api"
)

// RPCClient is a RPC service.Client.
type RPCClient struct {
	client *rpc.Client

	retValLoadCfg *api.LoadConfig
	eventsFn      func(*api.Event)
}

// Ensure the implementation satisfies the interface.
var _ service.Client = &RPCClient{}

// NewClient creates a new RPCClient.
func NewClient(addr string) *RPCClient {
	client, err := jsonrpc.Dial("tcp", addr)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	return newFromRPCClient(client)
}

func newFromRPCClient(client *rpc.Client) *RPCClient {
	c := &RPCClient{client: client}
	c.call("SetApiVersion", api.SetAPIVersionIn{APIVersion: 2}, &api.SetAPIVersionOut{})
	return c
}

// NewClientFromConn creates a new RPCClient from the given connection.
func NewClientFromConn(conn net.Conn) *RPCClient {
	return newFromRPCClient(jsonrpc.NewClient(conn))
}

func (c *RPCClient) GetVersion() *api.GetVersionOut {
	out := new(api.GetVersionOut)
	c.call("GetVersion", api.GetVersionIn{}, out)
	return out
}

func (c *RPCClient) ProcessPid() int {
	out := new(ProcessPidOut)
	c.call("ProcessPid", ProcessPidIn{}, out)
	return out.Pid
}

func (c *RPCClient) BuildID() string {
	out := new(BuildIDOut)
	c.call("BuildID", BuildIDIn{}, out)
	return out.BuildID
}

func (c *RPCClient) LastModified() time.Time {
	out := new(LastModifiedOut)
	c.call("LastModified", LastModifiedIn{}, out)
	return out.Time
}

func (c *RPCClient) Detach(kill bool) error {
	defer c.client.Close()
	out := new(DetachOut)
	return c.call("Detach", DetachIn{kill}, out)
}

func (c *RPCClient) Restart(rebuild bool) ([]api.DiscardedBreakpoint, error) {
	out := new(RestartOut)
	err := c.call("Restart", RestartIn{"", false, nil, false, rebuild, [3]string{}}, out)
	return out.DiscardedBreakpoints, err
}

func (c *RPCClient) RestartFrom(rerecord bool, pos string, resetArgs bool, newArgs []string, newRedirects [3]string, rebuild bool) ([]api.DiscardedBreakpoint, error) {
	out := new(RestartOut)
	err := c.call("Restart", RestartIn{pos, resetArgs, newArgs, rerecord, rebuild, newRedirects}, out)
	return out.DiscardedBreakpoints, err
}

func (c *RPCClient) GetState() (*api.DebuggerState, error) {
	var out StateOut
	err := c.call("State", StateIn{NonBlocking: false}, &out)
	return out.State, err
}

func (c *RPCClient) GetStateNonBlocking() (*api.DebuggerState, error) {
	var out StateOut
	err := c.call("State", StateIn{NonBlocking: true}, &out)
	return out.State, err
}

func (c *RPCClient) Continue() <-chan *api.DebuggerState {
	return c.continueDir(api.Continue)
}

func (c *RPCClient) Rewind() <-chan *api.DebuggerState {
	return c.continueDir(api.Rewind)
}

func (c *RPCClient) DirectionCongruentContinue() <-chan *api.DebuggerState {
	return c.continueDir(api.DirectionCongruentContinue)
}

func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState {
	ch := make(chan *api.DebuggerState)
	go func() {
		for {
			out := new(CommandOut)
			err := c.callWhileDrainingEvents("Command", &api.DebuggerCommand{Name: cmd, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out)
			state := out.State
			if err != nil {
				state.Err = err
			}
			if state.Exited {
				// Error types apparently cannot be marshalled by Go correctly. Must reset error here.
				//lint:ignore ST1005 backwards compatibility
				state.Err = fmt.Errorf("Process %d has exited with status %d", c.ProcessPid(), state.ExitStatus)
			}
			ch <- &state
			if err != nil || state.Exited {
				close(ch)
				return
			}

			isbreakpoint := false
			istracepoint := true
			for i := range state.Threads {
				if state.Threads[i].Breakpoint != nil {
					isbreakpoint = true
					istracepoint = istracepoint && (state.Threads[i].Breakpoint.Tracepoint || state.Threads[i].Breakpoint.TraceReturn)
				}
			}

			if !isbreakpoint || !istracepoint {
				close(ch)
				return
			}
		}
	}()
	return ch
}

func (c *RPCClient) drainEvents() <-chan struct{} {
	done := make(chan struct{})
	if c.eventsFn == nil {
		close(done)
		return done
	}
	go func() {
		defer close(done)
		for {
			out := new(GetEventsOut)
			err := c.call("GetEvents", &GetEventsIn{}, &out)
			if err != nil {
				break
			}
			for _, event := range out.Events {
				c.eventsFn(&event)
				if event.Kind == api.EventStopped {
					return
				}
			}
		}
	}()
	return done
}

func (c *RPCClient) Next() (*api.DebuggerState, error) {
	var out CommandOut
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.Next, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) ReverseNext() (*api.DebuggerState, error) {
	var out CommandOut
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.ReverseNext, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) Step() (*api.DebuggerState, error) {
	var out CommandOut
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.Step, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) ReverseStep() (*api.DebuggerState, error) {
	var out CommandOut
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.ReverseStep, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) StepOut() (*api.DebuggerState, error) {
	var out CommandOut
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.StepOut, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) ReverseStepOut() (*api.DebuggerState, error) {
	var out CommandOut
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.ReverseStepOut, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) Call(goroutineID int64, expr string, unsafe bool) (*api.DebuggerState, error) {
	var out CommandOut
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) StepInstruction(skipCalls bool) (*api.DebuggerState, error) {
	var out CommandOut
	name := api.StepInstruction
	if skipCalls {
		name = api.NextInstruction
	}
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: name, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) ReverseStepInstruction(skipCalls bool) (*api.DebuggerState, error) {
	var out CommandOut
	name := api.ReverseStepInstruction
	if skipCalls {
		name = api.ReverseNextInstruction
	}
	err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: name, WithEvents: c.eventsFn != nil}, &out)
	return &out.State, err
}

func (c *RPCClient) SwitchThread(threadID int) (*api.DebuggerState, error) {
	var out CommandOut
	cmd := api.DebuggerCommand{
		Name:     api.SwitchThread,
		ThreadID: threadID,
	}
	err := c.call("Command", cmd, &out)
	return &out.State, err
}

func (c *RPCClient) SwitchGoroutine(goroutineID int64) (*api.DebuggerState, error) {
	var out CommandOut
	cmd := api.DebuggerCommand{
		Name:        api.SwitchGoroutine,
		GoroutineID: goroutineID,
	}
	err := c.call("Command", cmd, &out)
	return &out.State, err
}

func (c *RPCClient) Halt() (*api.DebuggerState, error) {
	var out CommandOut
	err := c.call("Command", api.DebuggerCommand{Name: api.Halt}, &out)
	return &out.State, err
}

func (c *RPCClient) GetBufferedTracepoints() ([]api.TracepointResult, error) {
	var out GetBufferedTracepointsOut
	err := c.call("GetBufferedTracepoints", GetBufferedTracepointsIn{}, &out)
	return out.TracepointResults, err
}

func (c *RPCClient) GetBreakpoint(id int) (*api.Breakpoint, error) {
	var out GetBreakpointOut
	err := c.call("GetBreakpoint", GetBreakpointIn{id, ""}, &out)
	return &out.Breakpoint, err
}

func (c *RPCClient) GetBreakpointByName(name string) (*api.Breakpoint, error) {
	var out GetBreakpointOut
	err := c.call("GetBreakpoint", GetBreakpointIn{0, name}, &out)
	return &out.Breakpoint, err
}

// CreateBreakpoint will send a request to the RPC server to create a breakpoint.
// Please refer to the documentation for `Debugger.CreateBreakpoint` for a description of how
// the requested breakpoint parameters are interpreted and used:
// https://pkg.go.dev/github.com/go-delve/delve/service/debugger#Debugger.CreateBreakpoint
func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoint, error) {
	var out CreateBreakpointOut
	err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, "", nil, false}, &out)
	return &out.Breakpoint, err
}

// CreateBreakpointWithExpr is like CreateBreakpoint but will also set a
// location expression to be used to restore the breakpoint after it is
// disabled.
func (c *RPCClient) CreateBreakpointWithExpr(breakPoint *api.Breakpoint, locExpr string, substitutePathRules [][2]string, suspended bool) (*api.Breakpoint, error) {
	var out CreateBreakpointOut
	err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, locExpr, substitutePathRules, suspended}, &out)
	return &out.Breakpoint, err
}

func (c *RPCClient) CreateEBPFTracepoint(fnName string) error {
	var out CreateEBPFTracepointOut
	return c.call("CreateEBPFTracepoint", CreateEBPFTracepointIn{FunctionName: fnName}, &out)
}

func (c *RPCClient) CreateWatchpoint(scope api.EvalScope, expr string, wtype api.WatchType) (*api.Breakpoint, error) {
	var out CreateWatchpointOut
	err := c.call("CreateWatchpoint", CreateWatchpointIn{scope, expr, wtype}, &out)
	return out.Breakpoint, err
}

func (c *RPCClient) ListBreakpoints(all bool) ([]*api.Breakpoint, error) {
	var out ListBreakpointsOut
	err := c.call("ListBreakpoints", ListBreakpointsIn{all}, &out)
	return out.Breakpoints, err
}

func (c *RPCClient) ClearBreakpoint(id int) (*api.Breakpoint, error) {
	var out ClearBreakpointOut
	err := c.call("ClearBreakpoint", ClearBreakpointIn{id, ""}, &out)
	return out.Breakpoint, err
}

func (c *RPCClient) ClearBreakpointByName(name string) (*api.Breakpoint, error) {
	var out ClearBreakpointOut
	err := c.call("ClearBreakpoint", ClearBreakpointIn{0, name}, &out)
	return out.Breakpoint, err
}

func (c *RPCClient) ToggleBreakpoint(id int) (*api.Breakpoint, error) {
	var out ToggleBreakpointOut
	err := c.call("ToggleBreakpoint", ToggleBreakpointIn{id, ""}, &out)
	return out.Breakpoint, err
}

func (c *RPCClient) ToggleBreakpointByName(name string) (*api.Breakpoint, error) {
	var out ToggleBreakpointOut
	err := c.call("ToggleBreakpoint", ToggleBreakpointIn{0, name}, &out)
	return out.Breakpoint, err
}

func (c *RPCClient) AmendBreakpoint(bp *api.Breakpoint) error {
	out := new(AmendBreakpointOut)
	err := c.call("AmendBreakpoint", AmendBreakpointIn{*bp}, out)
	return err
}

func (c *RPCClient) CancelNext() error {
	var out CancelNextOut
	return c.call("CancelNext", CancelNextIn{}, &out)
}

func (c *RPCClient) ListThreads() ([]*api.Thread, error) {
	var out ListThreadsOut
	err := c.call("ListThreads", ListThreadsIn{}, &out)
	return out.Threads, err
}

func (c *RPCClient) GetThread(id int) (*api.Thread, error) {
	var out GetThreadOut
	err := c.call("GetThread", GetThreadIn{id}, &out)
	return out.Thread, err
}

func (c *RPCClient) EvalVariable(scope api.EvalScope, expr string, cfg api.LoadConfig) (*api.Variable, error) {
	var out EvalOut
	err := c.call("Eval", EvalIn{scope, expr, &cfg}, &out)
	return out.Variable, err
}

func (c *RPCClient) SetVariable(scope api.EvalScope, symbol, value string) error {
	out := new(SetOut)
	return c.call("Set", SetIn{scope, symbol, value}, out)
}

func (c *RPCClient) ListSources(filter string) ([]string, error) {
	sources := new(ListSourcesOut)
	err := c.call("ListSources", ListSourcesIn{filter}, sources)
	return sources.Sources, err
}

func (c *RPCClient) ListFunctions(filter string, TraceFollow int) ([]string, error) {
	funcs := new(ListFunctionsOut)
	err := c.call("ListFunctions", ListFunctionsIn{filter, TraceFollow}, funcs)
	return funcs.Funcs, err
}

func (c *RPCClient) ListTypes(filter string) ([]string, error) {
	types := new(ListTypesOut)
	err := c.call("ListTypes", ListTypesIn{filter}, types)
	return types.Types, err
}

func (c *RPCClient) ListPackageVariables(filter string, cfg api.LoadConfig) ([]api.Variable, error) {
	var out ListPackageVarsOut
	err := c.call("ListPackageVars", ListPackageVarsIn{filter, cfg}, &out)
	return out.Variables, err
}

func (c *RPCClient) ListPackagesBuildInfo(filter string, includeFiles bool) ([]api.PackageBuildInfo, error) {
	var out ListPackagesBuildInfoOut
	err := c.call("ListPackagesBuildInfo", ListPackagesBuildInfoIn{Filter: filter, IncludeFiles: includeFiles}, &out)
	return out.List, err
}

func (c *RPCClient) ListLocalVariables(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error) {
	var out ListLocalVarsOut
	err := c.call("ListLocalVars", ListLocalVarsIn{scope, cfg}, &out)
	return out.Variables, err
}

func (c *RPCClient) ListThreadRegisters(threadID int, includeFp bool) (api.Registers, error) {
	out := new(ListRegistersOut)
	err := c.call("ListRegisters", ListRegistersIn{ThreadID: threadID, IncludeFp: includeFp, Scope: nil}, out)
	return out.Regs, err
}

func (c *RPCClient) ListScopeRegisters(scope api.EvalScope, includeFp bool) (api.Registers, error) {
	out := new(ListRegistersOut)
	err := c.call("ListRegisters", ListRegistersIn{ThreadID: 0, IncludeFp: includeFp, Scope: &scope}, out)
	return out.Regs, err
}

func (c *RPCClient) ListFunctionArgs(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error) {
	var out ListFunctionArgsOut
	err := c.call("ListFunctionArgs", ListFunctionArgsIn{scope, cfg}, &out)
	return out.Args, err
}

func (c *RPCClient) ListGoroutines(start, count int) ([]*api.Goroutine, int, error) {
	var out ListGoroutinesOut
	err := c.call("ListGoroutines", ListGoroutinesIn{start, count, nil, api.GoroutineGroupingOptions{}, nil}, &out)
	return out.Goroutines, out.Nextg, err
}

func (c *RPCClient) ListGoroutinesWithFilter(start, count int, filters []api.ListGoroutinesFilter, group *api.GoroutineGroupingOptions, scope *api.EvalScope) ([]*api.Goroutine, []api.GoroutineGroup, int, bool, error) {
	if group == nil {
		group = &api.GoroutineGroupingOptions{}
	}
	var out ListGoroutinesOut
	err := c.call("ListGoroutines", ListGoroutinesIn{start, count, filters, *group, scope}, &out)
	return out.Goroutines, out.Groups, out.Nextg, out.TooManyGroups, err
}

func (c *RPCClient) Stacktrace(goroutineId int64, depth, skip int, opts api.StacktraceOptions, cfg *api.LoadConfig) ([]api.Stackframe, error) {
	var out StacktraceOut
	err := c.call("Stacktrace", StacktraceIn{goroutineId, depth, false, false, opts, cfg, skip}, &out)
	return out.Locations, err
}

func (c *RPCClient) Ancestors(goroutineID int64, numAncestors int, depth int) ([]api.Ancestor, error) {
	var out AncestorsOut
	err := c.call("Ancestors", AncestorsIn{goroutineID, numAncestors, depth}, &out)
	return out.Ancestors, err
}

func (c *RPCClient) AttachedToExistingProcess() bool {
	out := new(AttachedToExistingProcessOut)
	c.call("AttachedToExistingProcess", AttachedToExistingProcessIn{}, out)
	return out.Answer
}

func (c *RPCClient) FindLocation(scope api.EvalScope, loc string, findInstructions bool, substitutePathRules [][2]string) ([]api.Location, string, error) {
	var out FindLocationOut
	err := c.call("FindLocation", FindLocationIn{scope, loc, !findInstructions, substitutePathRules}, &out)
	return out.Locations, out.SubstituteLocExpr, err
}

// DisassembleRange disassembles code between startPC and endPC
func (c *RPCClient) DisassembleRange(scope api.EvalScope, startPC, endPC uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error) {
	var out DisassembleOut
	err := c.call("Disassemble", DisassembleIn{scope, startPC, endPC, flavour}, &out)
	return out.Disassemble, err
}

// DisassemblePC disassembles function containing pc
func (c *RPCClient) DisassemblePC(scope api.EvalScope, pc uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error) {
	var out DisassembleOut
	err := c.call("Disassemble", DisassembleIn{scope, pc, 0, flavour}, &out)
	return out.Disassemble, err
}

// Recorded returns true if the debugger target is a recording.
func (c *RPCClient) Recorded() bool {
	out := new(RecordedOut)
	c.call("Recorded", RecordedIn{}, out)
	return out.Recorded
}

// TraceDirectory returns the path to the trace directory for a recording.
func (c *RPCClient) TraceDirectory() (string, error) {
	var out RecordedOut
	err := c.call("Recorded", RecordedIn{}, &out)
	return out.TraceDirectory, err
}

// Checkpoint sets a checkpoint at the current position.
func (c *RPCClient) Checkpoint(where string) (checkpointID int, err error) {
	var out CheckpointOut
	err = c.call("Checkpoint", CheckpointIn{where}, &out)
	return out.ID, err
}

// ListCheckpoints gets all checkpoints.
func (c *RPCClient) ListCheckpoints() ([]api.Checkpoint, error) {
	var out ListCheckpointsOut
	err := c.call("ListCheckpoints", ListCheckpointsIn{}, &out)
	return out.Checkpoints, err
}

// ClearCheckpoint removes a checkpoint
func (c *RPCClient) ClearCheckpoint(id int) error {
	var out ClearCheckpointOut
	err := c.call("ClearCheckpoint", ClearCheckpointIn{id}, &out)
	return err
}

func (c *RPCClient) SetReturnValuesLoadConfig(cfg *api.LoadConfig) {
	c.retValLoadCfg = cfg
}

func (c *RPCClient) SetEventsFn(eventsFn func(*api.Event)) {
	c.eventsFn = eventsFn
}

func (c *RPCClient) FunctionReturnLocations(fnName string) ([]uint64, error) {
	var out FunctionReturnLocationsOut
	err := c.call("FunctionReturnLocations", FunctionReturnLocationsIn{fnName}, &out)
	return out.Addrs, err
}

func (c *RPCClient) IsMulticlient() bool {
	var out IsMulticlientOut
	c.call("IsMulticlient", IsMulticlientIn{}, &out)
	return out.IsMulticlient
}

func (c *RPCClient) Disconnect(cont bool) error {
	if cont {
		out := new(CommandOut)
		c.client.Go("RPCServer.Command", &api.DebuggerCommand{Name: api.Continue, ReturnInfoLoadConfig: c.retValLoadCfg}, &out, nil)
	}
	return c.client.Close()
}

func (c *RPCClient) ListDynamicLibraries() ([]api.Image, bool, error) {
	var out ListDynamicLibrariesOut
	c.call("ListDynamicLibraries", ListDynamicLibrariesIn{}, &out)
	return out.List, out.ExecutableTrimpath, nil
}

func (c *RPCClient) ExamineMemory(address uint64, count int) ([]byte, bool, error) {
	out := &ExaminedMemoryOut{}

	err := c.call("ExamineMemory", ExamineMemoryIn{Length: count, Address: address}, out)
	if err != nil {
		return nil, false, err
	}
	return out.Mem, out.IsLittleEndian, nil
}

func (c *RPCClient) StopRecording() error {
	return c.call("StopRecording", StopRecordingIn{}, &StopRecordingOut{})
}

func (c *RPCClient) CoreDumpStart(dest string) (api.DumpState, error) {
	out := &DumpStartOut{}
	err := c.call("DumpStart", DumpStartIn{Destination: dest}, out)
	return out.State, err
}

func (c *RPCClient) CoreDumpWait(msec int) api.DumpState {
	out := &DumpWaitOut{}
	_ = c.call("DumpWait", DumpWaitIn{Wait: msec}, out)
	return out.State
}

func (c *RPCClient) CoreDumpCancel() error {
	out := &DumpCancelOut{}
	return c.call("DumpCancel", DumpCancelIn{}, out)
}

// ListTargets returns the current list of debug targets.
func (c *RPCClient) ListTargets() ([]api.Target, error) {
	out := &ListTargetsOut{}
	err := c.call("ListTargets", ListTargetsIn{}, out)
	return out.Targets, err
}

// FollowExec enabled or disabled follow exec mode. When follow exec is
// enabled Delve will automatically attach to new subprocesses with a
// command line matched by regex, if regex is nil all new subprocesses are
// automatically debugged.
func (c *RPCClient) FollowExec(v bool, regex string) error {
	out := &FollowExecOut{}
	err := c.call("FollowExec", FollowExecIn{Enable: v, Regex: regex}, out)
	return err
}

// FollowExecEnabled returns true if follow exec mode is enabled.
func (c *RPCClient) FollowExecEnabled() bool {
	out := &FollowExecEnabledOut{}
	_ = c.call("FollowExecEnabled", FollowExecEnabledIn{}, out)
	return out.Enabled
}

func (c *RPCClient) SetDebugInfoDirectories(v []string) error {
	return c.call("DebugInfoDirectories", DebugInfoDirectoriesIn{Set: true, List: v}, &DebugInfoDirectoriesOut{})
}

func (c *RPCClient) GetDebugInfoDirectories() ([]string, error) {
	out := &DebugInfoDirectoriesOut{}
	err := c.call("DebugInfoDirectories", DebugInfoDirectoriesIn{Set: false, List: nil}, out)
	return out.List, err
}

type goListEntry struct {
	Dir        string
	ImportPath string
	Name       string
	Module     *goListModule
}

type goListModule struct {
	Path string
}

// MakeGuessSusbtitutePathIn returns a mapping from modules to client
// directories using "go list".
func MakeGuessSusbtitutePathIn() (*api.GuessSubstitutePathIn, error) {
	cmd := exec.Command("go", "list", "--json", "all")
	buf, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("error running go list %v: output %q", err, string(buf))
	}
	importPathOfMainPackage := ""
	importPathOfMainPackageOk := true
	mod2dir := make(map[string]string)
	d := json.NewDecoder(bytes.NewReader(buf))
	for d.More() {
		var e goListEntry
		err := d.Decode(&e)
		if err != nil {
			return nil, err
		}
		if e.Module == nil {
			continue
		}
		if !strings.HasPrefix(e.ImportPath, e.Module.Path) {
			continue
		}
		pkgWithoutModule := e.ImportPath[len(e.Module.Path):]
		elems := 0
		for _, c := range pkgWithoutModule {
			if c == '/' {
				elems++
			}
		}
		dir := e.Dir
		for i := 0; i < elems; i++ {
			dir = filepath.Dir(dir)
		}
		if mod2dir[e.Module.Path] != "" && mod2dir[e.Module.Path] != dir {
			return nil, fmt.Errorf("could not determine path for module %s (got %q and %q)", e.Module.Path, mod2dir[e.Module.Path], dir)
		}
		mod2dir[e.Module.Path] = dir
		if e.Name == "main" {
			if importPathOfMainPackage != "" && importPathOfMainPackage != e.ImportPath {
				importPathOfMainPackageOk = false
			}
			importPathOfMainPackage = e.ImportPath
		}
	}
	buf, err = exec.Command("go", "env", "GOROOT").Output()
	if err != nil {
		return nil, err
	}
	clientGoroot := strings.TrimSpace(string(buf))
	if !importPathOfMainPackageOk {
		// There were multiple main packages
		importPathOfMainPackage = ""
	}
	return &api.GuessSubstitutePathIn{ClientGOROOT: clientGoroot, ImportPathOfMainPackage: importPathOfMainPackage, ClientModuleDirectories: mod2dir}, nil
}

func (c *RPCClient) GuessSubstitutePath() ([][2]string, error) {
	in, err := MakeGuessSusbtitutePathIn()
	if err != nil {
		return nil, err
	}

	out := &GuessSubstitutePathOut{}
	err = c.call("GuessSubstitutePath", GuessSubstitutePathIn{*in}, out)
	return out.List, err
}

func (c *RPCClient) CancelDownloads() error {
	out := &CancelDownloadsOut{}
	return c.call("CancelDownloads", CancelDownloadsIn{}, out)
}

func (c *RPCClient) DownloadLibraryDebugInfo(n int) error {
	out := DownloadLibraryDebugInfoOut{}
	return c.call("DownloadLibraryDebugInfo", DownloadLibraryDebugInfoIn{n}, out)
}

func (c *RPCClient) TypeInfo(name string) (*api.TypeInfo, error) {
	var out TypeInfoOut
	err := c.call("TypeInfo", TypeInfoIn{name}, &out)
	if err != nil {
		return nil, err
	}
	return out.TypeInfo, err
}

func (c *RPCClient) call(method string, args, reply any) error {
	return c.client.Call("RPCServer."+method, args, reply)
}

func (c *RPCClient) callWhileDrainingEvents(method string, args, reply any) error {
	done := c.drainEvents()
	err := c.call(method, args, reply)
	<-done
	return err
}

func (c *RPCClient) CallAPI(method string, args, reply any) error {
	return c.call(method, args, reply)
}
