// Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved.
// Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore
// +build ignore

package main

import (
	"bytes"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"regexp"
	"strings"
	"text/template"
)

var (
	ErrUnknownType   = errors.New("unknown field type in gen")
	ErrUnknownDomain = errors.New("unknown domain type in gen")
)

var amqpTypeToNative = map[string]string{
	"bit":        "bool",
	"octet":      "byte",
	"shortshort": "uint8",
	"short":      "uint16",
	"long":       "uint32",
	"longlong":   "uint64",
	"timestamp":  "time.Time",
	"table":      "Table",
	"shortstr":   "string",
	"longstr":    "string",
}

type Rule struct {
	Name string   `xml:"name,attr"`
	Docs []string `xml:"doc"`
}

type Doc struct {
	Type string `xml:"type,attr"`
	Body string `xml:",innerxml"`
}

type Chassis struct {
	Name      string `xml:"name,attr"`
	Implement string `xml:"implement,attr"`
}

type Assert struct {
	Check  string `xml:"check,attr"`
	Value  string `xml:"value,attr"`
	Method string `xml:"method,attr"`
}

type Field struct {
	Name     string   `xml:"name,attr"`
	Domain   string   `xml:"domain,attr"`
	Type     string   `xml:"type,attr"`
	Label    string   `xml:"label,attr"`
	Reserved bool     `xml:"reserved,attr"`
	Docs     []Doc    `xml:"doc"`
	Asserts  []Assert `xml:"assert"`
}

type Response struct {
	Name string `xml:"name,attr"`
}

type Method struct {
	Name        string    `xml:"name,attr"`
	Response    Response  `xml:"response"`
	Synchronous bool      `xml:"synchronous,attr"`
	Content     bool      `xml:"content,attr"`
	Index       string    `xml:"index,attr"`
	Label       string    `xml:"label,attr"`
	Docs        []Doc     `xml:"doc"`
	Rules       []Rule    `xml:"rule"`
	Fields      []Field   `xml:"field"`
	Chassis     []Chassis `xml:"chassis"`
}

type Class struct {
	Name    string    `xml:"name,attr"`
	Handler string    `xml:"handler,attr"`
	Index   string    `xml:"index,attr"`
	Label   string    `xml:"label,attr"`
	Docs    []Doc     `xml:"doc"`
	Methods []Method  `xml:"method"`
	Chassis []Chassis `xml:"chassis"`
}

type Domain struct {
	Name  string `xml:"name,attr"`
	Type  string `xml:"type,attr"`
	Label string `xml:"label,attr"`
	Rules []Rule `xml:"rule"`
	Docs  []Doc  `xml:"doc"`
}

type Constant struct {
	Name  string `xml:"name,attr"`
	Value int    `xml:"value,attr"`
	Class string `xml:"class,attr"`
	Doc   string `xml:"doc"`
}

type Amqp struct {
	Major   int    `xml:"major,attr"`
	Minor   int    `xml:"minor,attr"`
	Port    int    `xml:"port,attr"`
	Comment string `xml:"comment,attr"`

	Constants []Constant `xml:"constant"`
	Domains   []Domain   `xml:"domain"`
	Classes   []Class    `xml:"class"`
}

type renderer struct {
	Root       Amqp
	bitcounter int
}

type fieldset struct {
	AmqpType   string
	NativeType string
	Fields     []Field
	*renderer
}

var (
	helpers = template.FuncMap{
		"public":  public,
		"private": private,
		"clean":   clean,
	}

	packageTemplate = template.Must(template.New("package").Funcs(helpers).Parse(`
	// Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved.
	// Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd.
	// Use of this source code is governed by a BSD-style
	// license that can be found in the LICENSE file.

	/* GENERATED FILE - DO NOT EDIT */
	/* Rebuild from the spec/gen.go tool */

	{{with .Root}}
	package amqp091

	import (
		"fmt"
		"encoding/binary"
		"io"
	)

	// Error codes that can be sent from the server during a connection or
	// channel exception or used by the client to indicate a class of error like
	// ErrCredentials.  The text of the error is likely more interesting than
	// these constants.
	const (
	{{range $c := .Constants}}
	{{if $c.IsError}}{{.Name | public}}{{else}}{{.Name | private}}{{end}} = {{.Value}}{{end}}
  )

	func isSoftExceptionCode(code int) bool {
		switch code {
		{{range $c := .Constants}} {{if $c.IsSoftError}} case {{$c.Value}}:
			return true
		{{end}}{{end}}
		}
		return false
	}

  {{range .Classes}}
    {{$class := .}}
    {{range .Methods}}
      {{$method := .}}
			{{$struct := $.StructName $class.Name $method.Name}}
      {{if .Docs}}/* {{range .Docs}} {{.Body | clean}} {{end}} */{{end}}
      type {{$struct}} struct {
        {{range .Fields}}
        {{$.FieldName .}} {{$.FieldType . | $.NativeType}} {{if .Label}}// {{.Label}}{{end}}{{end}}
				{{if .Content}}Properties properties
				Body []byte{{end}}
      }

			func (msg *{{$struct}}) id() (uint16, uint16) {
				return {{$class.Index}}, {{$method.Index}}
			}

			func (msg *{{$struct}}) wait() (bool) {
				return {{.Synchronous}}{{if $.HasField "NoWait" .}} && !msg.NoWait{{end}}
			}

			{{if .Content}}
      func (msg *{{$struct}}) getContent() (properties, []byte) {
        return msg.Properties, msg.Body
      }

      func (msg *{{$struct}}) setContent(props properties, body []byte) {
        msg.Properties, msg.Body = props, body
      }
			{{end}}
      func (msg *{{$struct}}) write(w io.Writer) (err error) {
				{{if $.HasType "bit" $method}}var bits byte{{end}}
        {{.Fields | $.Fieldsets | $.Partial "enc-"}}
        return
      }

      func (msg *{{$struct}}) read(r io.Reader) (err error) {
				{{if $.HasType "bit" $method}}var bits byte{{end}}
        {{.Fields | $.Fieldsets | $.Partial "dec-"}}
        return
      }
    {{end}}
  {{end}}

  func (r *reader) parseMethodFrame(channel uint16, size uint32) (f frame, err error) {
    mf := &methodFrame {
      ChannelId: channel,
    }

    if err = binary.Read(r.r, binary.BigEndian, &mf.ClassId); err != nil {
      return
    }

    if err = binary.Read(r.r, binary.BigEndian, &mf.MethodId); err != nil {
      return
    }

    switch mf.ClassId {
    {{range .Classes}}
    {{$class := .}}
    case {{.Index}}: // {{.Name}}
      switch mf.MethodId {
      {{range .Methods}}
      case {{.Index}}: // {{$class.Name}} {{.Name}}
        // fmt.Println("NextMethod: class:{{$class.Index}} method:{{.Index}}")
        method := &{{$.StructName $class.Name .Name}}{}
        if err = method.read(r.r); err != nil {
          return
        }
        mf.Method = method
      {{end}}
      default:
        return nil, fmt.Errorf("Bad method frame, unknown method %d for class %d", mf.MethodId, mf.ClassId)
      }
    {{end}}
    default:
      return nil, fmt.Errorf("Bad method frame, unknown class %d", mf.ClassId)
    }

    return mf, nil
  }
  {{end}}

  {{define "enc-bit"}}
    {{range $off, $field := .Fields}}
    if msg.{{$field | $.FieldName}} { bits |= 1 << {{$off}} }
    {{end}}
    if err = binary.Write(w, binary.BigEndian, bits); err != nil { return }
  {{end}}
  {{define "enc-octet"}}
    {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-shortshort"}}
    {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-short"}}
    {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-long"}}
    {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-longlong"}}
    {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-timestamp"}}
    {{range .Fields}} if err = writeTimestamp(w, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-shortstr"}}
    {{range .Fields}} if err = writeShortstr(w, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-longstr"}}
    {{range .Fields}} if err = writeLongstr(w, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "enc-table"}}
    {{range .Fields}} if err = writeTable(w, msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}

  {{define "dec-bit"}}
    if err = binary.Read(r, binary.BigEndian, &bits); err != nil {
      return
    }
    {{range $off, $field := .Fields}} msg.{{$field | $.FieldName}} = (bits & (1 << {{$off}}) > 0)
    {{end}}
  {{end}}
  {{define "dec-octet"}}
    {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-shortshort"}}
    {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-short"}}
    {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-long"}}
    {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-longlong"}}
    {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-timestamp"}}
    {{range .Fields}} if msg.{{. | $.FieldName}}, err = readTimestamp(r); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-shortstr"}}
    {{range .Fields}} if msg.{{. | $.FieldName}}, err = readShortstr(r); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-longstr"}}
    {{range .Fields}} if msg.{{. | $.FieldName}}, err = readLongstr(r); err != nil { return }
    {{end}}
  {{end}}
  {{define "dec-table"}}
    {{range .Fields}} if msg.{{. | $.FieldName}}, err = readTable(r); err != nil { return }
    {{end}}
  {{end}}

  `))
)

func (c *Constant) IsError() bool {
	return strings.Contains(c.Class, "error")
}

func (c *Constant) IsSoftError() bool {
	return c.Class == "soft-error"
}

func (renderer *renderer) Partial(prefix string, fields []fieldset) (s string, err error) {
	var buf bytes.Buffer
	for _, set := range fields {
		name := prefix + set.AmqpType
		t := packageTemplate.Lookup(name)
		if t == nil {
			return "", errors.New(fmt.Sprintf("Missing template: %s", name))
		}
		if err = t.Execute(&buf, set); err != nil {
			return
		}
	}
	return string(buf.Bytes()), nil
}

// Groups the fields so that the right encoder/decoder can be called
func (renderer *renderer) Fieldsets(fields []Field) (f []fieldset, err error) {
	if len(fields) > 0 {
		for _, field := range fields {
			cur := fieldset{}
			cur.AmqpType, err = renderer.FieldType(field)
			if err != nil {
				return
			}

			cur.NativeType, err = renderer.NativeType(cur.AmqpType)
			if err != nil {
				return
			}
			cur.Fields = append(cur.Fields, field)
			f = append(f, cur)
		}

		i, j := 0, 1
		for j < len(f) {
			if f[i].AmqpType == f[j].AmqpType {
				f[i].Fields = append(f[i].Fields, f[j].Fields...)
			} else {
				i++
				f[i] = f[j]
			}
			j++
		}
		return f[:i+1], nil
	}

	return
}

func (renderer *renderer) HasType(typ string, method Method) bool {
	for _, f := range method.Fields {
		name, _ := renderer.FieldType(f)
		if name == typ {
			return true
		}
	}
	return false
}

func (renderer *renderer) HasField(field string, method Method) bool {
	for _, f := range method.Fields {
		name := renderer.FieldName(f)
		if name == field {
			return true
		}
	}
	return false
}

func (renderer *renderer) Domain(field Field) (domain Domain, err error) {
	for _, domain = range renderer.Root.Domains {
		if field.Domain == domain.Name {
			return
		}
	}
	return domain, nil
	// return domain, ErrUnknownDomain
}

func (renderer *renderer) FieldName(field Field) (t string) {
	t = public(field.Name)

	if field.Reserved {
		t = strings.ToLower(t)
	}

	return
}

func (renderer *renderer) FieldType(field Field) (t string, err error) {
	t = field.Type

	if t == "" {
		var domain Domain
		domain, err = renderer.Domain(field)
		if err != nil {
			return "", err
		}
		t = domain.Type
	}

	return
}

func (renderer *renderer) NativeType(amqpType string) (t string, err error) {
	if t, ok := amqpTypeToNative[amqpType]; ok {
		return t, nil
	}
	return "", ErrUnknownType
}

func (renderer *renderer) Tag(d Domain) string {
	label := "`"

	label += `domain:"` + d.Name + `"`

	if len(d.Type) > 0 {
		label += `,type:"` + d.Type + `"`
	}

	label += "`"

	return label
}

func (renderer *renderer) StructName(parts ...string) string {
	return parts[0] + public(parts[1:]...)
}

func clean(body string) (res string) {
	return strings.Replace(body, "\r", "", -1)
}

func private(parts ...string) string {
	return export(regexp.MustCompile(`[-_]\w`), parts...)
}

func public(parts ...string) string {
	return export(regexp.MustCompile(`^\w|[-_]\w`), parts...)
}

func export(delim *regexp.Regexp, parts ...string) (res string) {
	for _, in := range parts {
		res += delim.ReplaceAllStringFunc(in, func(match string) string {
			switch len(match) {
			case 1:
				return strings.ToUpper(match)
			case 2:
				return strings.ToUpper(match[1:])
			}
			panic("unreachable")
		})
	}

	return
}

func main() {
	var r renderer

	spec, err := io.ReadAll(os.Stdin)
	if err != nil {
		log.Fatalln("Please pass spec on stdin", err)
	}

	err = xml.Unmarshal(spec, &r.Root)
	if err != nil {
		log.Fatalln("Could not parse XML:", err)
	}

	if err = packageTemplate.Execute(os.Stdout, &r); err != nil {
		log.Fatalln("Generate error: ", err)
	}
}
