package internalgengo

import (
	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/reflect/protoreflect"
)

var protowirePackage = protogen.GoImportPath("google.golang.org/protobuf/encoding/protowire")

func emitStableSize(g *protogen.GeneratedFile, msg *protogen.Message) {
	fs := sortFields(msg.Fields)

	g.P("// StableSize returns the size of x in protobuf format.")
	g.P("//")
	g.P("// Structures with the same field values have the same binary size.")
	g.P("func (x *", msg.GoIdent.GoName, ") StableSize() (size int) {")
	g.P("if x == nil { return 0 }")
	if len(fs) != 0 {
		for _, f := range fs {
			if f.Desc.IsList() && marshalers[f.Desc.Kind()].RepeatedDouble && !(f.Desc.Kind() == protoreflect.Uint64Kind && !f.Desc.IsPacked()) {
				g.P("var n int")
				break
			}
		}
		for _, f := range fs {
			emitFieldSize(g, f)
		}
	}
	g.P("return size")
	g.P("}\n")
}

func emitSignatureMethods(g *protogen.GeneratedFile, msg *protogen.Message) {
	// SignedDataSize implementation (only for requests and responses).
	g.P("// ReadSignedData fills buf with signed data of x.")
	g.P("// If buffer length is less than x.SignedDataSize(), new buffer is allocated.")
	g.P("//")
	g.P("// Returns any error encountered which did not allow writing the data completely.")
	g.P("// Otherwise, returns the buffer in which the data is written.")
	g.P("//")
	g.P("// Structures with the same field values have the same signed data.")
	g.P("func (x *", msg.GoIdent.GoName, ") SignedDataSize() int {")
	g.P("return x.GetBody().StableSize()")
	g.P("}\n")

	// ReadSignedData implementation (only for requests and responses).
	g.P("// SignedDataSize returns size of the request signed data in bytes.")
	g.P("//")
	g.P("// Structures with the same field values have the same signed data size.")
	g.P("func (x *", msg.GoIdent.GoName, ") ReadSignedData(buf []byte) ([]byte, error) {")
	g.P("return x.GetBody().MarshalProtobuf(buf), nil")
	g.P("}\n")
}

func emitFieldSize(g *protogen.GeneratedFile, f *protogen.Field) {
	m := marshalers[f.Desc.Kind()]
	if m.Prefix == "" {
		g.P("// FIXME missing field marshaler: ", f.GoName, " of type ", f.Desc.Kind().String())
		g.P(`panic("unimplemented")`)
		return
	}

	name := castFieldName(f)
	if f.Oneof != nil {
		name = "x." + f.Oneof.GoName
		g.P("if inner, ok := ", name, ".(*", f.GoIdent.GoName, "); ok {")
		defer g.P("}")
		name = "inner." + f.GoName
	}

	switch {
	case f.Desc.IsList() && (f.Desc.Kind() == protoreflect.MessageKind || f.Desc.Kind() == protoreflect.Uint64Kind && !f.Desc.IsPacked()):
		g.P("for i := range ", name, "{")
		if f.Desc.Kind() == protoreflect.MessageKind {
			g.P("size += ", protoPackage.Ident("NestedStructureSizeUnchecked"), "(", f.Desc.Number(), ", &", name, "[i])")
		} else {
			if f.Desc.Kind() != protoreflect.Uint64Kind {
				panic("only uint64 unpacked primitive is supported")
			}

			g.P("size += ", protowirePackage.Ident("SizeGroup"), "(",
				protowirePackage.Ident("Number"), "(", f.Desc.Number(), "), ",
				protowirePackage.Ident("SizeVarint"), "(", name, "[i]))")
		}
		g.P("}")

	case f.Desc.IsList():
		if m.RepeatedDouble {
			g.P("n, _ = ", protoPackage.Ident("Repeated"+m.Prefix+"Size"), "(", f.Desc.Number(), ", ", name, ")")
			g.P("size += n")
		} else {
			g.P("size += ", protoPackage.Ident("Repeated"+m.Prefix+"Size"), "(", f.Desc.Number(), ", ", name, ")")
		}
	default:
		g.P("size += ", protoPackage.Ident(m.Prefix+"Size"), "(", f.Desc.Number(), ", ", name, ")")
	}
}

type marshalerDesc struct {
	Prefix         string
	RepeatedDouble bool
}

// Unused kinds are commented.
var marshalers = map[protoreflect.Kind]marshalerDesc{
	protoreflect.BoolKind:  {Prefix: "Bool"},
	protoreflect.EnumKind:  {Prefix: "Enum"},
	protoreflect.Int32Kind: {Prefix: "Int32", RepeatedDouble: true},
	// protoreflect.Sint32Kind:   "",
	protoreflect.Uint32Kind: {Prefix: "UInt32", RepeatedDouble: true},
	protoreflect.Int64Kind:  {Prefix: "Int64", RepeatedDouble: true},
	// protoreflect.Sint64Kind:   "",
	protoreflect.Uint64Kind: {Prefix: "UInt64", RepeatedDouble: true},
	// protoreflect.Sfixed32Kind: "",
	protoreflect.Fixed32Kind: {Prefix: "Fixed32", RepeatedDouble: true},
	// protoreflect.FloatKind:    "",
	// protoreflect.Sfixed64Kind: "",
	protoreflect.Fixed64Kind: {Prefix: "Fixed64", RepeatedDouble: true},
	protoreflect.DoubleKind:  {Prefix: "Float64"},
	protoreflect.StringKind:  {Prefix: "String"},
	protoreflect.BytesKind:   {Prefix: "Bytes"},
	protoreflect.MessageKind: {Prefix: "NestedStructure"},
	// protoreflect.GroupKind:    "",
}