package yu_proto_old

import (
	"errors"
	"fmt"
	"io"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"text/template"
)

const protoTemplate = `[[range $name, $values := .Enums]]
enum [[$name|$.Renamer.TypeName]] {[[range $values]]
  [[.Name|$.Renamer.ConstName]] = [[.Value]];[[end]]
}

[[end]][[range .Types]]
message [[.Name|$.Renamer.TypeName]] {[[range .|Fields]]
  [[.|TypeName]] [[.|$.Renamer.FieldName]] = [[.ID]][[.|Options]];[[end]]
}
[[end]]
`

var splitName = regexp.MustCompile(`((?:ID)|(?:[A-Z][a-z_0-9]+)|([\w\d]+))`)

func typeIndirect(t reflect.Type) reflect.Type {
	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	return t
}

func typeName(f ProtoField, enums enumTypeMap, renamer GeneratorNamer) (s string) {
	defer func() {
		if e := recover(); e != nil {
			s = ""
			panic(e.(string))
		}
	}()
	t := f.Field.Type
	if t.Kind() == reflect.Slice {
		if t.Elem().Kind() == reflect.Uint8 {
			return fieldPrefix(f, TagNone) + "bytes"
		}
		return "repeated " + innerTypeName(typeIndirect(t.Elem()), enums, renamer)
	}
	if t.Kind() == reflect.Ptr {
		return fieldPrefix(f, TagOptional) + innerTypeName(t.Elem(), enums, renamer)
	}
	return fieldPrefix(f, TagNone) + innerTypeName(t, enums, renamer)
}

func fieldPrefix(f ProtoField, def TagPrefix) string {
	opt := def
	if def == TagNone {
		opt = f.Prefix
	}
	switch opt {
	case TagOptional:
		return "optional "
	case TagRequired:
		return "required "
	default:
		if f.Field.Type.Kind() == reflect.Ptr {
			return "optional "
		}
		return "required "
	}
}

func innerTypeName(t reflect.Type, enums enumTypeMap, renamer GeneratorNamer) string {
	if (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) && t.Elem().Kind() == reflect.Uint8 {
		return "bytes"
	}
	if t.PkgPath() == "time" {
		if t.Name() == "Time" {
			return "sfixed64"
		}
		if t.Name() == "Duration" {
			return "sint64"
		}
	}
	switch t.Name() {
	case "Ufixed32":
		return "fixed32"
	case "Ufixed64":
		return "ufixed64"
	case "Sfixed32":
		return "sfixed32"
	case "Sfixed64":
		return "sfixed64"
	}

	if _, ok := enums[t.Name()]; ok {
		return renamer.TypeName(t.Name())
	}

	switch t.Kind() {
	case reflect.Float64:
		return "double"
	case reflect.Float32:
		return "float"
	case reflect.Int32:
		return "sint32"
	case reflect.Int, reflect.Int64:
		return "sint64"
	case reflect.Bool:
		return "bool"
	case reflect.Uint32:
		return "uint32"
	case reflect.Uint, reflect.Uint64:
		return "uint64"
	case reflect.String:
		return "string"
	case reflect.Struct:
		return t.Name()
	case reflect.Map:
		// we have to do this again (otherwise we'll end up with an empty name for the value):
		var valTypeName string
		valType := t.Elem()
		if valType.Kind() == reflect.Slice {
			if valType.Elem().Kind() == reflect.Uint8 {
				valTypeName = "bytes"
			} else {
				valTypeName = innerTypeName(typeIndirect(valType.Elem()), enums, renamer)
			}
		} else if valType.Kind() == reflect.Ptr {
			valTypeName = innerTypeName(valType.Elem(), enums, renamer)
		} else {
			// here we can just use the value's type:
			valTypeName = innerTypeName(valType, enums, renamer)
		}
		return fmt.Sprintf("map<%s, %s>", innerTypeName(t.Key(), enums, renamer), valTypeName)
	default:
		panic("unsupported type " + t.Name())
	}
}

func options(f ProtoField) string {
	if f.Field.Type.Kind() == reflect.Slice {
		switch f.Field.Type.Elem().Kind() {
		case reflect.Bool,
			reflect.Int32, reflect.Int64,
			reflect.Uint32, reflect.Uint64,
			reflect.Float32, reflect.Float64:
			return " [packed=true]"
		}
	}
	return ""
}

type GeneratorNamer interface {
	FieldName(ProtoField) string
	TypeName(name string) string
	ConstName(name string) string
}

// DefaultGeneratorNamer renames symbols when mapping from Go to .proto files.
//
// The rules are:
// - Field names are mapped from SomeFieldName to some_field_name.
// - Type names are not modified.
// - Constants are mapped form SomeConstantName to SOME_CONSTANT_NAME.
type DefaultGeneratorNamer struct{}

func (d *DefaultGeneratorNamer) FieldName(f ProtoField) string {
	if f.Name != "" {
		return f.Name
	}
	parts := splitName.FindAllString(f.Field.Name, -1)
	for i := range parts {
		parts[i] = strings.ToLower(parts[i])
	}
	return strings.Join(parts, "_")
}

func (d *DefaultGeneratorNamer) TypeName(name string) string {
	return name
}

func (d *DefaultGeneratorNamer) ConstName(name string) string {
	parts := splitName.FindAllString(name, -1)
	for i := range parts {
		parts[i] = strings.ToUpper(parts[i])
	}
	return strings.Join(parts, "_")
}

type reflectedTypes []reflect.Type

func (r reflectedTypes) Len() int           { return len(r) }
func (r reflectedTypes) Swap(i, j int)      { r[i], r[j] = r[j], r[i] }
func (r reflectedTypes) Less(i, j int) bool { return r[i].Name() < r[j].Name() }

type EnumMap map[string]interface{}

type enumValue struct {
	Name  string
	Value Enum
}

type enumValues []enumValue

func (e enumValues) Len() int           { return len(e) }
func (e enumValues) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
func (e enumValues) Less(i, j int) bool { return e[i].Value < e[j].Value }

type enumTypeMap map[string]enumValues

// GenerateProtobufDefinition generates a .proto file from a list of structs via reflection.
// fieldNamer is a function that maps ProtoField types to generated protobuf field names.
func GenerateProtobufDefinition(w io.Writer, types []interface{}, enumMap EnumMap, renamer GeneratorNamer) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = errors.New(e.(string))
		}
	}()
	enums := enumTypeMap{}
	for name, value := range enumMap {
		v := reflect.ValueOf(value)
		t := v.Type()
		if t.Kind() != reflect.Uint32 {
			return fmt.Errorf("enum type aliases must be uint32")
		}
		if t.Name() == "uint32" {
			return fmt.Errorf("enum value must be a type alias, but got uint32")
		}
		enums[t.Name()] = append(enums[t.Name()], enumValue{name, Enum(v.Uint())})
	}
	for _, values := range enums {
		sort.Sort(values)
	}
	rt := reflectedTypes{}
	for _, t := range types {
		typ := reflect.Indirect(reflect.ValueOf(t)).Type()
		if typ.Kind() != reflect.Struct {
			continue
		}
		rt = append(rt, typ)
	}
	sort.Sort(rt)
	if renamer == nil {
		renamer = &DefaultGeneratorNamer{}
	}
	t := template.Must(template.New("protobuf").Funcs(template.FuncMap{
		"Fields":   ProtoFields,
		"TypeName": func(f ProtoField) string { return typeName(f, enums, renamer) },
		"Options":  options,
	}).Delims("[[", "]]").Parse(protoTemplate))
	return t.Execute(w, map[string]interface{}{
		"Renamer": renamer,
		"Enums":   enums,
		"Types":   rt,
		"Ptr":     reflect.Ptr,
		"Slice":   reflect.Slice,
		"Map":     reflect.Map,
	})
}
