package main

import (
	"fmt"
	"strconv"
	"time"

	"github.com/gdamore/tcell"
)

type typer struct {
	Scr      tcell.Screen
	OnStart  func()
	SkipWord bool

	currentWordStyle    tcell.Style
	nextWordStyle       tcell.Style
	incorrectSpaceStyle tcell.Style
	incorrectStyle      tcell.Style
	correctStyle        tcell.Style
	backgroundStyle     tcell.Style
}

func NewTyper(scr tcell.Screen, fgcol, bgcol, hicol, hicol2, hicol3, errcol tcell.Color) *typer {
	def := tcell.StyleDefault.
		Foreground(fgcol).
		Background(bgcol)

	return &typer{
		Scr:      scr,
		SkipWord: true,

		backgroundStyle:     def,
		correctStyle:        def.Foreground(hicol),
		currentWordStyle:    def.Foreground(hicol2),
		nextWordStyle:       def.Foreground(hicol3),
		incorrectStyle:      def.Foreground(errcol),
		incorrectSpaceStyle: def.Background(errcol),
	}
}

func (t *typer) highlight(text []cell, idx int, currentWordStyle, nextWordStyle tcell.Style) {
	for ; idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n'; idx++ {
		text[idx].style = currentWordStyle
	}

	for ; idx < len(text) && (text[idx].c == ' ' || text[idx].c == '\n'); idx++ {
	}

	for ; idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n'; idx++ {
		text[idx].style = nextWordStyle
	}
}

func (t *typer) Start(text []string, timeout time.Duration) (nerrs, ncorrect int, duration time.Duration, exitKey tcell.Key) {
	timeLeft := timeout

	for i, p := range text {
		startImmediately := true
		var d time.Duration
		var e, c int

		if i == 0 {
			startImmediately = false
		}

		e, c, exitKey, d = t.start(p, timeLeft, startImmediately)

		nerrs += e
		ncorrect += c
		duration += d

		if timeout != -1 {
			timeLeft -= d
			if timeLeft <= 0 {
				return
			}
		}

		if exitKey != 0 {
			return
		}
	}

	return
}

func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool) (nerrs int, ncorrect int, exitKey tcell.Key, duration time.Duration) {

	var startTime time.Time
	text := stringToCells(s)

	nc, nr := calcStringDimensions(s)
	sw, sh := scr.Size()
	x := (sw - nc) / 2
	y := (sh - nr) / 2

	for i, _ := range text {
		text[i].style = t.backgroundStyle
	}

	fmt.Printf("\033[5 q")

	//Assumes original cursor shape was a block (the one true cursor shape), there doesn't appear to be a
	//good way to save/restore the shape if the user has changed it from the otcs.
	defer fmt.Printf("\033[2 q")

	t.Scr.SetStyle(t.backgroundStyle)
	idx := 0

	redraw := func() {
		if timeLimit != -1 && !startTime.IsZero() {
			remaining := timeLimit - time.Now().Sub(startTime)
			drawString(t.Scr, x+nc/2, y+nr+1, strconv.Itoa(int(remaining/1E9)), -1, t.backgroundStyle)
		}

		//Potentially inefficient, but seems to be good enough
		drawCells(t.Scr, x, y, text, idx)

		t.Scr.Show()
	}

	calcStats := func() {
		nerrs = 0
		ncorrect = 0

		for _, c := range text {
			if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle {
				nerrs++
			} else if c.style == t.correctStyle {
				ncorrect++
			}
		}

		exitKey = 0
		duration = time.Now().Sub(startTime)
	}

	deleteWord := func() {
		t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle)

		if idx == 0 {
			return
		}

		idx--

		for idx > 0 && (text[idx].c == ' ' || text[idx].c == '\n') {
			text[idx].style = t.backgroundStyle
			idx--
		}

		for idx > 0 && text[idx].c != ' ' && text[idx].c != '\n' {
			text[idx].style = t.backgroundStyle
			idx--
		}

		if text[idx].c == ' ' || text[idx].c == '\n' {
			idx++
		}

		t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
	}

	tickerCloser := make(chan bool)

	//Inject nil events into the main event loop at regular invervals to force an update
	ticker := func() {
		for {
			select {
			case <-tickerCloser:
				return
			default:
			}

			time.Sleep(time.Duration(1E8))
			t.Scr.PostEventWait(nil)
		}
	}

	go ticker()
	defer close(tickerCloser)

	if startImmediately {
		startTime = time.Now()
	}

	t.Scr.Clear()
	for {
		t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
		redraw()

		ev := t.Scr.PollEvent()

		switch ev := ev.(type) {
		case *tcell.EventResize:
			t.Scr.Sync()
			t.Scr.Clear()

			nc, nr = calcStringDimensions(s)
			sw, sh = scr.Size()
			x = (sw - nc) / 2
			y = (sh - nr) / 2
		case *tcell.EventKey:
			if startTime.IsZero() {
				startTime = time.Now()
			}

			switch key := ev.Key(); key {
			case tcell.KeyEscape,
				tcell.KeyCtrlC:

				nerrs = -1
				ncorrect = -1
				exitKey = key

				return
			case tcell.KeyCtrlL:
				t.Scr.Sync()
			case tcell.KeyCtrlH:
				deleteWord()
			case tcell.KeyBackspace2:
				if ev.Modifiers() == tcell.ModAlt {
					deleteWord()
				} else {
					t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle)

					if idx == 0 {
						break
					}

					idx--

					for idx > 0 && text[idx].c == '\n' {
						idx--
					}

					text[idx].style = t.backgroundStyle

					t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
				}
			case tcell.KeyRune:
				if idx < len(text) {
					switch {
					case ev.Rune() == text[idx].c:
						text[idx].style = t.correctStyle
						idx++
					case ev.Rune() == ' ' && t.SkipWord:
						if idx > 0 && text[idx-1].c == ' ' && text[idx].c != ' ' { //Do nothing on word boundaries.
							break
						}

						for idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n' {
							text[idx].style = t.incorrectStyle
							idx++
						}

						if idx < len(text) {
							text[idx].style = t.incorrectSpaceStyle
							idx++
						}
					default:
						if text[idx].c == ' ' {
							text[idx].style = t.incorrectSpaceStyle
						} else {
							text[idx].style = t.incorrectStyle
						}
						idx++
					}

					for idx < len(text) && text[idx].c == '\n' {
						idx++
					}

					t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
				}

				if idx == len(text) {
					calcStats()
					return
				}
			}
		default: //tick
			if timeLimit != -1 && !startTime.IsZero() && timeLimit <= time.Now().Sub(startTime) {
				calcStats()
				return
			}

			redraw()
		}
	}
}