module builder

import os
import hash
import strings
import v.util
import v.pref
import v.vcache

pub fn (mut b Builder) rebuild_modules() {
	if !b.pref.use_cache || b.pref.build_mode == .build_module {
		return
	}
	util.timing_start('${@METHOD} source_hashing')
	mut new_hashes := map[string]string{}
	mut old_hashes := map[string]string{}
	mut sb_new_hashes := strings.new_builder(1024)
	all_files := b.parsed_files.map(it.path)
	//
	mut cm := vcache.new_cache_manager(all_files)
	sold_hashes := cm.load('.hashes', 'all_files') or { ' ' }
	// eprintln(sold_hashes)
	sold_hashes_lines := sold_hashes.split('\n')
	for line in sold_hashes_lines {
		if line.len == 0 {
			continue
		}
		x := line.split(' ')
		chash := x[0]
		cpath := x[1]
		old_hashes[cpath] = chash
	}
	// eprintln('old_hashes: $old_hashes')
	for p in b.parsed_files {
		cpath := p.path
		ccontent := util.read_file(cpath) or { '' }
		chash := hash.sum64_string(ccontent, 7).hex_full()
		new_hashes[cpath] = chash
		sb_new_hashes.write_string(chash)
		sb_new_hashes.write_byte(` `)
		sb_new_hashes.write_string(cpath)
		sb_new_hashes.write_byte(`\n`)
	}
	snew_hashes := sb_new_hashes.str()
	// eprintln('new_hashes: $new_hashes')
	// eprintln('> new_hashes != old_hashes: ' + ( old_hashes != new_hashes ).str())
	// eprintln(snew_hashes)
	cm.save('.hashes', 'all_files', snew_hashes) or {}
	util.timing_measure('${@METHOD} source_hashing')

	if new_hashes != old_hashes {
		util.timing_start('${@METHOD} rebuilding')
		// eprintln('> b.mod_invalidates_paths: $b.mod_invalidates_paths')
		// eprintln('> b.mod_invalidates_mods: $b.mod_invalidates_mods')
		// eprintln('> b.path_invalidates_mods: $b.path_invalidates_mods')
		$if trace_invalidations ? {
			for k, v in b.mod_invalidates_paths {
				mut m := map[string]bool{}
				for mm in b.mod_invalidates_mods[k] {
					m[mm] = true
				}
				eprintln('> module `$k` invalidates: $m.keys()')
				for fpath in v {
					eprintln('         $fpath')
				}
			}
		}
		mut invalidated_paths := map[string]int{}
		mut invalidated_mod_paths := map[string]int{}
		for npath, nhash in new_hashes {
			if npath !in old_hashes {
				invalidated_paths[npath]++
				continue
			}
			if old_hashes[npath] != nhash {
				invalidated_paths[npath]++
				continue
			}
		}
		for opath, ohash in old_hashes {
			if opath !in new_hashes {
				invalidated_paths[opath]++
				continue
			}
			if new_hashes[opath] != ohash {
				invalidated_paths[opath]++
				continue
			}
		}
		$if trace_invalidations ? {
			eprintln('invalidated_paths: $invalidated_paths')
		}
		mut rebuild_everything := false
		for cycle := 0; true; cycle++ {
			$if trace_invalidations ? {
				eprintln('> cycle: $cycle | invalidated_paths: $invalidated_paths')
			}
			mut new_invalidated_paths := map[string]int{}
			for npath, _ in invalidated_paths {
				invalidated_mods := b.path_invalidates_mods[npath]
				if invalidated_mods == ['main'] {
					continue
				}
				if 'builtin' in invalidated_mods {
					// When `builtin` is invalid, there is no point in
					// extracting a finer grained dependency resolution
					// of the dependencies any more. Instead, just rebuild
					// every module.
					rebuild_everything = true
					break
				}
				for imod in invalidated_mods {
					if imod == 'main' {
						continue
					}
					for np in b.mod_invalidates_paths[imod] {
						new_invalidated_paths[np]++
					}
				}
				$if trace_invalidations ? {
					eprintln('> npath -> invalidated_mods | $npath -> $invalidated_mods')
				}
				mpath := os.dir(npath)
				invalidated_mod_paths[mpath]++
			}
			if rebuild_everything {
				break
			}
			if new_invalidated_paths.len == 0 {
				break
			}
			invalidated_paths = new_invalidated_paths.clone()
		}
		if rebuild_everything {
			invalidated_mod_paths = {}
			for npath, _ in new_hashes {
				mpath := os.dir(npath)
				pimods := b.path_invalidates_mods[npath]
				if pimods == ['main'] {
					continue
				}
				invalidated_mod_paths[mpath]++
			}
		}
		$if trace_invalidations ? {
			eprintln('invalidated_mod_paths: $invalidated_mod_paths')
			eprintln('rebuild_everything: $rebuild_everything')
		}
		if invalidated_mod_paths.len > 0 {
			impaths := invalidated_mod_paths.keys()
			vexe := pref.vexe_path()
			for imp in impaths {
				b.v_build_module(vexe, imp)
			}
		}
		util.timing_measure('${@METHOD} rebuilding')
	}
}

fn (mut b Builder) v_build_module(vexe string, imp_path string) {
	pwd := os.getwd()
	defer {
		os.chdir(pwd) or {}
	}
	// do run `v build-module x` always in main vfolder; x can be a relative path
	vroot := os.dir(vexe)
	os.chdir(vroot) or {}
	boptions := b.pref.build_options.join(' ')
	rebuild_cmd := '${os.quoted_path(vexe)} $boptions build-module ${os.quoted_path(imp_path)}'
	vcache.dlog('| Builder.' + @FN, 'vexe: $vexe | imp_path: $imp_path | rebuild_cmd: $rebuild_cmd')
	$if trace_v_build_module ? {
		eprintln('> Builder.v_build_module: $rebuild_cmd')
	}
	os.system(rebuild_cmd)
}

fn (mut b Builder) rebuild_cached_module(vexe string, imp_path string) string {
	res := b.pref.cache_manager.exists('.o', imp_path) or {
		if b.pref.is_verbose {
			println('Cached $imp_path .o file not found... Building .o file for $imp_path')
		}
		b.v_build_module(vexe, imp_path)
		rebuilded_o := b.pref.cache_manager.exists('.o', imp_path) or {
			panic('could not rebuild cache module for $imp_path, error: $err.msg')
		}
		return rebuilded_o
	}
	return res
}

fn (mut b Builder) handle_usecache(vexe string) {
	if !b.pref.use_cache || b.pref.build_mode == .build_module {
		return
	}
	mut libs := []string{} // builtin.o os.o http.o etc
	mut built_modules := []string{}
	builtin_obj_path := b.rebuild_cached_module(vexe, 'vlib/builtin')
	libs << builtin_obj_path
	for ast_file in b.parsed_files {
		if b.pref.is_test && ast_file.mod.name != 'main' {
			imp_path := b.find_module_path(ast_file.mod.name, ast_file.path) or {
				verror('cannot import module "$ast_file.mod.name" (not found)')
				break
			}
			obj_path := b.rebuild_cached_module(vexe, imp_path)
			libs << obj_path
			built_modules << ast_file.mod.name
		}
		for imp_stmt in ast_file.imports {
			imp := imp_stmt.mod
			// strconv is already imported inside builtin, so skip generating its object file
			// TODO: incase we have other modules with the same name, make sure they are vlib
			// is this even doign anything?
			if imp in ['strconv', 'strings', 'dlmalloc'] {
				continue
			}
			if imp in built_modules {
				continue
			}
			if util.should_bundle_module(imp) {
				continue
			}
			// The problem is cmd/v is in module main and imports
			// the relative module named help, which is built as cmd.v.help not help
			// currently this got this workign by building into main, see ast.FnDecl in cgen
			if imp == 'help' {
				continue
			}
			imp_path := b.find_module_path(imp, ast_file.path) or {
				verror('cannot import module "$imp" (not found)')
				break
			}
			obj_path := b.rebuild_cached_module(vexe, imp_path)
			libs << obj_path
			built_modules << imp
		}
	}
	b.ccoptions.post_args << libs
}
