module builder

import os
import v.token
import v.pref
import v.errors
import v.util
import v.ast
import v.vmod
import v.checker
import v.transformer
import v.parser
import v.markused
import v.depgraph
import v.callgraph
import v.dotgraph
// import x.json2

pub struct Builder {
pub:
	compiled_dir string // contains os.real_path() of the dir of the final file being compiled, or the dir itself when doing `v .`
	module_path  string
pub mut:
	checker             &checker.Checker         = unsafe { nil }
	transformer         &transformer.Transformer = unsafe { nil }
	out_name_c          string
	out_name_js         string
	stats_lines         int // size of backend generated source code in lines
	stats_bytes         int // size of backend generated source code in bytes
	nr_errors           int // accumulated error count of scanner, parser, checker, and builder
	nr_warnings         int // accumulated warning count of scanner, parser, checker, and builder
	nr_notices          int // accumulated notice count of scanner, parser, checker, and builder
	pref                &pref.Preferences = unsafe { nil }
	module_search_paths []string
	parsed_files        []&ast.File
	//$if windows {
	cached_msvc MsvcResult
	//}
	table     &ast.Table = unsafe { nil }
	ccoptions CcompilerOptions
	// Note: changes in mod `builtin` force invalidation of every other .v file
	mod_invalidates_paths map[string][]string // changes in mod `os`, invalidate only .v files, that do `import os`
	mod_invalidates_mods  map[string][]string // changes in mod `os`, force invalidation of mods, that do `import os`
	path_invalidates_mods map[string][]string // changes in a .v file from `os`, invalidates `os`
	crun_cache_keys       []string            // target executable + top level source files; filled in by Builder.should_rebuild
	executable_exists     bool                // if the executable already exists, don't remove new executable after `v run`
	str_args              string              // for parallel_cc mode only, to know which cc args to use (like -I etc)
}

pub fn new_builder(pref_ &pref.Preferences) Builder {
	rdir := os.real_path(pref_.path)
	compiled_dir := if os.is_dir(rdir) { rdir } else { os.dir(rdir) }
	mut table := ast.new_table()
	table.is_fmt = false
	if pref_.use_color == .always {
		util.emanager.set_support_color(true)
	}
	if pref_.use_color == .never {
		util.emanager.set_support_color(false)
	}
	table.pointer_size = if pref_.m64 { 8 } else { 4 }
	mut msvc := MsvcResult{}
	if pref_.ccompiler == 'msvc' {
		$if windows {
			msvc = find_msvc(pref_.m64) or {
				MsvcResult{
					valid: false
				}
			}
		}
	}
	util.timing_set_should_print(pref_.show_timings || pref_.is_verbose)
	if pref_.show_callgraph || pref_.show_depgraph {
		dotgraph.start_digraph()
	}
	mut executable_name := pref_.out_name
	$if windows {
		executable_name += '.exe'
	}
	return Builder{
		pref:              pref_
		table:             table
		checker:           checker.new_checker(table, pref_)
		transformer:       transformer.new_transformer_with_table(table, pref_)
		compiled_dir:      compiled_dir
		cached_msvc:       msvc
		executable_exists: os.is_file(executable_name)
	}
}

pub fn (mut b Builder) interpret_text(code string, v_files []string) ! {
	b.parsed_files = parser.parse_files(v_files, mut b.table, b.pref)
	b.parsed_files << parser.parse_text(code, '', mut b.table, .skip_comments, b.pref)
	b.parse_imports()

	if b.pref.only_check_syntax {
		return error_with_code('stop_after_parser', 7001)
	}

	b.middle_stages()!
}

pub fn (mut b Builder) front_stages(v_files []string) ! {
	mut timers := util.get_timers()
	util.timing_start('ALL_FRONT_STAGES')
	defer {
		timers.show('ALL_FRONT_STAGES')
	}
	util.timing_start('PARSE')

	util.timing_start('Builder.front_stages.parse_files')
	b.parsed_files = parser.parse_files(v_files, mut b.table, b.pref)
	timers.show('Builder.front_stages.parse_files')

	b.parse_imports()

	timers.show('SCAN')
	timers.show('PARSE')
	timers.show_if_exists('PARSE stmt')
	if b.pref.only_check_syntax {
		return error_with_code('stop_after_parser', 7001)
	}
}

pub fn (mut b Builder) middle_stages() ! {
	util.timing_start('CHECK')

	util.timing_start('Checker.generic_insts_to_concrete')
	b.table.generic_insts_to_concrete()
	util.timing_measure('Checker.generic_insts_to_concrete')

	b.checker.check_files(b.parsed_files)
	util.timing_measure('CHECK')
	$if trace_type_symbols_after_checker ? {
		for t, s in b.table.type_symbols {
			println('> t: ${t:10} | s.mod: ${s.mod:-40} | s.name: ${'${s.name#[..30]}':-30} | s.is_builtin: ${s.is_builtin:6} | s.is_pub: ${s.is_pub}')
		}
	}
	if b.pref.dump_defines != '' {
		b.dump_defines()
	}
	mut mcache := vmod.get_cache()
	mcache.debug()
	b.print_warnings_and_errors()
	if b.checker.should_abort {
		return error('too many errors/warnings/notices')
	}
	if b.checker.unresolved_fixed_sizes.len > 0 {
		util.timing_start('Checker.update_unresolved_fixed_sizes')
		b.checker.update_unresolved_fixed_sizes()
		util.timing_measure('Checker.update_unresolved_fixed_sizes')
	}
	if b.pref.check_only {
		return error_with_code('stop_after_checker', 8001)
	}
	util.timing_start('TRANSFORM')
	b.transformer.transform_files(b.parsed_files)
	util.timing_measure('TRANSFORM')

	b.table.complete_interface_check()
	if b.pref.skip_unused {
		markused.mark_used(mut b.table, mut b.pref, b.parsed_files)
	}
	if b.pref.show_callgraph {
		callgraph.show(mut b.table, b.pref, b.parsed_files)
	}
}

pub fn (mut b Builder) front_and_middle_stages(v_files []string) ! {
	b.front_stages(v_files)!
	b.middle_stages()!
}

// parse all deps from already parsed files
pub fn (mut b Builder) parse_imports() {
	util.timing_start(@METHOD)
	defer {
		util.timing_measure(@METHOD)
	}
	mut done_imports := []string{}
	if b.pref.is_vsh {
		done_imports << 'os'
	}
	// TODO: (joe): decide if this is correct solution.
	// in the case of building a module, the actual module files
	// are passed via cmd line, so they have already been parsed
	// by this stage. note that if one files from a module was
	// parsed (but not all of them), then this will cause a problem.
	// we could add a list of parsed files instead, but I think
	// there is a better solution all around, I will revisit this.
	// NOTE: there is a very similar occurrence with the way
	// internal module test's work, and this was the reason there
	// were issues with duplicate declarations, so we should sort
	// that out in a similar way.
	for file in b.parsed_files {
		if file.mod.name != 'main' && file.mod.name !in done_imports {
			done_imports << file.mod.name
		}
	}
	// Note: b.parsed_files is appended in the loop,
	// so we can not use the shorter `for in` form.
	for i := 0; i < b.parsed_files.len; i++ {
		ast_file := b.parsed_files[i]
		b.path_invalidates_mods[ast_file.path] << ast_file.mod.name
		if ast_file.mod.name != 'builtin' {
			b.mod_invalidates_paths['builtin'] << ast_file.path
			b.mod_invalidates_mods['builtin'] << ast_file.mod.name
		}
		for imp in ast_file.imports {
			mod := imp.mod
			b.mod_invalidates_paths[mod] << ast_file.path
			b.mod_invalidates_mods[mod] << ast_file.mod.name
			if mod == 'builtin' {
				b.parsed_files[i].errors << b.error_with_pos('cannot import module "builtin"',
					ast_file.path, imp.pos)
				break
			}
			if mod in done_imports {
				continue
			}
			import_path := b.find_module_path(mod, ast_file.path) or {
				// v.parsers[i].error_with_token_index('cannot import module "$mod" (not found)', v.parsers[i].import_ast.get_import_tok_idx(mod))
				// break
				b.parsed_files[i].errors << b.error_with_pos('cannot import module "${mod}" (not found)',
					ast_file.path, imp.pos)
				break
			}
			v_files := b.v_files_from_dir(import_path)
			if v_files.len == 0 {
				// v.parsers[i].error_with_token_index('cannot import module "$mod" (no .v files in "$import_path")', v.parsers[i].import_ast.get_import_tok_idx(mod))
				b.parsed_files[i].errors << b.error_with_pos('cannot import module "${mod}" (no .v files in "${import_path}")',
					ast_file.path, imp.pos)
				continue
			}
			// eprintln('>> ast_file.path: $ast_file.path , done: $done_imports, `import $mod` => $v_files')
			// Add all imports referenced by these libs
			parsed_files := parser.parse_files(v_files, mut b.table, b.pref)
			for file in parsed_files {
				mut name := file.mod.name
				if name == '' {
					name = file.mod.short_name
				}
				sname := name.all_after_last('.')
				smod := mod.all_after_last('.')
				if sname != smod {
					msg := 'bad module definition: ${ast_file.path} imports module "${mod}" but ${file.path} is defined as module `${name}`'
					b.parsed_files[i].errors << b.error_with_pos(msg, ast_file.path, imp.pos)
				}
			}
			b.parsed_files << parsed_files
			done_imports << mod
		}
	}
	b.resolve_deps()
	$if trace_parsed_files ? {
		b.show_parsed_files()
	}
	if b.pref.print_v_files {
		b.show_parsed_files()
		exit(0)
	}
	if b.pref.print_watched_files {
		for p in b.parsed_files {
			if p.is_parse_text {
				// a generated snippet, `v watch` does not care about those, since they are duplicates for other files
				continue
			}
			println(p.path)
			for tp in p.template_paths {
				println(tp)
			}
		}
		exit(0)
	}
	if b.pref.dump_files != '' {
		b.dump_files(b.parsed_files.map(it.path))
	}
	b.rebuild_modules()
}

pub fn (mut b Builder) resolve_deps() {
	util.timing_start(@METHOD)
	defer {
		util.timing_measure(@METHOD)
	}
	graph := b.import_graph()
	deps_resolved := graph.resolve()
	if b.pref.is_verbose {
		eprintln('------ resolved dependencies graph: ------')
		eprintln(deps_resolved.display())
		eprintln('------------------------------------------')
	}
	if b.pref.show_depgraph {
		depgraph.show(deps_resolved, b.pref.path)
	}
	cycles := deps_resolved.display_cycles()
	if cycles.len > 1 {
		verror('error: import cycle detected between the following modules: \n' + cycles)
	}
	mut mods := []string{}
	for node in deps_resolved.nodes {
		mods << node.name
	}
	b.dump_modules(mods)
	if b.pref.is_verbose {
		eprintln('------ imported modules: ------')
		eprintln(mods.str())
		eprintln('-------------------------------')
	}
	unsafe {
		mut reordered_parsed_files := []&ast.File{}
		for m in mods {
			for pf in b.parsed_files {
				if m == pf.mod.name {
					reordered_parsed_files << pf
					// eprintln('pf.mod.name: $pf.mod.name | pf.path: $pf.path')
				}
			}
		}
		b.table.modules = mods
		b.parsed_files = reordered_parsed_files
	}
}

// graph of all imported modules
pub fn (b &Builder) import_graph() &depgraph.DepGraph {
	builtins := util.builtin_module_parts.clone()
	mut graph := depgraph.new_dep_graph()
	for p in b.parsed_files {
		// eprintln('p.path: $p.path')
		mut deps := []string{}
		if p.mod.name !in builtins {
			deps << 'builtin'
			if b.pref.backend == .c {
				// TODO: JavaScript backend doesn't handle os for now
				// os import libraries so we exclude anything which could cause a loop
				// git grep import vlib/os | cut -f2 -d: | cut -f2 -d" " | sort -u
				// dl, os, os.cmdline, os.filelock, os.notify, strings, strings.textscanner, term.termios, time
				if b.pref.is_vsh
					&& p.mod.name !in ['os', 'dl', 'strings.textscanner', 'term.termios'] {
					deps << 'os'
				}
			}
		}
		for m in p.imports {
			if m.mod == p.mod.name {
				continue
			}
			deps << m.mod
		}
		graph.add(p.mod.name, deps)
	}
	$if trace_import_graph ? {
		eprintln(graph.display())
	}
	return graph
}

pub fn (b &Builder) v_files_from_dir(dir string) []string {
	if !os.exists(dir) {
		if dir == 'compiler' && os.is_dir('vlib') {
			println('looks like you are trying to build V with an old command')
			println('use `v -o v cmd/v` instead of `v -o v compiler`')
		}
		verror("${dir} doesn't exist")
	} else if !os.is_dir(dir) {
		verror("${dir} isn't a directory!")
	}
	mut files := os.ls(dir) or { panic(err) }
	if b.pref.is_verbose {
		println('v_files_from_dir ("${dir}")')
	}
	res := b.pref.should_compile_filtered_files(dir, files)
	if res.len == 0 {
		// Perhaps the .v files are stored in /src/ ?
		src_path := os.join_path(dir, 'src')
		if os.is_dir(src_path) {
			if b.pref.is_verbose {
				println('v_files_from_dir ("${src_path}") (/src/)')
			}
			files = os.ls(src_path) or { panic(err) }
			return b.pref.should_compile_filtered_files(src_path, files)
		}
	}
	return res
}

pub fn (b &Builder) log(s string) {
	if b.pref.is_verbose {
		println(s)
	}
}

pub fn (b &Builder) info(s string) {
	if b.pref.is_verbose {
		println(s)
	}
}

@[inline]
pub fn module_path(mod string) string {
	// submodule support
	return mod.replace('.', os.path_separator)
}

// TODO: try to merge this & util.module functions to create a
// reliable multi use function. see comments in util/module.v
pub fn (b &Builder) find_module_path(mod string, fpath string) !string {
	// support @VROOT/v.mod relative paths:
	mut mcache := vmod.get_cache()
	vmod_file_location := mcache.get_by_file(fpath)
	mod_path := module_path(mod)
	mut module_lookup_paths := []string{}
	if vmod_file_location.vmod_file.len != 0
		&& vmod_file_location.vmod_folder !in b.module_search_paths {
		module_lookup_paths << vmod_file_location.vmod_folder
	}
	module_lookup_paths << b.module_search_paths
	module_lookup_paths << os.getwd()
	// go up through parents looking for modules a folder.
	// we need a proper solution that works most of the time. look at vdoc.get_parent_mod
	if fpath.contains(os.path_separator + 'modules' + os.path_separator) {
		parts := fpath.split(os.path_separator)
		for i := parts.len - 2; i >= 0; i-- {
			if parts[i] == 'modules' {
				module_lookup_paths << parts[0..i + 1].join(os.path_separator)
				break
			}
		}
	}
	for search_path in module_lookup_paths {
		try_path := os.join_path(search_path, mod_path)
		if b.pref.is_verbose {
			println('  >> trying to find ${mod} in ${try_path} ..')
		}
		if os.is_dir(try_path) {
			if b.pref.is_verbose {
				println('  << found ${try_path} .')
			}
			return try_path
		}
	}
	// look up through parents
	path_parts := fpath.split(os.path_separator)
	for i := path_parts.len - 2; i > 0; i-- {
		p1 := path_parts[0..i].join(os.path_separator)
		try_path := os.join_path(p1, mod_path)
		if b.pref.is_verbose {
			println('  >> trying to find ${mod} in ${try_path} ..')
		}
		if os.is_dir(try_path) {
			return try_path
		}
	}
	smodule_lookup_paths := module_lookup_paths.join(', ')
	return error('module "${mod}" not found in:\n${smodule_lookup_paths}')
}

pub fn (b &Builder) show_total_warns_and_errors_stats() {
	if b.nr_errors == 0 && b.nr_warnings == 0 && b.nr_notices == 0 {
		return
	}
	if b.pref.is_stats {
		mut nr_errors := b.checker.errors.len
		mut nr_warnings := b.checker.warnings.len
		mut nr_notices := b.checker.notices.len

		if b.pref.check_only {
			nr_errors = b.nr_errors
			nr_warnings = b.nr_warnings
			nr_notices = b.nr_notices
		}

		estring := util.bold(nr_errors.str())
		wstring := util.bold(nr_warnings.str())
		nstring := util.bold(nr_notices.str())

		if b.pref.check_only {
			println('summary: ${estring} V errors, ${wstring} V warnings, ${nstring} V notices')
		} else {
			println('checker summary: ${estring} V errors, ${wstring} V warnings, ${nstring} V notices')
		}
	}
	if !b.pref.is_vls && b.checker.nr_errors > 0 && b.pref.path.ends_with('.v')
		&& os.is_file(b.pref.path) {
		if b.checker.errors.any(it.message.starts_with('unknown ')) {
			// Sometimes users try to `v main.v`, when they have several .v files in their project.
			// Then, they encounter puzzling errors about missing or unknown types. In this case,
			// the intended command may have been `v .` instead, so just suggest that:
			old_cmd := util.bold('v ${b.pref.path}')
			new_cmd := util.bold('v ${os.dir(b.pref.path)}')
			eprintln(util.color('notice', 'If the code of your project is in multiple files, try with `${new_cmd}` instead of `${old_cmd}`'))
		}
	}
}

pub fn (mut b Builder) print_warnings_and_errors() {
	defer {
		b.show_total_warns_and_errors_stats()
	}

	for file in b.parsed_files {
		b.nr_errors += file.errors.len
		b.nr_warnings += file.warnings.len
		b.nr_notices += file.notices.len
	}

	if b.pref.output_mode == .silent {
		if b.nr_errors > 0 {
			exit(1)
		}
		return
	}

	if b.pref.check_only {
		if !b.pref.skip_notes {
			for file in b.parsed_files {
				for err in file.notices {
					kind := if b.pref.is_verbose {
						'${err.reporter} notice #${b.nr_notices}:'
					} else {
						'notice:'
					}
					util.show_compiler_message(kind, err.CompilerMessage)
				}
			}
		}

		mut json_errors := []util.JsonError{}
		for file in b.parsed_files {
			for err in file.errors {
				kind := if b.pref.is_verbose {
					'${err.reporter} error #${b.nr_errors}:'
				} else {
					'error:'
				}

				if b.pref.json_errors {
					json_errors << util.JsonError{
						message: err.message
						path:    err.file_path
						line_nr: err.pos.line_nr + 1
						col:     err.pos.col + 1
					}
					// util.print_json_error(kind, err.CompilerMessage)
				} else {
					util.show_compiler_message(kind, err.CompilerMessage)
				}
			}
		}
		if b.pref.json_errors {
			util.print_json_errors(json_errors)
			// eprintln(json2.encode_pretty(json_errors))
		}

		if !b.pref.skip_warnings {
			for file in b.parsed_files {
				for err in file.warnings {
					kind := if b.pref.is_verbose {
						'${err.reporter} warning #${b.nr_warnings}:'
					} else {
						'warning:'
					}
					util.show_compiler_message(kind, err.CompilerMessage)
				}
			}
		}

		b.show_total_warns_and_errors_stats()
		if b.nr_errors > 0 {
			exit(1)
		}
	}

	if b.pref.is_verbose && b.checker.nr_warnings > 1 {
		println('${b.checker.nr_warnings} warnings')
	}
	if b.pref.is_verbose && b.checker.nr_notices > 1 {
		println('${b.checker.nr_notices} notices')
	}
	if b.checker.nr_notices > 0 && !b.pref.skip_notes {
		for err in b.checker.notices {
			kind := if b.pref.is_verbose {
				'${err.reporter} notice #${b.checker.nr_notices}:'
			} else {
				'notice:'
			}
			util.show_compiler_message(kind, err.CompilerMessage)
		}
	}
	if b.checker.nr_warnings > 0 && !b.pref.skip_warnings {
		for err in b.checker.warnings {
			kind := if b.pref.is_verbose {
				'${err.reporter} warning #${b.checker.nr_warnings}:'
			} else {
				'warning:'
			}
			util.show_compiler_message(kind, err.CompilerMessage)
		}
	}

	if b.pref.is_verbose && b.checker.nr_errors > 1 {
		println('${b.checker.nr_errors} errors')
	}
	if b.checker.nr_errors > 0 {
		for err in b.checker.errors {
			kind := if b.pref.is_verbose {
				'${err.reporter} error #${b.checker.nr_errors}:'
			} else {
				'error:'
			}
			util.show_compiler_message(kind, err.CompilerMessage)
		}
		b.show_total_warns_and_errors_stats()
		exit(1)
	}
	if b.table.redefined_fns.len > 0 {
		mut total_conflicts := 0
		for fn_name in b.table.redefined_fns {
			// Find where this function was already declared
			mut redefines := []FunctionRedefinition{}
			mut redefine_conflicts := map[string]int{}
			for file in b.parsed_files {
				for stmt in file.stmts {
					if stmt is ast.FnDecl {
						if stmt.name == fn_name {
							fheader := b.table.stringify_fn_decl(&stmt, 'main', map[string]string{},
								false)
							redefines << FunctionRedefinition{
								fpath:   file.path
								fline:   stmt.pos.line_nr
								f:       stmt
								fheader: fheader
							}
							redefine_conflicts[fheader]++
						}
					}
				}
			}
			if redefines.len > 0 {
				util.show_compiler_message('builder error:',
					message: 'redefinition of function `${fn_name}`'
				)
				for redefine in redefines {
					util.show_compiler_message('conflicting declaration:',
						message:   redefine.fheader
						file_path: redefine.fpath
						pos:       redefine.f.pos
					)
				}
				total_conflicts++
			}
		}
		if total_conflicts > 0 {
			b.show_total_warns_and_errors_stats()
			exit(1)
		}
	}
}

struct FunctionRedefinition {
	fpath   string
	fline   int
	fheader string
	f       ast.FnDecl
}

pub fn (b &Builder) error_with_pos(s string, fpath string, pos token.Pos) errors.Error {
	if !b.pref.check_only {
		util.show_compiler_message('builder error:', pos: pos, file_path: fpath, message: s)
		exit(1)
	}

	return errors.Error{
		file_path: fpath
		pos:       pos
		reporter:  .builder
		message:   s
	}
}

@[noreturn]
pub fn verror(s string) {
	util.verror('builder error', s)
}

pub fn (mut b Builder) show_parsed_files() {
	for p in b.parsed_files {
		if p.is_parse_text {
			println(p.path + ':parse_text')
		}
		println(p.path)
	}
}
