// Copyright (c) 2019 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module compiler

import (
	os
	// strings
)

const (
	single_quote = `\'`
	double_quote = `"`
	error_context_before = 2 // how many lines of source context to print before the pointer line
	error_context_after = 2 // ^^^ same, but after
)

struct Scanner {
mut:
	file_path                string
	text                     string
	pos                      int
	line_nr                  int
	last_nl_pos              int // for calculating column
	inside_string            bool
	inter_start              bool // for hacky string interpolation TODO simplify
	inter_end                bool
	debug                    bool
	line_comment             string
	started                  bool
	// vfmt fields TODO move to a separate struct
	// fmt_out        strings.Builder
	fmt_lines                []string
	// fmt_line   string
	fmt_indent               int
	fmt_line_empty           bool
	// fmt_needs_nl bool
	prev_tok                 TokenKind
	fn_name                  string // needed for @FN
	print_line_on_error      bool
	print_colored_error      bool
	print_rel_paths_on_error bool
	quote                    byte // which quote is used to denote current string: ' or "
	line_ends                []int // the positions of source lines ends   (i.e. \n signs)
	nlines                   int // total number of lines in the source file that were scanned
	is_vh                    bool // Keep newlines
	is_fmt                   bool // Used only for skipping ${} in strings, since we need literal
	// string values when generating formatted code.
}
// new scanner from file.
fn new_scanner_file(file_path string) &Scanner {
	if !os.exists(file_path) {
		verror("$file_path doesn't exist")
	}
	mut raw_text := os.read_file(file_path)or{
		verror('scanner: failed to open $file_path')
		return 0
	}
	// BOM check
	if raw_text.len >= 3 {
		c_text := raw_text.str
		if c_text[0] == 0xEF && c_text[1] == 0xBB && c_text[2] == 0xBF {
			// skip three BOM bytes
			offset_from_begin := 3
			raw_text = tos(c_text[offset_from_begin], vstrlen(c_text) - offset_from_begin)
		}
	}
	mut s := new_scanner(raw_text)
	s.init_fmt()
	s.file_path = file_path
	return s
}

// new scanner from string.
fn new_scanner(text string) &Scanner {
	return &Scanner{
		text: text
		// fmt_out: strings.new_builder(1000)

		print_line_on_error: true
		print_colored_error: true
		print_rel_paths_on_error: true
	}
}

// TODO remove once multiple return values are implemented
struct ScanRes {
	tok TokenKind
	lit string
}

fn scan_res(tok TokenKind,lit string) ScanRes {
	return ScanRes{
		tok,lit}
}

fn (s mut Scanner) ident_name() string {
	start := s.pos
	for {
		s.pos++
		if s.pos >= s.text.len {
			break
		}
		c := s.text[s.pos]
		if !is_name_char(c) && !c.is_digit() {
			break
		}
	}
	name := s.text[start..s.pos]
	s.pos--
	return name
}

fn (s mut Scanner) ident_hex_number() string {
	start_pos := s.pos
	s.pos += 2 // skip '0x'
	for {
		if s.pos >= s.text.len {
			break
		}
		c := s.text[s.pos]
		if !c.is_hex_digit() {
			break
		}
		s.pos++
	}
	number := s.text[start_pos..s.pos]
	s.pos--
	return number
}

fn (s mut Scanner) ident_oct_number() string {
	start_pos := s.pos
	for {
		if s.pos >= s.text.len {
			break
		}
		c := s.text[s.pos]
		if c.is_digit() {
			if !c.is_oct_digit() {
				s.error('malformed octal constant')
			}
		}
		else {
			break
		}
		s.pos++
	}
	number := s.text[start_pos..s.pos]
	s.pos--
	return number
}

fn (s mut Scanner) ident_dec_number() string {
	start_pos := s.pos
	// scan integer part
	for s.pos < s.text.len && s.text[s.pos].is_digit() {
		s.pos++
	}
	// e.g. 1..9
	// we just return '1' and don't scan '..9'
	if s.expect('..', s.pos) {
		number := s.text[start_pos..s.pos]
		s.pos--
		return number
	}
	// scan fractional part
	if s.pos < s.text.len && s.text[s.pos] == `.` {
		s.pos++
		for s.pos < s.text.len && s.text[s.pos].is_digit() {
			s.pos++
		}
		if !s.inside_string && s.pos < s.text.len && s.text[s.pos] == `f` {
			s.error('no `f` is needed for floats')
		}
	}
	// scan exponential part
	mut has_exponential_part := false
	if s.expect('e+', s.pos) || s.expect('e-', s.pos) {
		exp_start_pos := s.pos += 2
		for s.pos < s.text.len && s.text[s.pos].is_digit() {
			s.pos++
		}
		if exp_start_pos == s.pos {
			s.error('exponent has no digits')
		}
		has_exponential_part = true
	}
	// error check: 1.23.4, 123.e+3.4
	if s.pos < s.text.len && s.text[s.pos] == `.` {
		if has_exponential_part {
			s.error('exponential part should be integer')
		}
		else {
			s.error('too many decimal points in number')
		}
	}
	number := s.text[start_pos..s.pos]
	s.pos--
	return number
}

fn (s mut Scanner) ident_number() string {
	if s.expect('0x', s.pos) {
		return s.ident_hex_number()
	}
	if s.expect('0.', s.pos) || s.expect('0e', s.pos) {
		return s.ident_dec_number()
	}
	if s.text[s.pos] == `0` {
		return s.ident_oct_number()
	}
	return s.ident_dec_number()
}

fn (s mut Scanner) skip_whitespace() {
	// if s.is_vh { println('vh') return }
	for s.pos < s.text.len && s.text[s.pos].is_white() {
		if is_nl(s.text[s.pos]) && s.is_vh {
			return
		}
		// Count \r\n as one line
		if is_nl(s.text[s.pos]) && !s.expect('\r\n', s.pos - 1) {
			s.inc_line_number()
		}
		s.pos++
	}
}

fn (s mut Scanner) end_of_file() ScanRes {
	s.pos = s.text.len
	s.inc_line_number()
	return scan_res(.eof, '')
}

fn (s mut Scanner) scan() ScanRes {
	// if s.line_comment != '' {
	// s.fgenln('// LC "$s.line_comment"')
	// s.line_comment = ''
	// }
	if s.started {
		s.pos++
	}
	s.started = true
	if s.pos >= s.text.len {
		return s.end_of_file()
	}
	if !s.inside_string {
		s.skip_whitespace()
	}
	// End of $var, start next string
	if s.inter_end {
		if s.text[s.pos] == s.quote {
			s.inter_end = false
			return scan_res(.str, '')
		}
		s.inter_end = false
		return scan_res(.str, s.ident_string())
	}
	s.skip_whitespace()
	// end of file
	if s.pos >= s.text.len {
		return s.end_of_file()
	}
	// handle each char
	c := s.text[s.pos]
	mut nextc := `\0`
	if s.pos + 1 < s.text.len {
		nextc = s.text[s.pos + 1]
	}
	// name or keyword
	if is_name_char(c) {
		name := s.ident_name()
		// tmp hack to detect . in ${}
		// Check if not .eof to prevent panic
		next_char := if s.pos + 1 < s.text.len {s.text[s.pos + 1]}else {`\0`}
		if is_key(name) {
			return scan_res(key_to_token(name), '')
		}
		// 'asdf $b' => "b" is the last name in the string, dont start parsing string
		// at the next ', skip it
		if s.inside_string {
			if next_char == s.quote {
				s.inter_end = true
				s.inter_start = false
				s.inside_string = false
			}
		}
		// end of `$expr`
		// allow `'$a.b'` and `'$a.c()'`
		if s.inter_start && next_char != `.` && next_char != `(` {
			s.inter_end = true
			s.inter_start = false
		}
		if s.pos == 0 && next_char == ` ` {
			// If a single letter name at the start of the file, increment
			// Otherwise the scanner would be stuck at s.pos = 0
			s.pos++
		}
		return scan_res(.name, name)
	}
	// `123`, `.123`
	else if c.is_digit() || (c == `.` && nextc.is_digit()) {
		num := s.ident_number()
		return scan_res(.number, num)
	}
	// Handle `'$fn()'`
	if c == `)` && s.inter_start {
		s.inter_end = true
		s.inter_start = false
		next_char := if s.pos + 1 < s.text.len {s.text[s.pos + 1]}else {`\0`}
		if next_char == s.quote {
			s.inside_string = false
		}
		return scan_res(.rpar, '')
	}
	// all other tokens
	match c {
		`+` {
			if nextc == `+` {
				s.pos++
				return scan_res(.inc, '')
			}
			else if nextc == `=` {
				s.pos++
				return scan_res(.plus_assign, '')
			}
			return scan_res(.plus, '')
		}
		`-` {
			if nextc == `-` {
				s.pos++
				return scan_res(.dec, '')
			}
			else if nextc == `=` {
				s.pos++
				return scan_res(.minus_assign, '')
			}
			return scan_res(.minus, '')
		}
		`*` {
			if nextc == `=` {
				s.pos++
				return scan_res(.mult_assign, '')
			}
			return scan_res(.mul, '')
		}
		`^` {
			if nextc == `=` {
				s.pos++
				return scan_res(.xor_assign, '')
			}
			return scan_res(.xor, '')
		}
		`%` {
			if nextc == `=` {
				s.pos++
				return scan_res(.mod_assign, '')
			}
			return scan_res(.mod, '')
		}
		`?` {
			return scan_res(.question, '')
		}
		single_quote,double_quote {
			return scan_res(.str, s.ident_string())
		}
		`\`` {
			// ` // apostrophe balance comment. do not remove
			return scan_res(.chartoken, s.ident_char())
		}
		`(` {
			return scan_res(.lpar, '')
		}
		`)` {
			return scan_res(.rpar, '')
		}
		`[` {
			return scan_res(.lsbr, '')
		}
		`]` {
			return scan_res(.rsbr, '')
		}
		`{` {
			// Skip { in `${` in strings
			if s.inside_string {
				return s.scan()
			}
			return scan_res(.lcbr, '')
		}
		`$` {
			if s.inside_string {
				return scan_res(.str_dollar, '')
			}
			else {
				return scan_res(.dollar, '')
			}
		}
		`}` {
			// s = `hello $name !`
			// s = `hello ${name} !`
			if s.inside_string {
				s.pos++
				if s.text[s.pos] == s.quote {
					s.inside_string = false
					return scan_res(.str, '')
				}
				return scan_res(.str, s.ident_string())
			}
			else {
				return scan_res(.rcbr, '')
			}
		}
		`&` {
			if nextc == `=` {
				s.pos++
				return scan_res(.and_assign, '')
			}
			if nextc == `&` {
				s.pos++
				return scan_res(.and, '')
			}
			return scan_res(.amp, '')
		}
		`|` {
			if nextc == `|` {
				s.pos++
				return scan_res(.logical_or, '')
			}
			if nextc == `=` {
				s.pos++
				return scan_res(.or_assign, '')
			}
			return scan_res(.pipe, '')
		}
		`,` {
			return scan_res(.comma, '')
		}
		`@` {
			s.pos++
			name := s.ident_name()
			// @FN => will be substituted with the name of the current V function
			// @FILE => will be substituted with the path of the V source file
			// @LINE => will be substituted with the V line number where it appears (as a string).
			// @COLUMN => will be substituted with the column where it appears (as a string).
			// @VHASH  => will be substituted with the shortened commit hash of the V compiler (as a string).
			// This allows things like this:
			// println( 'file: ' + @FILE + ' | line: ' + @LINE + ' | fn: ' + @FN)
			// ... which is useful while debugging/tracing
			if name == 'FN' {
				return scan_res(.str, s.fn_name)
			}
			if name == 'FILE' {
				return scan_res(.str, cescaped_path(os.realpath(s.file_path)))
			}
			if name == 'LINE' {
				return scan_res(.str, (s.line_nr + 1).str())
			}
			if name == 'COLUMN' {
				return scan_res(.str, (s.current_column()).str())
			}
			if name == 'VHASH' {
				return scan_res(.str, vhash())
			}
			if !is_key(name) {
				s.error('@ must be used before keywords (e.g. `@type string`)')
			}
			return scan_res(.name, name)
		}
		/*
	case `\r`:
		if nextc == `\n` {
			s.pos++
			s.last_nl_pos = s.pos
			return scan_res(.nl, '')
		}
	 }
	case `\n`:
		s.last_nl_pos = s.pos
		return scan_res(.nl, '')
	 }
	*/

		`.` {
			if nextc == `.` {
				s.pos++
				if s.text[s.pos + 1] == `.` {
					s.pos++
					return scan_res(.ellipsis, '')
				}
				return scan_res(.dotdot, '')
			}
			return scan_res(.dot, '')
		}
		`#` {
			start := s.pos + 1
			s.ignore_line()
			if nextc == `!` {
				// treat shebang line (#!) as a comment
				s.line_comment = s.text[start + 1..s.pos].trim_space()
				// s.fgenln('// shebang line "$s.line_comment"')
				return s.scan()
			}
			hash := s.text[start..s.pos]
			return scan_res(.hash, hash.trim_space())
		}
		`>` {
			if nextc == `=` {
				s.pos++
				return scan_res(.ge, '')
			}
			else if nextc == `>` {
				if s.pos + 2 < s.text.len && s.text[s.pos + 2] == `=` {
					s.pos += 2
					return scan_res(.righ_shift_assign, '')
				}
				s.pos++
				return scan_res(.righ_shift, '')
			}
			else {
				return scan_res(.gt, '')
			}
		}
		0xE2 {
			// case `≠`:
			if nextc == 0x89 && s.text[s.pos + 2] == 0xA0 {
				s.pos += 2
				return scan_res(.ne, '')
			}
			// ⩽
			else if nextc == 0x89 && s.text[s.pos + 2] == 0xBD {
				s.pos += 2
				return scan_res(.le, '')
			}
			// ⩾
			else if nextc == 0xA9 && s.text[s.pos + 2] == 0xBE {
				s.pos += 2
				return scan_res(.ge, '')
			}
		}
		`<` {
			if nextc == `=` {
				s.pos++
				return scan_res(.le, '')
			}
			else if nextc == `<` {
				if s.pos + 2 < s.text.len && s.text[s.pos + 2] == `=` {
					s.pos += 2
					return scan_res(.left_shift_assign, '')
				}
				s.pos++
				return scan_res(.left_shift, '')
			}
			else {
				return scan_res(.lt, '')
			}
		}
		`=` {
			if nextc == `=` {
				s.pos++
				return scan_res(.eq, '')
			}
			else if nextc == `>` {
				s.pos++
				return scan_res(.arrow, '')
			}
			else {
				return scan_res(.assign, '')
			}
		}
		`:` {
			if nextc == `=` {
				s.pos++
				return scan_res(.decl_assign, '')
			}
			else {
				return scan_res(.colon, '')
			}
		}
		`;` {
			return scan_res(.semicolon, '')
		}
		`!` {
			if nextc == `=` {
				s.pos++
				return scan_res(.ne, '')
			}
			else {
				return scan_res(.not, '')
			}
		}
		`~` {
			return scan_res(.bit_not, '')
		}
		`/` {
			if nextc == `=` {
				s.pos++
				return scan_res(.div_assign, '')
			}
			if nextc == `/` {
				start := s.pos + 1
				s.ignore_line()
				s.line_comment = s.text[start + 1..s.pos]
				s.line_comment = s.line_comment.trim_space()
				if s.is_fmt {
					s.pos-- // fix line_nr, \n was read, and the comment is marked on the next line
					s.line_nr--
					return scan_res(.line_comment, s.line_comment)
				}
				// s.fgenln('// ${s.prev_tok.str()} "$s.line_comment"')
				// Skip the comment (return the next token)
				return s.scan()
			}
			// Multiline comments
			if nextc == `*` {
				start := s.pos
				mut nest_count := 1
				// Skip comment
				for nest_count > 0 {
					s.pos++
					if s.pos >= s.text.len {
						s.line_nr--
						s.error('comment not terminated')
					}
					if s.text[s.pos] == `\n` {
						s.inc_line_number()
						continue
					}
					if s.expect('/*', s.pos) {
						nest_count++
						continue
					}
					if s.expect('*/', s.pos) {
						nest_count--
					}
				}
				s.pos++
				end := s.pos + 1
				comment := s.text[start..end]
				if s.is_fmt {
					s.line_comment = comment
					return scan_res(.mline_comment, s.line_comment)
				}
				// Skip if not in fmt mode
				return s.scan()
			}
			return scan_res(.div, '')
		}
		else {
		}}
	$if windows {
		if c == `\0` {
			return s.end_of_file()
		}
	}
	s.error('invalid character `${c.str()}`')
	return s.end_of_file()
}

fn (s &Scanner) current_column() int {
	return s.pos - s.last_nl_pos
}

fn (s Scanner) count_symbol_before(p int,sym byte) int {
	mut count := 0
	for i := p; i >= 0; i-- {
		if s.text[i] != sym {
			break
		}
		count++
	}
	return count
}

fn (s mut Scanner) ident_string() string {
	q := s.text[s.pos]
	is_quote := q == single_quote || q == double_quote
	is_raw := is_quote && s.text[s.pos - 1] == `r`
	if is_quote && !s.inside_string {
		s.quote = q
	}
	// if s.file_path.contains('string_test') {
	// println('\nident_string() at char=${s.text[s.pos].str()}')
	// println('linenr=$s.line_nr quote=  $qquote ${qquote.str()}')
	// }
	mut start := s.pos
	s.inside_string = false
	slash := `\\`
	for {
		s.pos++
		if s.pos >= s.text.len {
			break
		}
		c := s.text[s.pos]
		prevc := s.text[s.pos - 1]
		// end of string
		if c == s.quote && (prevc != slash || (prevc == slash && s.text[s.pos - 2] == slash)) {
			// handle '123\\'  slash at the end
			break
		}
		if c == `\n` {
			s.inc_line_number()
		}
		// Don't allow \0
		if c == `0` && s.pos > 2 && s.text[s.pos - 1] == slash {
			if s.pos < s.text.len - 1 && s.text[s.pos + 1].is_digit() {
			}
			else {
				s.error('0 character in a string literal')
			}
		}
		// Don't allow \x00
		if c == `0` && s.pos > 5 && s.expect('\\x0', s.pos - 3) {
			s.error('0 character in a string literal')
		}
		// ${var} (ignore in vfmt mode)
		if c == `{` && prevc == `$` && !is_raw && !s.is_fmt && s.count_symbol_before(s.pos - 2, slash) % 2 == 0 {
			s.inside_string = true
			// so that s.pos points to $ at the next step
			s.pos -= 2
			break
		}
		// $var
		if (c.is_letter() || c == `_`) && prevc == `$` && !s.is_fmt && !is_raw && s.count_symbol_before(s.pos - 2, slash) % 2 == 0 {
			s.inside_string = true
			s.inter_start = true
			s.pos -= 2
			break
		}
	}
	mut lit := ''
	if s.text[start] == s.quote {
		start++
	}
	mut end := s.pos
	if s.inside_string {
		end++
	}
	if start > s.pos {
	}
	else {
		lit = s.text[start..end]
	}
	return lit
}

fn (s mut Scanner) ident_char() string {
	start := s.pos
	slash := `\\`
	mut len := 0
	for {
		s.pos++
		if s.pos >= s.text.len {
			break
		}
		if s.text[s.pos] != slash {
			len++
		}
		double_slash := s.expect('\\\\', s.pos - 2)
		if s.text[s.pos] == `\`` && (s.text[s.pos - 1] != slash || double_slash) {
			// ` // apostrophe balance comment. do not remove
			if double_slash {
				len++
			}
			break
		}
	}
	len--
	c := s.text[start + 1..s.pos]
	if len != 1 {
		u := c.ustring()
		if u.len != 1 {
			s.error('invalid character literal (more than one character)\n' + 'use quotes for strings, backticks for characters')
		}
	}
	if c == '\\`' {
		return '`'
	}
	// Escapes a `'` character
	return if c == "\'" {'\\' + c}else {c}
}

fn (s &Scanner) expect(want string,start_pos int) bool {
	end_pos := start_pos + want.len
	if start_pos < 0 || start_pos >= s.text.len {
		return false
	}
	if end_pos < 0 || end_pos > s.text.len {
		return false
	}
	for pos in start_pos .. end_pos {
		if s.text[pos] != want[pos - start_pos] {
			return false
		}
	}
	return true
}

fn (s mut Scanner) debug_tokens() {
	s.pos = 0
	s.started = false
	s.debug = true
	fname := s.file_path.all_after(os.path_separator)
	println('\n===DEBUG TOKENS $fname===')
	for {
		res := s.scan()
		tok := res.tok
		lit := res.lit
		print(tok.str())
		if lit != '' {
			println(' `$lit`')
		}
		else {
			println('')
		}
		if tok == .eof {
			println('============ END OF DEBUG TOKENS ==================')
			break
		}
	}
}

fn (s mut Scanner) ignore_line() {
	s.eat_to_end_of_line()
	s.inc_line_number()
}

fn (s mut Scanner) eat_to_end_of_line() {
	for s.pos < s.text.len && s.text[s.pos] != `\n` {
		s.pos++
	}
}

fn (s mut Scanner) inc_line_number() {
	s.last_nl_pos = s.pos
	s.line_nr++
	s.line_ends << s.pos
	if s.line_nr > s.nlines {
		s.nlines = s.line_nr
	}
}

fn (s Scanner) line(n int) string {
	mut res := ''
	if n >= 0 && n < s.line_ends.len {
		nline_start := if n == 0 {0}else {s.line_ends[n - 1]}
		nline_end := s.line_ends[n]
		if nline_start <= nline_end {
			res = s.text[nline_start..nline_end]
		}
	}
	return res.trim_right('\r\n').trim_left('\r\n')
}

fn is_name_char(c byte) bool {
	return c.is_letter() || c == `_`
}

[inline]
fn is_nl(c byte) bool {
	return c == `\r` || c == `\n`
}

fn contains_capital(s string) bool {
	for c in s {
		if c >= `A` && c <= `Z` {
			return true
		}
	}
	return false
}

// HTTPRequest  bad
// HttpRequest  good
fn good_type_name(s string) bool {
	if s.len < 4 {
		return true
	}
	for i in 2 .. s.len {
		if s[i].is_capital() && s[i - 1].is_capital() && s[i - 2].is_capital() {
			return false
		}
	}
	return true
}

// registration_date good
// registrationdate  bad
fn (s &Scanner) validate_var_name(name string) {
	if name.len > 15 && !name.contains('_') {
		s.error('bad variable name `$name`\n' + 'looks like you have a multi-word name without separating them with `_`' + '\nfor example, use `registration_date` instead of `registrationdate` ')
	}
}