// Package protoutil contains functions for working with protobuf types.
package protoutil

import (
	"fmt"
	"reflect"

	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/structpb"
)

// ToStruct converts any value into a structpb Value.
func ToStruct(value interface{}) *structpb.Value {
	if value == nil {
		return NewStructNull()
	}

	switch v := value.(type) {
	case bool:
		return NewStructBool(v)
	case float64:
		return NewStructNumber(v)
	case float32:
		return NewStructNumber(float64(v))
	case int:
		return NewStructNumber(float64(v))
	case int8:
		return NewStructNumber(float64(v))
	case int16:
		return NewStructNumber(float64(v))
	case int32:
		return NewStructNumber(float64(v))
	case int64:
		return NewStructNumber(float64(v))
	case string:
		return NewStructString(v)
	case uint:
		return NewStructNumber(float64(v))
	case uint8:
		return NewStructNumber(float64(v))
	case uint16:
		return NewStructNumber(float64(v))
	case uint32:
		return NewStructNumber(float64(v))
	case uint64:
		return NewStructNumber(float64(v))
	}

	if msg, ok := value.(proto.Message); ok {
		bs, _ := protojson.Marshal(msg)
		var s structpb.Struct
		_ = protojson.Unmarshal(bs, &s)
		return &structpb.Value{
			Kind: &structpb.Value_StructValue{StructValue: &s},
		}
	}

	rv := reflect.ValueOf(value)
	switch rv.Kind() {
	case reflect.Slice:
		svs := make([]*structpb.Value, rv.Len())
		for i := range svs {
			svs[i] = ToStruct(rv.Index(i).Interface())
		}
		return NewStructList(svs...)
	case reflect.Map:
		svm := make(map[string]*structpb.Value)
		iter := rv.MapRange()
		for iter.Next() {
			svm[fmt.Sprint(iter.Key().Interface())] = ToStruct(iter.Value().Interface())
		}
		return NewStructMap(svm)
	}

	return NewStructNull()
}

// NewStructBool creates a new bool struct value.
func NewStructBool(v bool) *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_BoolValue{BoolValue: v},
	}
}

// NewStructMap creates a new map struct value.
func NewStructMap(v map[string]*structpb.Value) *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: v}},
	}
}

// NewStructNull creates a new null struct value.
func NewStructNull() *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_NullValue{},
	}
}

// NewStructNumber creates a new number struct value.
func NewStructNumber(v float64) *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_NumberValue{NumberValue: v},
	}
}

// NewStructList creates a new list struct value.
func NewStructList(vs ...*structpb.Value) *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: vs}},
	}
}

// NewStructString creates a new string struct value.
func NewStructString(v string) *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_StringValue{StringValue: v},
	}
}