package readline

import (
	"fmt"
	"strconv"
)

// InputMode - The shell input mode
type InputMode int

const (
	// Vim - Vim editing mode
	Vim = iota
	// Emacs - Emacs (classic) editing mode
	Emacs
)

type ViMode int

const (
	VimInsert ViMode = iota
	VimReplaceOnce
	VimReplaceMany
	VimDelete
	VimKeys
)

var (
	VimInsertStr      = "[I]"
	VimReplaceOnceStr = "[V]"
	VimReplaceManyStr = "[R]"
	VimDeleteStr      = "[D]"
	VimKeysStr        = "[N]"
)

type ViAction int
const (
	VimActionYank = iota
	VimActionPaste
)

var (
	// registerFreeKeys - Some Vim keys don't act on/ aren't affected by registers,
	// and using these keys will automatically cancel any active register.
	// NOTE: Don't forget to update if you add Vim bindings !!
	registerFreeKeys = []rune{'a', 'A', 'h', 'i', 'I', 'j', 'k', 'l', 'r', 'R', 'u', 'v', '$', '%', '[', ']'}

	// validRegisterKeys - All valid register IDs (keys) for read/write Vim registers
	validRegisterKeys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/-\""
)

// vi - Apply a key to a Vi action. Note that as in the rest of the code, all cursor movements
// have been moved away, and only the rl.pos is adjusted: when echoing the input line, the shell
// will compute the new cursor pos accordingly.
func (rl *Instance) vi(r rune) {
	activeRegister := string(rl.registers.currentRegister)

	// Check if we are in register mode. If yes, and for some characters,
	// we select the register and exit this func immediately.
	if rl.registers.registerSelectWait {
		for _, char := range validRegisterKeys {
			if r == char {
				rl.registers.setActiveRegister(r)
				return
			}
		}
	}

	// If we are on register mode and one is already selected,
	// check if the key stroke to be evaluated is acting on it
	// or not: if not, we cancel the active register now.
	if rl.registers.onRegister {
		for _, char := range registerFreeKeys {
			if char == r {
				rl.registers.resetRegister()
			}
		}
	}

	// Then evaluate the key.
	switch r {
	case 'a':
		if len(rl.line) > 0 {
			rl.pos++
		}
		rl.modeViMode = VimInsert
		rl.viIteration = ""
		rl.viUndoSkipAppend = true

	case 'A':
		if len(rl.line) > 0 {
			rl.pos = len(rl.line)
		}
		rl.modeViMode = VimInsert
		rl.viIteration = ""
		rl.viUndoSkipAppend = true

	case 'b':
		if rl.viIsYanking {
			vii := rl.getViIterations()
			rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpB, vii)
			rl.viIsYanking = false
			return
		}
		rl.viUndoSkipAppend = true
		vii := rl.getViIterations()
		for i := 1; i <= vii; i++ {
			rl.moveCursorByAdjust(rl.viJumpB(tokeniseLine))
		}

	case 'B':
		if rl.viIsYanking {
			vii := rl.getViIterations()
			rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpB, vii)
			rl.viIsYanking = false
			return
		}
		rl.viUndoSkipAppend = true
		vii := rl.getViIterations()
		for i := 1; i <= vii; i++ {
			rl.moveCursorByAdjust(rl.viJumpB(tokeniseSplitSpaces))
		}

	case 'd':
		rl.modeViMode = VimDelete
		rl.viUndoSkipAppend = true

	case 'D':
		rl.saveBufToRegister(rl.line[rl.pos-1:])
		rl.line = rl.line[:rl.pos]
		// Only go back if there is an input
		if len(rl.line) > 0 {
			rl.pos--
		}
		rl.resetHelpers()
		rl.updateHelpers()
		rl.viIteration = ""

	case 'e':
		if rl.viIsYanking {
			vii := rl.getViIterations()
			rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpE, vii)
			rl.viIsYanking = false
			return
		}

		rl.viUndoSkipAppend = true
		vii := rl.getViIterations()
		for i := 1; i <= vii; i++ {
			rl.moveCursorByAdjust(rl.viJumpE(tokeniseLine))
		}

	case 'E':
		if rl.viIsYanking {
			vii := rl.getViIterations()
			rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpE, vii)
			rl.viIsYanking = false
			return
		}

		rl.viUndoSkipAppend = true
		vii := rl.getViIterations()
		for i := 1; i <= vii; i++ {
			rl.moveCursorByAdjust(rl.viJumpE(tokeniseSplitSpaces))
		}

	case 'h':
		if rl.pos > 0 {
			rl.pos--
		}
		rl.viUndoSkipAppend = true

	case 'i':
		rl.modeViMode = VimInsert
		rl.viIteration = ""
		rl.viUndoSkipAppend = true
		rl.registers.resetRegister()

	case 'I':
		rl.modeViMode = VimInsert
		rl.viIteration = ""
		rl.viUndoSkipAppend = true
		rl.pos = 0

	case 'j':
		// Set the main history as the one we navigate, by default
		rl.mainHist = true
		rl.walkHistory(-1)
	case 'k':
		// Set the main history as the one we navigate, by default
		rl.mainHist = true
		rl.walkHistory(1)

	case 'l':
		if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
			(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
			rl.pos++
		}
		rl.viUndoSkipAppend = true

	case 'p':
		// paste after the cursor position
		rl.viUndoSkipAppend = true
		rl.pos++

		buffer := rl.pasteFromRegister()
		vii := rl.getViIterations()
		rl.ViActionCallback(VimActionPaste, []string{activeRegister, string(buffer)})
		for i := 1; i <= vii; i++ {
			rl.insert(buffer)
		}
		rl.pos--

	case 'P':
		// paste before
		rl.viUndoSkipAppend = true
		buffer := rl.pasteFromRegister()
		vii := rl.getViIterations()
		rl.ViActionCallback(VimActionPaste, []string{activeRegister, string(buffer)})
		for i := 1; i <= vii; i++ {
			rl.insert(buffer)
		}

	case 'r':
		rl.modeViMode = VimReplaceOnce
		rl.viIteration = ""
		rl.viUndoSkipAppend = true

	case 'R':
		rl.modeViMode = VimReplaceMany
		rl.viIteration = ""
		rl.viUndoSkipAppend = true

	case 'u':
		rl.undoLast()
		rl.viUndoSkipAppend = true

	case 'v':
		rl.clearHelpers()
		var multiline []rune
		if rl.GetMultiLine == nil {
			multiline = rl.line
		} else {
			multiline = rl.GetMultiLine(rl.line)
		}

		// Keep the previous cursor position
		//prev := rl.pos

		new, err := rl.StartEditorWithBuffer(multiline, "")
		if err != nil || len(new) == 0 || string(new) == string(multiline) {
			fmt.Println(err)
			rl.viUndoSkipAppend = true
			return
		}

		// Clean the shell and put the new buffer, with adjusted pos if needed.
		rl.clearLine()
		rl.line = new
		rl.pos = len(rl.line)
		/*if prev > len(rl.line) {
		} else {
			rl.pos = prev
		}*/

	case 'w':
		// If we were not yanking
		rl.viUndoSkipAppend = true
		// If the input line is empty, we don't do anything
		if rl.pos == 0 && len(rl.line) == 0 {
			return
		}

		// If we were yanking, we forge the new yank buffer
		// and return without moving the cursor.
		if rl.viIsYanking {
			vii := rl.getViIterations()
			rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpW, vii)
			rl.viIsYanking = false
			return
		}

		// Else get iterations and move
		vii := rl.getViIterations()
		for i := 1; i <= vii; i++ {
			rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine))
		}

	case 'W':
		// If the input line is empty, we don't do anything
		if rl.pos == 0 && len(rl.line) == 0 {
			return
		}
		rl.viUndoSkipAppend = true

		if rl.viIsYanking {
			vii := rl.getViIterations()
			rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpW, vii)
			rl.viIsYanking = false
			return
		}
		vii := rl.getViIterations()
		for i := 1; i <= vii; i++ {
			rl.moveCursorByAdjust(rl.viJumpW(tokeniseSplitSpaces))
		}

	case 'x':
		vii := rl.getViIterations()

		// We might be on an active register, but not yanking...
		rl.saveToRegister(vii)

		// Delete the chars in the line anyway
		for i := 1; i <= vii; i++ {
			rl.deleteX()
		}
		if rl.pos == len(rl.line) && len(rl.line) > 0 {
			rl.pos--
		}

	case 'y':
		if rl.viIsYanking {
			rl.ViActionCallback(VimActionYank, []string{activeRegister, string(rl.line)})
			rl.saveBufToRegister(rl.line)
			rl.viIsYanking = false
		}
		rl.viIsYanking = true
		rl.viUndoSkipAppend = true

	case 'Y':
		rl.ViActionCallback(VimActionYank, []string{activeRegister, string(rl.line)})
		rl.saveBufToRegister(rl.line)
		rl.viUndoSkipAppend = true

	case '[':
		if rl.viIsYanking {
			rl.saveToRegister(rl.viJumpPreviousBrace())
			rl.viIsYanking = false
			return
		}
		rl.viUndoSkipAppend = true
		rl.moveCursorByAdjust(rl.viJumpPreviousBrace())

	case ']':
		if rl.viIsYanking {
			rl.saveToRegister(rl.viJumpNextBrace())
			rl.viIsYanking = false
			return
		}
		rl.viUndoSkipAppend = true
		rl.moveCursorByAdjust(rl.viJumpNextBrace())

	case '$':
		if rl.viIsYanking {
			rl.saveBufToRegister(rl.line[rl.pos:])
			rl.viIsYanking = false
			return
		}
		rl.pos = len(rl.line)
		rl.viUndoSkipAppend = true

	case '%':
		if rl.viIsYanking {
			rl.saveToRegister(rl.viJumpBracket())
			rl.viIsYanking = false
			return
		}
		rl.viUndoSkipAppend = true
		rl.moveCursorByAdjust(rl.viJumpBracket())

	case '"':
		// We might be on a register already, so reset it,
		// and then wait again for a new register ID.
		if rl.registers.onRegister {
			rl.registers.resetRegister()
		}
		rl.registers.registerSelectWait = true

	default:
		if r <= '9' && '0' <= r {
			rl.viIteration += string(r)
		}
		rl.viUndoSkipAppend = true
	}
}

func (rl *Instance) getViIterations() int {
	i, _ := strconv.Atoi(rl.viIteration)
	if i < 1 {
		i = 1
	}
	rl.viIteration = ""
	return i
}

func (rl *Instance) refreshVimStatus() {
	rl.ViModeCallback(rl.modeViMode)
	rl.computePrompt()
	rl.updateHelpers()
}

// viInfoMessage - lmorg's way of showing Vim status is to overwrite the info.
// Currently not used, as there is a possibility to show the current Vim mode in the prompt.
func (rl *Instance) viInfoMessage() {
	switch rl.modeViMode {
	case VimKeys:
		rl.infoText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
	case VimInsert:
		rl.infoText = []rune("-- INSERT --")
	case VimReplaceOnce:
		rl.infoText = []rune("-- REPLACE CHARACTER --")
	case VimReplaceMany:
		rl.infoText = []rune("-- REPLACE --")
	case VimDelete:
		rl.infoText = []rune("-- DELETE --")
	default:
		rl.getInfoText()
	}

	rl.clearHelpers()
	rl.renderHelpers()
}

func (rl *Instance) viJumpB(tokeniser tokeniser) (adjust int) {
	split, index, pos := tokeniser(rl.line, rl.pos)
	switch {
	case len(split) == 0:
		return
	case index == 0 && pos == 0:
		return
	case pos == 0:
		adjust = len(split[index-1])
	default:
		adjust = pos
	}
	return adjust * -1
}

func (rl *Instance) viJumpE(tokeniser tokeniser) (adjust int) {
	split, index, pos := tokeniser(rl.line, rl.pos)
	if len(split) == 0 {
		return
	}

	word := rTrimWhiteSpace(split[index])

	switch {
	case len(split) == 0:
		return
	case index == len(split)-1 && pos >= len(word)-1:
		return
	case pos >= len(word)-1:
		word = rTrimWhiteSpace(split[index+1])
		adjust = len(split[index]) - pos
		adjust += len(word) - 1
	default:
		adjust = len(word) - pos - 1
	}
	return
}

func (rl *Instance) viJumpW(tokeniser tokeniser) (adjust int) {
	split, index, pos := tokeniser(rl.line, rl.pos)
	switch {
	case len(split) == 0:
		return
	case index+1 == len(split):
		adjust = len(rl.line) - rl.pos
	default:
		adjust = len(split[index]) - pos
	}
	return
}

func (rl *Instance) viJumpPreviousBrace() (adjust int) {
	if rl.pos == 0 {
		return 0
	}

	for i := rl.pos - 1; i != 0; i-- {
		if rl.line[i] == '{' {
			return i - rl.pos
		}
	}

	return 0
}

func (rl *Instance) viJumpNextBrace() (adjust int) {
	if rl.pos >= len(rl.line)-1 {
		return 0
	}

	for i := rl.pos + 1; i < len(rl.line); i++ {
		if rl.line[i] == '{' {
			return i - rl.pos
		}
	}

	return 0
}

func (rl *Instance) viJumpBracket() (adjust int) {
	split, index, pos := tokeniseBrackets(rl.line, rl.pos)
	switch {
	case len(split) == 0:
		return
	case pos == 0:
		adjust = len(split[index])
	default:
		adjust = pos * -1
	}
	return
}