package lexer

import (
	"html"
	"strings"
)

var goKeywords = map[string]bool{
	"break": true, "case": true, "chan": true, "const": true, "continue": true,
	"default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
	"func": true, "go": true, "goto": true, "if": true, "import": true,
	"interface": true, "map": true, "package": true, "range": true, "return": true,
	"select": true, "struct": true, "switch": true, "type": true, "var": true,
	// Not keywords to the Go spec — predeclared identifiers — but every editor colours
	// them, and a reader looking for `nil` is looking for the same kind of thing.
	"nil": true, "true": true, "false": true, "iota": true,
	"string": true, "int": true, "int64": true, "float64": true, "bool": true,
	"byte": true, "rune": true, "any": true, "error": true,
}

// HighlightGo turns Go source into HTML with the tokens wrapped in coloured spans.
func HighlightGo(src string) string {
	var b strings.Builder
	b.Grow(len(src) * 2)

	i := 0
	for i < len(src) {
		c := src[i]

		switch {
		// Line comment — including the //gowasm: directives, which are the most important
		// line in several of the snippets this was written for.
		case c == '/' && i+1 < len(src) && src[i+1] == '/':
			end := strings.IndexByte(src[i:], '\n')
			if end < 0 {
				end = len(src)
			} else {
				end += i
			}
			span(&b, CommentClass, src[i:end])
			i = end

		// Block comment.
		case c == '/' && i+1 < len(src) && src[i+1] == '*':
			end := strings.Index(src[i+2:], "*/")
			if end < 0 {
				end = len(src)
			} else {
				end = i + 2 + end + 2
			}
			span(&b, CommentClass, src[i:end])
			i = end

		// Interpreted string. Ends at the closing quote or the line's end — an unterminated
		// string is a typo in a snippet, not a reason to paint the rest of the file green.
		case c == '"':
			i = quoted(&b, src, i, '"', true)

		// Raw string: no escapes, and it may span lines.
		case c == '`':
			i = quoted(&b, src, i, '`', false)

		// Rune literal.
		case c == '\'':
			i = quoted(&b, src, i, '\'', true)

		case isDigit(c):
			j := i
			for j < len(src) && (isDigit(src[j]) || isHexish(src[j])) {
				j++
			}
			span(&b, NumberClass, src[i:j])
			i = j

		case isIdentStart(c):
			j := i
			for j < len(src) && isIdentPart(src[j]) {
				j++
			}
			word := src[i:j]
			switch {
			case goKeywords[word]:
				span(&b, KeywordClass, word)
			case callAhead(src, j):
				// Colouring the callee is what makes the shape of a snippet readable at a
				// glance: you see what it DOES before you read what it says.
				span(&b, FuncClass, word)
			default:
				b.WriteString(html.EscapeString(word))
			}
			i = j

		default:
			b.WriteString(html.EscapeString(string(c)))
			i++
		}
	}
	return b.String()
}
