package fzf

import (
	"os"
	"regexp"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/junegunn/fzf/src/curses"

	"github.com/junegunn/go-shellwords"
)

const usage = `usage: fzf [options]

  Search
    -x, --extended        Extended-search mode
                          (enabled by default; +x or --no-extended to disable)
    -e, --exact           Enable Exact-match
    -i                    Case-insensitive match (default: smart-case match)
    +i                    Case-sensitive match
    -n, --nth=N[,..]      Comma-separated list of field index expressions
                          for limiting search scope. Each can be a non-zero
                          integer or a range expression ([BEGIN]..[END]).
    --with-nth=N[,..]     Transform item using index expressions within finder
    -d, --delimiter=STR   Field delimiter regex for --nth (default: AWK-style)
    +s, --no-sort         Do not sort the result
    --tac                 Reverse the order of the input
    --tiebreak=CRITERION  Sort criterion when the scores are tied;
                          [length|begin|end|index] (default: length)

  Interface
    -m, --multi           Enable multi-select with tab/shift-tab
    --ansi                Enable processing of ANSI color codes
    --no-mouse            Disable mouse
    --color=COLSPEC       Base scheme (dark|light|16|bw) and/or custom colors
    --black               Use black background
    --reverse             Reverse orientation
    --margin=MARGIN       Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
    --tabstop=SPACES      Number of spaces for a tab character (default: 8)
    --cycle               Enable cyclic scroll
    --no-hscroll          Disable horizontal scroll
    --inline-info         Display finder info inline with the query
    --prompt=STR          Input prompt (default: '> ')
    --bind=KEYBINDS       Custom key bindings. Refer to the man page.
    --history=FILE        History file
    --history-size=N      Maximum number of history entries (default: 1000)
    --header=STR          String to print as header
    --header-lines=N      The first N lines of the input are treated as header

  Scripting
    -q, --query=STR       Start the finder with the given query
    -1, --select-1        Automatically select the only match
    -0, --exit-0          Exit immediately when there's no match
    -f, --filter=STR      Filter mode. Do not start interactive finder.
    --print-query         Print query as the first line
    --expect=KEYS         Comma-separated list of keys to complete fzf
    --sync                Synchronous search for multi-staged filtering

  Environment variables
    FZF_DEFAULT_COMMAND   Default command to use when input is tty
    FZF_DEFAULT_OPTS      Default options (e.g. '--reverse --inline-info')

`

// Case denotes case-sensitivity of search
type Case int

// Case-sensitivities
const (
	CaseSmart Case = iota
	CaseIgnore
	CaseRespect
)

// Sort criteria
type tiebreak int

const (
	byLength tiebreak = iota
	byBegin
	byEnd
	byIndex
)

func defaultMargin() [4]string {
	return [4]string{"0", "0", "0", "0"}
}

// Options stores the values of command-line options
type Options struct {
	Fuzzy       bool
	Extended    bool
	Case        Case
	Nth         []Range
	WithNth     []Range
	Delimiter   Delimiter
	Sort        int
	Tac         bool
	Tiebreak    tiebreak
	Multi       bool
	Ansi        bool
	Mouse       bool
	Theme       *curses.ColorTheme
	Black       bool
	Reverse     bool
	Cycle       bool
	Hscroll     bool
	InlineInfo  bool
	Prompt      string
	Query       string
	Select1     bool
	Exit0       bool
	Filter      *string
	ToggleSort  bool
	Expect      map[int]string
	Keymap      map[int]actionType
	Execmap     map[int]string
	PrintQuery  bool
	ReadZero    bool
	Sync        bool
	History     *History
	Header      []string
	HeaderLines int
	Margin      [4]string
	Tabstop     int
	Version     bool
}

func defaultTheme() *curses.ColorTheme {
	if strings.Contains(os.Getenv("TERM"), "256") {
		return curses.Dark256
	}
	return curses.Default16
}

func defaultOptions() *Options {
	return &Options{
		Fuzzy:       true,
		Extended:    true,
		Case:        CaseSmart,
		Nth:         make([]Range, 0),
		WithNth:     make([]Range, 0),
		Delimiter:   Delimiter{},
		Sort:        1000,
		Tac:         false,
		Tiebreak:    byLength,
		Multi:       false,
		Ansi:        false,
		Mouse:       true,
		Theme:       defaultTheme(),
		Black:       false,
		Reverse:     false,
		Cycle:       false,
		Hscroll:     true,
		InlineInfo:  false,
		Prompt:      "> ",
		Query:       "",
		Select1:     false,
		Exit0:       false,
		Filter:      nil,
		ToggleSort:  false,
		Expect:      make(map[int]string),
		Keymap:      defaultKeymap(),
		Execmap:     make(map[int]string),
		PrintQuery:  false,
		ReadZero:    false,
		Sync:        false,
		History:     nil,
		Header:      make([]string, 0),
		HeaderLines: 0,
		Margin:      defaultMargin(),
		Tabstop:     8,
		Version:     false}
}

func help(code int) {
	os.Stderr.WriteString(usage)
	os.Exit(code)
}

func errorExit(msg string) {
	os.Stderr.WriteString(msg + "\n")
	os.Exit(exitError)
}

func optString(arg string, prefixes ...string) (bool, string) {
	for _, prefix := range prefixes {
		if strings.HasPrefix(arg, prefix) {
			return true, arg[len(prefix):]
		}
	}
	return false, ""
}

func nextString(args []string, i *int, message string) string {
	if len(args) > *i+1 {
		*i++
	} else {
		errorExit(message)
	}
	return args[*i]
}

func optionalNextString(args []string, i *int) string {
	if len(args) > *i+1 {
		*i++
		return args[*i]
	}
	return ""
}

func atoi(str string) int {
	num, err := strconv.Atoi(str)
	if err != nil {
		errorExit("not a valid integer: " + str)
	}
	return num
}

func atof(str string) float64 {
	num, err := strconv.ParseFloat(str, 64)
	if err != nil {
		errorExit("not a valid number: " + str)
	}
	return num
}

func nextInt(args []string, i *int, message string) int {
	if len(args) > *i+1 {
		*i++
	} else {
		errorExit(message)
	}
	return atoi(args[*i])
}

func optionalNumeric(args []string, i *int) int {
	if len(args) > *i+1 {
		if strings.IndexAny(args[*i+1], "0123456789") == 0 {
			*i++
		}
	}
	return 1 // Don't care
}

func splitNth(str string) []Range {
	if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match {
		errorExit("invalid format: " + str)
	}

	tokens := strings.Split(str, ",")
	ranges := make([]Range, len(tokens))
	for idx, s := range tokens {
		r, ok := ParseRange(&s)
		if !ok {
			errorExit("invalid format: " + str)
		}
		ranges[idx] = r
	}
	return ranges
}

func delimiterRegexp(str string) Delimiter {
	// Special handling of \t
	str = strings.Replace(str, "\\t", "\t", -1)

	// 1. Pattern does not contain any special character
	if regexp.QuoteMeta(str) == str {
		return Delimiter{str: &str}
	}

	rx, e := regexp.Compile(str)
	// 2. Pattern is not a valid regular expression
	if e != nil {
		return Delimiter{str: &str}
	}

	// 3. Pattern as regular expression. Slow.
	return Delimiter{regex: rx}
}

func isAlphabet(char uint8) bool {
	return char >= 'a' && char <= 'z'
}

func parseKeyChords(str string, message string) map[int]string {
	if len(str) == 0 {
		errorExit(message)
	}

	tokens := strings.Split(str, ",")
	if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
		tokens = append(tokens, ",")
	}

	chords := make(map[int]string)
	for _, key := range tokens {
		if len(key) == 0 {
			continue // ignore
		}
		lkey := strings.ToLower(key)
		chord := 0
		switch lkey {
		case "up":
			chord = curses.Up
		case "down":
			chord = curses.Down
		case "left":
			chord = curses.Left
		case "right":
			chord = curses.Right
		case "enter", "return":
			chord = curses.CtrlM
		case "space":
			chord = curses.AltZ + int(' ')
		case "bspace", "bs":
			chord = curses.BSpace
		case "alt-bs", "alt-bspace":
			chord = curses.AltBS
		case "tab":
			chord = curses.Tab
		case "btab", "shift-tab":
			chord = curses.BTab
		case "esc":
			chord = curses.ESC
		case "del":
			chord = curses.Del
		case "home":
			chord = curses.Home
		case "end":
			chord = curses.End
		case "pgup", "page-up":
			chord = curses.PgUp
		case "pgdn", "page-down":
			chord = curses.PgDn
		case "shift-left":
			chord = curses.SLeft
		case "shift-right":
			chord = curses.SRight
		case "double-click":
			chord = curses.DoubleClick
		default:
			if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
				chord = curses.CtrlA + int(lkey[5]) - 'a'
			} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
				chord = curses.AltA + int(lkey[4]) - 'a'
			} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
				chord = curses.F1 + int(key[1]) - '1'
			} else if utf8.RuneCountInString(key) == 1 {
				chord = curses.AltZ + int([]rune(key)[0])
			} else {
				errorExit("unsupported key: " + key)
			}
		}
		if chord > 0 {
			chords[chord] = key
		}
	}
	return chords
}

func parseTiebreak(str string) tiebreak {
	switch strings.ToLower(str) {
	case "length":
		return byLength
	case "index":
		return byIndex
	case "begin":
		return byBegin
	case "end":
		return byEnd
	default:
		errorExit("invalid sort criterion: " + str)
	}
	return byLength
}

func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
	if theme != nil {
		dupe := *theme
		return &dupe
	}
	return nil
}

func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
	theme := dupeTheme(defaultTheme)
	for _, str := range strings.Split(strings.ToLower(str), ",") {
		switch str {
		case "dark":
			theme = dupeTheme(curses.Dark256)
		case "light":
			theme = dupeTheme(curses.Light256)
		case "16":
			theme = dupeTheme(curses.Default16)
		case "bw", "no":
			theme = nil
		default:
			fail := func() {
				errorExit("invalid color specification: " + str)
			}
			// Color is disabled
			if theme == nil {
				continue
			}

			pair := strings.Split(str, ":")
			if len(pair) != 2 {
				fail()
			}
			ansi32, err := strconv.Atoi(pair[1])
			if err != nil || ansi32 < -1 || ansi32 > 255 {
				fail()
			}
			ansi := int16(ansi32)
			switch pair[0] {
			case "fg":
				theme.Fg = ansi
				theme.UseDefault = theme.UseDefault && ansi < 0
			case "bg":
				theme.Bg = ansi
				theme.UseDefault = theme.UseDefault && ansi < 0
			case "fg+":
				theme.Current = ansi
			case "bg+":
				theme.DarkBg = ansi
			case "hl":
				theme.Match = ansi
			case "hl+":
				theme.CurrentMatch = ansi
			case "prompt":
				theme.Prompt = ansi
			case "spinner":
				theme.Spinner = ansi
			case "info":
				theme.Info = ansi
			case "pointer":
				theme.Cursor = ansi
			case "marker":
				theme.Selected = ansi
			case "header":
				theme.Header = ansi
			default:
				fail()
			}
		}
	}
	return theme
}

var executeRegexp *regexp.Regexp

func firstKey(keymap map[int]string) int {
	for k := range keymap {
		return k
	}
	return 0
}

const (
	escapedColon = 0
	escapedComma = 1
)

func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
	if executeRegexp == nil {
		// Backreferences are not supported.
		// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
		executeRegexp = regexp.MustCompile(
			"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
	}
	masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
		if strings.HasPrefix(src, ":execute-multi") {
			return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")"
		}
		return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")"
	})
	masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
	masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)

	idx := 0
	for _, pairStr := range strings.Split(masked, ",") {
		origPairStr := str[idx : idx+len(pairStr)]
		idx += len(pairStr) + 1

		pair := strings.SplitN(pairStr, ":", 2)
		if len(pair) < 2 {
			errorExit("bind action not specified: " + origPairStr)
		}
		var key int
		if len(pair[0]) == 1 && pair[0][0] == escapedColon {
			key = ':' + curses.AltZ
		} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
			key = ',' + curses.AltZ
		} else {
			keys := parseKeyChords(pair[0], "key name required")
			key = firstKey(keys)
		}

		act := origPairStr[len(pair[0])+1 : len(origPairStr)]
		actLower := strings.ToLower(act)
		switch actLower {
		case "ignore":
			keymap[key] = actIgnore
		case "beginning-of-line":
			keymap[key] = actBeginningOfLine
		case "abort":
			keymap[key] = actAbort
		case "accept":
			keymap[key] = actAccept
		case "backward-char":
			keymap[key] = actBackwardChar
		case "backward-delete-char":
			keymap[key] = actBackwardDeleteChar
		case "backward-word":
			keymap[key] = actBackwardWord
		case "clear-screen":
			keymap[key] = actClearScreen
		case "delete-char":
			keymap[key] = actDeleteChar
		case "delete-char/eof":
			keymap[key] = actDeleteCharEOF
		case "end-of-line":
			keymap[key] = actEndOfLine
		case "cancel":
			keymap[key] = actCancel
		case "forward-char":
			keymap[key] = actForwardChar
		case "forward-word":
			keymap[key] = actForwardWord
		case "kill-line":
			keymap[key] = actKillLine
		case "kill-word":
			keymap[key] = actKillWord
		case "unix-line-discard", "line-discard":
			keymap[key] = actUnixLineDiscard
		case "unix-word-rubout", "word-rubout":
			keymap[key] = actUnixWordRubout
		case "yank":
			keymap[key] = actYank
		case "backward-kill-word":
			keymap[key] = actBackwardKillWord
		case "toggle-down":
			keymap[key] = actToggleDown
		case "toggle-up":
			keymap[key] = actToggleUp
		case "toggle-all":
			keymap[key] = actToggleAll
		case "select-all":
			keymap[key] = actSelectAll
		case "deselect-all":
			keymap[key] = actDeselectAll
		case "toggle":
			keymap[key] = actToggle
		case "down":
			keymap[key] = actDown
		case "up":
			keymap[key] = actUp
		case "page-up":
			keymap[key] = actPageUp
		case "page-down":
			keymap[key] = actPageDown
		case "previous-history":
			keymap[key] = actPreviousHistory
		case "next-history":
			keymap[key] = actNextHistory
		case "toggle-sort":
			keymap[key] = actToggleSort
			toggleSort = true
		default:
			if isExecuteAction(actLower) {
				var offset int
				if strings.HasPrefix(actLower, "execute-multi") {
					keymap[key] = actExecuteMulti
					offset = len("execute-multi")
				} else {
					keymap[key] = actExecute
					offset = len("execute")
				}
				if act[offset] == ':' {
					execmap[key] = act[offset+1:]
				} else {
					execmap[key] = act[offset+1 : len(act)-1]
				}
			} else {
				errorExit("unknown action: " + act)
			}
		}
	}
	return keymap, execmap, toggleSort
}

func isExecuteAction(str string) bool {
	if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") {
		return false
	}
	b := str[len("execute")]
	if strings.HasPrefix(str, "execute-multi") {
		if len(str) < len("execute-multi()") {
			return false
		}
		b = str[len("execute-multi")]
	}
	e := str[len(str)-1]
	if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
		b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
		return true
	}
	return false
}

func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
	keys := parseKeyChords(str, "key name required")
	if len(keys) != 1 {
		errorExit("multiple keys specified")
	}
	keymap[firstKey(keys)] = actToggleSort
	return keymap
}

func strLines(str string) []string {
	return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
}

func parseMargin(margin string) [4]string {
	margins := strings.Split(margin, ",")
	checked := func(str string) string {
		if strings.HasSuffix(str, "%") {
			val := atof(str[:len(str)-1])
			if val < 0 {
				errorExit("margin must be non-negative")
			}
			if val > 100 {
				errorExit("margin too large")
			}
		} else {
			val := atoi(str)
			if val < 0 {
				errorExit("margin must be non-negative")
			}
		}
		return str
	}
	switch len(margins) {
	case 1:
		m := checked(margins[0])
		return [4]string{m, m, m, m}
	case 2:
		tb := checked(margins[0])
		rl := checked(margins[1])
		return [4]string{tb, rl, tb, rl}
	case 3:
		t := checked(margins[0])
		rl := checked(margins[1])
		b := checked(margins[2])
		return [4]string{t, rl, b, rl}
	case 4:
		return [4]string{
			checked(margins[0]), checked(margins[1]),
			checked(margins[2]), checked(margins[3])}
	default:
		errorExit("invalid margin: " + margin)
	}
	return defaultMargin()
}

func parseOptions(opts *Options, allArgs []string) {
	keymap := make(map[int]actionType)
	var historyMax int
	if opts.History == nil {
		historyMax = defaultHistoryMax
	} else {
		historyMax = opts.History.maxSize
	}
	setHistory := func(path string) {
		h, e := NewHistory(path, historyMax)
		if e != nil {
			errorExit(e.Error())
		}
		opts.History = h
	}
	setHistoryMax := func(max int) {
		historyMax = max
		if historyMax < 1 {
			errorExit("history max must be a positive integer")
		}
		if opts.History != nil {
			opts.History.maxSize = historyMax
		}
	}
	for i := 0; i < len(allArgs); i++ {
		arg := allArgs[i]
		switch arg {
		case "-h", "--help":
			help(exitOk)
		case "-x", "--extended":
			opts.Extended = true
		case "-e", "--exact":
			opts.Fuzzy = false
		case "--extended-exact":
			// Note that we now don't have --no-extended-exact
			opts.Fuzzy = false
			opts.Extended = true
		case "+x", "--no-extended":
			opts.Extended = false
		case "+e", "--no-exact":
			opts.Fuzzy = true
		case "-q", "--query":
			opts.Query = nextString(allArgs, &i, "query string required")
		case "-f", "--filter":
			filter := nextString(allArgs, &i, "query string required")
			opts.Filter = &filter
		case "--expect":
			opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
		case "--tiebreak":
			opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
		case "--bind":
			keymap, opts.Execmap, opts.ToggleSort =
				parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
		case "--color":
			spec := optionalNextString(allArgs, &i)
			if len(spec) == 0 {
				opts.Theme = defaultTheme()
			} else {
				opts.Theme = parseTheme(opts.Theme, spec)
			}
		case "--toggle-sort":
			keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required"))
			opts.ToggleSort = true
		case "-d", "--delimiter":
			opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
		case "-n", "--nth":
			opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required"))
		case "--with-nth":
			opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
		case "-s", "--sort":
			opts.Sort = optionalNumeric(allArgs, &i)
		case "+s", "--no-sort":
			opts.Sort = 0
		case "--tac":
			opts.Tac = true
		case "--no-tac":
			opts.Tac = false
		case "-i":
			opts.Case = CaseIgnore
		case "+i":
			opts.Case = CaseRespect
		case "-m", "--multi":
			opts.Multi = true
		case "+m", "--no-multi":
			opts.Multi = false
		case "--ansi":
			opts.Ansi = true
		case "--no-ansi":
			opts.Ansi = false
		case "--no-mouse":
			opts.Mouse = false
		case "+c", "--no-color":
			opts.Theme = nil
		case "+2", "--no-256":
			opts.Theme = curses.Default16
		case "--black":
			opts.Black = true
		case "--no-black":
			opts.Black = false
		case "--reverse":
			opts.Reverse = true
		case "--no-reverse":
			opts.Reverse = false
		case "--cycle":
			opts.Cycle = true
		case "--no-cycle":
			opts.Cycle = false
		case "--hscroll":
			opts.Hscroll = true
		case "--no-hscroll":
			opts.Hscroll = false
		case "--inline-info":
			opts.InlineInfo = true
		case "--no-inline-info":
			opts.InlineInfo = false
		case "-1", "--select-1":
			opts.Select1 = true
		case "+1", "--no-select-1":
			opts.Select1 = false
		case "-0", "--exit-0":
			opts.Exit0 = true
		case "+0", "--no-exit-0":
			opts.Exit0 = false
		case "--read0":
			opts.ReadZero = true
		case "--no-read0":
			opts.ReadZero = false
		case "--print-query":
			opts.PrintQuery = true
		case "--no-print-query":
			opts.PrintQuery = false
		case "--prompt":
			opts.Prompt = nextString(allArgs, &i, "prompt string required")
		case "--sync":
			opts.Sync = true
		case "--no-sync":
			opts.Sync = false
		case "--async":
			opts.Sync = false
		case "--no-history":
			opts.History = nil
		case "--history":
			setHistory(nextString(allArgs, &i, "history file path required"))
		case "--history-size":
			setHistoryMax(nextInt(allArgs, &i, "history max size required"))
		case "--no-header":
			opts.Header = []string{}
		case "--no-header-lines":
			opts.HeaderLines = 0
		case "--header":
			opts.Header = strLines(nextString(allArgs, &i, "header string required"))
		case "--header-lines":
			opts.HeaderLines = atoi(
				nextString(allArgs, &i, "number of header lines required"))
		case "--no-margin":
			opts.Margin = defaultMargin()
		case "--margin":
			opts.Margin = parseMargin(
				nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
		case "--tabstop":
			opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
		case "--version":
			opts.Version = true
		default:
			if match, value := optString(arg, "-q", "--query="); match {
				opts.Query = value
			} else if match, value := optString(arg, "-f", "--filter="); match {
				opts.Filter = &value
			} else if match, value := optString(arg, "-d", "--delimiter="); match {
				opts.Delimiter = delimiterRegexp(value)
			} else if match, value := optString(arg, "--prompt="); match {
				opts.Prompt = value
			} else if match, value := optString(arg, "-n", "--nth="); match {
				opts.Nth = splitNth(value)
			} else if match, value := optString(arg, "--with-nth="); match {
				opts.WithNth = splitNth(value)
			} else if match, _ := optString(arg, "-s", "--sort="); match {
				opts.Sort = 1 // Don't care
			} else if match, value := optString(arg, "--toggle-sort="); match {
				keymap = checkToggleSort(keymap, value)
				opts.ToggleSort = true
			} else if match, value := optString(arg, "--expect="); match {
				opts.Expect = parseKeyChords(value, "key names required")
			} else if match, value := optString(arg, "--tiebreak="); match {
				opts.Tiebreak = parseTiebreak(value)
			} else if match, value := optString(arg, "--color="); match {
				opts.Theme = parseTheme(opts.Theme, value)
			} else if match, value := optString(arg, "--bind="); match {
				keymap, opts.Execmap, opts.ToggleSort =
					parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
			} else if match, value := optString(arg, "--history="); match {
				setHistory(value)
			} else if match, value := optString(arg, "--history-size="); match {
				setHistoryMax(atoi(value))
			} else if match, value := optString(arg, "--header="); match {
				opts.Header = strLines(value)
			} else if match, value := optString(arg, "--header-lines="); match {
				opts.HeaderLines = atoi(value)
			} else if match, value := optString(arg, "--margin="); match {
				opts.Margin = parseMargin(value)
			} else if match, value := optString(arg, "--tabstop="); match {
				opts.Tabstop = atoi(value)
			} else {
				errorExit("unknown option: " + arg)
			}
		}
	}

	if opts.HeaderLines < 0 {
		errorExit("header lines must be a non-negative integer")
	}

	if opts.Tabstop < 1 {
		errorExit("tab stop must be a positive integer")
	}

	// Change default actions for CTRL-N / CTRL-P when --history is used
	if opts.History != nil {
		if _, prs := keymap[curses.CtrlP]; !prs {
			keymap[curses.CtrlP] = actPreviousHistory
		}
		if _, prs := keymap[curses.CtrlN]; !prs {
			keymap[curses.CtrlN] = actNextHistory
		}
	}

	// Override default key bindings
	for key, act := range keymap {
		opts.Keymap[key] = act
	}

	// If we're not using extended search mode, --nth option becomes irrelevant
	// if it contains the whole range
	if !opts.Extended || len(opts.Nth) == 1 {
		for _, r := range opts.Nth {
			if r.begin == rangeEllipsis && r.end == rangeEllipsis {
				opts.Nth = make([]Range, 0)
				return
			}
		}
	}
}

// ParseOptions parses command-line options
func ParseOptions() *Options {
	opts := defaultOptions()

	// Options from Env var
	words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
	parseOptions(opts, words)

	// Options from command-line arguments
	parseOptions(opts, os.Args[1:])
	return opts
}
