// Copyright 2022 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.

// The generate command generates Go declarations from VSCode's
// description of the Language Server Protocol.
//
// To run it, type 'go generate' in the parent (protocol) directory.
package main

// see https://github.com/golang/go/issues/61217 for discussion of an issue

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"go/format"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"
)

const vscodeRepo = "https://github.com/microsoft/vscode-languageserver-node"

// lspGitRef names a branch or tag in vscodeRepo.
// It implicitly determines the protocol version of the LSP used by gopls.
// For example, tag release/protocol/3.17.3 of the repo defines
// protocol version 3.17.0 (as declared by the metaData.version field).
// (Point releases are reflected in the git tag version even when they are cosmetic
// and don't change the protocol.)
var lspGitRef = "release/protocol/3.17.6-next.14"

var (
	repodir   = flag.String("d", "", "directory containing clone of "+vscodeRepo)
	outputdir = flag.String("o", ".", "output directory")
	// PJW: not for real code
	lineNumbers = flag.Bool("l", false, "add line numbers to generated output")
)

func main() {
	log.SetFlags(log.Lshortfile) // log file name and line number, not time
	flag.Parse()

	processinline()
}

func processinline() {
	// A local repository may be specified during debugging.
	// The default behavior is to download the canonical version.
	if *repodir == "" {
		tmpdir, err := os.MkdirTemp("", "")
		if err != nil {
			log.Fatal(err)
		}
		defer os.RemoveAll(tmpdir) // ignore error

		// Clone the repository.
		cmd := exec.Command("git", "clone", "--quiet", "--depth=1", "-c", "advice.detachedHead=false", vscodeRepo, "--branch="+lspGitRef, "--single-branch", tmpdir)
		cmd.Stdout = os.Stderr
		cmd.Stderr = os.Stderr
		if err := cmd.Run(); err != nil {
			log.Fatal(err)
		}

		*repodir = tmpdir
	} else {
		lspGitRef = fmt.Sprintf("(not git, local dir %s)", *repodir)
	}

	model := parse(filepath.Join(*repodir, "protocol/metaModel.json"))

	// Although the LSP specification defines RenameParams as extending
	// TextDocumentPositionParams, the metaModel.json definition flattens
	// these properties (likely due to specific comments in the TS definition).
	// See microsoft/vscode-languageserver-node#1698.
	//
	// TODO(hxjiang): delete the patch logic after releasing lsp 3.18.
	for _, s := range model.Structures {
		if s.Name == "RenameParams" {
			s.Properties = slices.DeleteFunc(s.Properties, func(t NameType) bool {
				return t.Name == "position" || t.Name == "textDocument"
			})
			if !slices.ContainsFunc(s.Extends, func(t *Type) bool {
				return t.Kind == "reference" && t.Name == "TextDocumentPositionParams"
			}) {
				s.Extends = append(s.Extends, &Type{
					Kind: "reference",
					Name: "TextDocumentPositionParams",
				})
			}
		}
	}

	// Add a client to server LSP method "command/resolve" for interactive
	// refactoring. The method's param and result are both "ExecuteCommandParams".
	// The types are only accessible from the "workspace/executeCommand" request.
	//
	// See microsoft/language-server-protocol#1164.
	for _, req := range model.Requests {
		if req.Method == "workspace/executeCommand" {
			model.Requests = append(model.Requests, &Request{
				Method:    "command/resolve",
				ErrorData: req.ErrorData,
				Direction: "clientToServer",
				Params:    req.Params,
				Result:    req.Params,
			})
			break
		}
	}

	model.Requests = append(model.Requests, &Request{
		Method:    "interactive/listEnum",
		Direction: "clientToServer",
		Params: &Type{
			Kind: "reference",
			Name: "InteractiveListEnumParams",
		},
		Result: &Type{
			Kind: "array",
			Element: &Type{
				Kind: "reference",
				Name: "FormEnumEntry",
			},
		},
	})

	findTypeNames(model)
	generateOutput(model)

	fileHdr = fileHeader(model)

	// write the files
	writeclient()
	writeserver()
	writeprotocol()
	writejsons()

	checkTables()
}

// common file header for output files
var fileHdr string

func writeclient() {
	out := new(bytes.Buffer)
	fmt.Fprintln(out, fileHdr)
	out.WriteString(
		`import (
	"context"
	"encoding/json"
	"fmt"

	"golang.org/x/tools/internal/jsonrpc2"
)
`)
	out.WriteString("type Client interface {\n")
	for _, k := range cdecls.keys() {
		out.WriteString(cdecls[k])
	}
	out.WriteString("}\n\n")
	out.WriteString(`
func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {
	resp, valid, err := ClientDispatchCall(ctx, client, r.Method(), r.Params())
	if !valid {
		return false, nil
	}

	if err != nil {
		return valid, reply(ctx, nil, err)
	} else {
	 	return valid, reply(ctx, resp, nil)
	}
}

func ClientDispatchCall(ctx context.Context, client Client, method string, raw json.RawMessage) (resp any, _ bool, err error) {
	switch method {
`)
	for _, k := range ccases.keys() {
		out.WriteString(ccases[k])
	}
	out.WriteString(("\tdefault:\n\t\treturn nil, false, nil\n\t}\n}\n\n"))
	for _, k := range cfuncs.keys() {
		out.WriteString(cfuncs[k])
	}
	formatTo("tsclient.go", out.Bytes())
}

func writeserver() {
	out := new(bytes.Buffer)
	fmt.Fprintln(out, fileHdr)
	out.WriteString(
		`import (
	"context"
	"encoding/json"
	"fmt"

	"golang.org/x/tools/internal/jsonrpc2"
)
`)
	out.WriteString("type Server interface {\n")
	for _, k := range sdecls.keys() {
		out.WriteString(sdecls[k])
	}
	out.WriteString(`
}
func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {
	resp, valid, err := ServerDispatchCall(ctx, server, r.Method(), r.Params())
	if !valid {
		return false, nil
	}

	if err != nil {
		return valid, reply(ctx, nil, err)
	} else {
	 	return valid, reply(ctx, resp, nil)
	}
}

func ServerDispatchCall(ctx context.Context, server Server, method string, raw json.RawMessage) (resp any, _ bool, err error) {
	switch method {
`)
	for _, k := range scases.keys() {
		out.WriteString(scases[k])
	}
	out.WriteString(("\tdefault:\n\t\treturn nil, false, nil\n\t}\n}\n\n"))
	for _, k := range sfuncs.keys() {
		out.WriteString(sfuncs[k])
	}
	formatTo("tsserver.go", out.Bytes())
}

func writeprotocol() {
	out := new(bytes.Buffer)
	fmt.Fprintln(out, fileHdr)
	out.WriteString("import \"encoding/json\"\n\n")

	// The following are unneeded, but make the new code a superset of the old
	hack := func(newer, existing string) {
		if _, ok := types[existing]; !ok {
			log.Fatalf("types[%q] not found", existing)
		}
		types[newer] = strings.Replace(types[existing], existing, newer, 1)
	}
	hack("ConfigurationParams", "ParamConfiguration")
	hack("InitializeParams", "ParamInitialize")
	hack("PreviousResultId", "PreviousResultID")
	hack("WorkspaceFoldersServerCapabilities", "WorkspaceFolders5Gn")
	hack("_InitializeParams", "XInitializeParams")

	for _, k := range types.keys() {
		if k == "WatchKind" {
			types[k] = "type WatchKind = uint32" // strict gopls compatibility needs the '='
		}
		out.WriteString(types[k])
	}

	out.WriteString("\nconst (\n")
	for _, k := range consts.keys() {
		out.WriteString(consts[k])
	}
	out.WriteString(")\n\n")
	formatTo("tsprotocol.go", out.Bytes())
}

func writejsons() {
	out := new(bytes.Buffer)
	fmt.Fprintln(out, fileHdr)
	out.WriteString("import \"encoding/json\"\n\n")
	out.WriteString("import \"fmt\"\n")

	out.WriteString(`
// UnmarshalError indicates that a JSON value did not conform to
// one of the expected cases of an LSP union type.
type UnmarshalError struct {
	msg string
}

func (e UnmarshalError) Error() string {
	return e.msg
}
`)

	for _, k := range jsons.keys() {
		out.WriteString(jsons[k])
	}
	formatTo("tsjson.go", out.Bytes())
}

// formatTo formats the Go source and writes it to *outputdir/basename.
func formatTo(basename string, src []byte) {
	formatted, err := format.Source(src)
	if err != nil {
		failed := filepath.Join("/tmp", basename+".fail")
		if err := os.WriteFile(failed, src, 0644); err != nil {
			log.Fatal(err)
		}
		log.Fatalf("formatting %s: %v (see %s)", basename, err, failed)
	}
	if err := os.WriteFile(filepath.Join(*outputdir, basename), formatted, 0644); err != nil {
		log.Fatal(err)
	}
}

// create the common file header for the output files
func fileHeader(model *Model) string {
	fname := filepath.Join(*repodir, ".git", "HEAD")
	buf, err := os.ReadFile(fname)
	if err != nil {
		log.Fatal(err)
	}
	buf = bytes.TrimSpace(buf)
	var githash string
	if len(buf) == 40 {
		githash = string(buf[:40])
	} else if bytes.HasPrefix(buf, []byte("ref: ")) {
		fname = filepath.Join(*repodir, ".git", string(buf[5:]))
		buf, err = os.ReadFile(fname)
		if err != nil {
			log.Fatal(err)
		}
		githash = string(buf[:40])
	} else {
		log.Fatalf("githash cannot be recovered from %s", fname)
	}

	format := `// Copyright 2023 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.

// Code generated for LSP. DO NOT EDIT.

package protocol

// Code generated from %[1]s at ref %[2]s (hash %[3]s).
// %[4]s/blob/%[2]s/%[1]s
// LSP metaData.version = %[5]s.

`
	return fmt.Sprintf(format,
		"protocol/metaModel.json", // 1
		lspGitRef,                 // 2
		githash,                   // 3
		vscodeRepo,                // 4
		model.Version.Version)     // 5
}

func parse(fname string) *Model {
	buf, err := os.ReadFile(fname)
	if err != nil {
		log.Fatal(err)
	}
	buf = addLineNumbers(buf)
	model := new(Model)
	if err := json.Unmarshal(buf, model); err != nil {
		log.Fatal(err)
	}
	return model
}

// Type.Value has to be treated specially for literals and maps
func (t *Type) UnmarshalJSON(data []byte) error {
	// First unmarshal only the unambiguous fields.
	var x struct {
		Kind    string  `json:"kind"`
		Items   []*Type `json:"items"`
		Element *Type   `json:"element"`
		Name    string  `json:"name"`
		Key     *Type   `json:"key"`
		Value   any     `json:"value"`
		Line    int     `json:"line"`
	}
	if err := json.Unmarshal(data, &x); err != nil {
		return err
	}
	*t = Type{
		Kind:    x.Kind,
		Items:   x.Items,
		Element: x.Element,
		Name:    x.Name,
		Value:   x.Value,
		Line:    x.Line,
	}

	// Then unmarshal the 'value' field based on the kind.
	// This depends on Unmarshal ignoring fields it doesn't know about.
	switch x.Kind {
	case "map":
		var x struct {
			Key   *Type `json:"key"`
			Value *Type `json:"value"`
		}
		if err := json.Unmarshal(data, &x); err != nil {
			return fmt.Errorf("Type.kind=map: %v", err)
		}
		t.Key = x.Key
		t.Value = x.Value

	case "literal":
		var z struct {
			Value ParseLiteral `json:"value"`
		}

		if err := json.Unmarshal(data, &z); err != nil {
			return fmt.Errorf("Type.kind=literal: %v", err)
		}
		t.Value = z.Value

	case "base", "reference", "array", "and", "or", "tuple",
		"stringLiteral":
		// no-op. never seen integerLiteral or booleanLiteral.

	default:
		return fmt.Errorf("cannot decode Type.kind %q: %s", x.Kind, data)
	}
	return nil
}

// which table entries were not used
func checkTables() {
	for k := range disambiguate {
		if !usedDisambiguate[k] {
			log.Printf("disambiguate[%v] unused", k)
		}
	}
	for k := range renameProp {
		if !usedRenameProp[k] {
			log.Printf("renameProp {%q, %q} unused", k[0], k[1])
		}
	}
	for k := range goplsStar {
		if !usedGoplsStar[k] {
			log.Printf("goplsStar {%q, %q} unused", k[0], k[1])
		}
	}
	for k := range goplsType {
		if !usedGoplsType[k] {
			log.Printf("unused goplsType[%q]->%s", k, goplsType[k])
		}
	}
}
