package main

import (
	"bufio"
	"bytes"
	"log"
	"os"

	"editor/utils"
	"fmt"
	"golang.org/x/crypto/ssh/terminal"
	"regexp"
	"strings"
)

// Editor is the initializing class
type Editor struct {
}

// Buffer is the temporary view presented to the user
type Buffer struct {
	Lines []string
}

// Cursor is the object containing cursor point position
type Cursor struct {
	Row int
	Col int
}

func clamp(max, cur int) int {
	if cur <= max {
		return cur
	}
	return max
}

var filePath string
var editor Editor
var buffer Buffer
var cursor Cursor
var multiplier int

func main() {
	if len(os.Args) == 1 {
		log.Println("a file is required")
		return
	}
	filePath = os.Args[1]
	editor.initialize()
	editor.run()
}

func (e *Editor) initialize() {
	// Load a file
	file, err := os.Open(filePath)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// RAW terminal
	oldState, err := terminal.MakeRaw(0)
	if err != nil {
		log.Fatal(err)
	}
	defer terminal.Restore(0, oldState)

	var lines []string
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		log.Fatal(err)
	}

	multiplier = 1
	buffer = buffer.new(lines)
	cursor = cursor.new()
}

func (e *Editor) run() {
	for {
		e.render()
		e.handleInput()
	}
}

func (e *Editor) render() {
	clearScreen()
	buffer.render()

	moveCursor(cursor.Row, cursor.Col) // restore cursor
}

func (e *Editor) handleInput() {
	c := utils.Getch()
	// log.Printf("%#v\t%s\n", c, string(c))
	switch {
	case bytes.Equal(c, []byte{0x3}), bytes.Equal(c, []byte{0x11}), bytes.Equal(c, []byte{0x18}): // C-c, C-q, C-x
		// quit
		clearScreen()
		moveCursor(1, 1)
		os.Exit(0)
		return
	case bytes.Equal(c, []byte{0x13}): // C-s
		// save
		buffer.save()
	case bytes.Equal(c, []byte{0x5}): // C-e
		// end of line
		cursorEOL()
	case bytes.Equal(c, []byte{0x1}): // C-a
		// beginning of line
		cursorBOL()
	case bytes.Equal(c, []byte{0x15}): // C-u
		if multiplier == 1 {
			multiplier = 4
		} else {
			multiplier = multiplier + multiplier
		}
	case bytes.Equal(c, []byte{0x1b, 0x5b, 0x41}), bytes.Equal(c, []byte{0x10}): // UP, C-p
		cursorUp(multiplier)
		multiplier = 1
	case bytes.Equal(c, []byte{0x1b, 0x5b, 0x42}), bytes.Equal(c, []byte{0xe}): // DOWN, C-n
		cursorDown(multiplier)
		multiplier = 1
	case bytes.Equal(c, []byte{0x1b, 0x5b, 0x43}), bytes.Equal(c, []byte{0x6}): // RIGHT, C-f
		cursorForward(multiplier)
		multiplier = 1
	case bytes.Equal(c, []byte{0x1b, 0x5b, 0x44}), bytes.Equal(c, []byte{0x2}): // LEFT, C-b
		cursorBackward(multiplier)
		multiplier = 1
	case bytes.Equal(c, []byte{0x1b, 0x62}): // M-b
		cursorBackwardWord()
		multiplier = 1
	case bytes.Equal(c, []byte{0x1b, 0x66}): // M-f
		cursorForwardWord()
		multiplier = 1
	case bytes.Equal(c, []byte{0x7f}): // backspace
		buffer.deleteChar()
		multiplier = 1
	case bytes.Equal(c, []byte{0xb}): // C-k
		buffer.deleteForward()
		multiplier = 1
	default:
		buffer.insertChar(string(c))
		multiplier = 1
	}
}

func (b *Buffer) new(lines []string) Buffer {
	b.Lines = lines

	return *b
}

func (b *Buffer) fetch(line int) string {
	// fix the index 1 issue
	if line > len(b.Lines) {
		return ""
	}
	return b.Lines[line-1]
}

func (b *Buffer) render() {
	moveCursor(1, 1) // reset cursor for printing buffer
	for _, str := range buffer.Lines {
		// fmt.Printf("%d| %s\r\n", num, str) // with line nums
		fmt.Printf("%s\r\n", str)
	}
}

func (b *Buffer) save() {
	// save file
	file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	out := strings.Join(b.Lines, "\n")
	_, err = file.WriteString(out)
	if err != nil {
		log.Fatal(err)
	}
}

func (b *Buffer) insertChar(inp string) {
	if len(b.Lines) < cursor.Row {
		b.Lines = append(b.Lines, inp)
	} else {
		b.Lines[cursor.Row-1] = strings.Join(
			[]string{
				b.Lines[cursor.Row-1][:cursor.Col-1],
				inp,
				b.Lines[cursor.Row-1][cursor.Col-1:],
			}, "")
	}
	cursor.Col++
	moveCursor(cursor.Row, cursor.Col)
}

func (b *Buffer) deleteChar() {
	if cursor.Col == 1 {
		return
	}
	b.Lines[cursor.Row-1] = strings.Join(
		[]string{
			b.Lines[cursor.Row-1][:cursor.Col-2],
			b.Lines[cursor.Row-1][cursor.Col-1:],
		}, "")
	cursor.Col--
	moveCursor(cursor.Row, cursor.Col)
}

func (b *Buffer) deleteForward() {
	b.Lines[cursor.Row-1] = b.Lines[cursor.Row-1][:cursor.Col-1]
}

func (c *Cursor) new() Cursor {
	return Cursor{
		1,
		1,
	}
}

func (c *Cursor) clampCol() {
	line := buffer.fetch(c.Row)
	if c.Col > len(line) {
		c.Col = len(line) + 1
	} else if c.Col < 1 {
		c.Col = 1
	}
}

func (c *Cursor) clampRow() {
	rows := len(buffer.Lines)
	if c.Row > rows+1 {
		c.Row = rows + 1
	} else if c.Row < 1 {
		c.Row = 1
	}
}

//
//
//

func clearScreen() {
	fmt.Print("[2J")
}
func moveCursor(row, col int) {
	fmt.Printf("[%d;%dH", row, col)
}

func cursorEOL() {
	line := buffer.fetch(cursor.Row)
	cursor.Col = len(line) + 1
	moveCursor(cursor.Row, cursor.Col)
}

func cursorBOL() {
	cursor.Col = 1
	moveCursor(cursor.Row, cursor.Col)
}

func cursorUp(multiplier int) {
	for k := 0; k < multiplier; k++ {
		cursor.Row = cursor.Row - 1
	}
	cursor.clampRow()
	cursor.clampCol()
	moveCursor(cursor.Row, cursor.Col)
}

func cursorDown(multiplier int) {
	for k := 0; k < multiplier; k++ {
		cursor.Row = cursor.Row + 1
	}
	cursor.clampRow()
	cursor.clampCol()
	moveCursor(cursor.Row, cursor.Col)
}

func cursorForward(multiplier int) {
	for k := 0; k < multiplier; k++ {
		cursor.Col = cursor.Col + 1
	}
	cursor.clampCol()
	cursor.clampRow()
	moveCursor(cursor.Row, cursor.Col)
}

func cursorForwardWord() {
	// split line into array
	line := buffer.fetch(cursor.Row)
	line = line[cursor.Col-1:]

	content := []byte(line)
	pattern := regexp.MustCompile(`\W\w`)
	loc := pattern.FindIndex(content)
	if loc != nil {
		cursor.Col = cursor.Col + loc[0] + 1

		cursor.clampCol()
		cursor.clampRow()
		moveCursor(cursor.Row, cursor.Col)
	}
}

func cursorBackward(multiplier int) {
	for k := 0; k < multiplier; k++ {
		cursor.Col = cursor.Col - 1
	}
	cursor.clampCol()
	cursor.clampRow()
	moveCursor(cursor.Row, cursor.Col)
}

func cursorBackwardWord() {
	line := buffer.fetch(cursor.Row)
	line = strings.TrimRight(line[:cursor.Col-1], " ")

	content := []byte(line)
	pattern := regexp.MustCompile(`\w+.?$`)
	loc := pattern.FindIndex(content)
	if loc != nil {
		cursor.Col = loc[0] + 1

		cursor.clampCol()
		cursor.clampRow()
		moveCursor(cursor.Row, cursor.Col)
	}
}
