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

//go:build go1.19
// +build go1.19

package main

import (
	"bytes"
	"fmt"
	"log"
	"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(r.Method, r.Params, r.Result, r.Direction)
		genCase(r.Method, r.Params, r.Result, r.Direction)
		genFunc(r.Method, r.Params, r.Result, r.Direction, false)
	}
	for _, n := range model.Notifications {
		if n.Method == "$/cancelRequest" {
			continue // handled internally by jsonrpc2
		}
		genDecl(n.Method, n.Params, nil, n.Direction)
		genCase(n.Method, n.Params, nil, n.Direction)
		genFunc(n.Method, n.Params, nil, n.Direction, true)
	}
	genStructs(model)
	genAliases(model)
	genGenTypes() // generate the unnamed types
	genConsts(model)
	genMarshal()
}

func genDecl(method string, param, result *Type, dir string) {
	fname := methodNames[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)"
	}
	msg := fmt.Sprintf("\t%s(context.Context%s) %s // %s\n", fname, p, ret, method)
	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(method string, param, result *Type, dir string) {
	out := new(bytes.Buffer)
	fmt.Fprintf(out, "\tcase %q:\n", method)
	var p string
	fname := methodNames[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)
		fmt.Fprintf(out, "\t\tif err := json.Unmarshal(r.Params(), &params); err != nil {\n")
		fmt.Fprintf(out, "\t\t\treturn true, sendParseError(ctx, reply, err)\n\t\t}\n")
		p = ", &params"
	}
	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 true, reply(ctx, nil, err)\n")
		out.WriteString("\t\t}\n")
		out.WriteString("\t\treturn true, reply(ctx, resp, nil)\n")
	} else {
		fmt.Fprintf(out, "\t\terr := %%s.%s(ctx%s)\n", fname, p)
		out.WriteString("\t\treturn true, reply(ctx, nil, err)\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(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 := methodNames[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, "type %s struct { // line %d\n", nm, s.Line)
		// for gpls compatibilitye, embed most extensions, but expand the rest some day
		props := append([]NameType{}, 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))
		}
		out.WriteString("}\n")
		types[nm] = out.String()
	}
	// base types
	types["DocumentURI"] = "type DocumentURI string\n"
	types["URI"] = "type URI = string\n"

	types["LSPAny"] = "type LSPAny = interface{}\n"
	// A special case, the only previously existing Or type
	types["DocumentDiagnosticReport"] = "type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909\n"

}

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
		opt, star := propStar(name, p, tp)
		json := fmt.Sprintf(" `json:\"%s%s\"`", p.Name, opt)
		generateDoc(out, p.Documentation)
		fmt.Fprintf(out, "\t%s %s%s %s\n", goName(p.Name), star, tp, json)
	}
}

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, "type %s = %s // (alias) line %d\n", nm, tp, ta.Line)
		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 { // line %d\n", nm, 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 { // line %d\n", nm, nt.line+1)
			fmt.Fprintf(out, "\tValue interface{} `json:\"value\"`\n")
		case "and":
			fmt.Fprintf(out, "// created for And\n")
			fmt.Fprintf(out, "type %s struct { // line %d\n", nm, 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 { // line %d\n", nm, 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 // line %d\n", nm, tp, 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 // line %d\n", nm, e.Name, val, 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, "// from line %d\n", nt.line)
		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 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
}
