/*
 * Copyright 2022 CloudWeGo Authors
 *
 * 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.
 */

package util

import (
	"errors"
	"fmt"
	"net/url"
	"path"
	"path/filepath"
	"reflect"
	"regexp"
	"strconv"
	"strings"

	"github.com/cloudwego/hertz/cmd/hz/util/logs"
)

func CopyStringSlice(from, to *[]string) {
	n := len(*from)
	m := len(*to)
	if n > m {
		n = m
	}
	for i := 0; i < n; i++ {
		(*to)[i] = (*from)[i]
	}
	*to = (*to)[:n]
}

func CopyString2StringMap(from, to map[string]string) {
	for k := range to {
		delete(to, k)
	}
	for k, v := range from {
		to[k] = v
	}
}

func PackArgs(c interface{}) (res []string, err error) {
	t := reflect.TypeOf(c)
	v := reflect.ValueOf(c)
	if reflect.TypeOf(c).Kind() == reflect.Ptr {
		t = t.Elem()
		v = v.Elem()
	}
	if t.Kind() != reflect.Struct {
		return nil, errors.New("passed c must be struct or pointer of struct")
	}

	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		x := v.Field(i)
		n := f.Name

		if x.IsZero() {
			continue
		}

		switch x.Kind() {
		case reflect.Bool:
			if x.Bool() == false {
				continue
			}
			res = append(res, n+"="+fmt.Sprint(x.Bool()))
		case reflect.String:
			if x.String() == "" {
				continue
			}
			res = append(res, n+"="+x.String())
		case reflect.Slice:
			if x.Len() == 0 {
				continue
			}
			ft := f.Type.Elem()
			if ft.Kind() != reflect.String {
				return nil, fmt.Errorf("slice field %v must be '[]string', err: %v", f.Name, err.Error())
			}
			var ss []string
			for i := 0; i < x.Len(); i++ {
				ss = append(ss, x.Index(i).String())
			}
			res = append(res, n+"="+strings.Join(ss, ";"))
		case reflect.Map:
			if x.Len() == 0 {
				continue
			}
			fk := f.Type.Key()
			if fk.Kind() != reflect.String {
				return nil, fmt.Errorf("map field %v must be 'map[string]string', err: %v", f.Name, err.Error())
			}
			fv := f.Type.Elem()
			if fv.Kind() != reflect.String {
				return nil, fmt.Errorf("map field %v must be 'map[string]string', err: %v", f.Name, err.Error())
			}
			var sk []string
			it := x.MapRange()
			for it.Next() {
				sk = append(sk, it.Key().String()+"="+it.Value().String())
			}
			res = append(res, n+"="+strings.Join(sk, ";"))
		default:
			return nil, fmt.Errorf("unsupported field type: %+v, err: %v", f, err.Error())
		}
	}
	return res, nil
}

func UnpackArgs(args []string, c interface{}) error {
	m, err := MapForm(args)
	if err != nil {
		return fmt.Errorf("unmarshal args failed, err: %v", err.Error())
	}

	t := reflect.TypeOf(c).Elem()
	v := reflect.ValueOf(c).Elem()
	if t.Kind() != reflect.Struct {
		return errors.New("passed c must be struct or pointer of struct")
	}

	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		x := v.Field(i)
		n := f.Name
		values, ok := m[n]
		if !ok || len(values) == 0 || values[0] == "" {
			continue
		}
		switch x.Kind() {
		case reflect.Bool:
			if len(values) != 1 {
				return fmt.Errorf("field %s can't be assigned multi values: %v", n, values)
			}
			x.SetBool(values[0] == "true")
		case reflect.String:
			if len(values) != 1 {
				return fmt.Errorf("field %s can't be assigned multi values: %v", n, values)
			}
			x.SetString(values[0])
		case reflect.Slice:
			if len(values) != 1 {
				return fmt.Errorf("field %s can't be assigned multi values: %v", n, values)
			}
			ss := strings.Split(values[0], ";")
			if x.Type().Elem().Kind() == reflect.Int {
				n := reflect.MakeSlice(x.Type(), len(ss), len(ss))
				for i, s := range ss {
					val, err := strconv.ParseInt(s, 10, 64)
					if err != nil {
						return err
					}
					n.Index(i).SetInt(val)
				}
				x.Set(n)
			} else {
				for _, s := range ss {
					val := reflect.Append(x, reflect.ValueOf(s))
					x.Set(val)
				}
			}
		case reflect.Map:
			if len(values) != 1 {
				return fmt.Errorf("field %s can't be assigned multi values: %v", n, values)
			}
			ss := strings.Split(values[0], ";")
			out := make(map[string]string, len(ss))
			for _, s := range ss {
				sk := strings.SplitN(s, "=", 2)
				if len(sk) != 2 {
					return fmt.Errorf("map filed %v invalid key-value pair '%v'", n, s)
				}
				out[sk[0]] = sk[1]
			}
			x.Set(reflect.ValueOf(out))
		default:
			return fmt.Errorf("field %s has unsupported type %+v", n, f.Type)
		}
	}
	return nil
}

func MapForm(input []string) (map[string][]string, error) {
	out := make(map[string][]string, len(input))

	for _, str := range input {
		parts := strings.SplitN(str, "=", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("invalid argument: '%s'", str)
		}
		key, val := parts[0], parts[1]
		out[key] = append(out[key], val)
	}

	return out, nil
}

func GetFirstKV(m map[string][]string) (string, []string) {
	for k, v := range m {
		return k, v
	}
	return "", nil
}

func ToCamelCase(name string) string {
	return CamelString(name)
}

func ToSnakeCase(name string) string {
	return SnakeString(name)
}

// unifyPath will convert "\" to "/" in path if the os is windows
func unifyPath(path string) string {
	if IsWindows() {
		path = strings.ReplaceAll(path, "\\", "/")
	}
	return path
}

// BaseName get base name for path. ex: "github.com/p.s.m" => "p.s.m"
func BaseName(include, subFixToTrim string) string {
	include = unifyPath(include)
	subFixToTrim = unifyPath(subFixToTrim)
	last := include
	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
		last = last[id+1:]
	}
	if !strings.HasSuffix(last, subFixToTrim) {
		return last
	}
	return last[:len(last)-len(subFixToTrim)]
}

func BaseNameAndTrim(include string) string {
	include = unifyPath(include)
	last := include
	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
		last = last[id+1:]
	}

	if id := strings.LastIndex(last, "."); id != -1 {
		last = last[:id]
	}
	return last
}

func SplitPackageName(pkg, subFixToTrim string) string {
	pkg = unifyPath(pkg)
	subFixToTrim = unifyPath(subFixToTrim)
	last := SplitPackage(pkg, subFixToTrim)
	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
		last = last[id+1:]
	}
	return last
}

func SplitPackage(pkg, subFixToTrim string) string {
	pkg = unifyPath(pkg)
	subFixToTrim = unifyPath(subFixToTrim)
	last := strings.TrimSuffix(pkg, subFixToTrim)
	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
		last = last[id+1:]
	}
	return strings.ReplaceAll(last, ".", "/")
}

// ImportToSanitizedPath converts import path to file system path and replaces dots with underscores in the last component.
// For example: "github.com/example/v1.2" becomes "github.com/example/v1_2" on Unix systems.
// NOTE: no idea about the background, it might caused go import package issues before?
func ImportToSanitizedPath(path string) string {
	path = filepath.FromSlash(path)
	if i := strings.LastIndex(path, string(filepath.Separator)); i >= 0 && i < len(path)-1 && strings.Contains(path[i+1:], ".") {
		base := strings.ReplaceAll(path[i+1:], ".", "_")
		dir := path[:i]
		return dir + string(filepath.Separator) + base
	}
	return path
}

func ToVarName(paths []string) string {
	ps := strings.Join(paths, "__")
	input := []byte(url.PathEscape(ps))
	out := make([]byte, 0, len(input))
	for i := 0; i < len(input); i++ {
		c := input[i]
		if c == ':' || c == '*' {
			continue
		}
		if (c >= '0' && c <= '9' && i != 0) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c == '_') {
			out = append(out, c)
		} else {
			out = append(out, '_')
		}
	}

	return string(out)
}

func SplitGoTags(input string) []string {
	out := make([]string, 0, 4)
	ns := len(input)

	flag := false
	prev := 0
	i := 0
	for i = 0; i < ns; i++ {
		c := input[i]
		if c == '"' {
			flag = !flag
		}
		if !flag && c == ' ' {
			if prev < i {
				out = append(out, input[prev:i])
			}
			prev = i + 1
		}
	}
	if i != 0 && prev < i {
		out = append(out, input[prev:i])
	}

	return out
}

func SubPackage(mod, dir string) string {
	if dir == "" {
		return mod
	}
	return path.Join(mod, filepath.ToSlash(dir))
}

func SubDir(root, subPkg string) string {
	return filepath.Join(root, filepath.FromSlash(subPkg))
}

var (
	uniquePackageName        = map[string]bool{}
	uniqueMiddlewareName     = map[string]bool{}
	uniqueHandlerPackageName = map[string]bool{}
)

// GetPackageUniqueName can get a non-repeating variable name for package alias
func GetPackageUniqueName(name string) (string, error) {
	name, err := getUniqueName(name, uniquePackageName)
	if err != nil {
		return "", fmt.Errorf("can not generate unique name for package '%s', err: %v", name, err)
	}

	return name, nil
}

// GetMiddlewareUniqueName can get a non-repeating variable name for middleware name
func GetMiddlewareUniqueName(name string) (string, error) {
	name, err := getUniqueName(name, uniqueMiddlewareName)
	if err != nil {
		return "", fmt.Errorf("can not generate routing group for path '%s', err: %v", name, err)
	}

	return name, nil
}

func GetHandlerPackageUniqueName(name string) (string, error) {
	name, err := getUniqueName(name, uniqueHandlerPackageName)
	if err != nil {
		return "", fmt.Errorf("can not generate unique handler package name: '%s', err: %v", name, err)
	}

	return name, nil
}

// getUniqueName can get a non-repeating variable name
func getUniqueName(name string, uniqueNameSet map[string]bool) (string, error) {
	uniqueName := name
	if _, exist := uniqueNameSet[uniqueName]; exist {
		for i := 0; i < 10000; i++ {
			uniqueName = uniqueName + fmt.Sprintf("%d", i)
			if _, exist := uniqueNameSet[uniqueName]; !exist {
				logs.Infof("There is a package name with the same name, change %s to %s", name, uniqueName)
				break
			}
			uniqueName = name
			if i == 9999 {
				return "", fmt.Errorf("there is too many same package for %s", name)
			}
		}
	}
	uniqueNameSet[uniqueName] = true

	return uniqueName, nil
}

var validFuncReg = regexp.MustCompile("[_0-9a-zA-Z]")

// ToGoFuncName converts a string to a function naming style for go
func ToGoFuncName(s string) string {
	ss := []byte(s)
	for i := range ss {
		if !validFuncReg.Match([]byte{s[i]}) {
			ss[i] = '_'
		}
	}
	return string(ss)
}
