package gengo

import (
	"bytes"
	"errors"
	"fmt"
	"go/format"
	"io"
	"os"
	"strconv"
	"strings"

	"go.e43.eu/xdrgen/ast"
)

func DocComment(a ast.Attributes, def string) string {
	doc := a.GetStringDefault("doc", def)
	if doc == "" {
		return ""
	}

	lines := strings.Split(doc, "\n")
	return "// " + strings.Join(lines, "\n 
}

func GenSpecification(s *ast.Specification) ([]byte, error) {
	buf := new(bytes.Buffer)
	if err := genSpecification(buf, s); err != nil {
		return nil, err
	}

	out, err := format.Source(buf.Bytes())
	if err != nil {
		fmt.Fprintln(os.Stderr, string(buf.Bytes()))
		return nil, err
	}
	return out, nil
}

func genSpecification(w io.Writer, s *ast.Specification) error {
	packageName := s.Attributes.GetStringDefault("go_package", "x")

	if err := headerTemplate.Execute(w, map[string]interface{}{
		"Doc":         DocComment(s.Attributes, fmt.Sprintf("%s is an autogenerated XDR package", packageName)),
		"PackageName": packageName,
	}); err != nil {
		return err
	}

	for _, d := range s.Definitions {

		if d.Body.Kind == ast.DEFINITION_KIND_CONSTANT && d.Body.Constant.Type == ast.CONST_ENUM {
			continue
		}

		if err := GenDefinition(w, s, d); err != nil {
			return err
		}
		fmt.Fprintln(w)
	}

	return footerTemplate.Execute(w, nil)
}

func GenDefinition(w io.Writer, s *ast.Specification, d *ast.Definition) (err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("Rendering '%s': %v", d.Name, err)
		}
	}()
	switch d.Body.Kind {
	case ast.DEFINITION_KIND_TYPE:
		return GenTypeDefinition(w, s, d.Name, d.Body.Type, d.Attributes)

	case ast.DEFINITION_KIND_CONSTANT:
		return GenValueDefinition(w, d.Name, d.Body.Constant, d.Attributes)

	default:
		return errors.New("Unknown definition kind")
	}
}

func GenTypeDefinition(w io.Writer, s *ast.Specification, name string, t *ast.Type, a ast.Attributes) error {
	switch t.Kind {
	case ast.TYPE_ENUM:
		return GenEnumDefinition(w, s, name, t.EnumSpec, a)

	case ast.TYPE_STRUCT:
		return GenStructDefinition(w, s, name, t.StructSpec, a)

	case ast.TYPE_UNION:
		return GenUnionDefinition(w, s, name, t.UnionSpec, a)

	case ast.TYPE_TYPEDEF:
		return GenTypedefDefinition(w, s, name, t.TypeDef, a)

	default:
		_, err := fmt.Fprintln(w, "// Warning: Don't know how to generate ", t.Kind, name)
		return err
	}
}

func GenTypedefDefinition(w io.Writer, s *ast.Specification, name string, t *ast.Declaration, a ast.Attributes) error {
	decl, tags, err := GenTypedefDeclaration(s, t)
	if err != nil {
		return err
	}

	comment := DocComment(a, "")
	if comment != "" {
		_, err := fmt.Fprintln(w, comment)
		if err != nil {
			return err
		}
	}

	_, err = fmt.Fprintf(w, "type %s\n", decl)

	if len(tags) > 0 {
		return typedefProxyTemplate.Execute(w, map[string]interface{}{
			"Specification": s,
			"TypeName":      name,
			"Decl":          t,
		})
	}
	return err
}

func GenEnumDefinition(w io.Writer, s *ast.Specification, name string, es *ast.EnumSpec, a ast.Attributes) error {
	options := es.GetOptions(s)
	values := make(map[uint32]string, len(options))
	for _, m := range options {
		if _, exists := values[m.Value]; !exists {
			values[m.Value] = m.Name
		}
	}

	return enumTemplate.Execute(w, map[string]interface{}{
		"Doc":      DocComment(a, fmt.Sprintf("%s is enum %s", CamelCase(name), name)),
		"TypeName": name,
		"Options":  options,
		"Values":   values,
	})
}

func NameIfAnonymous(w io.Writer, s *ast.Specification, parentName string, d *ast.Declaration) error {
	switch d.Type.Kind {
	case ast.TYPE_ENUM,
		ast.TYPE_STRUCT,
		ast.TYPE_UNION:

	default:
		return nil
	}

	name := fmt.Sprintf("%s.%s", parentName, d.Name)
	nType := *d.Type
	dIdx, err := s.PutDefinition(&ast.Definition{
		Name: name,
		Body: &ast.Definition_Body{
			Kind: ast.DEFINITION_KIND_TYPE,
			Type: &nType,
		},
		Attributes: d.Attributes,
	})
	if err != nil {
		return err
	}

	if err := GenTypeDefinition(w, s, name, d.Type, d.Attributes); err != nil {
		return err
	}

	*d.Type = ast.Type{
		Kind: ast.TYPE_REF,
		Ref:  dIdx,
	}
	return nil
}

var structTemplate = compileTemplate("struct", `
{{- $Spec := .Specification}}
{{- $TypeName := .TypeName}}
{{- $GoType := GoName $TypeName}}
{{.Doc}}
type {{$GoType}} struct {
{{- range .Members}}
	{{Declaration $Spec .}}
{{- end}}
}
`)

func GenStructDefinition(w io.Writer, s *ast.Specification, name string, ss *ast.StructSpec, a ast.Attributes) error {
	for i := range ss.Members {
		if err := NameIfAnonymous(w, s, name, ss.Members[i]); err != nil {
			return err
		}
	}

	return structTemplate.Execute(w, map[string]interface{}{
		"Doc":           DocComment(a, fmt.Sprintf("%s is struct %s", CamelCase(name), name)),
		"TypeName":      name,
		"Members":       ss.Members,
		"Specification": s,
	})
}

func GenUnionDefinition(w io.Writer, s *ast.Specification, name string, us *ast.UnionSpec, a ast.Attributes) error {
	if err := NameIfAnonymous(w, s, name, us.Discriminant); err != nil {
		return err
	}

	for i := range us.Members {
		if err := NameIfAnonymous(w, s, name, us.Members[i]); err != nil {
			return err
		}
	}

	discrimType, err := us.Discriminant.Type.Resolve(s)
	if err != nil {
		return err
	}

	var discrimEnum *ast.EnumSpec
	if discrimType.Kind == ast.TYPE_ENUM {
		discrimEnum = discrimType.EnumSpec
	}

	optNameToField := make(map[string]string)
	for value, membPos := range us.Options {
		var name string
		if discrimEnum != nil {
			name = CamelCase(discrimEnum.GetName(s, value))
		}
		if name == "" {
			name = strconv.FormatUint(uint64(value), 10)
		}
		optNameToField[name] = us.Members[membPos].Name
	}

	annotatedMembers := make([]*UnionDeclaration, len(us.Members))
	for i, d := range us.Members {
		annotatedMembers[i] = &UnionDeclaration{decl: d}
	}

	for value, membPos := range us.Options {
		m := annotatedMembers[membPos]
		m.variants = append(m.variants, value)
	}

	if us.DefaultMember != nil {
		annotatedMembers[*us.DefaultMember].isDefault = true
	}

	return unionTemplate.Execute(w, map[string]interface{}{
		"Doc":              DocComment(a, fmt.Sprintf("%s is union %s", CamelCase(name), name)),
		"TypeName":         name,
		"Discriminant":     us.Discriminant,
		"AnnotatedMembers": annotatedMembers,
		"Members":          us.Members,
		"NameToField":      optNameToField,
		"Specification":    s,
	})
}

func GenValueDefinition(w io.Writer, name string, v *ast.Constant, a ast.Attributes) error {
	comment := DocComment(a, "")
	if comment != "" {
		_, err := fmt.Fprintln(w, comment)
		if err != nil {
			return err
		}
	}
	_, err := fmt.Fprintf(w, "const %s = %s\n", CamelCase(name), GoValue(v))
	return err
}
package gengo

import (
	"bytes"
	"errors"
	"fmt"
	"go/format"
	"io"
	"os"
	"strconv"
	"strings"

	"go.e43.eu/xdrgen/ast"
)

func DocComment(a ast.Attributes, def string) string {
	doc := a.GetStringDefault("doc", def)
	if doc == "" {
		return ""
	}

	lines := strings.Split(doc, "\n")
	return "// " + strings.Join(lines, "\n //")
}

func GenSpecification(s *ast.Specification) ([]byte, error) {
	buf := new(bytes.Buffer)
	if err := genSpecification(buf, s); err != nil {
		return nil, err
	}

	out, err := format.Source(buf.Bytes())
	if err != nil {
		fmt.Fprintln(os.Stderr, string(buf.Bytes()))
		return nil, err
	}
	return out, nil
}

func genSpecification(w io.Writer, s *ast.Specification) error {
	packageName := s.Attributes.GetStringDefault("go_package", "x")

	if err := headerTemplate.Execute(w, map[string]interface{}{
		"Doc":         DocComment(s.Attributes, fmt.Sprintf("%s is an autogenerated XDR package", packageName)),
		"PackageName": packageName,
	}); err != nil {
		return err
	}

	for _, d := range s.Definitions {
		// Skip enums - we will generate their definitions while generating the enum itself
		if d.Body.Kind == ast.DEFINITION_KIND_CONSTANT && d.Body.Constant.Type == ast.CONST_ENUM {
			continue
		}

		if err := GenDefinition(w, s, d); err != nil {
			return err
		}
		fmt.Fprintln(w)
	}

	return footerTemplate.Execute(w, nil)
}

func GenDefinition(w io.Writer, s *ast.Specification, d *ast.Definition) (err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("Rendering '%s': %v", d.Name, err)
		}
	}()
	switch d.Body.Kind {
	case ast.DEFINITION_KIND_TYPE:
		return GenTypeDefinition(w, s, d.Name, d.Body.Type, d.Attributes)

	case ast.DEFINITION_KIND_CONSTANT:
		return GenValueDefinition(w, d.Name, d.Body.Constant, d.Attributes)

	default:
		return errors.New("Unknown definition kind")
	}
}

func GenTypeDefinition(w io.Writer, s *ast.Specification, name string, t *ast.Type, a ast.Attributes) error {
	switch t.Kind {
	case ast.TYPE_ENUM:
		return GenEnumDefinition(w, s, name, t.EnumSpec, a)

	case ast.TYPE_STRUCT:
		return GenStructDefinition(w, s, name, t.StructSpec, a)

	case ast.TYPE_UNION:
		return GenUnionDefinition(w, s, name, t.UnionSpec, a)

	case ast.TYPE_TYPEDEF:
		return GenTypedefDefinition(w, s, name, t.TypeDef, a)

	default:
		_, err := fmt.Fprintln(w, "// Warning: Don't know how to generate ", t.Kind, name)
		return err
	}
}

func GenTypedefDefinition(w io.Writer, s *ast.Specification, name string, t *ast.Declaration, a ast.Attributes) error {
	decl, tags, err := GenTypedefDeclaration(s, t)
	if err != nil {
		return err
	}

	comment := DocComment(a, "")
	if comment != "" {
		_, err := fmt.Fprintln(w, comment)
		if err != nil {
			return err
		}
	}

	_, err = fmt.Fprintf(w, "type %s\n", decl)

	// We have tags that need applying, so we need to generate proxy
	// MarshalXDR/UnmarshalXDR methods
	//
	// TODO: Implement specialised/less generic support for types like string/opaque
	//       so we avoid having to go through the generic marshaller indirections
	if len(tags) > 0 {
		return typedefProxyTemplate.Execute(w, map[string]interface{}{
			"Specification": s,
			"TypeName":      name,
			"Decl":          t,
		})
	}
	return err
}

func GenEnumDefinition(w io.Writer, s *ast.Specification, name string, es *ast.EnumSpec, a ast.Attributes) error {
	options := es.GetOptions(s)
	values := make(map[uint32]string, len(options))
	for _, m := range options {
		if _, exists := values[m.Value]; !exists {
			values[m.Value] = m.Name
		}
	}

	return enumTemplate.Execute(w, map[string]interface{}{
		"Doc":      DocComment(a, fmt.Sprintf("%s is enum %s", CamelCase(name), name)),
		"TypeName": name,
		"Options":  options,
		"Values":   values,
	})
}

func NameIfAnonymous(w io.Writer, s *ast.Specification, parentName string, d *ast.Declaration) error {
	switch d.Type.Kind {
	case ast.TYPE_ENUM,
		ast.TYPE_STRUCT,
		ast.TYPE_UNION:
		// Continue
	default:
		return nil
	}

	name := fmt.Sprintf("%s.%s", parentName, d.Name)
	nType := *d.Type
	dIdx, err := s.PutDefinition(&ast.Definition{
		Name: name,
		Body: &ast.Definition_Body{
			Kind: ast.DEFINITION_KIND_TYPE,
			Type: &nType,
		},
		Attributes: d.Attributes,
	})
	if err != nil {
		return err
	}

	if err := GenTypeDefinition(w, s, name, d.Type, d.Attributes); err != nil {
		return err
	}

	*d.Type = ast.Type{
		Kind: ast.TYPE_REF,
		Ref:  dIdx,
	}
	return nil
}

var structTemplate = compileTemplate("struct", `
{{- $Spec := .Specification}}
{{- $TypeName := .TypeName}}
{{- $GoType := GoName $TypeName}}
{{.Doc}}
type {{$GoType}} struct {
{{- range .Members}}
	{{Declaration $Spec .}}
{{- end}}
}
`)

func GenStructDefinition(w io.Writer, s *ast.Specification, name string, ss *ast.StructSpec, a ast.Attributes) error {
	for i := range ss.Members {
		if err := NameIfAnonymous(w, s, name, ss.Members[i]); err != nil {
			return err
		}
	}

	return structTemplate.Execute(w, map[string]interface{}{
		"Doc":           DocComment(a, fmt.Sprintf("%s is struct %s", CamelCase(name), name)),
		"TypeName":      name,
		"Members":       ss.Members,
		"Specification": s,
	})
}

func GenUnionDefinition(w io.Writer, s *ast.Specification, name string, us *ast.UnionSpec, a ast.Attributes) error {
	if err := NameIfAnonymous(w, s, name, us.Discriminant); err != nil {
		return err
	}

	for i := range us.Members {
		if err := NameIfAnonymous(w, s, name, us.Members[i]); err != nil {
			return err
		}
	}

	discrimType, err := us.Discriminant.Type.Resolve(s)
	if err != nil {
		return err
	}

	var discrimEnum *ast.EnumSpec
	if discrimType.Kind == ast.TYPE_ENUM {
		discrimEnum = discrimType.EnumSpec
	}

	optNameToField := make(map[string]string)
	for value, membPos := range us.Options {
		var name string
		if discrimEnum != nil {
			name = CamelCase(discrimEnum.GetName(s, value))
		}
		if name == "" {
			name = strconv.FormatUint(uint64(value), 10)
		}
		optNameToField[name] = us.Members[membPos].Name
	}

	annotatedMembers := make([]*UnionDeclaration, len(us.Members))
	for i, d := range us.Members {
		annotatedMembers[i] = &UnionDeclaration{decl: d}
	}

	for value, membPos := range us.Options {
		m := annotatedMembers[membPos]
		m.variants = append(m.variants, value)
	}

	if us.DefaultMember != nil {
		annotatedMembers[*us.DefaultMember].isDefault = true
	}

	return unionTemplate.Execute(w, map[string]interface{}{
		"Doc":              DocComment(a, fmt.Sprintf("%s is union %s", CamelCase(name), name)),
		"TypeName":         name,
		"Discriminant":     us.Discriminant,
		"AnnotatedMembers": annotatedMembers,
		"Members":          us.Members,
		"NameToField":      optNameToField,
		"Specification":    s,
	})
}

func GenValueDefinition(w io.Writer, name string, v *ast.Constant, a ast.Attributes) error {
	comment := DocComment(a, "")
	if comment != "" {
		_, err := fmt.Fprintln(w, comment)
		if err != nil {
			return err
		}
	}
	_, err := fmt.Fprintf(w, "const %s = %s\n", CamelCase(name), GoValue(v))
	return err
}
