// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// protocol_gen (re)generates the cppdap .h and .cpp files that describe the
// DAP protocol.
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path"
	"reflect"
	"runtime"
	"sort"
	"strings"
)

const (
	protocolURL = "https://raw.githubusercontent.com/microsoft/vscode-debugadapter-node/master/debugProtocol.json"
	packageURL  = "https://raw.githubusercontent.com/microsoft/vscode-debugadapter-node/master/protocol/package.json"

	versionTag     = "${version}"
	commonPrologue = `// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Generated with protocol_gen.go -- do not edit this file.
//   go run scripts/protocol_gen/protocol_gen.go
//
// DAP version ${version}
`

	headerPrologue = commonPrologue + `
#ifndef dap_protocol_h
#define dap_protocol_h

#include "optional.h"
#include "typeinfo.h"
#include "typeof.h"
#include "variant.h"

#include <string>
#include <type_traits>
#include <vector>

namespace dap {

struct Request {};
struct Response {};
struct Event {};

`

	headerEpilogue = `}  // namespace dap

#endif  // dap_protocol_h
`

	cppPrologue = commonPrologue + `

#include "dap/protocol.h"

namespace dap {

`

	cppEpilogue = `}  // namespace dap
`
)

func main() {
	flag.Parse()
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}
}

type root struct {
	Schema      string                `json:"$schema"`
	Title       string                `json:"title"`
	Description string                `json:"description"`
	Ty          string                `json:"type"`
	Definitions map[string]definition `json:"definitions"`
}

func (r *root) definitions() []namedDefinition {
	sortedDefinitions := make([]namedDefinition, 0, len(r.Definitions))
	for name, def := range r.Definitions {
		sortedDefinitions = append(sortedDefinitions, namedDefinition{name, def})
	}
	sort.Slice(sortedDefinitions, func(i, j int) bool { return sortedDefinitions[i].name < sortedDefinitions[j].name })
	return sortedDefinitions
}

func (r *root) getRef(ref string) (namedDefinition, error) {
	if !strings.HasPrefix(ref, "#/definitions/") {
		return namedDefinition{}, fmt.Errorf("Unknown $ref '%s'", ref)
	}
	name := strings.TrimPrefix(ref, "#/definitions/")
	def, ok := r.Definitions[name]
	if !ok {
		return namedDefinition{}, fmt.Errorf("Unknown $ref '%s'", ref)
	}
	return namedDefinition{name, def}, nil
}

type namedDefinition struct {
	name string
	def  definition
}

type definition struct {
	Ty          string       `json:"type"`
	Title       string       `json:"title"`
	Description string       `json:"description"`
	Properties  properties   `json:"properties"`
	Required    []string     `json:"required"`
	AllOf       []definition `json:"allOf"`
	Ref         string       `json:"$ref"`
	ClosedEnum  []string     `json:"enum"`
}

type properties map[string]property

func (p *properties) foreach(cb func(string, property) error) error {
	type namedProperty struct {
		name     string
		property property
	}
	sorted := make([]namedProperty, 0, len(*p))
	for name, property := range *p {
		sorted = append(sorted, namedProperty{name, property})
	}
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].name < sorted[j].name })
	for _, entry := range sorted {
		if err := cb(entry.name, entry.property); err != nil {
			return err
		}
	}
	return nil
}

type property struct {
	typed
	Description string `json:"description"`
}

func (p *property) properties(r *root) (properties, []string, error) {
	if p.Ref == "" {
		return p.Properties, p.Required, nil
	}

	d, err := r.getRef(p.Ref)
	if err != nil {
		return nil, nil, err
	}
	return d.def.Properties, d.def.Required, nil
}

type typed struct {
	Ty         interface{} `json:"type"`
	Items      *typed      `json:"items"`
	Ref        string      `json:"$ref"`
	Properties properties  `json:"properties"`
	Required   []string    `json:"required"`
	ClosedEnum []string    `json:"enum"`
	OpenEnum   []string    `json:"_enum"`
}

func (t typed) typename(r *root, refs *[]string) (string, error) {
	if t.Ref != "" {
		d, err := r.getRef(t.Ref)
		if err != nil {
			return "", err
		}
		*refs = append(*refs, d.name)
		return d.name, nil
	}

	if t.Ty == nil {
		return "", fmt.Errorf("No type specified")
	}

	var typeof func(v reflect.Value) (string, error)
	typeof = func(v reflect.Value) (string, error) {
		if v.Kind() == reflect.Interface {
			v = v.Elem()
		}
		switch v.Kind() {
		case reflect.String:
			ty := v.Interface().(string)
			switch ty {
			case "boolean", "string", "integer", "number", "object", "null":
				return ty, nil
			case "array":
				if t.Items != nil {
					el, err := t.Items.typename(r, refs)
					if err != nil {
						return "", err
					}
					return fmt.Sprintf("array<%s>", el), nil
				}
				return "array<any>", nil
			default:
				return "", fmt.Errorf("Unhandled property type '%v'", ty)
			}

		case reflect.Slice, reflect.Array:
			ty := "variant<"
			for i := 0; i < v.Len(); i++ {
				if i > 0 {
					ty += ", "
				}
				el, err := typeof(v.Index(i))
				if err != nil {
					return "", err
				}
				ty += el
			}
			ty += ">"
			return ty, nil
		}

		return "", fmt.Errorf("Unsupported type '%v' kind: %v", v.Interface(), v.Kind())
	}

	return typeof(reflect.ValueOf(t.Ty))
}

type cppField struct {
	desc         string
	ty           string
	name         string
	defaultValue string
	optional     bool
}

type cppType interface {
	Name() string
	Dependencies() []string
	File() cppFile
	DefaultValue() string
	WriteHeader(w io.Writer)
	WriteCPP(w io.Writer)
}

type cppStruct struct {
	desc     string
	name     string
	typename string
	base     string
	fields   []cppField
	deps     []string
	emit     bool
	typedefs []cppTypedef
	file     cppFile
}

func (s *cppStruct) Name() string {
	return s.name
}

func (s *cppStruct) Dependencies() []string {
	return s.deps
}

func (s *cppStruct) File() cppFile {
	return s.file
}

func (s *cppStruct) DefaultValue() string { return "" }

func (s *cppStruct) WriteHeader(w io.Writer) {
	if s.desc != "" {
		io.WriteString(w, "// ")
		io.WriteString(w, strings.ReplaceAll(s.desc, "\n", "\n// "))
		io.WriteString(w, "\n")
	}
	io.WriteString(w, "struct ")
	io.WriteString(w, s.name)
	if s.base != "" {
		io.WriteString(w, " : public ")
		io.WriteString(w, s.base)
	}
	io.WriteString(w, " {")

	// typedefs
	for _, t := range s.typedefs {
		io.WriteString(w, "\n  using ")
		io.WriteString(w, t.from)
		io.WriteString(w, " = ")
		io.WriteString(w, t.to)
		io.WriteString(w, ";")
	}

	for _, f := range s.fields {
		if f.desc != "" {
			io.WriteString(w, "\n  // ")
			io.WriteString(w, strings.ReplaceAll(f.desc, "\n", "\n  // "))
		}
		io.WriteString(w, "\n  ")
		if f.optional {
			io.WriteString(w, "optional<")
			io.WriteString(w, f.ty)
			io.WriteString(w, ">")
		} else {
			io.WriteString(w, f.ty)
		}
		io.WriteString(w, " ")
		io.WriteString(w, sanitize(f.name))
		if !f.optional && f.defaultValue != "" {
			io.WriteString(w, " = ")
			io.WriteString(w, f.defaultValue)
		}
		io.WriteString(w, ";")
	}

	io.WriteString(w, "\n};\n\n")

	io.WriteString(w, "DAP_DECLARE_STRUCT_TYPEINFO(")
	io.WriteString(w, s.name)
	io.WriteString(w, ");\n\n")
}

func (s *cppStruct) WriteCPP(w io.Writer) {
	// typeinfo
	io.WriteString(w, "DAP_IMPLEMENT_STRUCT_TYPEINFO(")
	io.WriteString(w, s.name)
	io.WriteString(w, ",\n                    \"")
	io.WriteString(w, s.typename)
	io.WriteString(w, "\"")
	for _, f := range s.fields {
		io.WriteString(w, ",\n                    ")
		io.WriteString(w, "DAP_FIELD(")
		io.WriteString(w, sanitize(f.name))
		io.WriteString(w, ", \"")
		io.WriteString(w, f.name)
		io.WriteString(w, "\")")
	}
	io.WriteString(w, ");\n\n")
}

type cppTypedef struct {
	from         string
	to           string
	deps         []string
	desc         string
	defaultValue string
}

func (ty *cppTypedef) Name() string {
	return ty.from
}

func (ty *cppTypedef) Dependencies() []string {
	return ty.deps
}

func (ty *cppTypedef) File() cppFile {
	return types
}

func (ty *cppTypedef) DefaultValue() string { return ty.defaultValue }

func (ty *cppTypedef) WriteHeader(w io.Writer) {
	if ty.desc != "" {
		io.WriteString(w, "// ")
		io.WriteString(w, strings.ReplaceAll(ty.desc, "\n", "\n// "))
		io.WriteString(w, "\n")
	}

	io.WriteString(w, "using ")
	io.WriteString(w, ty.from)
	io.WriteString(w, " = ")
	io.WriteString(w, ty.to)
	io.WriteString(w, ";\n\n")
}

func (ty *cppTypedef) WriteCPP(w io.Writer) {
}

func sanitize(s string) string {
	s = strings.Trim(s, "_")
	switch s {
	case "default":
		return "def"
	default:
		return s
	}
}

func appendEnumDetails(desc string, openEnum []string, closedEnum []string) string {
	if len(closedEnum) > 0 {
		desc += "\n\nMust be one of the following enumeration values:\n"
		for i, enum := range closedEnum {
			if i > 0 {
				desc += ", "
			}
			desc += "'" + enum + "'"
		}
	}

	if len(openEnum) > 0 {
		desc += "\n\nMay be one of the following enumeration values:\n"
		for i, enum := range openEnum {
			if i > 0 {
				desc += ", "
			}
			desc += "'" + enum + "'"
		}
	}
	return desc
}

func (r *root) buildObject(entry namedDefinition) (*cppStruct, error) {
	defName := entry.name
	def := entry.def

	base := ""
	if len(def.AllOf) > 1 && def.AllOf[0].Ref != "" {
		ref, err := r.getRef(def.AllOf[0].Ref)
		if err != nil {
			return nil, err
		}
		base = ref.name
		if len(def.AllOf) > 2 {
			return nil, fmt.Errorf("Cannot handle allOf with more than 2 entries")
		}
		def = def.AllOf[1]
	}

	s := &cppStruct{
		desc: def.Description,
		name: defName,
		base: base,
	}

	var props properties
	var required []string
	var err error
	switch base {
	case "Request":
		if arguments, ok := def.Properties["arguments"]; ok {
			props, required, err = arguments.properties(r)
		}
		if command, ok := def.Properties["command"]; ok {
			s.typename = command.ClosedEnum[0]
		}
		response := strings.TrimSuffix(s.name, "Request") + "Response"
		s.deps = append(s.deps, response)
		s.typedefs = append(s.typedefs, cppTypedef{from: "Response", to: response})
		s.emit = true
		s.file = request
	case "Response":
		if body, ok := def.Properties["body"]; ok {
			props, required, err = body.properties(r)
		}
		s.emit = true
		s.file = response
	case "Event":
		if body, ok := def.Properties["body"]; ok {
			props, required, err = body.properties(r)
		}
		if command, ok := def.Properties["event"]; ok {
			s.typename = command.ClosedEnum[0]
		}
		s.emit = true
		s.file = event
	default:
		props = def.Properties
		required = def.Required
		s.file = types
	}
	if err != nil {
		return nil, err
	}

	if err = props.foreach(func(propName string, property property) error {
		ty, err := property.typename(r, &s.deps)
		if err != nil {
			return fmt.Errorf("While processing %v.%v: %v", defName, propName, err)
		}

		optional := true
		for _, r := range required {
			if propName == r {
				optional = false
			}
		}

		desc := appendEnumDetails(property.Description, property.OpenEnum, property.ClosedEnum)

		defaultValue := ""
		if len(property.ClosedEnum) > 0 {
			defaultValue = `"` + property.ClosedEnum[0] + `"`
		}

		s.fields = append(s.fields, cppField{
			desc:         desc,
			defaultValue: defaultValue,
			ty:           ty,
			name:         propName,
			optional:     optional,
		})

		return nil
	}); err != nil {
		return nil, err
	}

	return s, nil
}

func (r *root) buildTypes() ([]cppType, error) {
	ignore := map[string]bool{
		// These are handled internally.
		"ProtocolMessage": true,
		"Request":         true,
		"Event":           true,
		"Response":        true,
	}

	out := []cppType{}
	for _, entry := range r.definitions() {
		if ignore[entry.name] {
			continue
		}
		switch entry.def.Ty {
		case "", "object":
			ty, err := r.buildObject(entry)
			if err != nil {
				return nil, err
			}
			out = append(out, ty)
		case "string":
			desc := appendEnumDetails(entry.def.Description, nil, entry.def.ClosedEnum)
			defaultValue := ""
			if len(entry.def.ClosedEnum) > 0 {
				defaultValue = entry.def.ClosedEnum[0]
			}
			ty := &cppTypedef{from: entry.name, to: "std::string", desc: desc, defaultValue: defaultValue}
			out = append(out, ty)
		default:
			return nil, fmt.Errorf("Unhandled type '%v' for '%v'", entry.def.Ty, entry.name)
		}
	}

	return out, nil
}

type cppFile string

const (
	request  = cppFile("request")
	response = cppFile("response")
	event    = cppFile("event")
	types    = cppFile("types")
)

type cppFilePaths map[cppFile]string

type cppFiles map[cppFile]*os.File

func run() error {
	pkg := struct {
		Version string `json:"version"`
	}{}
	if err := loadJSONFile(packageURL, &pkg); err != nil {
		return fmt.Errorf("Failed to load JSON file from '%v': %w", packageURL, err)
	}

	protocol := root{}
	if err := loadJSONFile(protocolURL, &protocol); err != nil {
		return fmt.Errorf("Failed to load JSON file from '%v': %w", protocolURL, err)
	}

	hPath, cppPaths := outputPaths()
	if err := emitFiles(&protocol, hPath, cppPaths, pkg.Version); err != nil {
		return fmt.Errorf("Failed to emit files: %w", err)
	}

	if clangfmt, err := exec.LookPath("clang-format"); err == nil {
		if out, err := exec.Command(clangfmt, "-i", hPath).CombinedOutput(); err != nil {
			return fmt.Errorf("Failed to run clang-format on '%v':\n%v\n%w", hPath, string(out), err)
		}
		for _, p := range cppPaths {
			if out, err := exec.Command(clangfmt, "-i", p).CombinedOutput(); err != nil {
				return fmt.Errorf("Failed to run clang-format on '%v':\n%v\n%w", p, string(out), err)
			}
		}
	} else {
		fmt.Printf("clang-format not found on PATH. Please format before committing.")
	}

	return nil
}

func emitFiles(r *root, hPath string, cppPaths map[cppFile]string, version string) error {
	h, err := os.Create(hPath)
	if err != nil {
		return err
	}
	defer h.Close()
	cppFiles := map[cppFile]*os.File{}
	for ty, p := range cppPaths {
		f, err := os.Create(p)
		if err != nil {
			return err
		}
		cppFiles[ty] = f
		defer f.Close()
	}

	h.WriteString(strings.ReplaceAll(headerPrologue, versionTag, version))
	for _, f := range cppFiles {
		f.WriteString(strings.ReplaceAll(cppPrologue, versionTag, version))
	}

	types, err := r.buildTypes()
	if err != nil {
		return err
	}

	typesByName := map[string]cppType{}
	for _, s := range types {
		typesByName[s.Name()] = s
	}

	seen := map[string]bool{}
	var emit func(cppType) error
	emit = func(ty cppType) error {
		name := ty.Name()
		if seen[name] {
			return nil
		}
		seen[name] = true
		for _, depName := range ty.Dependencies() {
			dep, ok := typesByName[depName]
			if !ok {
				return fmt.Errorf("'%v' depends on unknown type '%v'", name, depName)
			}
			if err := emit(dep); err != nil {
				return err
			}
		}
		ty.WriteHeader(h)
		ty.WriteCPP(cppFiles[ty.File()])
		return nil
	}

	// emit message types.
	// Referenced types will be transitively emitted.
	for _, s := range types {
		switch s.File() {
		case request, response, event:
			if err := emit(s); err != nil {
				return err
			}
		}
	}

	h.WriteString(headerEpilogue)
	for _, f := range cppFiles {
		f.WriteString(cppEpilogue)
	}

	return nil
}

func loadJSONFile(url string, obj interface{}) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if err := json.NewDecoder(bytes.NewReader(data)).Decode(obj); err != nil {
		return err
	}
	return nil
}

func outputPaths() (string, cppFilePaths) {
	_, thisFile, _, _ := runtime.Caller(1)
	thisDir := path.Dir(thisFile)
	h := path.Join(thisDir, "../../include/dap/protocol.h")
	cpp := cppFilePaths{
		request:  path.Join(thisDir, "../../src/protocol_requests.cpp"),
		response: path.Join(thisDir, "../../src/protocol_response.cpp"),
		event:    path.Join(thisDir, "../../src/protocol_events.cpp"),
		types:    path.Join(thisDir, "../../src/protocol_types.cpp"),
	}
	return h, cpp
}
