package flag

import (
	"fmt"
	"reflect"
	"regexp"
	"strings"

	"github.com/containous/traefik/v2/pkg/config/parser"
)

// Parse parses the command-line flag arguments into a map,
// using the type information in element to discriminate whether a flag is supposed to be a bool,
// and other such ambiguities.
func Parse(args []string, element interface{}) (map[string]string, error) {
	f := flagSet{
		flagTypes: getFlagTypes(element),
		args:      args,
		values:    make(map[string]string),
		keys:      make(map[string]string),
	}

	for {
		seen, err := f.parseOne()
		if seen {
			continue
		}
		if err == nil {
			break
		}
		return nil, err
	}
	return f.values, nil
}

type flagSet struct {
	flagTypes map[string]reflect.Kind
	args      []string
	values    map[string]string
	keys      map[string]string
}

func (f *flagSet) parseOne() (bool, error) {
	if len(f.args) == 0 {
		return false, nil
	}

	s := f.args[0]
	if len(s) < 2 || s[0] != '-' {
		return false, nil
	}
	numMinuses := 1
	if s[1] == '-' {
		numMinuses++
		if len(s) == 2 { // "--" terminates the flags
			f.args = f.args[1:]
			return false, nil
		}
	}

	name := s[numMinuses:]
	if len(name) == 0 || name[0] == '-' || name[0] == '=' {
		return false, fmt.Errorf("bad flag syntax: %s", s)
	}

	// it's a flag. does it have an argument?
	f.args = f.args[1:]
	hasValue := false
	value := ""
	for i := 1; i < len(name); i++ { // equals cannot be first
		if name[i] == '=' {
			value = name[i+1:]
			hasValue = true
			name = name[0:i]
			break
		}
	}

	if hasValue {
		f.setValue(name, value)
		return true, nil
	}

	flagType := f.getFlagType(name)
	if flagType == reflect.Bool || flagType == reflect.Ptr {
		f.setValue(name, "true")
		return true, nil
	}

	if len(f.args) > 0 {
		// value is the next arg
		hasValue = true
		value, f.args = f.args[0], f.args[1:]
	}

	if !hasValue {
		return false, fmt.Errorf("flag needs an argument: -%s", name)
	}

	f.setValue(name, value)
	return true, nil
}

func (f *flagSet) setValue(name string, value string) {
	srcKey := parser.DefaultRootName + "." + name
	neutralKey := strings.ToLower(srcKey)

	key, ok := f.keys[neutralKey]
	if !ok {
		f.keys[neutralKey] = srcKey
		key = srcKey
	}

	v, ok := f.values[key]
	if ok && f.getFlagType(name) == reflect.Slice {
		f.values[key] = v + "," + value
		return
	}

	f.values[key] = value
}

func (f *flagSet) getFlagType(name string) reflect.Kind {
	neutral := strings.ToLower(name)

	kind, ok := f.flagTypes[neutral]
	if ok {
		return kind
	}

	for n, k := range f.flagTypes {
		if strings.Contains(n, parser.MapNamePlaceholder) {
			p := strings.NewReplacer(".", `\.`, parser.MapNamePlaceholder, `([^.]+)`).Replace(n)
			if regexp.MustCompile(p).MatchString(neutral) {
				return k
			}
		}
	}

	return reflect.Invalid
}
