package protomsg

import (
	"errors"
	"fmt"
	"reflect"
	"strings"

	"github.com/golang/protobuf/proto"
)

// WalkFunc is the type of the function called for each field in the protobuf
// message tree. It is called with the dot-delimited selector of the field, the
// properties of that field, and the field's value. The field may by assigned
// using val.
type WalkFunc func(path string, prop *proto.Properties, val reflect.Value) error

// ErrSkipStruct may be returned from a WalkFunc to indicate that
// fields of a nested struct should not be visited.
var ErrSkipStruct = errors.New("skip struct")

// WalkStruct walks the tree rooted at message. It calls walkFn for each field
// of each message encountered in depth-first order. If walkFn returns
// ErrSkipStruct, WalkStruct will not visit the subtree rooted at the current
// field.
func WalkStruct(message proto.Message, walkFn WalkFunc) error {
	if message == nil {
		return fmt.Errorf("restclient: cannot look up fields on nil message")
	}
	mv := reflect.ValueOf(message)
	if mv.Kind() == reflect.Ptr {
		mv = mv.Elem()
	}
	if !mv.IsValid() {
		return nil
	}

	return visitStructFields(mv, "", walkFn)
}

func visitStructFields(mv reflect.Value, path string, walkFn WalkFunc) error {
	props := proto.GetProperties(mv.Type())

	for i, prop := range props.Prop {
		fv := mv.Field(i)
		fieldPath := prop.OrigName

		if prop.Wire == "" {
			// This is a hidden field, such as XXX_unrecognized or a oneof
			// field. If it's a oneof, the field will be an interface. Inside
			// that interface value (if set) will be a pointer to a struct,
			// which will have contain the value that has been set to the
			// oneof. Bear with us while we unravel these layers.
			if fv.Kind() == reflect.Interface {
				fv = fv.Elem()
			}
			if !fv.IsValid() {
				continue
			}
			if fv.Kind() == reflect.Ptr {
				fv = fv.Elem()
			}
			if fv.Kind() != reflect.Struct {
				continue
			}
			if prop.Name == "XXX_NoUnkeyedLiteral" {
				continue
			}

			if props := proto.GetProperties(fv.Type()); len(props.Prop) > 0 {
				fieldPath = props.Prop[0].OrigName
				fv = fv.Field(0)
			}
		}

		if len(path) > 0 {
			fieldPath = path + "." + fieldPath
		}

		err := walkFn(fieldPath, prop, fv)
		if err == ErrSkipStruct {
			continue
		} else if err != nil {
			return err
		}

		// If the field is a pointer, follow it. If that leads to a struct,
		// make a recursive call to visit its fields. We don't follow leaf
		// fields here, they lead nowhere.
		if fv.Kind() == reflect.Ptr {
			fv = fv.Elem()
		}
		if fv.Kind() != reflect.Struct {
			continue
		}
		err = visitStructFields(fv, fieldPath, walkFn)
		if err != nil {
			return err
		}
	}

	return nil
}

// LookupField walks the provided message in search of the field matching the
// key selector. It does not alter the message.
func LookupField(message proto.Message, key string) (reflect.Value, error) {
	var val reflect.Value
	done := errors.New("done")
	err := WalkStruct(message, func(path string, prop *proto.Properties, fv reflect.Value) error {
		if path != key && !strings.HasPrefix(key, path+".") {
			return ErrSkipStruct
		}
		if prop.Repeated {
			return fmt.Errorf("restclient: repeated field %q not valid for lookup", prop.Name)
		}
		if path != key {
			return nil
		}
		val = fv
		return done
	})
	if err == done {
		return val, nil
	} else if err != nil {
		return val, err
	}
	return val, fmt.Errorf("restclient: field %q not found", key)
}
