package config

import (
	"strings"

	"code.rocketnine.space/tslocum/cview"
	"github.com/gdamore/tcell/v2"
	"github.com/spf13/viper"
)

// NOTE: CmdLink[1-90], CmdTab[1-90], CmdCommand[1-90], and
// CmdCommandTarget[1-90] need to be in-order and consecutive. This
// property is used to simplify key handling in display/display.go
// and display/tab.go
type Command int

const (
	CmdInvalid        Command = 0
	CmdLink1                  = 1
	CmdLink2                  = 2
	CmdLink3                  = 3
	CmdLink4                  = 4
	CmdLink5                  = 5
	CmdLink6                  = 6
	CmdLink7                  = 7
	CmdLink8                  = 8
	CmdLink9                  = 9
	CmdLink0                  = 10
	CmdTab1                   = 11
	CmdTab2                   = 12
	CmdTab3                   = 13
	CmdTab4                   = 14
	CmdTab5                   = 15
	CmdTab6                   = 16
	CmdTab7                   = 17
	CmdTab8                   = 18
	CmdTab9                   = 19
	CmdTab0                   = 20
	CmdCommand1               = 21
	CmdCommand2               = 22
	CmdCommand3               = 23
	CmdCommand4               = 24
	CmdCommand5               = 25
	CmdCommand6               = 26
	CmdCommand7               = 27
	CmdCommand8               = 28
	CmdCommand9               = 29
	CmdCommand0               = 30
	CmdCommandTarget1         = 31
	CmdCommandTarget2         = 32
	CmdCommandTarget3         = 33
	CmdCommandTarget4         = 34
	CmdCommandTarget5         = 35
	CmdCommandTarget6         = 36
	CmdCommandTarget7         = 37
	CmdCommandTarget8         = 38
	CmdCommandTarget9         = 39
	CmdCommandTarget0         = 40
	CmdBottom                 = iota
	CmdEdit
	CmdHome
	CmdBookmarks
	CmdAddBookmark
	CmdSave
	CmdReload
	CmdBack
	CmdForward
	CmdMoveUp
	CmdMoveDown
	CmdMoveLeft
	CmdMoveRight
	CmdPgup
	CmdPgdn
	CmdNewTab
	CmdCloseTab
	CmdNextTab
	CmdPrevTab
	CmdQuit
	CmdHelp
	CmdSub
	CmdAddSub
	CmdCopyPageURL
	CmdCopyTargetURL
	CmdBeginning
	CmdEnd
	CmdURLHandlerOpen // See #143
	CmdSearch
	CmdNextMatch
	CmdPrevMatch
)

type keyBinding struct {
	key tcell.Key
	mod tcell.ModMask
	r   rune
}

// Map of active keybindings to commands.
var bindings map[keyBinding]Command

// inversion of tcell.KeyNames, used to simplify config parsing.
// used by parseBinding() below.
var tcellKeys map[string]tcell.Key

// helper function that takes a single keyBinding object and returns
// a string in the format used by the configuration file.  Support
// function for GetKeyBinding(), used to make the help panel helpful.
func keyBindingToString(kb keyBinding) (string, bool) {
	var prefix string

	if kb.mod&tcell.ModAlt == tcell.ModAlt {
		prefix = "Alt-"
	}

	if kb.key == tcell.KeyRune {
		if kb.r == ' ' {
			return prefix + "Space", true
		}
		return prefix + string(kb.r), true
	}
	s, ok := tcell.KeyNames[kb.key]
	if ok {
		return prefix + s, true
	}
	return "", false
}

// Get all keybindings for a Command as a string.
// Used by the help panel so bindable keys display with their
// bound values rather than hardcoded defaults.
func GetKeyBinding(cmd Command) string {
	var s string
	for kb, c := range bindings {
		if c == cmd {
			t, ok := keyBindingToString(kb)
			if ok {
				s += t + ", "
			}
		}
	}

	if len(s) > 0 {
		return s[:len(s)-2]
	}
	return s
}

// Parse a single keybinding string and add it to the binding map
func parseBinding(cmd Command, binding string) {
	var k tcell.Key
	var m tcell.ModMask
	var r rune

	if strings.HasPrefix(binding, "Alt-") {
		m = tcell.ModAlt
		binding = binding[4:]
	}

	if strings.HasPrefix(binding, "Shift-") {
		m += tcell.ModShift
		binding = binding[6:]
	}

	if len([]rune(binding)) == 1 {
		k = tcell.KeyRune
		r = []rune(binding)[0]
	} else if len(binding) == 0 {
		return
	} else if binding == "Space" {
		k = tcell.KeyRune
		r = ' '
	} else {
		var ok bool
		k, ok = tcellKeys[binding]
		if !ok { // Bad keybinding!  Quietly ignore...
			return
		}
		if strings.HasPrefix(binding, "Ctrl") {
			m += tcell.ModCtrl
		}
	}

	bindings[keyBinding{k, m, r}] = cmd
}

// Generate the bindings map from the TOML configuration file.
// Called by config.Init()
func KeyInit() {
	configBindings := map[Command]string{
		CmdLink1:          "keybindings.bind_link1",
		CmdLink2:          "keybindings.bind_link2",
		CmdLink3:          "keybindings.bind_link3",
		CmdLink4:          "keybindings.bind_link4",
		CmdLink5:          "keybindings.bind_link5",
		CmdLink6:          "keybindings.bind_link6",
		CmdLink7:          "keybindings.bind_link7",
		CmdLink8:          "keybindings.bind_link8",
		CmdLink9:          "keybindings.bind_link9",
		CmdLink0:          "keybindings.bind_link0",
		CmdBottom:         "keybindings.bind_bottom",
		CmdEdit:           "keybindings.bind_edit",
		CmdHome:           "keybindings.bind_home",
		CmdBookmarks:      "keybindings.bind_bookmarks",
		CmdAddBookmark:    "keybindings.bind_add_bookmark",
		CmdSave:           "keybindings.bind_save",
		CmdReload:         "keybindings.bind_reload",
		CmdBack:           "keybindings.bind_back",
		CmdForward:        "keybindings.bind_forward",
		CmdMoveUp:         "keybindings.bind_moveup",
		CmdMoveDown:       "keybindings.bind_movedown",
		CmdMoveLeft:       "keybindings.bind_moveleft",
		CmdMoveRight:      "keybindings.bind_moveright",
		CmdPgup:           "keybindings.bind_pgup",
		CmdPgdn:           "keybindings.bind_pgdn",
		CmdNewTab:         "keybindings.bind_new_tab",
		CmdCloseTab:       "keybindings.bind_close_tab",
		CmdNextTab:        "keybindings.bind_next_tab",
		CmdPrevTab:        "keybindings.bind_prev_tab",
		CmdQuit:           "keybindings.bind_quit",
		CmdHelp:           "keybindings.bind_help",
		CmdSub:            "keybindings.bind_sub",
		CmdAddSub:         "keybindings.bind_add_sub",
		CmdCopyPageURL:    "keybindings.bind_copy_page_url",
		CmdCopyTargetURL:  "keybindings.bind_copy_target_url",
		CmdBeginning:      "keybindings.bind_beginning",
		CmdEnd:            "keybindings.bind_end",
		CmdURLHandlerOpen: "keybindings.bind_url_handler_open",
		CmdCommand1:       "keybindings.bind_command1",
		CmdCommand2:       "keybindings.bind_command2",
		CmdCommand3:       "keybindings.bind_command3",
		CmdCommand4:       "keybindings.bind_command4",
		CmdCommand5:       "keybindings.bind_command5",
		CmdCommand6:       "keybindings.bind_command6",
		CmdCommand7:       "keybindings.bind_command7",
		CmdCommand8:       "keybindings.bind_command8",
		CmdCommand9:       "keybindings.bind_command9",
		CmdCommand0:       "keybindings.bind_command0",
		CmdCommandTarget1: "keybindings.bind_command_target1",
		CmdCommandTarget2: "keybindings.bind_command_target2",
		CmdCommandTarget3: "keybindings.bind_command_target3",
		CmdCommandTarget4: "keybindings.bind_command_target4",
		CmdCommandTarget5: "keybindings.bind_command_target5",
		CmdCommandTarget6: "keybindings.bind_command_target6",
		CmdCommandTarget7: "keybindings.bind_command_target7",
		CmdCommandTarget8: "keybindings.bind_command_target8",
		CmdCommandTarget9: "keybindings.bind_command_target9",
		CmdCommandTarget0: "keybindings.bind_command_target0",
		CmdSearch:         "keybindings.bind_search",
		CmdNextMatch:      "keybindings.bind_next_match",
		CmdPrevMatch:      "keybindings.bind_prev_match",
	}
	// This is split off to allow shift_numbers to override bind_tab[1-90]
	// (This is needed for older configs so that the default bind_tab values
	// aren't used)
	configTabNBindings := map[Command]string{
		CmdTab1: "keybindings.bind_tab1",
		CmdTab2: "keybindings.bind_tab2",
		CmdTab3: "keybindings.bind_tab3",
		CmdTab4: "keybindings.bind_tab4",
		CmdTab5: "keybindings.bind_tab5",
		CmdTab6: "keybindings.bind_tab6",
		CmdTab7: "keybindings.bind_tab7",
		CmdTab8: "keybindings.bind_tab8",
		CmdTab9: "keybindings.bind_tab9",
		CmdTab0: "keybindings.bind_tab0",
	}
	tcellKeys = make(map[string]tcell.Key)
	bindings = make(map[keyBinding]Command)

	for k, kname := range tcell.KeyNames {
		tcellKeys[kname] = k
	}

	// Set cview navigation keys to use user-set ones
	cview.Keys.MoveUp2 = viper.GetStringSlice(configBindings[CmdMoveUp])
	cview.Keys.MoveDown2 = viper.GetStringSlice(configBindings[CmdMoveDown])
	cview.Keys.MoveLeft2 = viper.GetStringSlice(configBindings[CmdMoveLeft])
	cview.Keys.MoveRight2 = viper.GetStringSlice(configBindings[CmdMoveRight])
	cview.Keys.MoveFirst = viper.GetStringSlice(configBindings[CmdBeginning])
	cview.Keys.MoveFirst2 = nil
	cview.Keys.MoveLast = viper.GetStringSlice(configBindings[CmdEnd])
	cview.Keys.MoveLast2 = nil

	for c, allb := range configBindings {
		for _, b := range viper.GetStringSlice(allb) {
			parseBinding(c, b)
		}
	}

	// Backwards compatibility with the old shift_numbers config line.
	shiftNumbers := []rune(viper.GetString("keybindings.shift_numbers"))
	if len(shiftNumbers) > 0 && len(shiftNumbers) <= 10 {
		for i, r := range shiftNumbers {
			bindings[keyBinding{tcell.KeyRune, 0, r}] = CmdTab1 + Command(i)
		}
	} else {
		for c, allb := range configTabNBindings {
			for _, b := range viper.GetStringSlice(allb) {
				parseBinding(c, b)
			}
		}
	}
}

// Used by the display package to turn a tcell.EventKey into a Command
func TranslateKeyEvent(e *tcell.EventKey) Command {
	var ok bool
	var cmd Command
	k := e.Key()
	if k == tcell.KeyRune {
		cmd, ok = bindings[keyBinding{k, e.Modifiers(), e.Rune()}]
	} else { // Sometimes tcell sets e.Rune() on non-KeyRune events.
		cmd, ok = bindings[keyBinding{k, e.Modifiers(), 0}]
	}
	if ok {
		return cmd
	}
	return CmdInvalid
}
