// Copyright (c) 2019-2022 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 os

import strings

pub const max_path_len = 4096

pub const wd_at_startup = getwd()

const f_ok = 0

const x_ok = 1

const w_ok = 2

const r_ok = 4

pub struct Result {
pub:
	exit_code int
	output    string
	// stderr string // TODO
}

pub struct Command {
mut:
	f voidptr
pub mut:
	eof       bool
	exit_code int
pub:
	path            string
	redirect_stdout bool
}

[unsafe]
pub fn (mut result Result) free() {
	unsafe { result.output.free() }
}

// cp_all will recursively copy `src` to `dst`,
// optionally overwriting files or dirs in `dst`.
pub fn cp_all(src string, dst string, overwrite bool) ? {
	source_path := real_path(src)
	dest_path := real_path(dst)
	if !exists(source_path) {
		return error("Source path doesn't exist")
	}
	// single file copy
	if !is_dir(source_path) {
		fname := file_name(source_path)
		adjusted_path := if is_dir(dest_path) {
			join_path_single(dest_path, fname)
		} else {
			dest_path
		}
		if exists(adjusted_path) {
			if overwrite {
				rm(adjusted_path) ?
			} else {
				return error('Destination file path already exist')
			}
		}
		cp(source_path, adjusted_path) ?
		return
	}
	if !exists(dest_path) {
		mkdir(dest_path) ?
	}
	if !is_dir(dest_path) {
		return error('Destination path is not a valid directory')
	}
	files := ls(source_path) ?
	for file in files {
		sp := join_path_single(source_path, file)
		dp := join_path_single(dest_path, file)
		if is_dir(sp) {
			if !exists(dp) {
				mkdir(dp) ?
			}
		}
		cp_all(sp, dp, overwrite) or {
			rmdir(dp) or { return err }
			return err
		}
	}
}

// mv_by_cp first copies the source file, and if it is copied successfully, deletes the source file.
// may be used when you are not sure that the source and target are on the same mount/partition.
pub fn mv_by_cp(source string, target string) ? {
	cp(source, target) ?
	rm(source) ?
}

// read_lines reads the file in `path` into an array of lines.
[manualfree]
pub fn read_lines(path string) ?[]string {
	buf := read_file(path) ?
	res := buf.split_into_lines()
	unsafe { buf.free() }
	return res
}

// sigint_to_signal_name will translate `si` signal integer code to it's string code representation.
pub fn sigint_to_signal_name(si int) string {
	// POSIX signals:
	match si {
		1 { return 'SIGHUP' }
		2 { return 'SIGINT' }
		3 { return 'SIGQUIT' }
		4 { return 'SIGILL' }
		6 { return 'SIGABRT' }
		8 { return 'SIGFPE' }
		9 { return 'SIGKILL' }
		11 { return 'SIGSEGV' }
		13 { return 'SIGPIPE' }
		14 { return 'SIGALRM' }
		15 { return 'SIGTERM' }
		else {}
	}
	$if linux {
		// From `man 7 signal` on linux:
		match si {
			// TODO dependent on platform
			// works only on x86/ARM/most others
			10 /* , 30, 16 */ { return 'SIGUSR1' }
			12 /* , 31, 17 */ { return 'SIGUSR2' }
			17 /* , 20, 18 */ { return 'SIGCHLD' }
			18 /* , 19, 25 */ { return 'SIGCONT' }
			19 /* , 17, 23 */ { return 'SIGSTOP' }
			20 /* , 18, 24 */ { return 'SIGTSTP' }
			21 /* , 26 */ { return 'SIGTTIN' }
			22 /* , 27 */ { return 'SIGTTOU' }
			// /////////////////////////////
			5 { return 'SIGTRAP' }
			7 { return 'SIGBUS' }
			else {}
		}
	}
	return 'unknown'
}

// rmdir_all recursively removes the specified directory.
pub fn rmdir_all(path string) ? {
	mut ret_err := ''
	items := ls(path) ?
	for item in items {
		fullpath := join_path_single(path, item)
		if is_dir(fullpath) && !is_link(fullpath) {
			rmdir_all(fullpath) or { ret_err = err.msg() }
		} else {
			rm(fullpath) or { ret_err = err.msg() }
		}
	}
	rmdir(path) or { ret_err = err.msg() }
	if ret_err.len > 0 {
		return error(ret_err)
	}
}

// is_dir_empty will return a `bool` whether or not `path` is empty.
[manualfree]
pub fn is_dir_empty(path string) bool {
	items := ls(path) or { return true }
	res := items.len == 0
	unsafe { items.free() }
	return res
}

// file_ext will return the part after the last occurence of `.` in `path`.
// The `.` is included.
pub fn file_ext(path string) string {
	pos := path.last_index('.') or { return '' }
	return path[pos..]
}

// dir returns all but the last element of path, typically the path's directory.
// After dropping the final element, trailing slashes are removed.
// If the path is empty, dir returns ".". If the path consists entirely of separators,
// dir returns a single separator.
// The returned path does not end in a separator unless it is the root directory.
pub fn dir(opath string) string {
	if opath == '' {
		return '.'
	}
	path := opath.replace_each(['/', path_separator, r'\', path_separator])
	pos := path.last_index(path_separator) or { return '.' }
	if pos == 0 && path_separator == '/' {
		return '/'
	}
	return path[..pos]
}

// base returns the last element of path.
// Trailing path separators are removed before extracting the last element.
// If the path is empty, base returns ".". If the path consists entirely of separators, base returns a
// single separator.
pub fn base(opath string) string {
	if opath == '' {
		return '.'
	}
	path := opath.replace_each(['/', path_separator, r'\', path_separator])
	if path == path_separator {
		return path_separator
	}
	if path.ends_with(path_separator) {
		path2 := path[..path.len - 1]
		pos := path2.last_index(path_separator) or { return path2.clone() }
		return path2[pos + 1..]
	}
	pos := path.last_index(path_separator) or { return path.clone() }
	return path[pos + 1..]
}

// file_name will return all characters found after the last occurence of `path_separator`.
// file extension is included.
pub fn file_name(opath string) string {
	path := opath.replace_each(['/', path_separator, r'\', path_separator])
	return path.all_after_last(path_separator)
}

// input_opt returns a one-line string from stdin, after printing a prompt.
// In the event of error (end of input), it returns `none`.
pub fn input_opt(prompt string) ?string {
	print(prompt)
	flush()
	res := get_raw_line()
	if res.len > 0 {
		return res.trim_right('\r\n')
	}
	return none
}

// input returns a one-line string from stdin, after printing a prompt.
// In the event of error (end of input), it returns '<EOF>'.
pub fn input(prompt string) string {
	res := input_opt(prompt) or { return '<EOF>' }
	return res
}

// get_line returns a one-line string from stdin
pub fn get_line() string {
	str := get_raw_line()
	$if windows {
		return str.trim_right('\r\n')
	}
	return str.trim_right('\n')
}

// get_lines returns an array of strings read from from stdin.
// reading is stopped when an empty line is read.
pub fn get_lines() []string {
	mut line := ''
	mut inputstr := []string{}
	for {
		line = get_line()
		if line.len <= 0 {
			break
		}
		line = line.trim_space()
		inputstr << line
	}
	return inputstr
}

// get_lines_joined returns a string of the values read from from stdin.
// reading is stopped when an empty line is read.
pub fn get_lines_joined() string {
	mut line := ''
	mut inputstr := ''
	for {
		line = get_line()
		if line.len <= 0 {
			break
		}
		line = line.trim_space()
		inputstr += line
	}
	return inputstr
}

// get_raw_lines_joined reads *all* input lines from stdin.
// It returns them as one large string. NB: unlike os.get_lines_joined,
// empty lines (that contain only `\r\n` or `\n`), will be present in
// the output.
// Reading is stopped, only on EOF of stdin.
pub fn get_raw_lines_joined() string {
	mut line := ''
	mut lines := []string{}
	for {
		line = get_raw_line()
		if line.len <= 0 {
			break
		}
		lines << line
	}
	res := lines.join('')
	return res
}

// user_os returns current user operating system name.
pub fn user_os() string {
	$if linux {
		return 'linux'
	}
	$if macos {
		return 'macos'
	}
	$if windows {
		return 'windows'
	}
	$if freebsd {
		return 'freebsd'
	}
	$if openbsd {
		return 'openbsd'
	}
	$if netbsd {
		return 'netbsd'
	}
	$if dragonfly {
		return 'dragonfly'
	}
	$if android {
		return 'android'
	}
	$if solaris {
		return 'solaris'
	}
	$if haiku {
		return 'haiku'
	}
	$if serenity {
		return 'serenity'
	}
	$if vinix {
		return 'vinix'
	}
	return 'unknown'
}

// home_dir returns path to the user's home directory.
pub fn home_dir() string {
	$if windows {
		return getenv('USERPROFILE')
	} $else {
		// println('home_dir() call')
		// res:= os.getenv('HOME')
		// println('res="$res"')
		return getenv('HOME')
	}
}

// expand_tilde_to_home expands the character `~` in `path` to the user's home directory.
// See also `home_dir()`.
pub fn expand_tilde_to_home(path string) string {
	if path == '~' {
		return home_dir().trim_right(path_separator)
	}
	if path.starts_with('~' + path_separator) {
		return path.replace_once('~' + path_separator, home_dir().trim_right(path_separator) +
			path_separator)
	}
	return path
}

// write_file writes `text` data to a file in `path`.
pub fn write_file(path string, text string) ? {
	mut f := create(path) ?
	unsafe { f.write_full_buffer(text.str, usize(text.len)) ? }
	f.close()
}

// executable_fallback is used when there is not a more platform specific and accurate implementation.
// It relies on path manipulation of os.args[0] and os.wd_at_startup, so it may not work properly in
// all cases, but it should be better, than just using os.args[0] directly.
fn executable_fallback() string {
	if args.len == 0 {
		// we are early in the bootstrap, os.args has not been initialized yet :-|
		return ''
	}
	mut exepath := args[0]
	$if windows {
		if !exepath.contains('.exe') {
			exepath += '.exe'
		}
	}
	if !is_abs_path(exepath) {
		rexepath := exepath.replace_each(['/', path_separator, r'\', path_separator])
		if rexepath.contains(path_separator) {
			exepath = join_path_single(os.wd_at_startup, exepath)
		} else {
			// no choice but to try to walk the PATH folders :-| ...
			foundpath := find_abs_path_of_executable(exepath) or { '' }
			if foundpath.len > 0 {
				exepath = foundpath
			}
		}
	}
	exepath = real_path(exepath)
	return exepath
}

pub struct ExecutableNotFoundError {
	Error
}

pub fn (err ExecutableNotFoundError) msg() string {
	return 'os: failed to find executable'
}

fn error_failed_to_find_executable() IError {
	return IError(&ExecutableNotFoundError{})
}

// find_exe_path walks the environment PATH, just like most shell do, it returns
// the absolute path of the executable if found
pub fn find_abs_path_of_executable(exepath string) ?string {
	if exepath == '' {
		return error('expected non empty `exepath`')
	}
	if is_abs_path(exepath) {
		return real_path(exepath)
	}
	mut res := ''
	path := getenv('PATH')
	paths := path.split(path_delimiter)
	for p in paths {
		found_abs_path := join_path_single(p, exepath)
		if exists(found_abs_path) && is_executable(found_abs_path) {
			res = found_abs_path
			break
		}
	}
	if res.len > 0 {
		return real_path(res)
	}
	return error_failed_to_find_executable()
}

// exists_in_system_path returns `true` if `prog` exists in the system's PATH
pub fn exists_in_system_path(prog string) bool {
	find_abs_path_of_executable(prog) or { return false }
	return true
}

// is_file returns a `bool` indicating whether the given `path` is a file.
pub fn is_file(path string) bool {
	return exists(path) && !is_dir(path)
}

// is_abs_path returns `true` if `path` is absolute.
pub fn is_abs_path(path string) bool {
	if path.len == 0 {
		return false
	}
	$if windows {
		return path[0] == `/` || // incase we're in MingGW bash
		(path[0].is_letter() && path.len > 1 && path[1] == `:`)
	}
	return path[0] == `/`
}

// join_path returns a path as string from input string parameter(s).
[manualfree]
pub fn join_path(base string, dirs ...string) string {
	// TODO: fix freeing of `dirs` when the passed arguments are variadic,
	// but do not free the arr, when `os.join_path(base, ...arr)` is called.
	mut sb := strings.new_builder(base.len + dirs.len * 50)
	defer {
		unsafe { sb.free() }
	}
	sbase := base.trim_right('\\/')
	defer {
		unsafe { sbase.free() }
	}
	sb.write_string(sbase)
	for d in dirs {
		sb.write_string(path_separator)
		sb.write_string(d)
	}
	return sb.str()
}

// join_path_single appends the `elem` after `base`, using a platform specific
// path_separator.
[manualfree]
pub fn join_path_single(base string, elem string) string {
	// TODO: deprecate this and make it `return os.join_path(base, elem)`,
	// when freeing variadic args vs ...arr is solved in the compiler
	mut sb := strings.new_builder(base.len + elem.len + 1)
	defer {
		unsafe { sb.free() }
	}
	sbase := base.trim_right('\\/')
	defer {
		unsafe { sbase.free() }
	}
	sb.write_string(sbase)
	sb.write_string(path_separator)
	sb.write_string(elem)
	return sb.str()
}

// walk_ext returns a recursive list of all files in `path` ending with `ext`.
pub fn walk_ext(path string, ext string) []string {
	mut res := []string{}
	impl_walk_ext(path, ext, mut res)
	return res
}

fn impl_walk_ext(path string, ext string, mut out []string) {
	if !is_dir(path) {
		return
	}
	mut files := ls(path) or { return }
	separator := if path.ends_with(path_separator) { '' } else { path_separator }
	for file in files {
		if file.starts_with('.') {
			continue
		}
		p := path + separator + file
		if is_dir(p) && !is_link(p) {
			impl_walk_ext(p, ext, mut out)
		} else if file.ends_with(ext) {
			out << p
		}
	}
}

// walk traverses the given directory `path`.
// When a file is encountred it will call the
// callback function `f` with current file as argument.
pub fn walk(path string, f fn (string)) {
	if path.len == 0 {
		return
	}
	if !is_dir(path) {
		return
	}
	mut files := ls(path) or { return }
	mut local_path_separator := path_separator
	if path.ends_with(path_separator) {
		local_path_separator = ''
	}
	for file in files {
		p := path + local_path_separator + file
		if is_dir(p) && !is_link(p) {
			walk(p, f)
		} else if exists(p) {
			f(p)
		}
	}
	return
}

// FnWalkContextCB is used to define the callback functions, passed to os.walk_context
pub type FnWalkContextCB = fn (voidptr, string)

// walk_with_context traverses the given directory `path`.
// For each encountred file, it will call your `fcb` callback,
// passing it the arbitrary `context` in its first parameter,
// and the path to the file in its second parameter.
pub fn walk_with_context(path string, context voidptr, fcb FnWalkContextCB) {
	if path.len == 0 {
		return
	}
	if !is_dir(path) {
		return
	}
	mut files := ls(path) or { return }
	mut local_path_separator := path_separator
	if path.ends_with(path_separator) {
		local_path_separator = ''
	}
	for file in files {
		p := path + local_path_separator + file
		if is_dir(p) && !is_link(p) {
			walk_with_context(p, context, fcb)
		} else {
			fcb(context, p)
		}
	}
	return
}

// log will print "os.log: "+`s` ...
pub fn log(s string) {
	println('os.log: ' + s)
}

// mkdir_all will create a valid full path of all directories given in `path`.
pub fn mkdir_all(opath string) ? {
	path := opath.replace('/', path_separator)
	mut p := if path.starts_with(path_separator) { path_separator } else { '' }
	path_parts := path.trim_left(path_separator).split(path_separator)
	for subdir in path_parts {
		p += subdir + path_separator
		if exists(p) && is_dir(p) {
			continue
		}
		mkdir(p) or { return error('folder: $p, error: $err') }
	}
}

// cache_dir returns the path to a *writable* user specific folder, suitable for writing non-essential data.
pub fn cache_dir() string {
	// See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
	// There is a single base directory relative to which user-specific non-essential
	// (cached) data should be written. This directory is defined by the environment
	// variable $XDG_CACHE_HOME.
	// $XDG_CACHE_HOME defines the base directory relative to which user specific
	// non-essential data files should be stored. If $XDG_CACHE_HOME is either not set
	// or empty, a default equal to $HOME/.cache should be used.
	xdg_cache_home := getenv('XDG_CACHE_HOME')
	if xdg_cache_home != '' {
		return xdg_cache_home
	}
	cdir := join_path_single(home_dir(), '.cache')
	if !is_dir(cdir) && !is_link(cdir) {
		mkdir(cdir) or { panic(err) }
	}
	return cdir
}

// temp_dir returns the path to a folder, that is suitable for storing temporary files.
pub fn temp_dir() string {
	mut path := getenv('TMPDIR')
	$if windows {
		if path == '' {
			// TODO see Qt's implementation?
			// https://doc.qt.io/qt-5/qdir.html#tempPath
			// https://github.com/qt/qtbase/blob/e164d61ca8263fc4b46fdd916e1ea77c7dd2b735/src/corelib/io/qfilesystemengine_win.cpp#L1275
			path = getenv('TEMP')
			if path == '' {
				path = getenv('TMP')
			}
			if path == '' {
				path = 'C:/tmp'
			}
		}
	}
	$if macos {
		// avoid /var/folders/6j/cmsk8gd90pd.... on macs
		return '/tmp'
	}
	$if android {
		// TODO test+use '/data/local/tmp' on Android before using cache_dir()
		if path == '' {
			path = cache_dir()
		}
	}
	if path == '' {
		path = '/tmp'
	}
	return path
}

fn default_vmodules_path() string {
	hdir := home_dir()
	res := join_path_single(hdir, '.vmodules')
	return res
}

// vmodules_dir returns the path to a folder, where v stores its global modules.
pub fn vmodules_dir() string {
	paths := vmodules_paths()
	if paths.len > 0 {
		return paths[0]
	}
	return default_vmodules_path()
}

// vmodules_paths returns a list of paths, where v looks up for modules.
// You can customize it through setting the environment variable VMODULES
// [manualfree]
pub fn vmodules_paths() []string {
	mut path := getenv('VMODULES')
	if path == '' {
		// unsafe { path.free() }
		path = default_vmodules_path()
	}
	defer {
		// unsafe { path.free() }
	}
	splitted := path.split(path_delimiter)
	defer {
		// unsafe { splitted.free() }
	}
	mut list := []string{cap: splitted.len}
	for i in 0 .. splitted.len {
		si := splitted[i]
		trimmed := si.trim_right(path_separator)
		list << trimmed
		// unsafe { trimmed.free() }
		// unsafe { si.free() }
	}
	return list
}

// resource_abs_path returns an absolute path, for the given `path`.
// (the path is expected to be relative to the executable program)
// See https://discordapp.com/channels/592103645835821068/592294828432424960/630806741373943808
// It gives a convenient way to access program resources like images, fonts, sounds and so on,
// *no matter* how the program was started, and what is the current working directory.
[manualfree]
pub fn resource_abs_path(path string) string {
	exe := executable()
	dexe := dir(exe)
	mut base_path := real_path(dexe)
	vresource := getenv('V_RESOURCE_PATH')
	if vresource.len != 0 {
		unsafe { base_path.free() }
		base_path = vresource
	}
	fp := join_path_single(base_path, path)
	res := real_path(fp)
	unsafe {
		fp.free()
		vresource.free()
		base_path.free()
		dexe.free()
		exe.free()
	}
	return res
}

pub struct Uname {
pub mut:
	sysname  string
	nodename string
	release  string
	version  string
	machine  string
}

pub fn execute_or_panic(cmd string) Result {
	res := execute(cmd)
	if res.exit_code != 0 {
		eprintln('failed    cmd: $cmd')
		eprintln('failed   code: $res.exit_code')
		panic(res.output)
	}
	return res
}

pub fn execute_or_exit(cmd string) Result {
	res := execute(cmd)
	if res.exit_code != 0 {
		eprintln('failed    cmd: $cmd')
		eprintln('failed   code: $res.exit_code')
		eprintln(res.output)
		exit(1)
	}
	return res
}

// quoted path - return a quoted version of the path, depending on the platform.
pub fn quoted_path(path string) string {
	$if windows {
		return '"$path"'
	} $else {
		return "'$path'"
	}
}