package validator

import (
	"fmt"
	"reflect"
	"time"
	"unicode"

	"strconv"

	"github.com/NyaaPantsu/nyaa/utils/log"
	msg "github.com/NyaaPantsu/nyaa/utils/messages"
	"github.com/go-playground/validator"
)

var validate *validator.Validate

func init() {
	validate = validator.New()
	validate.RegisterValidation("isutf", IsUTFLetterNumericValidator)
	validate.RegisterValidation("default", DefaultValidator)
}

// ValidateForm : Check if a form is valid according to its tags
func ValidateForm(form interface{}, mes *msg.Messages) {
	err := validate.Struct(form)
	if err != nil {
		for _, fieldError := range err.(validator.ValidationErrors) {
			switch fieldError.Tag() {
			case "required":
				mes.AddErrorTf(fieldError.Field(), "error_field_needed", fieldError.Field())
			case "eq":
				mes.AddErrorTf(fieldError.Field(), "error_equal", fieldError.Field(), fieldError.Param())
			case "ne", "nefield", "necsfield":
				mes.AddErrorTf(fieldError.Field(), "error_not_equal", fieldError.Field(), fieldError.Param())
			case "eqfield", "eqcsfield":
				mes.AddErrorTf(fieldError.Field(), "error_same_value", fieldError.Field(), fieldError.Param())
			case "gtfield", "gtcsfield":
				mes.AddErrorTf(fieldError.Field(), "error_greater_number", fieldError.Field(), fieldError.Param())
			case "ltfield", "ltcsfield":
				mes.AddErrorTf(fieldError.Field(), "error_less_number", fieldError.Field(), fieldError.Param())
			case "gtefield", "gtecsfield":
				mes.AddErrorTf(fieldError.Field(), "error_min_field", fieldError.Field(), fieldError.Param())
			case "ltefield", "ltecsfield":
				mes.AddErrorTf(fieldError.Field(), "error_max_field", fieldError.Field(), fieldError.Param())
			case "alpha":
				mes.AddErrorTf(fieldError.Field(), "error_alpha", fieldError.Field())
			case "alphanum":
				mes.AddErrorTf(fieldError.Field(), "error_alphanum", fieldError.Field())
			case "numeric":
				mes.AddErrorTf(fieldError.Field(), "error_numeric_valid", fieldError.Field())
			case "number":
				mes.AddErrorTf(fieldError.Field(), "error_number_valid", fieldError.Field())
			case "hexadecimal":
				mes.AddErrorTf(fieldError.Field(), "error_hexadecimal_valid", fieldError.Field())
			case "hexcolor":
				mes.AddErrorTf(fieldError.Field(), "error_hex_valid", fieldError.Field())
			case "rgb":
				mes.AddErrorTf(fieldError.Field(), "error_rgb_valid", fieldError.Field())
			case "rgba":
				mes.AddErrorTf(fieldError.Field(), "error_rgba_valid", fieldError.Field())
			case "hsl":
				mes.AddErrorTf(fieldError.Field(), "error_hsl_valid", fieldError.Field())
			case "hsla":
				mes.AddErrorTf(fieldError.Field(), "error_hsla_valid", fieldError.Field())
			case "url":
				mes.AddErrorTf(fieldError.Field(), "error_url_valid", fieldError.Field())
			case "uri":
				mes.AddErrorTf(fieldError.Field(), "error_uri_valid", fieldError.Field())
			case "base64":
				mes.AddErrorTf(fieldError.Field(), "error_base64_valid", fieldError.Field())
			case "contains":
				mes.AddErrorTf(fieldError.Field(), "error_contains", fieldError.Field(), fieldError.Param())
			case "containsany":
				mes.AddErrorTf(fieldError.Field(), "error_contains_any", fieldError.Field(), fieldError.Param())
			case "excludes":
				mes.AddErrorTf(fieldError.Field(), "error_excludes", fieldError.Field(), fieldError.Param())
			case "excludesall":
				mes.AddErrorTf(fieldError.Field(), "error_excludes_all", fieldError.Field(), fieldError.Param())
			case "excludesrune":
				mes.AddErrorTf(fieldError.Field(), "error_excludes_rune", fieldError.Field(), fieldError.Param())
			case "iscolor":
				mes.AddErrorTf(fieldError.Field(), "error_color_valid", fieldError.Field())
			default:
				switch fieldError.Kind() {
				case reflect.String:
					stringErrors(fieldError, mes)
				case reflect.Slice, reflect.Map, reflect.Array:
				case reflect.Struct:
					if fieldError.Type() != reflect.TypeOf(time.Time{}) {
						fmt.Printf("tag '%s' cannot be used on a struct type.", fieldError.Tag())
						mes.AddErrorTf(fieldError.Field(), "error_field", fieldError.Field())
					}
					dateErrors(fieldError, mes)
				default:
					numberErrors(fieldError, mes)
				}
			}
		}
	}
}

// Bind a validated form to a model, tag omit to not bind a field
func Bind(model interface{}, form interface{}) {
	modelIndirect := reflect.Indirect(reflect.ValueOf(model))
	formElem := reflect.ValueOf(form).Elem()
	typeOfTForm := formElem.Type()
	for i := 0; i < formElem.NumField(); i++ {
		tag := typeOfTForm.Field(i).Tag
		if tag.Get("omit") != "true" {
			modelField := modelIndirect.FieldByName(typeOfTForm.Field(i).Name)
			if modelField.IsValid() {
				formField := formElem.Field(i)
				modelField.Set(formField)
			} else {
				log.Warnf("modelField : %s - %s", typeOfTForm.Field(i).Name, modelField)
			}
		}
	}
}

// IsUTFLetterNumeric check if the string contains only unicode letters and numbers. Empty string is valid.
func IsUTFLetterNumeric(str string) bool {
	if len(str) == 0 {
		return true
	}
	for _, c := range str {
		if !unicode.IsLetter(c) && !unicode.IsNumber(c) { //letters && numbers are ok
			return false
		}
	}
	return true

}

// IsUTFLetterNumericValidator is an interface to IsUTFLetterNumeric from validator
func IsUTFLetterNumericValidator(fl validator.FieldLevel) bool {
	value := fl.Field().String() // value
	return IsUTFLetterNumeric(value)
}

// DefaultValidator default validator
func DefaultValidator(fl validator.FieldLevel) bool {
	switch fl.Field().Kind() {
	case reflect.String:
		if len(fl.Field().String()) == 0 {
			fl.Field().SetString(fl.Param())
		}
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		if fl.Field().Int() == 0 {
			number, err := strconv.Atoi(fl.Param())
			if err != nil {
				fmt.Printf("Couldn't convert default value for field %s", fl.FieldName())
				return false
			}
			fl.Field().SetInt(int64(number))
		}
	case reflect.Float32, reflect.Float64:
		if fl.Field().Float() == 0 {
			number, err := strconv.ParseFloat(fl.Param(), 64)
			if err != nil {
				fmt.Printf("Couldn't convert default value for field %s", fl.FieldName())
				return false
			}
			fl.Field().SetFloat(number)
		}
	}
	return true
}
