package utils

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
	"syscall"

	"github.com/alibaba/pouch/pkg/log"
)

// If implements ternary operator. if cond is true return v1, or return v2 instead.
func If(cond bool, v1, v2 interface{}) interface{} {
	if cond {
		return v1
	}
	return v2
}

// FormatSize format image size to B/KB/MB/GB
func FormatSize(size int64) string {
	if size <= 0 {
		return "0.00 B"
	}
	// we consider image size less than 1024 GB
	suffixes := []string{"B", "KB", "MB", "GB"}

	var count int
	formattedSize := float64(size)
	for count = 0; count < 3; count++ {
		if formattedSize < 1024 {
			break
		}
		formattedSize /= 1024
	}

	return fmt.Sprintf("%.2f %s", formattedSize, suffixes[count])
}

// TruncateID is used to transfer image ID from digest to short ID.
func TruncateID(id string) string {
	var shortLen = 12

	id = strings.TrimPrefix(id, "sha256:")
	if len(id) > shortLen {
		return id[:shortLen]
	}
	return id
}

// Merge merge object from src to dest, dest object should be pointer, only accept struct type, notice: src will overwrite dest's data
func Merge(src, dest interface{}) error {
	if src == nil || dest == nil {
		return fmt.Errorf("merged object can not be nil")
	}

	destType := reflect.TypeOf(dest)
	if destType.Kind() != reflect.Ptr {
		return fmt.Errorf("merged object not pointer")
	}
	destVal := reflect.ValueOf(dest).Elem()

	if destVal.Kind() != reflect.Struct {
		return fmt.Errorf("merged object type should be struct")
	}

	srcVal := reflect.ValueOf(src)
	if srcVal.Kind() == reflect.Ptr {
		srcVal = srcVal.Elem()
	}
	if destVal.Type() != srcVal.Type() {
		return fmt.Errorf("src and dest object type must same")
	}

	return doMerge(srcVal, destVal)
}

// doMerge, begin merge action, note that we will merge slice type,
// but we do not validate if slice has duplicate values.
func doMerge(src, dest reflect.Value) error {
	if !src.IsValid() || !dest.CanSet() || isEmptyValue(src) {
		return nil
	}

	switch dest.Kind() {
	case reflect.Struct:
		for i := 0; i < dest.NumField(); i++ {
			if err := doMerge(src.Field(i), dest.Field(i)); err != nil {
				return err
			}
		}

	case reflect.Map:
		for _, key := range src.MapKeys() {
			srcElem := src.MapIndex(key)
			if !srcElem.IsValid() || isEmptyValue(srcElem) {
				continue
			}
			if dest.IsNil() {
				dest.Set(reflect.MakeMap(dest.Type()))
			}
			dest.SetMapIndex(key, srcElem)
		}

	case reflect.Slice:
		dest.Set(reflect.AppendSlice(dest, src))

	default:
		dest.Set(src)
	}

	return nil
}

// From src/pkg/encoding/json,
// we recognize nullable values like `false` `0` as not empty.
func isEmptyValue(v reflect.Value) bool {
	switch v.Kind() {
	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
		return v.Len() == 0
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return v.Int() == 0
	case reflect.Uintptr:
		return v.Uint() == 0
	case reflect.Float32, reflect.Float64:
		return v.Float() == 0
	case reflect.Interface, reflect.Ptr:
		return v.IsNil()
	}
	return false
}

// DeDuplicate make a slice with no duplicated elements.
func DeDuplicate(input []string) []string {
	if input == nil {
		return nil
	}
	result := []string{}
	internal := map[string]struct{}{}
	for _, value := range input {
		if _, exist := internal[value]; !exist {
			internal[value] = struct{}{}
			result = append(result, value)
		}
	}
	return result
}

// FormatErrMsgFunc is a function which used by CombineErrors to
// format error message
type FormatErrMsgFunc func(idx int, err error) (string, error)

// CombineErrors is a function which used by Inspect to merge multiple errors
// into one error.
func CombineErrors(errs []error, formatErrMsg FormatErrMsgFunc) error {
	var errMsgs []string
	for idx, err := range errs {
		formattedErrMsg, formatError := formatErrMsg(idx, err)
		if formatError != nil {
			return fmt.Errorf("Combine errors error: %s", formatError.Error())
		}
		errMsgs = append(errMsgs, formattedErrMsg)
	}
	combinedErrMsg := strings.Join(errMsgs, "\n")
	return errors.New(combinedErrMsg)
}

// Contains check if a interface in a interface slice.
func Contains(input []interface{}, value interface{}) (bool, error) {
	if value == nil || len(input) == 0 {
		return false, nil
	}

	if reflect.TypeOf(input[0]) != reflect.TypeOf(value) {
		return false, fmt.Errorf("interface type not equals")
	}

	switch v := value.(type) {
	case int, int64, float64, string:
		for _, v := range input {
			if v == value {
				return true, nil
			}
		}
		return false, nil
	// TODO: add more types
	default:
		r := reflect.TypeOf(v)
		return false, fmt.Errorf("Not support: %s", r)
	}
}

// StringInSlice checks if a string in the slice.
func StringInSlice(input []string, str string) bool {
	if str == "" || len(input) == 0 {
		return false
	}

	result := make([]interface{}, len(input))
	for i, v := range input {
		result[i] = v
	}

	exists, _ := Contains(result, str)
	return exists
}

// checkPidfileStatus check if pidfile exist and validate pid exist in /proc, but not validate whether process is running.
func checkPidfileStatus(path string) error {
	if pidByte, err := ioutil.ReadFile(path); err == nil {
		if _, err := os.Stat("/proc/" + string(pidByte)); err == nil {
			return fmt.Errorf("found daemon pid %s, check it status", string(pidByte))
		}
	}

	return nil
}

// NewPidfile checks if pidfile exist, and saves daemon pid.
func NewPidfile(path string) error {
	if err := checkPidfileStatus(path); err != nil {
		return err
	}
	return ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0644)
}

// IsProcessAlive returns true if process with a given pid is running.
func IsProcessAlive(pid int) bool {
	err := syscall.Kill(pid, syscall.Signal(0))
	if err == nil || err == syscall.EPERM {
		return true
	}

	return false
}

// KillProcess force-stops a process.
func KillProcess(pid int) {
	syscall.Kill(pid, syscall.SIGKILL)
}

// SetOOMScore sets process's oom_score value
// The higher the value of oom_score of any process, the higher is its
// likelihood of getting killed by the OOM Killer in an out-of-memory situation.
func SetOOMScore(pid, score int) error {
	f, err := os.OpenFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid), os.O_WRONLY, 0)
	if err != nil {
		return err
	}
	_, err = f.WriteString(strconv.Itoa(score))
	f.Close()
	return err
}

// ConvertKVStringsToMap converts ["key=value"] into {"key":"value"}
func ConvertKVStringsToMap(values []string) (map[string]string, error) {
	kvs := make(map[string]string, len(values))

	for _, value := range values {
		terms := strings.SplitN(value, "=", 2)
		if len(terms) != 2 {
			return nil, fmt.Errorf("input %s must have format of key=value", value)
		}
		kvs[terms[0]] = terms[1]
	}
	return kvs, nil
}

// ConvertKVStrToMapWithNoErr converts input strings and converts them all in a map,
// When there is invalid input, the dealing procedure ignores the error and log a warning message.
func ConvertKVStrToMapWithNoErr(values []string) map[string]string {
	kvs := make(map[string]string, len(values))
	for _, value := range values {
		k, v, err := ConvertStrToKV(value)
		if err != nil {
			log.With(nil).Warnf("input %s should have a format of key=value", value)
			continue
		}
		kvs[k] = v
	}
	return kvs
}

// ConvertStrToKV converts an string into key and value string without returning an error.
// For example, for input "a=b", it should return "a", "b".
func ConvertStrToKV(input string) (string, string, error) {
	results := strings.SplitN(input, "=", 2)
	if len(results) != 2 {
		return "", "", fmt.Errorf("input string %s must have format key=value", input)
	}
	return results[0], results[1], nil
}

// IsFileExist checks if file is exits on host.
func IsFileExist(file string) bool {
	if _, err := os.Stat(file); err == nil {
		return true
	}

	return false
}

// StringSliceEqual compares two string slice, ignore the order.
// If all items in the two string slice are equal, this function will return true
// even though there may have duplicate elements in the slice, otherwise reture false.
func StringSliceEqual(s1, s2 []string) bool {
	if s1 == nil && s2 == nil {
		return true
	}

	if s1 == nil || s2 == nil {
		return false
	}

	if len(s1) != len(s2) {
		return false
	}

	// mapKeys to remember keys that exist in s1
	mapKeys := map[string]int{}

	// first list all items in s1
	for _, v := range s1 {
		mapKeys[v]++
	}

	// second list all items in s2
	for _, v := range s2 {
		mapKeys[v]--

		// we may get -1 in two cases:
		// 1. the item exists in the s2, but not in the s1;
		// 2. the item exists both in s1 and s2, but has different copies.
		// Under the condition that the length of slices are equals,
		// so we can quickly return false.
		if mapKeys[v] < 0 {
			return false
		}
	}

	return true
}

// MergeMap merges the m2 into m1, if it has the same keys, m2 will overwrite m1.
func MergeMap(m1 map[string]interface{}, m2 map[string]interface{}) (map[string]interface{}, error) {
	if m1 == nil && m2 == nil {
		return nil, fmt.Errorf("all of maps are nil")
	}

	if m1 == nil {
		return m2, nil
	}

	if m2 == nil {
		return m1, nil
	}

	for k, v := range m2 {
		m1[k] = v
	}

	return m1, nil
}

// StringDefault return default value if s is empty, otherwise return s.
func StringDefault(s string, val string) string {
	if s != "" {
		return s
	}
	return val
}

// ToStringMap changes the map[string]interface{} to map[string]string,
// If the interface is not string, it will be ignore.
func ToStringMap(in map[string]interface{}) map[string]string {
	if in == nil {
		return nil
	}
	out := make(map[string]string)
	for k, v := range in {
		if s, ok := v.(string); ok {
			out[k] = s
		}
	}
	return out
}

// StringSliceDelete deletes the `del` string in string slice.
func StringSliceDelete(in []string, del string) []string {
	if in == nil {
		return nil
	}

	out := make([]string, 0)
	for _, value := range in {
		if value != del {
			out = append(out, value)
		}
	}

	return out
}

// ResolveHomeDir resolve a target path from home dir, home dir must not be a relative
// path, must not be a file, create directory if not exist, returns the target
// directory if directory is symlink.
func ResolveHomeDir(path string) (string, error) {
	if path == "" {
		return "", fmt.Errorf("home dir should not be empty")
	}
	if !filepath.IsAbs(path) {
		return "", fmt.Errorf("home dir %s should be an absolute path", path)
	}

	// create directory for home-dir if is not exist, or check if exist home-dir
	// is directory.
	if pinfo, err := os.Stat(path); err != nil {
		if err := os.MkdirAll(path, 0666); err != nil {
			return "", fmt.Errorf("failed to mkdir for home dir %s: %v", path, err)
		}
	} else if !pinfo.Mode().IsDir() {
		return "", fmt.Errorf("home dir %s should be directory", path)
	}

	realPath, err := filepath.EvalSymlinks(path)
	if err != nil {
		return "", fmt.Errorf("failed to acquire real path for %s: %s", path, err)
	}

	return realPath, nil
}

// MatchLabelSelector returns true if labels cover selector.
func MatchLabelSelector(selector, labels map[string]string) bool {
	for k, v := range selector {
		if val, ok := labels[k]; ok {
			if v != val {
				return false
			}
		} else {
			return false
		}
	}
	return true
}

// ExtractIPAndPortFromAddresses extract first valid ip and port from addresses.
func ExtractIPAndPortFromAddresses(addresses []string) (string, string) {
	for _, addr := range addresses {
		addrParts := strings.SplitN(addr, "://", 2)
		if len(addrParts) != 2 {
			log.With(nil).Errorf("invalid listening address %s: must be in format [protocol]://[address]", addr)
			continue
		}

		switch addrParts[0] {
		case "tcp":
			host, port, err := net.SplitHostPort(addrParts[1])
			if err != nil {
				log.With(nil).Errorf("failed to split host and port from address: %v", err)
				continue
			}
			return host, port
		case "unix":
			continue
		default:
			log.With(nil).Errorf("only unix socket or tcp address is support")
		}
	}
	return "", ""
}
