//go:build ignore

package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"html/template"
	"log"
	"strings"

	"codeberg.org/miekg/dns/internal/generate"
)

var hdr = `
// Code generated by "go run compare_generate.go"; DO NOT EDIT.

package dns

import (
	"strings"
	"slices"
	"cmp"
)

`

var compareFunc = template.Must(template.New("compareFunc").Parse(`
func compare(a, b RR) int {
	switch x := a.(type) {
{{range .}}  case *{{.}}:
	return x.compare(b)
{{end}} }
	if x, ok := a.(Comparer); ok {
		return x.Compare(b)
	}
	return 0
}

`))

const out = "zcompare.go"

func main() {
	flag.Parse()
	specs, err := generate.StructTypeSpecs("rdata/rdata.go")
	if err != nil {
		log.Fatalf("Failed to generate %s: %v", out, err)
	}
	emptydata, err := generate.StructTypeSpecs("types.go")
	if err != nil {
		log.Fatalf("Failed to generate %s: %v", out, err)
	}
	specs = append(specs, generate.FilterTypeSpecs(emptydata, generate.EmptyData)...)
	b := &bytes.Buffer{}
	b.WriteString(hdr)

	// main compare func
	types, err := generate.Types("types.go")
	if err != nil {
		log.Fatalf("Failed to generate %s: %v", out, err)
	}

	if err := compareFunc.Execute(b, types); err != nil {
		log.Fatalf("Failed to generate %s: %v", out, err)
	}

	for _, spec := range specs {
		rrname := spec.Name.Name
		//
		// Compare functions, does compare(a, b RR) as a.compare(b)
		//

		fmt.Fprintf(b, "func (rr *%s) compare(b RR) (x int) {\n", rrname)
		strct := spec.Type.(*ast.StructType)

		if generate.IsEmbedded(strct) {
			fmt.Fprintf(b, "return rr.%[1]s.compare(&b.(*%[2]s).%[1]s)\n", strct.Fields.List[0].Type, rrname)
			fmt.Fprint(b, "}\n\n")
			continue
		}
		for _, field := range strct.Fields.List {

			if len(field.Names) == 0 {
				continue
			}
			fieldname := field.Names[0].String()
			if fieldname == "Hdr" {
				continue
			}

			o := func(s string) {
				fmt.Fprintf(b, s+`;if x != 0 {
if x < 0 {
	return -1
}
return 1
}
`, fieldname, rrname, fieldname)
			}
			// fieldtype is either slice or the actual singular type name.
			fieldtype := ""
			if id, ok := field.Type.(*ast.Ident); ok {
				fieldtype = id.Name
			}
			if _, ok := field.Type.(*ast.ArrayType); ok {
				fieldtype = "slice"
			}

			tag := ""
			if field.Tag != nil {
				tag = strings.Trim(field.Tag.Value, "`")
			}

			if fieldtype == "slice" {
				switch tag {
				case `dns:"-"`: // ignored
				case `dns:"txt"`:
					o(`j := 0
						for i := range rr.%[1]s {
						if i > j || x != 0 {
							break
						}
						x = len(rr.%[1]s[i]) - len(b.(*%[2]s).%[3]s[j])
						if x == 0 {
							x = strings.Compare(rr.%[1]s[i], b.(*%[2]s).%[3]s[j])
						}
						j++
					}`)
				case `dns:"opt"`:
				// never signed, but compare would be nice...

				case `dns:"nsec"`:
					o(`x = slices.Compare(rr.%s, b.(*%s).%s)`)

				case `dns:"pairs"`:
					o(`x = comparepair(rr.%s, b.(*%s).%s)`)

				case `dns:"infos"`:
					o(`x = compareinfo(rr.%s, b.(*%s).%s)`)

				case `dns:"name"`:
					o(`j := 0
						for i := range rr.%[1]s {
						if i > j || x != 0 {
							break
						}
						x = comparename(rr.%[1]s[i], b.(*%[2]s).%[3]s[j])
						j++
					}`)
				}
				continue
			}

			// non-slice fields

			switch {
			case tag == `dns:"-"`: // ignored
			case tag == `dns:"cname"`:
				fallthrough
			case tag == `dns:"mname"`:
				fallthrough
			case tag == `dns:"name"`:
				o("x = comparename(rr.%s, b.(*%s).%s)")
			case tag == `dns:"a"`:
				o("x = rr.%s.Compare(b.(*%s).%s)")
			case tag == `dns:"aaaa"`:
				o("x = rr.%s.Compare(b.(*%s).%s)")
			case tag == `dns:"ttl"`:
				o(`x = int(rr.%s) - int(b.(*%s).%s)`)
			case tag == `dns:"uint48"`:
				o(`x = cmp.Compare(rr.%s, b.(*%s).%s)`)
			case strings.HasPrefix(tag, `dns:"size-base32`): // size-base32 can be packed just like base32
				fallthrough
			case tag == `dns:"base32"`:
				o(`x = comparebase32(rr.%s, b.(*%s).%s)`)

			case strings.HasPrefix(tag, `dns:"size-base64`): // size-base64 can be packed just like base64
				fallthrough
			case tag == `dns:"base64"`:
				o(`x = comparebase64(rr.%s, b.(*%s).%s)`)

			case strings.HasPrefix(tag, `dns:"size-hex:SaltLength`):
				fallthrough
			case strings.HasPrefix(tag, `dns:"size-hex`): // size-hex can be packed just like hex
				fallthrough
			case tag == `dns:"hex"`:
				if fieldname == "Data" {
					o(`x = comparehex(rr.RFC3597.%s, b.(*%s).RFC3597.%s)`)
				} else {
					o(`x = comparehex(rr.%s, b.(*%s).%s)`)
				}

			case tag == `dns:"any"`:
				// can we do strings.Compare here? TODO(miek)
				o("x = len(rr.%s) - len(b.(*%s).%s)")
				o("x = strings.Compare(rr.%s, b.(*%s).%s)")

			case tag == "":
				switch fieldtype {
				case "uint8":
					fallthrough
				case "uint16":
					fallthrough
				case "uint32":
					fallthrough
				case "uint64":
					o(`x = cmp.Compare(rr.%s, b.(*%s).%s)`)
				case "string":
					o("x = len(rr.%s) - len(b.(*%s).%s)")
					o("x = strings.Compare(rr.%s, b.(*%s).%s)")
				}
			}
		}
		fmt.Fprint(b, "return 0\n}\n\n")
	}

	generate.Write(b, out)
}

// structMember will take a tag like dns:"size-base32:SaltLength" and return the last part of this string.
func structMember(s string) string {
	idx := strings.LastIndex(s, ":")
	return strings.TrimSuffix(s[idx+1:], `"`)
}

// structTag will take a tag like dns:"size-base32:SaltLength" and return base32.
func structTag(s string) string {
	s = strings.TrimPrefix(s, `dns:"size-`)
	s, _, _ = strings.Cut(s, ":")
	return s
}
