// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// 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 paramtable

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/milvus-io/milvus/internal/config"
	"github.com/milvus-io/milvus/internal/util/funcutil"
)

type ParamItem struct {
	Key          string // which should be named as "A.B.C"
	Version      string
	Doc          string
	DefaultValue string
	FallbackKeys []string
	PanicIfEmpty bool
	Export       bool

	Formatter func(originValue string) string
	Forbidden bool

	manager *config.Manager
}

func (pi *ParamItem) Init(manager *config.Manager) {
	pi.manager = manager
	if pi.Forbidden {
		pi.manager.ForbidUpdate(pi.Key)
	}
}

// Get original value with error
func (pi *ParamItem) get() (string, error) {
	if pi.manager == nil {
		panic(fmt.Sprintf("manager is nil %s", pi.Key))
	}
	ret, err := pi.manager.GetConfig(pi.Key)
	if err != nil {
		for _, key := range pi.FallbackKeys {
			ret, err = pi.manager.GetConfig(key)
			if err == nil {
				break
			}
		}
	}
	if err != nil {
		ret = pi.DefaultValue
	}
	if pi.Formatter != nil {
		ret = pi.Formatter(ret)
	}
	if ret == "" && pi.PanicIfEmpty {
		panic(fmt.Sprintf("%s is empty", pi.Key))
	}
	return ret, err
}

func (pi *ParamItem) GetValue() string {
	v, _ := pi.get()
	return v
}

func (pi *ParamItem) GetAsStrings() []string {
	return getAsStrings(pi.GetValue())
}

func (pi *ParamItem) GetAsBool() bool {
	return getAsBool(pi.GetValue())
}

func (pi *ParamItem) GetAsInt() int {
	return getAsInt(pi.GetValue())
}

func (pi *ParamItem) GetAsInt32() int32 {
	return int32(getAsInt64(pi.GetValue()))
}

func (pi *ParamItem) GetAsUint32() uint32 {
	return uint32(getAsInt64(pi.GetValue()))
}

func (pi *ParamItem) GetAsInt64() int64 {
	return getAsInt64(pi.GetValue())
}

func (pi *ParamItem) GetAsFloat() float64 {
	return getAsFloat(pi.GetValue())
}

func (pi *ParamItem) GetAsDuration(unit time.Duration) time.Duration {
	return getAsDuration(pi.GetValue(), unit)
}

func (pi *ParamItem) GetAsJSONMap() map[string]string {
	return getAndConvert(pi.GetValue(), funcutil.JSONToMap, nil)
}

type CompositeParamItem struct {
	Items  []*ParamItem
	Format func(map[string]string) string
}

func (cpi *CompositeParamItem) GetValue() string {
	kvs := make(map[string]string, len(cpi.Items))
	for _, v := range cpi.Items {
		kvs[v.Key] = v.GetValue()
	}
	return cpi.Format(kvs)
}

type ParamGroup struct {
	KeyPrefix string // which should be named as "A.B."
	Version   string
	Doc       string
	Export    bool

	GetFunc func() map[string]string

	manager *config.Manager
}

func (pg *ParamGroup) Init(manager *config.Manager) {
	pg.manager = manager
}

func (pg *ParamGroup) GetValue() map[string]string {
	if pg.GetFunc != nil {
		return pg.GetFunc()
	}
	values := pg.manager.GetBy(config.WithPrefix(pg.KeyPrefix), config.RemovePrefix(pg.KeyPrefix))
	return values
}

func getAsStrings(v string) []string {
	return getAndConvert(v, func(value string) ([]string, error) {
		return strings.Split(value, ","), nil
	}, []string{})
}

func getAsBool(v string) bool {
	return getAndConvert(v, strconv.ParseBool, false)
}

func getAsInt(v string) int {
	return getAndConvert(v, strconv.Atoi, 0)
}

func getAsInt64(v string) int64 {
	return getAndConvert(v, func(value string) (int64, error) {
		return strconv.ParseInt(value, 10, 64)
	}, 0)
}

func getAsFloat(v string) float64 {
	return getAndConvert(v, func(value string) (float64, error) {
		return strconv.ParseFloat(value, 64)
	}, 0.0)
}

func getAsDuration(v string, unit time.Duration) time.Duration {
	return getAndConvert(v, func(value string) (time.Duration, error) {
		v, err := strconv.ParseInt(value, 10, 64)
		if err != nil {
			fv, err := strconv.ParseFloat(value, 64)
			return time.Duration(fv * float64(unit)), err
		}
		return time.Duration(v) * unit, err
	}, 0)
}
func getAndConvert[T any](v string, converter func(input string) (T, error), defaultValue T) T {
	t, err := converter(v)
	if err != nil {
		return defaultValue
	}
	return t
}
