/*
 * Copyright 2012-2022 Li Kexian
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * A toolkit for Golang development
 * https://www.likexian.com/
 */

package assert

import (
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
)

var (
	// ErrInvalid is value invalid for operation
	ErrInvalid = errors.New("assert: value if invalid")
	// ErrLess is expect to be greater error
	ErrLess = errors.New("assert: left is less the right")
	// ErrGreater is expect to be less error
	ErrGreater = errors.New("assert: left is greater then right")
)

// Comparer is compare operator
var Comparer = struct {
	LT string
	LE string
	GT string
	GE string
}{
	"<",
	"<=",
	">",
	">=",
}

// IsZero returns value is zero value
func IsZero(v interface{}) bool {
	vv := reflect.ValueOf(v)
	switch vv.Kind() {
	case reflect.Invalid:
		return true
	case reflect.Bool:
		return !vv.Bool()
	case reflect.Ptr, reflect.Interface:
		return vv.IsNil()
	case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
		return vv.Len() == 0
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return vv.Int() == 0
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return vv.Uint() == 0
	case reflect.Float32, reflect.Float64:
		return vv.Float() == 0
	default:
		return false
	}
}

// IsContains returns whether value is within array
func IsContains(array interface{}, value interface{}) bool {
	vv := reflect.ValueOf(array)
	if vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface {
		if vv.IsNil() {
			return false
		}
		vv = vv.Elem()
	}

	switch vv.Kind() {
	case reflect.Invalid:
		return false
	case reflect.Slice:
		for i := 0; i < vv.Len(); i++ {
			if reflect.DeepEqual(value, vv.Index(i).Interface()) {
				return true
			}
		}
		return false
	case reflect.Map:
		s := vv.MapKeys()
		for i := 0; i < len(s); i++ {
			if reflect.DeepEqual(value, s[i].Interface()) {
				return true
			}
		}
		return false
	case reflect.String:
		ss := reflect.ValueOf(value)
		switch ss.Kind() {
		case reflect.String:
			return strings.Contains(vv.String(), ss.String())
		}
		return false
	default:
		return reflect.DeepEqual(array, value)
	}
}

// IsMatch returns if value v contains any match of pattern r
//   IsMatch(regexp.MustCompile("v\d+"), "v100")
//   IsMatch("v\d+", "v100")
//   IsMatch("\d+\.\d+", 100.1)
func IsMatch(r interface{}, v interface{}) bool {
	var re *regexp.Regexp

	if v, ok := r.(*regexp.Regexp); ok {
		re = v
	} else {
		re = regexp.MustCompile(fmt.Sprint(r))
	}

	return re.MatchString(fmt.Sprint(v))
}

// Length returns length of value
func Length(v interface{}) int {
	vv := reflect.ValueOf(v)
	if vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface {
		if vv.IsNil() {
			return 0
		}
		vv = vv.Elem()
	}

	switch vv.Kind() {
	case reflect.Invalid:
		return 0
	case reflect.Ptr, reflect.Interface:
		return 0
	case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
		return vv.Len()
	default:
		return len(fmt.Sprintf("%#v", v))
	}
}

// IsLt returns if x less than y, value invalid will returns false
func IsLt(x, y interface{}) bool {
	return Compare(x, y, Comparer.LT) == nil
}

// IsLe returns if x less than or equal to y, value invalid will returns false
func IsLe(x, y interface{}) bool {
	return Compare(x, y, Comparer.LE) == nil
}

// IsGt returns if x greater than y, value invalid will returns false
func IsGt(x, y interface{}) bool {
	return Compare(x, y, Comparer.GT) == nil
}

// IsGe returns if x greater than or equal to y, value invalid will returns false
func IsGe(x, y interface{}) bool {
	return Compare(x, y, Comparer.GE) == nil
}

// Compare compare x and y, by operation
// It returns nil for true, ErrInvalid for invalid operation, err for false
//   Compare(1, 2, ">") // number compare -> true
//   Compare("a", "a", ">=") // string compare -> true
//   Compare([]string{"a", "b"}, []string{"a"}, "<") // slice len compare -> false
func Compare(x, y interface{}, op string) error { //nolint:cyclop
	if !IsContains([]string{Comparer.LT, Comparer.LE, Comparer.GT, Comparer.GE}, op) {
		return ErrInvalid
	}

	vv := reflect.ValueOf(x)
	if vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface {
		if vv.IsNil() {
			return ErrInvalid
		}
		vv = vv.Elem()
	}

	var c float64
	switch vv.Kind() {
	case reflect.Invalid:
		return ErrInvalid
	case reflect.String:
		yy := reflect.ValueOf(y)
		switch yy.Kind() {
		case reflect.String:
			c = float64(strings.Compare(vv.String(), yy.String()))
		default:
			return ErrInvalid
		}
	case reflect.Slice, reflect.Map, reflect.Array:
		yy := reflect.ValueOf(y)
		switch yy.Kind() {
		case reflect.Slice, reflect.Map, reflect.Array:
			c = float64(vv.Len() - yy.Len())
		default:
			return ErrInvalid
		}
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		yy, err := ToInt64(y)
		if err != nil {
			return ErrInvalid
		}
		c = float64(vv.Int() - yy)
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		yy, err := ToUint64(y)
		if err != nil {
			return ErrInvalid
		}
		c = float64(vv.Uint()) - float64(yy)
	case reflect.Float32, reflect.Float64:
		yy, err := ToFloat64(y)
		if err != nil {
			return ErrInvalid
		}
		c = vv.Float() - yy
	default:
		return ErrInvalid
	}

	switch {
	case c < 0:
		switch op {
		case Comparer.LT, Comparer.LE:
			return nil
		default:
			return ErrLess
		}
	case c > 0:
		switch op {
		case Comparer.GT, Comparer.GE:
			return nil
		default:
			return ErrGreater
		}
	default:
		switch op {
		case Comparer.LT:
			return ErrGreater
		case Comparer.GT:
			return ErrLess
		default:
			return nil
		}
	}
}

// ToInt64 returns int value for int or uint or float
func ToInt64(v interface{}) (int64, error) {
	vv := reflect.ValueOf(v)
	switch vv.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return vv.Int(), nil
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return int64(vv.Uint()), nil
	case reflect.Float32, reflect.Float64:
		return int64(vv.Float()), nil
	case reflect.String:
		r, err := strconv.ParseInt(vv.String(), 10, 64)
		if err != nil {
			return 0, ErrInvalid
		}
		return r, nil
	default:
		return 0, ErrInvalid
	}
}

// ToUint64 returns uint value for int or uint or float
func ToUint64(v interface{}) (uint64, error) {
	vv := reflect.ValueOf(v)
	switch vv.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return uint64(vv.Int()), nil
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return vv.Uint(), nil
	case reflect.Float32, reflect.Float64:
		return uint64(vv.Float()), nil
	case reflect.String:
		r, err := strconv.ParseUint(vv.String(), 10, 64)
		if err != nil {
			return 0, ErrInvalid
		}
		return r, nil
	default:
		return 0, ErrInvalid
	}
}

// ToFloat64 returns float64 value for int or uint or float
func ToFloat64(v interface{}) (float64, error) {
	vv := reflect.ValueOf(v)
	switch vv.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return float64(vv.Int()), nil
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return float64(vv.Uint()), nil
	case reflect.Float32, reflect.Float64:
		return vv.Float(), nil
	case reflect.String:
		r, err := strconv.ParseFloat(vv.String(), 64)
		if err != nil {
			return 0, ErrInvalid
		}
		return r, nil
	default:
		return 0, ErrInvalid
	}
}

// If returns x if c is true, else y
//   z = If(c, x, y)
// equal to:
//   z = c ? x : y
func If(c bool, x, y interface{}) interface{} {
	if c {
		return x
	}

	return y
}