// Copyright (c) 2019-2024 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 main

import os
import os.cmdline
import rand
import term
import v.ast
import v.pref
import v.fmt
import v.util
import v.util.diff
import v.parser
import v.help

struct FormatOptions {
	is_l       bool
	is_c       bool // Note: This refers to the '-c' fmt flag, NOT the C backend
	is_w       bool
	is_diff    bool
	is_verbose bool
	is_debug   bool
	is_noerror bool
	is_verify  bool // exit(1) if the file is not vfmt'ed
	is_worker  bool // true *only* in the worker processes. Note: workers can crash.
	is_backup  bool // make a `file.v.bak` copy *before* overwriting a `file.v` in place with `-w`
	in_process bool // do not fork a worker process; potentially faster, but more prone to crashes for invalid files
	is_new_int bool // Forcefully cast the `int` type in @[translated] modules or in the definition of `C.func` to the `i32` type.
mut:
	diff_cmd string // filled in when -diff or -verify is passed
}

const formatted_file_token = '\@\@\@' + 'FORMATTED_FILE: '
const vtmp_folder = os.vtmp_dir()
const term_colors = term.can_show_color_on_stderr()

fn main() {
	// if os.getenv('VFMT_ENABLE') == '' {
	// eprintln('v fmt is disabled for now')
	// exit(1)
	// }
	toolexe := os.executable()
	util.set_vroot_folder(os.dir(os.dir(os.dir(toolexe))))
	args := util.join_env_vflags_and_os_args()
	mut foptions := FormatOptions{
		is_c:       '-c' in args
		is_l:       '-l' in args
		is_w:       '-w' in args
		is_diff:    '-diff' in args
		is_verbose: '-verbose' in args || '--verbose' in args
		is_worker:  '-worker' in args
		is_debug:   '-debug' in args
		is_noerror: '-noerror' in args
		is_verify:  '-verify' in args
		is_backup:  '-backup' in args
		in_process: '-inprocess' in args
		is_new_int: '-new_int' in args
	}
	if term_colors {
		os.setenv('VCOLORS', 'always', true)
	}
	foptions.vlog('vfmt foptions: ${foptions}')
	if foptions.is_worker {
		// -worker should be added by a parent vfmt process.
		// We launch a sub process for each file because
		// the v compiler can do an early exit if it detects
		// a syntax error, but we want to process ALL passed
		// files if possible.
		foptions.format_file(cmdline.option(args, '-worker', ''))
		exit(0)
	}
	// we are NOT a worker at this stage, i.e. we are a parent vfmt process
	possible_files := cmdline.only_non_options(cmdline.options_after(args, ['fmt']))
	if foptions.is_verbose {
		eprintln('vfmt toolexe: ${toolexe}')
		eprintln('vfmt args: ' + os.args.str())
		eprintln('vfmt env_vflags_and_os_args: ' + args.str())
		eprintln('vfmt possible_files: ' + possible_files.str())
	}
	files := util.find_all_v_files(possible_files) or {
		verror(err.msg())
		return
	}
	if os.is_atty(0) == 0 && files.len == 0 {
		foptions.format_pipe()
		exit(0)
	}
	if files.len == 0 || '-help' in args || '--help' in args {
		help.print_and_exit('fmt')
	}
	mut cli_args_no_files := []string{}
	for idx, a in os.args {
		if idx == 0 {
			cli_args_no_files << os.quoted_path(a)
			continue
		}
		if a !in files {
			cli_args_no_files << a
		}
	}
	mut errors := 0
	mut has_internal_error := false
	mut prefs := setup_preferences()
	for file in files {
		fpath := os.real_path(file)
		if foptions.is_verify && foptions.in_process {
			// For a small amount of files, it is faster to process
			// everything directly in the same process, single threaded,
			// when vfmt is compiled with `-gc none`:
			if !foptions.verify_file(prefs, fpath) {
				println("${file} is not vfmt'ed")
				errors++
			}
			continue
		}
		mut worker_command_array := cli_args_no_files.clone()
		worker_command_array << ['-worker', util.quote_path(fpath)]
		worker_cmd := worker_command_array.join(' ')
		foptions.vlog('vfmt worker_cmd: ${worker_cmd}')
		worker_result := os.execute(worker_cmd)
		// Guard against a possibly crashing worker process.
		if worker_result.exit_code != 0 {
			eprintln(worker_result.output)
			if worker_result.exit_code == 1 {
				eprintln('Internal vfmt error while formatting file: ${file}.')
				has_internal_error = true
				continue
			}
			errors++
			continue
		}
		if worker_result.output.len > 0 {
			if worker_result.output.contains(formatted_file_token) {
				wresult := worker_result.output.split(formatted_file_token)
				formatted_warn_errs := wresult[0]
				formatted_file_path := wresult[1].trim_right('\n\r')
				foptions.post_process_file(fpath, formatted_file_path) or { errors = errors + 1 }
				if formatted_warn_errs.len > 0 {
					eprintln(formatted_warn_errs)
				}
				continue
			}
		}
		errors++
	}
	ecode := if has_internal_error { 5 } else { 0 }
	if errors > 0 {
		if !foptions.is_diff {
			eprintln('Encountered a total of: ${errors} formatting errors.')
		}
		match true {
			foptions.is_noerror { exit(0 + ecode) }
			foptions.is_verify { exit(1 + ecode) }
			foptions.is_c { exit(2 + ecode) }
			else { exit(1 + ecode) }
		}
	}
	exit(ecode)
}

fn (foptions &FormatOptions) verify_file(prefs &pref.Preferences, fpath string) bool {
	fcontent := foptions.formated_content_from_file(prefs, fpath)
	content := os.read_file(fpath) or { return false }
	return fcontent == content
}

fn setup_preferences() &pref.Preferences {
	mut prefs := pref.new_preferences()
	prefs.is_fmt = true
	prefs.skip_warnings = true
	return prefs
}

fn setup_preferences_and_table() (&pref.Preferences, &ast.Table) {
	return setup_preferences(), ast.new_table()
}

fn (foptions &FormatOptions) vlog(msg string) {
	if foptions.is_verbose {
		eprintln(msg)
	}
}

fn (foptions &FormatOptions) formated_content_from_file(prefs &pref.Preferences, file string) string {
	mut table := ast.new_table()
	file_ast := parser.parse_file(file, mut table, .parse_comments, prefs)
	table.new_int = foptions.is_new_int
	formated_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug)
	return formated_content
}

fn (foptions &FormatOptions) format_file(file string) {
	file_name := os.file_name(file)
	ulid := rand.ulid()
	vfmt_output_path := os.join_path(vtmp_folder, 'vfmt_${ulid}_${file_name}')
	if file.contains('_vfmt_off') {
		os.cp(file, vfmt_output_path) or { panic(err) }
		foptions.vlog('format_file copied the file ${file} as it was, 1:1, since its name contains `_vfmt_off`.')
		eprintln('${formatted_file_token}${vfmt_output_path}')
		return
	}
	foptions.vlog('vfmt2 running fmt.fmt over file: ${file}')
	prefs, mut table := setup_preferences_and_table()
	file_ast := parser.parse_file(file, mut table, .parse_comments, prefs)
	// checker.new_checker(table, prefs).check(file_ast)
	table.new_int = foptions.is_new_int
	formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug)
	os.write_file(vfmt_output_path, formatted_content) or { panic(err) }
	foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to ${vfmt_output_path} .')
	eprintln('${formatted_file_token}${vfmt_output_path}')
}

fn (foptions &FormatOptions) format_pipe() {
	foptions.vlog('vfmt2 running fmt.fmt over stdin')
	prefs, mut table := setup_preferences_and_table()
	input_text := os.get_raw_lines_joined()
	file_ast := parser.parse_text(input_text, '', mut table, .parse_comments, prefs)
	// checker.new_checker(table, prefs).check(file_ast)
	table.new_int = foptions.is_new_int
	formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug,
		source_text: input_text
	)
	print(formatted_content)
	flush_stdout()
	foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to stdout.')
}

fn print_compiler_options(compiler_params &pref.Preferences) {
	eprintln('         os: ' + compiler_params.os.str())
	eprintln('  ccompiler: ${compiler_params.ccompiler}')
	eprintln('       path: ${compiler_params.path} ')
	eprintln('   out_name: ${compiler_params.out_name} ')
	eprintln('      vroot: ${compiler_params.vroot} ')
	eprintln('lookup_path: ${compiler_params.lookup_path} ')
	eprintln('   out_name: ${compiler_params.out_name} ')
	eprintln('     cflags: ${compiler_params.cflags} ')
	eprintln('    is_test: ${compiler_params.is_test} ')
	eprintln('  is_script: ${compiler_params.is_script} ')
}

fn (mut foptions FormatOptions) post_process_file(file string, formatted_file_path string) ! {
	if formatted_file_path == '' {
		return
	}
	fc := os.read_file(file) or {
		eprintln('File ${file} could not be read')
		return
	}
	formatted_fc := os.read_file(formatted_file_path) or {
		eprintln('File ${formatted_file_path} could not be read')
		return
	}
	is_formatted_different := fc != formatted_fc
	if foptions.is_diff {
		if !is_formatted_different {
			return
		}
		println(diff.compare_files(file, formatted_file_path)!)
		return error('')
	}
	if foptions.is_verify {
		if !is_formatted_different {
			return
		}
		println("${file} is not vfmt'ed")
		return error('')
	}
	if foptions.is_c {
		if is_formatted_different {
			eprintln('File is not formatted: ${file}')
			return error('')
		}
		return
	}
	if foptions.is_l {
		if is_formatted_different {
			eprintln('File needs formatting: ${file}')
		}
		return
	}
	if foptions.is_w {
		if is_formatted_different {
			if foptions.is_backup {
				file_bak := '${file}.bak'
				os.cp(file, file_bak) or {}
			}
			mut perms_to_restore := u32(0)
			$if !windows {
				fm := os.inode(file)
				perms_to_restore = fm.bitmask()
			}
			os.mv_by_cp(formatted_file_path, file) or { panic(err) }
			$if !windows {
				os.chmod(file, int(perms_to_restore)) or { panic(err) }
			}
			eprintln('Reformatted file: ${file}')
		} else {
			eprintln('Already formatted file: ${file}')
		}
		return
	}
	print(formatted_fc)
	flush_stdout()
}

fn read_source_lines(file string) ![]string {
	source_lines := os.read_lines(file) or { return error('can not read ${file}') }
	return source_lines
}

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

fn (f FormatOptions) str() string {
	return
		'FormatOptions{ is_l: ${f.is_l}, is_w: ${f.is_w}, is_diff: ${f.is_diff}, is_verbose: ${f.is_verbose},' +
		' is_worker: ${f.is_worker}, is_debug: ${f.is_debug}, is_noerror: ${f.is_noerror},' +
		' is_verify: ${f.is_verify}" }'
}
