package protoprint

import (
	"bytes"
	"fmt"
	"sort"
	"strings"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/reflect/protoregistry"
	"google.golang.org/protobuf/types/dynamicpb"
	"google.golang.org/protobuf/types/known/anypb"
)

func (p *Printer) printMessageLiteralCompact(msg protoreflect.Message, res *protoregistry.Types, pkg, scope protoreflect.FullName) string {
	var buf bytes.Buffer
	p.printMessageLiteralToBuffer(&buf, msg, res, pkg, scope, 0, -1)
	return buf.String()
}

var (
	anyTypeName = (&anypb.Any{}).ProtoReflect().Descriptor().FullName()
)

const (
	anyTypeUrlTag = 1
	anyValueTag   = 2
)

func (p *Printer) printMessageLiteralToBuffer(
	buf *bytes.Buffer,
	msg protoreflect.Message,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
	threshold, indent int,
) {
	if p.maybePrintAnyMessageToBuffer(buf, msg, res, pkg, scope, threshold, indent) {
		return
	}

	buf.WriteRune('{')
	if indent >= 0 {
		indent++
	}

	type fieldVal struct {
		fld protoreflect.FieldDescriptor
		val protoreflect.Value
	}
	var fields []fieldVal
	msg.Range(func(fld protoreflect.FieldDescriptor, val protoreflect.Value) bool {
		fields = append(fields, fieldVal{fld: fld, val: val})
		return true
	})
	sort.Slice(fields, func(i, j int) bool {
		return fields[i].fld.Number() < fields[j].fld.Number()
	})

	for i, fldVal := range fields {
		fld, val := fldVal.fld, fldVal.val
		if i > 0 {
			buf.WriteRune(',')
		}
		p.maybeNewline(buf, indent)
		if fld.IsExtension() {
			buf.WriteRune('[')
			buf.WriteString(p.qualifyExtensionLiteralName(pkg, scope, fld.FullName()))
			buf.WriteRune(']')
		} else {
			buf.WriteString(string(fld.Name()))
		}
		buf.WriteString(": ")
		switch {
		case fld.IsList():
			p.printArrayLiteralToBufferMaybeCompact(buf, fld, val.List(), res, pkg, scope, threshold, indent)
		case fld.IsMap():
			p.printMapLiteralToBufferMaybeCompact(buf, fld, val.Map(), res, pkg, scope, threshold, indent)
		case fld.Kind() == protoreflect.MessageKind || fld.Kind() == protoreflect.GroupKind:
			p.printMessageLiteralToBufferMaybeCompact(buf, val.Message(), res, pkg, scope, threshold, indent)
		default:
			p.printValueLiteralToBuffer(buf, fld, val.Interface())
		}
	}

	if indent >= 0 {
		indent--
	}
	p.maybeNewline(buf, indent)
	buf.WriteRune('}')
}

func (p *Printer) printMessageLiteralToBufferMaybeCompact(
	buf *bytes.Buffer,
	msg protoreflect.Message,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
	threshold, indent int,
) {
	if indent >= 0 {
		// first see if the message is compact enough to fit on one line
		str := p.printMessageLiteralCompact(msg, res, pkg, scope)
		fieldCount := strings.Count(str, ",")
		nestedCount := strings.Count(str, "{") - 1
		if fieldCount <= 1 && nestedCount == 0 {
			// can't expand
			buf.WriteString(str)
			return
		}
		if len(str) <= threshold {
			// no need to expand
			buf.WriteString(str)
			return
		}
	}
	p.printMessageLiteralToBuffer(buf, msg, res, pkg, scope, threshold, indent)
}

func (p *Printer) maybePrintAnyMessageToBuffer(
	buf *bytes.Buffer,
	msg protoreflect.Message,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
	threshold, indent int,
) bool {
	md := msg.Descriptor()
	if md.FullName() != anyTypeName {
		return false
	}
	typeUrlFld := md.Fields().ByNumber(anyTypeUrlTag)
	if typeUrlFld == nil || typeUrlFld.Kind() != protoreflect.StringKind || typeUrlFld.Cardinality() == protoreflect.Repeated {
		return false
	}
	valueFld := md.Fields().ByNumber(anyValueTag)
	if valueFld == nil || valueFld.Kind() != protoreflect.BytesKind || valueFld.Cardinality() == protoreflect.Repeated {
		return false
	}
	typeUrl := msg.Get(typeUrlFld).String()
	if typeUrl == "" {
		return false
	}
	mt, err := res.FindMessageByURL(typeUrl)
	if err != nil {
		return false
	}
	valueMsg := mt.New()
	valueBytes := msg.Get(valueFld).Bytes()
	if err := (proto.UnmarshalOptions{Resolver: res}).Unmarshal(valueBytes, valueMsg.Interface()); err != nil {
		return false
	}

	buf.WriteRune('{')
	if indent >= 0 {
		indent++
	}
	p.maybeNewline(buf, indent)

	buf.WriteRune('[')
	buf.WriteString(typeUrl)
	buf.WriteString("]: ")
	p.printMessageLiteralToBufferMaybeCompact(buf, valueMsg, res, pkg, scope, threshold, indent)

	if indent >= 0 {
		indent--
	}
	p.maybeNewline(buf, indent)
	buf.WriteRune('}')

	return true
}

func (p *Printer) printValueLiteralToBuffer(buf *bytes.Buffer, fld protoreflect.FieldDescriptor, value interface{}) {
	switch val := value.(type) {
	case protoreflect.EnumNumber:
		ev := fld.Enum().Values().ByNumber(val)
		if ev == nil {
			_, _ = fmt.Fprintf(buf, "%v", value)
		} else {
			buf.WriteString(string(ev.Name()))
		}
	case string:
		buf.WriteString(quotedString(val))
	case []byte:
		buf.WriteString(quotedBytes(string(val)))
	case int32, uint32, int64, uint64:
		_, _ = fmt.Fprintf(buf, "%d", val)
	case float32, float64:
		_, _ = fmt.Fprintf(buf, "%f", val)
	default:
		_, _ = fmt.Fprintf(buf, "%v", val)
	}
}

func (p *Printer) maybeNewline(buf *bytes.Buffer, indent int) {
	if indent < 0 {
		// compact form
		buf.WriteRune(' ')
		return
	}
	buf.WriteRune('\n')
	p.indent(buf, indent)
}

func (p *Printer) printArrayLiteralToBufferMaybeCompact(
	buf *bytes.Buffer,
	fld protoreflect.FieldDescriptor,
	val protoreflect.List,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
	threshold, indent int,
) {
	if indent >= 0 {
		// first see if the array is compact enough to fit on one line
		str := p.printArrayLiteralCompact(fld, val, res, pkg, scope)
		elementCount := strings.Count(str, ",")
		nestedCount := strings.Count(str, "{") - 1
		if elementCount <= 1 && nestedCount == 0 {
			// can't expand
			buf.WriteString(str)
			return
		}
		if len(str) <= threshold {
			// no need to expand
			buf.WriteString(str)
			return
		}
	}
	p.printArrayLiteralToBuffer(buf, fld, val, res, pkg, scope, threshold, indent)
}

func (p *Printer) printArrayLiteralCompact(
	fld protoreflect.FieldDescriptor,
	val protoreflect.List,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
) string {
	var buf bytes.Buffer
	p.printArrayLiteralToBuffer(&buf, fld, val, res, pkg, scope, 0, -1)
	return buf.String()
}

func (p *Printer) printArrayLiteralToBuffer(
	buf *bytes.Buffer,
	fld protoreflect.FieldDescriptor,
	val protoreflect.List,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
	threshold, indent int,
) {
	buf.WriteRune('[')
	if indent >= 0 {
		indent++
	}

	for i := 0; i < val.Len(); i++ {
		if i > 0 {
			buf.WriteRune(',')
		}
		p.maybeNewline(buf, indent)
		if fld.Kind() == protoreflect.MessageKind || fld.Kind() == protoreflect.GroupKind {
			p.printMessageLiteralToBufferMaybeCompact(buf, val.Get(i).Message(), res, pkg, scope, threshold, indent)
		} else {
			p.printValueLiteralToBuffer(buf, fld, val.Get(i).Interface())
		}
	}

	if indent >= 0 {
		indent--
	}
	p.maybeNewline(buf, indent)
	buf.WriteRune(']')
}

func (p *Printer) printMapLiteralToBufferMaybeCompact(
	buf *bytes.Buffer,
	fld protoreflect.FieldDescriptor,
	val protoreflect.Map,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
	threshold, indent int,
) {
	if indent >= 0 {
		// first see if the map is compact enough to fit on one line
		str := p.printMapLiteralCompact(fld, val, res, pkg, scope)
		if len(str) <= threshold {
			buf.WriteString(str)
			return
		}
	}
	p.printMapLiteralToBuffer(buf, fld, val, res, pkg, scope, threshold, indent)
}

func (p *Printer) printMapLiteralCompact(
	fld protoreflect.FieldDescriptor,
	val protoreflect.Map,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
) string {
	var buf bytes.Buffer
	p.printMapLiteralToBuffer(&buf, fld, val, res, pkg, scope, 0, -1)
	return buf.String()
}

func (p *Printer) printMapLiteralToBuffer(
	buf *bytes.Buffer,
	fld protoreflect.FieldDescriptor,
	val protoreflect.Map,
	res *protoregistry.Types,
	pkg, scope protoreflect.FullName,
	threshold, indent int,
) {
	keys := sortKeys(val)
	l := &mapAsList{
		m:      val,
		entry:  dynamicpb.NewMessageType(fld.Message()),
		keyFld: fld.MapKey(),
		valFld: fld.MapValue(),
		keys:   keys,
	}
	p.printArrayLiteralToBuffer(buf, fld, l, res, pkg, scope, threshold, indent)
}

type mapAsList struct {
	m              protoreflect.Map
	entry          protoreflect.MessageType
	keyFld, valFld protoreflect.FieldDescriptor
	keys           []protoreflect.MapKey
}

func (m *mapAsList) Len() int {
	return len(m.keys)
}

func (m *mapAsList) Get(i int) protoreflect.Value {
	msg := m.entry.New()
	key := m.keys[i]
	msg.Set(m.keyFld, key.Value())
	val := m.m.Get(key)
	msg.Set(m.valFld, val)
	return protoreflect.ValueOfMessage(msg)
}

func (m *mapAsList) Set(_ int, _ protoreflect.Value) {
	panic("Set is not implemented")
}

func (m *mapAsList) Append(_ protoreflect.Value) {
	panic("Append is not implemented")
}

func (m *mapAsList) AppendMutable() protoreflect.Value {
	panic("AppendMutable is not implemented")
}

func (m *mapAsList) Truncate(_ int) {
	panic("Truncate is not implemented")
}

func (m *mapAsList) NewElement() protoreflect.Value {
	panic("NewElement is not implemented")
}

func (m *mapAsList) IsValid() bool {
	return true
}
