package syntax

import (
	"fmt"
	"unicode"
	"unicode/utf8"
)

const eoi = rune(0)

type Lexer struct {
	lexer  *lexerInner
	peeked *Token
}

func lex(input string) *Lexer {
	return &Lexer{lexer: &lexerInner{input: input}}
}

func (p *Lexer) Peek() Token {
	if p.peeked != nil {
		return *p.peeked
	}

	peeked := p.lexer.next()
	p.peeked = &peeked
	return peeked
}

func (p *Lexer) Next() (peeked Token) {
	if p.peeked != nil {
		peeked, p.peeked = *p.peeked, nil
		return peeked
	}

	return p.lexer.next()
}

type lexerState byte

const (
	lexerStateDefault lexerState = iota
	lexerStateElement
)

type lexerInner struct {
	input    string
	start    int // Start of the current token
	position int // Current position in the input
	width    int // Width of the last rune
	// State machine
	state lexerState
}

func (l *lexerInner) next() Token {
	l.start = l.position

	if l.state == lexerStateElement {
		return l.nextElement()
	}

	switch nextRune := l.nextRune(); {
	case nextRune == eoi:
		return Token{tokenKind_EOI, l.start - 1, l.position, ""}

	case isSpace(nextRune):
		l.backupRune()
		return l.lexSpace()

	case isEscape(nextRune):
		return l.lexEscaped()

	case isReserved(nextRune):
		l.backupRune()
		return l.lexReserved()

	case isDigit(nextRune):
		l.backupRune()
		return l.lexNumber()

	case isTagStart(nextRune):
		l.backupRune()
		l.state = lexerStateElement
		return l.nextElement()

	default:
		l.backupRune()
		return l.lexText()
	}
}

func (l *lexerInner) nextElement() Token {
	nextRune := l.nextRune()
	switch {
	case nextRune == '<':
		l.backupRune()
		return l.lexElementStart()

	case nextRune == '>':
		l.backupRune()
		l.state = lexerStateDefault
		return l.lexElementEnd()

	case isSpace(nextRune):
		l.backupRune()
		return l.lexSpace()

	case isElementRune(nextRune):
		l.backupRune()
		return l.lexElementName()
	}

	// NOTE: Return an error if this point is reached.
	l.state = lexerStateDefault
	return Token{tokenKind_Error, l.start - 1, l.position, fmt.Sprintf("unexpected character while parsing element: %s", string(nextRune))}
}

func (l *lexerInner) nextRune() (nextRune rune) {
	if l.position >= len(l.input) {
		l.width = 0
		return eoi
	}

	nextRune, l.width = utf8.DecodeRuneInString(l.input[l.position:])
	l.position += l.width

	return nextRune
}

func (l *lexerInner) backupRune() {
	l.position -= l.width
}

func (l *lexerInner) lexSpace() Token {
	_ = l.nextRune()

	for {
		nextRune := l.nextRune()

		if nextRune == eoi {
			l.backupRune()
			break
		}

		if !isSpace(nextRune) {
			l.backupRune()
			break
		}
	}

	return Token{tokenKind_WS, l.start, l.position, ""}
}

func (l *lexerInner) lexEscaped() Token {
	skip := l.position
	nextRune := l.nextRune()

	if nextRune != '{' && nextRune != '}' && nextRune != '#' && nextRune != '\'' && nextRune != '<' {
		l.backupRune()
		return Token{tokenKind_Text, l.start, l.position, ""}
		//return Token{tokenKind_Error, l.start - 1, l.position, fmt.Sprintf("unexpected escape sequence: '%s", string(nextRune))}
	}

	l.start = skip
	return Token{tokenKind_Text, l.start, l.position, ""}
}

func (l *lexerInner) lexReserved() Token {
	nextRune := l.nextRune()
	var kind tokenKind

	switch nextRune {
	case '{':
		kind = tokenKind_OpenBrace
	case '}':
		kind = tokenKind_CloseBrace
	case '#':
		kind = tokenKind_Hash
	case ':':
		kind = tokenKind_Colon
	case ',':
		kind = tokenKind_Comma
	case '=':
		kind = tokenKind_Equal
	default:
		kind = tokenKind_Error
	}

	return Token{kind, l.start, l.position, ""}
}

func (l *lexerInner) lexNumber() Token {
	_ = l.nextRune()

	for {
		nextRune := l.nextRune()

		if nextRune == eoi {
			l.backupRune()
			break
		}

		if !isDigit(nextRune) {
			l.backupRune()
			break
		}
	}

	return Token{tokenKind_Number, l.start, l.position, ""}
}

func (l *lexerInner) lexElementStart() Token {
	_ = l.nextRune()
	tokenKind := tokenKind_ElementOpenStart

	if nextRune := l.nextRune(); nextRune == '/' {
		tokenKind = tokenKind_ElementCloseStart
	} else {
		l.backupRune()
	}

	return Token{tokenKind, l.start, l.position, ""}
}

func (l *lexerInner) lexElementEnd() Token {
	_ = l.nextRune()
	return Token{tokenKind_ElementEnd, l.start, l.position, ""}
}

func (l *lexerInner) lexElementName() Token {
	for {
		nextRune := l.nextRune()

		if nextRune == eoi {
			return Token{tokenKind_Error, l.start, l.position, "unexpected end of input reached"}
		}

		if nextRune == '>' {
			l.backupRune()
			break
		}

		if isSpace(nextRune) {
			l.backupRune()
			break
		}

		if !isElementRune(nextRune) {
			return Token{tokenKind_Error, l.start, l.position, fmt.Sprintf("unexpected character in element name: %s", string(nextRune))}
		}
	}

	return Token{tokenKind_ElementName, l.start, l.position, ""}
}

func (l *lexerInner) lexText() Token {
	_ = l.nextRune()

	for {
		nextRune := l.nextRune()

		if nextRune == eoi {
			l.backupRune()
			break
		}

		if !isText(nextRune) {
			l.backupRune()
			break
		}
	}

	kind, ok := known_keywords[l.input[l.start:l.position]]
	if !ok {
		kind = tokenKind_Text
	}

	return Token{kind, l.start, l.position, ""}
}

func isSpace(r rune) bool {
	return unicode.IsSpace(r)
}

func isDigit(r rune) bool {
	return r >= '0' && r <= '9'
}

func isReserved(r rune) bool {
	return r == '{' || r == '}' || r == '#' || r == ',' || r == '\'' || r == '=' || r == ':'
}

func isTagStart(r rune) bool {
	return r == '<'
}

func isElementRune(r rune) bool {
	return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == ':'
}

func isText(r rune) bool {
	return !isSpace(r) && !isDigit(r) && !isReserved(r) && !isTagStart(r)
}

func isEscape(r rune) bool {
	return r == '\''
}
