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

package main

import (
	"bytes"
	"fmt"
	"log"
	"slices"
	"sort"
	"strings"
)

var (
	// tsclient.go has 3 sections
	cdecls = make(sortedMap[string])
	ccases = make(sortedMap[string])
	cfuncs = make(sortedMap[string])
	// tsserver.go has 3 sections
	sdecls = make(sortedMap[string])
	scases = make(sortedMap[string])
	sfuncs = make(sortedMap[string])
	// tsprotocol.go has 2 sections
	types  = make(sortedMap[string])
	consts = make(sortedMap[string])
	// tsjson has 1 section
	jsons = make(sortedMap[string])
)

func generateOutput(model *Model) {
	for _, r := range model.Requests {
		genDecl(model, r.Method, r.Params, r.Result, r.Direction, r.Line == 0)
		genCase(model, r.Method, r.Params, r.Result, r.Direction)
		genFunc(model, r.Method, r.Params, r.Result, r.Direction, false)
	}
	for _, n := range model.Notifications {
		if n.Method == "$/cancelRequest" {
			continue // handled internally by jsonrpc2
		}
		genDecl(model, n.Method, n.Params, nil, n.Direction, n.Line == 0)
		genCase(model, n.Method, n.Params, nil, n.Direction)
		genFunc(model, n.Method, n.Params, nil, n.Direction, true)
	}
	genStructs(model)
	genAliases(model)
	genGenTypes() // generate the unnamed types
	genConsts(model)
	genMarshal()
}

func genDecl(model *Model, method string, param, result *Type, dir string, experiment bool) {
	fname := methodName(method)
	p := ""
	if notNil(param) {
		p = ", *" + goplsName(param)
	}
	ret := "error"
	if notNil(result) {
		tp := goplsName(result)
		if !hasNilValue(tp) {
			tp = "*" + tp
		}
		ret = fmt.Sprintf("(%s, error)", tp)
	}
	// special gopls compatibility case (PJW: still needed?)
	switch method {
	case "workspace/configuration":
		// was And_Param_workspace_configuration, but the type substitution doesn't work,
		// as ParamConfiguration is embedded in And_Param_workspace_configuration
		p = ", *ParamConfiguration"
		ret = "([]LSPAny, error)"
	}

	var msg string
	{
		var sb strings.Builder

		if doc, ok := prependMethodDocComments[fname]; ok {
			sb.WriteString(doc)
			sb.WriteString("\n\t//\n")
		}

		if experiment {
			sb.WriteString("\t// Note: This is a non-standard protocol extension.\n")
		} else {
			fragment := strings.ReplaceAll(strings.TrimPrefix(method, "$/"), "/", "_")
			fmt.Fprintf(&sb, "\t%s", lspLink(model, fragment))
		}

		fmt.Fprintf(&sb, "\t%s(context.Context%s) %s\n", fname, p, ret)

		msg = sb.String()
	}

	switch dir {
	case "clientToServer":
		sdecls[method] = msg
	case "serverToClient":
		cdecls[method] = msg
	case "both":
		sdecls[method] = msg
		cdecls[method] = msg
	default:
		log.Fatalf("impossible direction %q", dir)
	}
}

func genCase(model *Model, method string, param, result *Type, dir string) {
	extends := func(x, y string) bool {
		for _, struc := range model.Structures {
			if struc.Name == x {
				if contains := slices.ContainsFunc(struc.Extends, func(t *Type) bool {
					return t.Name == y
				}); contains {
					return true
				}
			}
		}
		return false
	}
	out := new(bytes.Buffer)
	fmt.Fprintf(out, "\tcase %q:\n", method)
	var p string
	fname := methodName(method)
	if notNil(param) {
		nm := goplsName(param)
		if method == "workspace/configuration" { // gopls compatibility
			// was And_Param_workspace_configuration, which contains ParamConfiguration
			// so renaming the type leads to circular definitions
			nm = "ParamConfiguration" // gopls compatibility
		}
		fmt.Fprintf(out, "\t\tvar params %s\n", nm)
		out.WriteString("\t\tif err := UnmarshalJSON(raw, &params); err != nil {\n")
		out.WriteString("\t\t\treturn nil, true, fmt.Errorf(\"%%w: %%s\", jsonrpc2.ErrParse, err)\n\t\t}\n")
		p = ", &params"

		// Ensure consistency between Range and Position. If the client provides
		// only a Position, synthesize a zero-width Range at that location.
		//
		// Crucially, we do not clear the Position field. Since this request may
		// be forwarded, the downstream receiver (gopls) will re-validate that
		// the Position lies within the Range.
		//
		// TODO(hxjiang): define util function
		// func (*protocol.Position) EmptyRange() protocol.Range.
		if extends(nm, "TextDocumentPositionParams") {
			out.WriteString(`		if params.Range == (Range{}) {
			params.Range = Range{
				Start: params.Position,
				End:   params.Position,
			}
		} else if !params.Range.Contains(params.Position) {
			return nil, true, fmt.Errorf("position %%v is outside the provided range %%v.", params.Position, params.Range)
		}
`)
		}

	}
	if notNil(result) {
		fmt.Fprintf(out, "\t\tresp, err := %%s.%s(ctx%s)\n", fname, p)
		out.WriteString("\t\tif err != nil {\n")
		out.WriteString("\t\t\treturn nil, true, err\n")
		out.WriteString("\t\t}\n")
		out.WriteString("\t\treturn resp, true, nil\n")
	} else {
		fmt.Fprintf(out, "\t\terr := %%s.%s(ctx%s)\n", fname, p)
		out.WriteString("\t\treturn nil, true, err\n")
	}
	out.WriteString("\n")
	msg := out.String()
	switch dir {
	case "clientToServer":
		scases[method] = fmt.Sprintf(msg, "server")
	case "serverToClient":
		ccases[method] = fmt.Sprintf(msg, "client")
	case "both":
		scases[method] = fmt.Sprintf(msg, "server")
		ccases[method] = fmt.Sprintf(msg, "client")
	default:
		log.Fatalf("impossible direction %q", dir)
	}
}

func genFunc(_ *Model, method string, param, result *Type, dir string, isnotify bool) {
	out := new(bytes.Buffer)
	var p, r string
	var goResult string
	if notNil(param) {
		p = ", params *" + goplsName(param)
	}
	if notNil(result) {
		goResult = goplsName(result)
		if !hasNilValue(goResult) {
			goResult = "*" + goResult
		}
		r = fmt.Sprintf("(%s, error)", goResult)
	} else {
		r = "error"
	}
	// special gopls compatibility case
	switch method {
	case "workspace/configuration":
		// was And_Param_workspace_configuration, but the type substitution doesn't work,
		// as ParamConfiguration is embedded in And_Param_workspace_configuration
		p = ", params *ParamConfiguration"
		r = "([]LSPAny, error)"
		goResult = "[]LSPAny"
	}
	fname := methodName(method)
	fmt.Fprintf(out, "func (s *%%sDispatcher) %s(ctx context.Context%s) %s {\n",
		fname, p, r)

	if !notNil(result) {
		if isnotify {
			if notNil(param) {
				fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, params)\n", method)
			} else {
				fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, nil)\n", method)
			}
		} else {
			if notNil(param) {
				fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, params, nil)\n", method)
			} else {
				fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, nil, nil)\n", method)
			}
		}
	} else {
		fmt.Fprintf(out, "\tvar result %s\n", goResult)
		if isnotify {
			if notNil(param) {
				fmt.Fprintf(out, "\ts.sender.Notify(ctx, %q, params)\n", method)
			} else {
				fmt.Fprintf(out, "\t\tif err := s.sender.Notify(ctx, %q, nil); err != nil {\n", method)
			}
		} else {
			if notNil(param) {
				fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, params, &result); err != nil {\n", method)
			} else {
				fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, nil, &result); err != nil {\n", method)
			}
		}
		fmt.Fprintf(out, "\t\treturn nil, err\n\t}\n\treturn result, nil\n")
	}
	out.WriteString("}\n")
	msg := out.String()
	switch dir {
	case "clientToServer":
		sfuncs[method] = fmt.Sprintf(msg, "server")
	case "serverToClient":
		cfuncs[method] = fmt.Sprintf(msg, "client")
	case "both":
		sfuncs[method] = fmt.Sprintf(msg, "server")
		cfuncs[method] = fmt.Sprintf(msg, "client")
	default:
		log.Fatalf("impossible direction %q", dir)
	}
}

func genStructs(model *Model) {
	structures := make(map[string]*Structure) // for expanding Extends
	for _, s := range model.Structures {
		structures[s.Name] = s
	}
	for _, s := range model.Structures {
		out := new(bytes.Buffer)
		generateDoc(out, s.Documentation)
		nm := goName(s.Name)
		if nm == "string" { // an unacceptable strut name
			// a weird case, and needed only so the generated code contains the old gopls code
			nm = "DocumentDiagnosticParams"
		}
		fmt.Fprintf(out, "//\n")
		out.WriteString(lspLink(model, camelCase(s.Name)))
		fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(s.Line))
		// for gopls compatibility, embed most extensions, but expand the rest some day
		props := slices.Clone(s.Properties)
		if s.Name == "SymbolInformation" { // but expand this one
			for _, ex := range s.Extends {
				fmt.Fprintf(out, "\t// extends %s\n", ex.Name)
				props = append(props, structures[ex.Name].Properties...)
			}
			genProps(out, props, nm)
		} else {
			genProps(out, props, nm)
			for _, ex := range s.Extends {
				fmt.Fprintf(out, "\t%s\n", goName(ex.Name))
			}
		}
		for _, ex := range s.Mixins {
			fmt.Fprintf(out, "\t%s\n", goName(ex.Name))
		}
		// TODO(hxjiang): clean this up after microsoft/language-server-protocol#377
		// is fixed and released.
		if nm == "TextDocumentPositionParams" {
			out.WriteString("\t// Range is an optional field representing the user's text selection in the document.\n")
			out.WriteString("\t// If provided, the Position must be contained within this range.\n")
			out.WriteString("\t//\n")
			out.WriteString("\t// Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#377.\n")
			out.WriteString("\tRange Range `json:\"range\"`")
		}
		out.WriteString("}\n")
		types[nm] = out.String()
	}

	// base types
	// (For URI and DocumentURI, see ../uri.go.)
	types["LSPAny"] = "type LSPAny = any\n"
	// A special case, the only previously existing Or type
	types["DocumentDiagnosticReport"] = "type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) \n"

}

// "FooBar" -> "fooBar"
func camelCase(TitleCased string) string {
	return strings.ToLower(TitleCased[:1]) + TitleCased[1:]
}

func lspLink(model *Model, fragment string) string {
	// Derive URL version from metaData.version in JSON file.
	parts := strings.Split(model.Version.Version, ".") // e.g. "3.17.0"
	return fmt.Sprintf("// See https://microsoft.github.io/language-server-protocol/specifications/lsp/%s.%s/specification#%s\n",
		parts[0], parts[1], // major.minor
		fragment)
}

func genProps(out *bytes.Buffer, props []NameType, name string) {
	for _, p := range props {
		tp := goplsName(p.Type)
		if newNm, ok := renameProp[prop{name, p.Name}]; ok {
			usedRenameProp[prop{name, p.Name}] = true
			if tp == newNm {
				log.Printf("renameProp useless {%q, %q} for %s", name, p.Name, tp)
			}
			tp = newNm
		}
		// it's a pointer if it is optional, or for gopls compatibility
		omit, star := propStar(name, p, tp)
		json := fmt.Sprintf(" `json:\"%s\"`", p.Name)
		if omit {
			json = fmt.Sprintf(" `json:\"%s,omitempty\"`", p.Name)
		}
		generateDoc(out, p.Documentation)
		if docs := appendTypePropDocComments[name]; docs != nil {
			if doc, ok := docs[p.Name]; ok {
				out.WriteString(doc)
			}
		}
		if star {
			fmt.Fprintf(out, "\t%s *%s %s\n", goName(p.Name), tp, json)
		} else {
			fmt.Fprintf(out, "\t%s %s %s\n", goName(p.Name), tp, json)
		}
	}

	if block, ok := appendTypeProp[name]; ok {
		out.WriteString(block)
	}
}

func genAliases(model *Model) {
	for _, ta := range model.TypeAliases {
		out := new(bytes.Buffer)
		generateDoc(out, ta.Documentation)
		nm := goName(ta.Name)
		if nm != ta.Name {
			continue // renamed the type, e.g., "DocumentDiagnosticReport", an or-type to "string"
		}
		tp := goplsName(ta.Type)
		fmt.Fprintf(out, "//\n")
		out.WriteString(lspLink(model, camelCase(ta.Name)))
		fmt.Fprintf(out, "type %s = %s // (alias)\n", nm, tp)
		types[nm] = out.String()
	}
}

func genGenTypes() {
	for _, nt := range genTypes {
		out := new(bytes.Buffer)
		nm := goplsName(nt.typ)
		switch nt.kind {
		case "literal":
			fmt.Fprintf(out, "// created for Literal (%s)\n", nt.name)
			fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1))
			genProps(out, nt.properties, nt.name) // systematic name, not gopls name; is this a good choice?
		case "or":
			if !strings.HasPrefix(nm, "Or") {
				// It was replaced by a narrower type defined elsewhere
				continue
			}
			names := []string{}
			for _, t := range nt.items {
				if notNil(t) {
					names = append(names, goplsName(t))
				}
			}
			sort.Strings(names)
			fmt.Fprintf(out, "// created for Or %v\n", names)
			fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1))
			fmt.Fprintf(out, "\tValue any `json:\"value\"`\n")
		case "and":
			fmt.Fprintf(out, "// created for And\n")
			fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1))
			for _, x := range nt.items {
				nm := goplsName(x)
				fmt.Fprintf(out, "\t%s\n", nm)
			}
		case "tuple": // there's only this one
			nt.name = "UIntCommaUInt"
			fmt.Fprintf(out, "//created for Tuple\ntype %s struct {%s\n", nm, linex(nt.line+1))
			fmt.Fprintf(out, "\tFld0 uint32 `json:\"fld0\"`\n")
			fmt.Fprintf(out, "\tFld1 uint32 `json:\"fld1\"`\n")
		default:
			log.Fatalf("%s not handled", nt.kind)
		}
		out.WriteString("}\n")
		types[nm] = out.String()
	}
}
func genConsts(model *Model) {
	for _, e := range model.Enumerations {
		out := new(bytes.Buffer)
		generateDoc(out, e.Documentation)
		tp := goplsName(e.Type)
		nm := goName(e.Name)
		fmt.Fprintf(out, "type %s %s%s\n", nm, tp, linex(e.Line))
		types[nm] = out.String()
		vals := new(bytes.Buffer)
		generateDoc(vals, e.Documentation)
		for _, v := range e.Values {
			generateDoc(vals, v.Documentation)
			nm := goName(v.Name)
			more, ok := disambiguate[e.Name]
			if ok {
				usedDisambiguate[e.Name] = true
				nm = more.prefix + nm + more.suffix
				nm = goName(nm) // stringType
			}
			var val string
			switch v := v.Value.(type) {
			case string:
				val = fmt.Sprintf("%q", v)
			case float64:
				val = fmt.Sprintf("%d", int(v))
			default:
				log.Fatalf("impossible type %T", v)
			}
			fmt.Fprintf(vals, "\t%s %s = %s%s\n", nm, e.Name, val, linex(v.Line))
		}
		consts[nm] = vals.String()
	}
}
func genMarshal() {
	for _, nt := range genTypes {
		nm := goplsName(nt.typ)
		if !strings.HasPrefix(nm, "Or") {
			continue
		}
		names := []string{}
		for _, t := range nt.items {
			if notNil(t) {
				names = append(names, goplsName(t))
			}
		}
		sort.Strings(names)
		var buf bytes.Buffer
		fmt.Fprintf(&buf, "func (t %s) MarshalJSON() ([]byte, error) {\n", nm)
		buf.WriteString("\tswitch x := t.Value.(type){\n")
		for _, nmx := range names {
			fmt.Fprintf(&buf, "\tcase %s:\n", nmx)
			fmt.Fprintf(&buf, "\t\treturn json.Marshal(x)\n")
		}
		buf.WriteString("\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\t}\n")
		fmt.Fprintf(&buf, "\treturn nil, fmt.Errorf(\"type %%T not one of %v\", t)\n", names)
		buf.WriteString("}\n\n")

		fmt.Fprintf(&buf, "func (t *%s) UnmarshalJSON(x []byte) error {\n", nm)
		buf.WriteString("\tif string(x) == \"null\" {\n\t\tt.Value = nil\n\t\t\treturn nil\n\t}\n")
		for i, nmx := range names {
			fmt.Fprintf(&buf, "\tvar h%d %s\n", i, nmx)
			fmt.Fprintf(&buf, "\tif err := json.Unmarshal(x, &h%d); err == nil {\n\t\tt.Value = h%d\n\t\t\treturn nil\n\t\t}\n", i, i)
		}
		fmt.Fprintf(&buf, "return &UnmarshalError{\"unmarshal failed to match one of %v\"}", names)
		buf.WriteString("}\n\n")
		jsons[nm] = buf.String()
	}
}

func linex(n int) string {
	if *lineNumbers {
		return fmt.Sprintf(" // line %d", n)
	}
	return ""
}

func goplsName(t *Type) string {
	nm := typeNames[t]
	// translate systematic name to gopls name
	if newNm, ok := goplsType[nm]; ok {
		usedGoplsType[nm] = true
		nm = newNm
	}
	return nm
}

func notNil(t *Type) bool { // shutdwon is the special case that needs this
	return t != nil && (t.Kind != "base" || t.Name != "null")
}

func hasNilValue(t string) bool {
	// this may be unreliable, and need a supplementary table
	if strings.HasPrefix(t, "[]") || strings.HasPrefix(t, "*") {
		return true
	}
	if t == "interface{}" || t == "any" {
		return true
	}
	// that's all the cases that occur currently
	return false
}
