// Copyright 2017 Google Inc.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package getopt

import (
	"fmt"
	"strings"
)

// An Option can be either a Flag or a Value
type Option interface {
	// Name returns the name of the option.  If the option has been seen
	// then the last way it was referenced (short or long) is returned
	// otherwise if there is a short name then this will be the short name
	// as a string, else it will be the long name.
	Name() string

	// ShortName always returns the short name of the option, or "" if there
	// is no short name.  The name does not include the "-".
	ShortName() string

	// LongName always returns the long name of the option, or "" if there
	// is no long name.  The name does not include the "--".
	LongName() string

	// IsFlag returns true if Option is a flag.
	IsFlag() bool

	// Seen returns true if the flag was seen.
	Seen() bool

	// Count returns the number of times the flag was seen.
	Count() int

	// String returns the last value the option was set to.
	String() string

	// Value returns the Value of the option.
	Value() Value

	// SetOptional makes the value optional.  The option and value are
	// always a single argument.  Either --option or --option=value.  In
	// the former case the value of the option does not change but the Set()
	// will return true and the value returned by Count() is incremented.
	// The short form is either -o or -ovalue.  SetOptional returns
	// the Option
	SetOptional() Option

	// SetFlag makes the value a flag.  Flags are boolean values and
	// normally do not taken a value.  They are set to true when seen.
	// If a value is passed in the long form then it must be on, case
	// insensitivinsensitive, one of "true", "false", "t", "f", "on", "off", "1", "0".
	// SetFlag returns the Option
	SetFlag() Option

	// Reset resets the state of the option so it appears it has not
	// yet been seen, including resetting the value of the option
	// to its original default state.
	Reset()

	// Mandataory sets the mandatory flag of the option.  Parse will
	// fail if a mandatory option is missing.
	Mandatory() Option

	// SetGroup sets the option as part of a radio group.  Parse will
	// fail if two options in the same group are seen.
	SetGroup(string) Option
}

type option struct {
	short     rune   // 0 means no short name
	long      string // "" means no long name
	isLong    bool   // True if they used the long name
	flag      bool   // true if a boolean flag
	defval    string // default value
	optional  bool   // true if we take an optional value
	help      string // help message
	where     string // file where the option was defined
	value     Value  // current value of option
	count     int    // number of times we have seen this option
	name      string // name of the value (for usage)
	uname     string // name of the option (for usage)
	mandatory bool   // this option must be specified
	group     string // mutual exclusion group
}

// usageName returns the name of the option for printing usage lines in one
// of the following forms:
//
//  -f
//      --flag
//  -f, --flag
//  -s value
//      --set=value
//  -s, --set=value
func (o *option) usageName() string {
	// Don't print help messages if we have none and there is only one
	// way to specify the option.
	if o.help == "" && (o.short == 0 || o.long == "") {
		return ""
	}
	n := ""

	switch {
	case o.short != 0 && o.long == "":
		n = "-" + string(o.short)
	case o.short == 0 && o.long != "":
		n = "    --" + o.long
	case o.short != 0 && o.long != "":
		n = "-" + string(o.short) + ", --" + o.long
	}

	switch {
	case o.flag:
		return n
	case o.optional:
		return n + "[=" + o.name + "]"
	case o.long != "":
		return n + "=" + o.name
	}
	return n + " " + o.name
}

// sortName returns the name to sort the option on.
func (o *option) sortName() string {
	if o.short != 0 {
		return string(o.short) + o.long
	}
	return o.long[:1] + o.long
}

func (o *option) Seen() bool               { return o.count > 0 }
func (o *option) Count() int               { return o.count }
func (o *option) IsFlag() bool             { return o.flag }
func (o *option) String() string           { return o.value.String() }
func (o *option) SetOptional() Option      { o.optional = true; return o }
func (o *option) SetFlag() Option          { o.flag = true; return o }
func (o *option) Mandatory() Option        { o.mandatory = true; return o }
func (o *option) SetGroup(g string) Option { o.group = g; return o }

func (o *option) Value() Value {
	if o == nil {
		return nil
	}
	return o.value
}

func (o *option) Name() string {
	if !o.isLong && o.short != 0 {
		return "-" + string(o.short)
	}
	return "--" + o.long
}

func (o *option) ShortName() string {
	if o.short != 0 {
		return string(o.short)
	}
	return ""
}

func (o *option) LongName() string {
	return o.long
}

// Reset rests an option so that it appears it has not yet been seen.
func (o *option) Reset() {
	o.isLong = false
	o.count = 0
	o.value.Set(o.defval, o)
}

type optionList []*option

func (ol optionList) Len() int      { return len(ol) }
func (ol optionList) Swap(i, j int) { ol[i], ol[j] = ol[j], ol[i] }
func (ol optionList) Less(i, j int) bool {
	// first check the short names (or the first letter of the long name)
	// If they are not equal (case insensitive) then we have our answer
	n1 := ol[i].sortName()
	n2 := ol[j].sortName()
	l1 := strings.ToLower(n1)
	l2 := strings.ToLower(n2)
	if l1 < l2 {
		return true
	}
	if l2 < l1 {
		return false
	}
	return n1 < n2
}

// AddOption add the option o to set CommandLine if o is not already in set
// CommandLine.
func AddOption(o Option) {
	CommandLine.AddOption(o)
}

// AddOption add the option o to set s if o is not already in set s.
func (s *Set) AddOption(o Option) {
	opt := o.(*option)
	for _, eopt := range s.options {
		if opt == eopt {
			return
		}
	}
	if opt.short != 0 {
		if oo, ok := s.shortOptions[opt.short]; ok {
			fmt.Fprintf(stderr, "%s: -%c already declared at %s\n", opt.where, opt.short, oo.where)
			exit(1)
		}
		s.shortOptions[opt.short] = opt
	}
	if opt.long != "" {
		if oo, ok := s.longOptions[opt.long]; ok {
			fmt.Fprintf(stderr, "%s: --%s already declared at %s\n", opt.where, opt.long, oo.where)
			exit(1)
		}
		s.longOptions[opt.long] = opt
	}
	s.options = append(s.options, opt)
}
