package protobuf

import (
	"github.com/wader/fq/format"
	"github.com/wader/fq/format/registry"
	"github.com/wader/fq/internal/num"
	"github.com/wader/fq/pkg/decode"
)

func init() {
	registry.MustRegister(&decode.Format{
		Name:        format.PROTOBUF,
		Description: "Protobuf",
		DecodeFn:    protobufDecode,
	})
}

const (
	wireTypeVarint          = 0
	wireType64Bit           = 1
	wireTypeLengthDelimited = 2
	wireType32Bit           = 5
)

var wireTypeNames = map[uint64]string{
	0: "Varint",
	1: "64-bit",
	2: "Length-delimited",
	5: "32-bit",
}

func varInt(d *decode.D) uint64 {
	var n uint64
	for i := 0; ; i++ {
		b := d.U8()
		n = n | (b&0x7f)<<(7*i)
		if b&0x80 == 0 {
			break
		}
	}

	return n
}

func fieldVarInt(d *decode.D, name string) uint64 {
	return d.FieldUFn(name, func() (uint64, decode.DisplayFormat, string) {
		return varInt(d), decode.NumberDecimal, ""
	})
}

func protobufDecodeField(d *decode.D, pbm *format.ProtoBufMessage) {
	d.FieldStructFn("field", func(d *decode.D) {
		keyN := fieldVarInt(d, "key_n")
		fieldNumber := keyN >> 3
		wireType := keyN & 0x7
		d.FieldValueU("field_number", fieldNumber, "")
		d.FieldValueU("wire_type", wireType, wireTypeNames[wireType])

		var value uint64
		var length uint64
		var valueStart int64
		switch wireType {
		case wireTypeVarint:
			value = fieldVarInt(d, "wire_value")
		case wireType64Bit:
			value = d.FieldU64("wire_value")
		case wireTypeLengthDelimited:
			length = fieldVarInt(d, "length")
			valueStart = d.Pos()
			d.FieldBitBufLen("wire_value", int64(length)*8)
		case wireType32Bit:
			value = d.FieldU32("wire_value")
		}

		if pbm != nil {
			if pbf, ok := (*pbm)[int(fieldNumber)]; ok {
				d.FieldValueStr("name", pbf.Name, "")
				d.FieldValueStr("type", format.ProtoBufTypeNames[uint64(pbf.Type)], "")

				switch pbf.Type {
				case format.ProtoBufTypeInt32, format.ProtoBufTypeInt64:
					v := num.ZigZag(value)
					d.FieldValueS("value", v, "")
					if len(pbf.Enums) > 0 {
						d.FieldValueStr("enum", pbf.Enums[uint64(v)], "")
					}
				case format.ProtoBufTypeUInt32, format.ProtoBufTypeUInt64:
					d.FieldValueU("value", value, "")
					if len(pbf.Enums) > 0 {
						d.FieldValueStr("enum", pbf.Enums[value], "")
					}
				case format.ProtoBufTypeSInt32, format.ProtoBufTypeSInt64:
					// TODO: correct? 32 different?
					v := num.TwosComplement(64, value)
					d.FieldValueS("value", v, "")
					if len(pbf.Enums) > 0 {
						d.FieldValueStr("enum", pbf.Enums[uint64(v)], "")
					}
				case format.ProtoBufTypeBool:
					d.FieldValueBool("value", value != 0, "")
				case format.ProtoBufTypeEnum:
					d.FieldValueStr("enum", pbf.Enums[value], "")
				case format.ProtoBufTypeFixed64:
					// TODO:
				case format.ProtoBufTypeSFixed64:
					// TODO:
				case format.ProtoBufTypeDouble:
					// TODO:
				case format.ProtoBufTypeString:
					d.FieldValueStr("value", string(d.BytesRange(valueStart, int(length))), "")
				case format.ProtoBufTypeBytes:
					d.FieldValueBytes("value", d.BytesRange(valueStart, int(length)), "")
				case format.ProtoBufTypeMessage:
					// TODO: test
					d.DecodeLenFn(int64(length)*8, func(d *decode.D) {
						protobufDecodeFields(d, &pbf.Message)
					})
				case format.ProtoBufTypePackedRepeated:
					// TODO:
				case format.ProtoBufTypeFixed32:
					// TODO:
				case format.ProtoBufTypeSFixed32:
					// TODO:
				case format.ProtoBufTypeFloat:
					// TODO:
				}
			}
		}
	})
}

func protobufDecodeFields(d *decode.D, pbm *format.ProtoBufMessage) {
	d.FieldArrayFn("fields", func(d *decode.D) {
		for d.BitsLeft() > 0 {
			protobufDecodeField(d, pbm)
			//break
		}
	})
}

func protobufDecode(d *decode.D, in interface{}) interface{} {
	var pbm *format.ProtoBufMessage
	pbi, ok := in.(format.ProtoBufIn)
	if ok {
		pbm = &pbi.Message
	}

	protobufDecodeFields(d, pbm)

	return nil
}