// Copyright 2016 Qiang Xue. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package validation

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"
)

var (
	// ErrStructPointer is the error that a struct being validated is not specified as a pointer.
	ErrStructPointer = errors.New("only a pointer to a struct can be validated")
)

type (
	// ErrFieldPointer is the error that a field is not specified as a pointer.
	ErrFieldPointer int

	// ErrFieldNotFound is the error that a field cannot be found in the struct.
	ErrFieldNotFound int

	// FieldRules represents a rule set associated with a struct field.
	FieldRules struct {
		fieldPtr interface{}
		rules    []Rule
	}
)

// Error returns the error string of ErrFieldPointer.
func (e ErrFieldPointer) Error() string {
	return fmt.Sprintf("field #%v must be specified as a pointer", int(e))
}

// Error returns the error string of ErrFieldNotFound.
func (e ErrFieldNotFound) Error() string {
	return fmt.Sprintf("field #%v cannot be found in the struct", int(e))
}

// ValidateStruct validates a struct by checking the specified struct fields against the corresponding validation rules.
// Note that the struct being validated must be specified as a pointer to it. If the pointer is nil, it is considered valid.
// Use Field() to specify struct fields that need to be validated. Each Field() call specifies a single field which
// should be specified as a pointer to the field. A field can be associated with multiple rules.
// For example,
//
//    value := struct {
//        Name  string
//        Value string
//    }{"name", "demo"}
//    err := validation.ValidateStruct(&value,
//        validation.Field(&a.Name, validation.Required),
//        validation.Field(&a.Value, validation.Required, validation.Length(5, 10)),
//    )
//    fmt.Println(err)
//    // Value: the length must be between 5 and 10.
//
// An error will be returned if validation fails.
func ValidateStruct(structPtr interface{}, fields ...*FieldRules) error {
	return ValidateStructWithContext(nil, structPtr, fields...)
}

// ValidateStructWithContext validates a struct with the given context.
// The only difference between ValidateStructWithContext and ValidateStruct is that the former will
// validate struct fields with the provided context.
// Please refer to ValidateStruct for the detailed instructions on how to use this function.
func ValidateStructWithContext(ctx context.Context, structPtr interface{}, fields ...*FieldRules) error {
	value := reflect.ValueOf(structPtr)
	if value.Kind() != reflect.Ptr || !value.IsNil() && value.Elem().Kind() != reflect.Struct {
		// must be a pointer to a struct
		return NewInternalError(ErrStructPointer)
	}
	if value.IsNil() {
		// treat a nil struct pointer as valid
		return nil
	}
	value = value.Elem()

	errs := Errors{}

	for i, fr := range fields {
		fv := reflect.ValueOf(fr.fieldPtr)
		if fv.Kind() != reflect.Ptr {
			return NewInternalError(ErrFieldPointer(i))
		}
		ft := findStructField(value, fv)
		if ft == nil {
			return NewInternalError(ErrFieldNotFound(i))
		}
		var err error
		if ctx == nil {
			err = Validate(fv.Elem().Interface(), fr.rules...)
		} else {
			err = ValidateWithContext(ctx, fv.Elem().Interface(), fr.rules...)
		}
		if err != nil {
			if ie, ok := err.(InternalError); ok && ie.InternalError() != nil {
				return err
			}
			if ft.Anonymous {
				// merge errors from anonymous struct field
				if es, ok := err.(Errors); ok {
					for name, value := range es {
						errs[name] = value
					}
					continue
				}
			}
			errs[getErrorFieldName(ft)] = err
		}
	}

	if len(errs) > 0 {
		return errs
	}
	return nil
}

// Field specifies a struct field and the corresponding validation rules.
// The struct field must be specified as a pointer to it.
func Field(fieldPtr interface{}, rules ...Rule) *FieldRules {
	return &FieldRules{
		fieldPtr: fieldPtr,
		rules:    rules,
	}
}

// findStructField looks for a field in the given struct.
// The field being looked for should be a pointer to the actual struct field.
// If found, the field info will be returned. Otherwise, nil will be returned.
func findStructField(structValue reflect.Value, fieldValue reflect.Value) *reflect.StructField {
	ptr := fieldValue.Pointer()
	for i := structValue.NumField() - 1; i >= 0; i-- {
		sf := structValue.Type().Field(i)
		if ptr == structValue.Field(i).UnsafeAddr() {
			// do additional type comparison because it's possible that the address of
			// an embedded struct is the same as the first field of the embedded struct
			if sf.Type == fieldValue.Elem().Type() {
				return &sf
			}
		}
		if sf.Anonymous {
			// delve into anonymous struct to look for the field
			fi := structValue.Field(i)
			if sf.Type.Kind() == reflect.Ptr {
				fi = fi.Elem()
			}
			if fi.Kind() == reflect.Struct {
				if f := findStructField(fi, fieldValue); f != nil {
					return f
				}
			}
		}
	}
	return nil
}

// getErrorFieldName returns the name that should be used to represent the validation error of a struct field.
func getErrorFieldName(f *reflect.StructField) string {
	if tag := f.Tag.Get(ErrorTag); tag != "" && tag != "-" {
		if cps := strings.SplitN(tag, ",", 2); cps[0] != "" {
			return cps[0]
		}
	}
	return f.Name
}
