package fieldmask

import (
	"fmt"
	"strings"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/types/known/fieldmaskpb"
)

// Validate validates that the paths in the provided field mask are syntactically valid and
// refer to known fields in the specified message type.
func Validate(fm *fieldmaskpb.FieldMask, m proto.Message) error {
	// special case for '*'
	if stringsContain(WildcardPath, fm.GetPaths()) {
		if len(fm.GetPaths()) != 1 {
			return fmt.Errorf("invalid field path: '*' must not be used with other paths")
		}
		return nil
	}
	md0 := m.ProtoReflect().Descriptor()
	for _, path := range fm.GetPaths() {
		md := md0
		if !rangeFields(path, func(field string) bool {
			// Search the field within the message.
			if md == nil {
				return false // not within a message
			}
			fd := md.Fields().ByName(protoreflect.Name(field))
			// The real field name of a group is the message name.
			if fd == nil {
				gd := md.Fields().ByName(protoreflect.Name(strings.ToLower(field)))
				if gd != nil && gd.Kind() == protoreflect.GroupKind && string(gd.Message().Name()) == field {
					fd = gd
				}
			} else if fd.Kind() == protoreflect.GroupKind && string(fd.Message().Name()) != field {
				fd = nil
			}
			if fd == nil {
				return false // message has does not have this field
			}
			// Identify the next message to search within.
			md = fd.Message() // may be nil
			if fd.IsMap() {
				md = fd.MapValue().Message() // may be nil
			}
			return true
		}) {
			return fmt.Errorf("invalid field path: %s", path)
		}
	}
	return nil
}

func stringsContain(str string, ss []string) bool {
	for _, s := range ss {
		if s == str {
			return true
		}
	}
	return false
}

func rangeFields(path string, f func(field string) bool) bool {
	for {
		var field string
		if i := strings.IndexByte(path, '.'); i >= 0 {
			field, path = path[:i], path[i:]
		} else {
			field, path = path, ""
		}
		if !f(field) {
			return false
		}
		if len(path) == 0 {
			return true
		}
		path = strings.TrimPrefix(path, ".")
	}
}
