package flag

import (
	"time"

	charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
	"github.com/spf13/pflag"
)

type (
	// Flag is an interface which describes a command line flag that affects the
	// Helm Values used to render Helm charts.  This interface allows us to
	// iterate over flags which have been set and apply their effects to the
	// Values.
	Flag interface {
		Apply(values *charts.Values) error
		IsSet() bool
		Name() string
	}

	// UintFlag is a Flag with a uint typed value.
	UintFlag struct {
		name    string
		Value   uint
		flagSet *pflag.FlagSet
		apply   func(values *charts.Values, value uint) error
	}

	// Int64Flag is a Flag with an int64 typed value.
	Int64Flag struct {
		name    string
		Value   int64
		flagSet *pflag.FlagSet
		apply   func(values *charts.Values, value int64) error
	}

	// StringFlag is a Flag with a string typed value.
	StringFlag struct {
		name    string
		Value   string
		flagSet *pflag.FlagSet
		apply   func(values *charts.Values, value string) error
	}

	// StringSliceFlag is a Flag with a []string typed value.
	StringSliceFlag struct {
		name    string
		Value   []string
		flagSet *pflag.FlagSet
		apply   func(values *charts.Values, value []string) error
	}

	// BoolFlag is a Flag with a bool typed value.
	BoolFlag struct {
		name    string
		Value   bool
		flagSet *pflag.FlagSet
		apply   func(values *charts.Values, value bool) error
	}

	// DurationFlag is a Flag with a time.Duration typed value.
	DurationFlag struct {
		name    string
		Value   time.Duration
		flagSet *pflag.FlagSet
		apply   func(values *charts.Values, value time.Duration) error
	}
)

// NewUintFlag creates a new uint typed Flag that executes the given function
// when applied.  The flag is attached to the given FlagSet.
func NewUintFlag(flagSet *pflag.FlagSet, name string, defaultValue uint, description string, apply func(values *charts.Values, value uint) error) *UintFlag {
	flag := UintFlag{
		name:    name,
		flagSet: flagSet,
		apply:   apply,
	}
	flagSet.UintVar(&flag.Value, name, defaultValue, description)
	return &flag
}

// NewInt64Flag creates a new int64 typed Flag that executes the given function
// when applied.  The flag is attached to the given FlagSet.
func NewInt64Flag(flagSet *pflag.FlagSet, name string, defaultValue int64, description string, apply func(values *charts.Values, value int64) error) *Int64Flag {
	flag := Int64Flag{
		name:    name,
		flagSet: flagSet,
		apply:   apply,
	}
	flagSet.Int64Var(&flag.Value, name, defaultValue, description)
	return &flag
}

// NewStringFlag creates a new string typed Flag that executes the given function
// when applied.  The flag is attached to the given FlagSet.
func NewStringFlag(flagSet *pflag.FlagSet, name string, defaultValue string, description string, apply func(values *charts.Values, value string) error) *StringFlag {
	flag := StringFlag{
		name:    name,
		flagSet: flagSet,
		apply:   apply,
	}
	flagSet.StringVar(&flag.Value, name, defaultValue, description)
	return &flag
}

// NewStringSliceFlag creates a new []string typed Flag that executes the given function
// when applied.  The flag is attached to the given FlagSet.
func NewStringSliceFlag(flagSet *pflag.FlagSet, name string, defaultValue []string, description string, apply func(values *charts.Values, value []string) error) *StringSliceFlag {
	flag := StringSliceFlag{
		name:    name,
		flagSet: flagSet,
		apply:   apply,
	}
	flagSet.StringSliceVar(&flag.Value, name, defaultValue, description)
	return &flag
}

// NewStringFlagP creates a new string typed Flag that executes the given function
// when applied.  The flag is attached to the given FlagSet.
func NewStringFlagP(flagSet *pflag.FlagSet, name string, short string, defaultValue string, description string, apply func(values *charts.Values, value string) error) *StringFlag {
	flag := StringFlag{
		name:    name,
		flagSet: flagSet,
		apply:   apply,
	}
	flagSet.StringVarP(&flag.Value, name, short, defaultValue, description)
	return &flag
}

// NewBoolFlag creates a new bool typed Flag that executes the given function
// when applied.  The flag is attached to the given FlagSet.
func NewBoolFlag(flagSet *pflag.FlagSet, name string, defaultValue bool, description string, apply func(values *charts.Values, value bool) error) *BoolFlag {
	flag := BoolFlag{
		name:    name,
		flagSet: flagSet,
		apply:   apply,
	}
	flagSet.BoolVar(&flag.Value, name, defaultValue, description)
	return &flag
}

// NewDurationFlag creates a new time.Duration typed Flag that executes the given function
// when applied.  The flag is attached to the given FlagSet.
func NewDurationFlag(flagSet *pflag.FlagSet, name string, defaultValue time.Duration, description string, apply func(values *charts.Values, value time.Duration) error) *DurationFlag {
	flag := DurationFlag{
		name:    name,
		flagSet: flagSet,
		apply:   apply,
	}
	flagSet.DurationVar(&flag.Value, name, defaultValue, description)
	return &flag
}

// Apply executes the stored apply function on the given Values.
func (flag *UintFlag) Apply(values *charts.Values) error {
	return flag.apply(values, flag.Value)
}

// IsSet returns true if and only if the Flag has been explicitly set with a value.
func (flag *UintFlag) IsSet() bool {
	return flag.flagSet.Changed(flag.name)
}

// Name returns the name of the flag.
func (flag *UintFlag) Name() string {
	return flag.name
}

// Apply executes the stored apply function on the given Values.
func (flag *Int64Flag) Apply(values *charts.Values) error {
	return flag.apply(values, flag.Value)
}

// IsSet returns true if and only if the Flag has been explicitly set with a value.
func (flag *Int64Flag) IsSet() bool {
	return flag.flagSet.Changed(flag.name)
}

// Name returns the name of the flag.
func (flag *Int64Flag) Name() string {
	return flag.name
}

// Apply executes the stored apply function on the given Values.
func (flag *StringFlag) Apply(values *charts.Values) error {
	return flag.apply(values, flag.Value)
}

// Set sets the given value to the underlying Flag
func (flag *StringFlag) Set(value string) error {
	return flag.flagSet.Set(flag.name, value)
}

// IsSet returns true if and only if the Flag has been explicitly set with a value.
func (flag *StringFlag) IsSet() bool {
	return flag.flagSet.Changed(flag.name)
}

// Name returns the name of the flag.
func (flag *StringFlag) Name() string {
	return flag.name
}

// Apply executes the stored apply function on the given Values.
func (flag *StringSliceFlag) Apply(values *charts.Values) error {
	return flag.apply(values, flag.Value)
}

// IsSet returns true if and only if the Flag has been explicitly set with a value.
func (flag *StringSliceFlag) IsSet() bool {
	return flag.flagSet.Changed(flag.name)
}

// Name returns the name of the flag.
func (flag *StringSliceFlag) Name() string {
	return flag.name
}

// Apply executes the stored apply function on the given Values.
func (flag *BoolFlag) Apply(values *charts.Values) error {
	return flag.apply(values, flag.Value)
}

// IsSet returns true if and only if the Flag has been explicitly set with a value.
func (flag *BoolFlag) IsSet() bool {
	return flag.flagSet.Changed(flag.name)
}

// Name returns the name of the flag.
func (flag *BoolFlag) Name() string {
	return flag.name
}

// Apply executes the stored apply function on the given Values.
func (flag *DurationFlag) Apply(values *charts.Values) error {
	return flag.apply(values, flag.Value)
}

// IsSet returns true if and only if the Flag has been explicitly set with a value.
func (flag *DurationFlag) IsSet() bool {
	return flag.flagSet.Changed(flag.name)
}

// Name returns the name of the flag.
func (flag *DurationFlag) Name() string {
	return flag.name
}

// ApplySetFlags iterates through the given slice of Flags and applies the
// effect of each set flag to the given Values.  Flags effects are applied
// in the order they appear in the slice.
func ApplySetFlags(values *charts.Values, flags []Flag) error {
	for _, flag := range flags {
		if flag.IsSet() {
			err := flag.Apply(values)
			if err != nil {
				return err
			}
		}
	}
	return nil
}
