// lexer.v — tokenizer for the VuurRaaf source language (.vr).
module compiler

pub fn tokenize(src string) ![]Tok {
	// a script may start with a shebang line (#!...) so it can be executed
	// directly (e.g. `#!/usr/bin/env vr`). Drop it, but keep the trailing
	// newline so error line numbers stay aligned with the file on disk.
	mut s := src
	if s.starts_with('#!') {
		nl := s.index('\n') or { s.len }
		s = s[nl..]
	}
	mut l := Lexer{ src: s, line: 1 }
	mut toks := []Tok{}
	for {
		t := l.next()!
		toks << t
		if t.kind == .eof {
			break
		}
	}
	return toks
}

struct Lexer {
mut:
	src        string
	pos        int
	line       int
	line_start int // byte offset where the current line begins
}

fn (mut l Lexer) peek() u8 {
	if l.pos >= l.src.len {
		return 0
	}
	return l.src[l.pos]
}

fn (mut l Lexer) peek2() u8 {
	if l.pos + 1 >= l.src.len {
		return 0
	}
	return l.src[l.pos + 1]
}

fn (mut l Lexer) advance() u8 {
	c := l.src[l.pos]
	l.pos++
	if c == `\n` {
		l.line++
		l.line_start = l.pos
	}
	return c
}

fn (mut l Lexer) next() !Tok {
	// skip whitespace and // comments
	for l.pos < l.src.len {
		c := l.peek()
		if c == ` ` || c == `\t` || c == `\r` || c == `\n` {
			l.advance()
			continue
		}
		if c == `/` && l.peek2() == `/` {
			for l.pos < l.src.len && l.peek() != `\n` {
				l.advance()
			}
			continue
		}
		break
	}
	line := l.line
	col := l.pos - l.line_start + 1 // 1-based column
	if l.pos >= l.src.len {
		return Tok{ kind: .eof, lit: '', line: line, col: col }
	}
	c := l.peek()
	match c {
		`(` {
			l.advance()
			return Tok{ kind: .lparen, lit: '(', line: line, col: col }
		}
		`)` {
			l.advance()
			return Tok{ kind: .rparen, lit: ')', line: line, col: col }
		}
		`{` {
			l.advance()
			return Tok{ kind: .lbrace, lit: '{', line: line, col: col }
		}
		`}` {
			l.advance()
			return Tok{ kind: .rbrace, lit: '}', line: line, col: col }
		}
		`[` {
			l.advance()
			return Tok{ kind: .lbracket, lit: '[', line: line, col: col }
		}
		`]` {
			l.advance()
			return Tok{ kind: .rbracket, lit: ']', line: line, col: col }
		}
		`.` {
			l.advance()
			if l.peek() == `.` {
				l.advance()
				if l.peek() == `.` {
					l.advance()
					return Tok{ kind: .dotdotdot, lit: '...', line: line, col: col }
				}
				return Tok{ kind: .dotdot, lit: '..', line: line, col: col }
			}
			return Tok{ kind: .dot, lit: '.', line: line, col: col }
		}
		`,` {
			l.advance()
			return Tok{ kind: .comma, lit: ',', line: line, col: col }
		}
		`:` {
			l.advance()
			return Tok{ kind: .colon, lit: ':', line: line, col: col }
		}
		`+` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .plus_eq, lit: '+=', line: line, col: col }
			}
			return Tok{ kind: .plus, lit: '+', line: line, col: col }
		}
		`-` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .minus_eq, lit: '-=', line: line, col: col }
			}
			return Tok{ kind: .minus, lit: '-', line: line, col: col }
		}
		`*` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .star_eq, lit: '*=', line: line, col: col }
			}
			return Tok{ kind: .star, lit: '*', line: line, col: col }
		}
		`/` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .slash_eq, lit: '/=', line: line, col: col }
			}
			return Tok{ kind: .slash, lit: '/', line: line, col: col }
		}
		`%` {
			l.advance()
			return Tok{ kind: .percent, lit: '%', line: line, col: col }
		}
		`&` {
			l.advance()
			return Tok{ kind: .amp, lit: '&', line: line, col: col }
		}
		`|` {
			l.advance()
			return Tok{ kind: .pipe, lit: '|', line: line, col: col }
		}
		`^` {
			l.advance()
			return Tok{ kind: .caret, lit: '^', line: line, col: col }
		}
		`~` {
			l.advance()
			return Tok{ kind: .tilde, lit: '~', line: line, col: col }
		}
		`=` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .eq_eq, lit: '==', line: line, col: col }
			}
			return Tok{ kind: .assign, lit: '=', line: line, col: col }
		}
		`!` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .not_eq, lit: '!=', line: line, col: col }
			}
			return error('unexpected character "!" at line ${line}, col ${col} (did you mean "not"?)')
		}
		`<` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .le, lit: '<=', line: line, col: col }
			}
			if l.peek() == `<` {
				l.advance()
				return Tok{ kind: .lt_lt, lit: '<<', line: line, col: col }
			}
			return Tok{ kind: .lt, lit: '<', line: line, col: col }
		}
		`>` {
			l.advance()
			if l.peek() == `=` {
				l.advance()
				return Tok{ kind: .ge, lit: '>=', line: line, col: col }
			}
			if l.peek() == `>` {
				l.advance()
				return Tok{ kind: .gt_gt, lit: '>>', line: line, col: col }
			}
			return Tok{ kind: .gt, lit: '>', line: line, col: col }
		}
		`\"` {
			return l.lex_string(line, col)!
		}
		`0`...`9` {
			return l.lex_number(line, col)
		}
		else {
			// r"..." raw strings (no escape processing) — handy for regex
			// patterns like r"\d+" that would otherwise need double escaping
			if c == `r` && l.peek2() == `\"` {
				return l.lex_raw_string(line, col)!
			}
			if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` {
				return l.lex_ident(line, col)
			}
			return error('unexpected character "${c.ascii_str()}" at line ${line}, col ${col}')
		}
	}
}

fn (mut l Lexer) lex_number(line int, col int) Tok {
	start := l.pos
	for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` {
		l.advance()
	}
	mut is_float := false
	// fractional part: `.` followed by a digit (so `1..3` and `1...3` stay ints)
	if l.peek() == `.` && l.pos + 1 < l.src.len && l.peek2() >= `0` && l.peek2() <= `9` {
		is_float = true
		l.advance() // consume `.`
		for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` {
			l.advance()
		}
	}
	// exponent part: e / E followed by optional sign and digits
	if l.peek() == `e` || l.peek() == `E` {
		save := l.pos
		l.advance()
		if l.peek() == `+` || l.peek() == `-` {
			l.advance()
		}
		if l.peek() >= `0` && l.peek() <= `9` {
			is_float = true
			for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` {
				l.advance()
			}
		} else {
			l.pos = save // not an exponent after all
		}
	}
	lit := l.src[start..l.pos]
	if is_float {
		return Tok{ kind: .float_lit, lit: lit, line: line, col: col }
	}
	return Tok{ kind: .int_lit, lit: lit, line: line, col: col }
}

fn (mut l Lexer) lex_ident(line int, col int) Tok {
	start := l.pos
	for l.pos < l.src.len {
		c := l.peek()
		if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || (c >= `0` && c <= `9`) || c == `_` {
			l.advance()
		} else {
			break
		}
	}
	lit := l.src[start..l.pos]
	kind := match lit {
		'fn' { TokKind.kw_fn }
		'struct' { TokKind.kw_struct }
		'let' { TokKind.kw_let }
		'if' { TokKind.kw_if }
		'else' { TokKind.kw_else }
		'while' { TokKind.kw_while }
		'for' { TokKind.kw_for }
		'in' { TokKind.kw_in }
		'match' { TokKind.kw_match }
		'break' { TokKind.kw_break }
		'continue' { TokKind.kw_continue }
		'return' { TokKind.kw_return }
		'true' { TokKind.kw_true }
		'false' { TokKind.kw_false }
		'none' { TokKind.kw_none }
		'and' { TokKind.kw_and }
		'or' { TokKind.kw_or }
		'not' { TokKind.kw_not }
		'print' { TokKind.kw_print }
		'println' { TokKind.kw_println }
		'assert' { TokKind.kw_assert }
		'import' { TokKind.kw_import }
		'enum' { TokKind.kw_enum }
		'const' { TokKind.kw_const }
		'interface' { TokKind.kw_interface }
		'try' { TokKind.kw_try }
		'catch' { TokKind.kw_catch }
		'throw' { TokKind.kw_throw }
		else { TokKind.ident }
	}
	return Tok{ kind: kind, lit: lit, line: line, col: col }
}

// lex_raw_string reads an r"..." string verbatim: backslashes, quotes and
// ${...} sequences are all kept literally, so regex patterns pass through
// untouched. The token is a plain str_lit whose content is the raw text.
fn (mut l Lexer) lex_raw_string(line int, col int) !Tok {
	l.advance() // 'r'
	l.advance() // opening quote
	start := l.pos
	for l.pos < l.src.len {
		if l.advance() == `\"` {
			return Tok{ kind: .str_lit, lit: l.src[start..l.pos - 1], line: line, col: col }
		}
	}
	return error('unterminated raw string at line ${line}, col ${col}')
}

fn (mut l Lexer) lex_string(line int, col int) !Tok {
	l.advance() // opening quote
	mut s := ''
	mut has_interp := false
	for l.pos < l.src.len {
		c := l.advance()
		if c == `\"` {
			if has_interp {
				return Tok{ kind: .str_interp, lit: s, line: line, col: col }
			}
			return Tok{ kind: .str_lit, lit: s, line: line, col: col }
		}
		if c == `\\` {
			if l.pos >= l.src.len {
				break
			}
			e := l.advance()
			match e {
				`n` {
					s += '\n'
				}
				`t` {
					s += '\t'
				}
				`\"` {
					s += '"'
				}
				`\\` {
					s += '\\'
				}
				else {
					return error('invalid escape \\${e.ascii_str()} at line ${line}, col ${col}')
				}
			}
			continue
		}
		if c == `$` && l.peek() == `{` {
			has_interp = true
			s += '\${'
			l.advance() // skip the '{'
			continue
		}
		s += c.ascii_str()
	}
	return error('unterminated string at line ${line}, col ${col}')
}
