/* vifm
 * Copyright (C) 2001 Ken Steen.
 * Copyright (C) 2011 xaizek.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "path.h"

#ifndef _WIN32
#include <pwd.h> /* getpwnam() */
#endif
#include <unistd.h>

#include <assert.h> /* assert() */
#ifdef _WIN32
#include <ctype.h>
#endif
#include <errno.h> /* errno */
#include <stddef.h> /* NULL size_t */
#include <stdio.h>  /* snprintf() */
#include <stdlib.h> /* free() */
#include <string.h> /* memset() strcat() strcmp() strdup() strncmp() strchr()
                       strcpy() strlen() strpbrk() strrchr() */

#include "../cfg/config.h"
#include "../compat/fs_limits.h"
#include "../compat/os.h"
#include "../int/path_env.h"
#include "env.h"
#include "fs.h"
#include "str.h"
#include "utils.h"

static void make_canonic_path(const char directory[], char buf[],
		size_t buf_size, int strict_rel_paths);
static int skip_dotdir_if_any(const char *path[], int has_parent);
static const char * find_home_tail(const char path[]);
static char * try_replace_tilde(const char path[]);
static char * find_ext_dot(const char path[]);

void
chosp(char path[])
{
	size_t len;

	if(path[0] == '\0')
	{
		return;
	}

	len = strlen(path);
#ifndef _WIN32
	if(path[len - 1] == '/')
#else
	if(path[len - 1] == '/' || path[len - 1] == '\\')
#endif
	{
		path[len - 1] = '\0';
	}
}

int
ends_with_slash(const char *str)
{
	return str[0] != '\0' && str[strlen(str) - 1] == '/';
}

int
path_starts_with(const char path[], const char prefix[])
{
	size_t len = strlen(prefix);

	/* Special case for "/", because it doesn't fit general pattern (slash
	 * shouldn't be stripped or we get empty prefix). */
	if(prefix[0] == '/' && prefix[1] == '\0')
	{
		return (path[0] == '/');
	}

	if(len > 0U && prefix[len - 1] == '/')
	{
		--len;
	}

	return strnoscmp(path, prefix, len) == 0
	    && (path[len] == '\0' || path[len] == '/');
}

int
paths_are_equal(const char s[], const char t[])
{
	/* Some additional space is allocated for adding slashes. */
	char s_can[strlen(s) + 8];
	char t_can[strlen(t) + 8];

	make_canonic_path(s, s_can, sizeof(s_can), /*strict_rel_paths=*/1);
	make_canonic_path(t, t_can, sizeof(t_can), /*strict_rel_paths=*/1);

	return stroscmp(s_can, t_can) == 0;
}

void
canonicalize_path(const char directory[], char buf[], size_t buf_size)
{
	make_canonic_path(directory, buf, buf_size, /*strict_rel_paths=*/0);
}

/* Removes excess slashes, "../" and "./" from the path.  buf will always
 * contain trailing forward slash (empty result will become "./").  Non-zero
 * strict_rel_paths results in relative paths getting "./" prefix if they
 * didn't have it, which doesn't happen otherwise. */
static void
make_canonic_path(const char directory[], char buf[], size_t buf_size,
		int strict_rel_paths)
{
	/* Source string pointer. */
	const char *p = directory;
	/* Destination string pointer. */
	char *q = buf - 1;

	memset(buf, '\0', buf_size);

#ifdef _WIN32
	/* Handle first component of a UNC path. */
	if(is_unc_path(p))
	{
		strcpy(buf, "//");
		q = buf + 1;
		p += 2;
		while(*p != '\0' && *p != '/')
		{
			*++q = *p++;
		}
		buf = q + 1;
	}
	else /* note this else */
#endif
	if(strict_rel_paths && !is_path_absolute(directory))
	{
		strcpy(buf, "./");
		q += 2;
	}

	while(*p != '\0' && (size_t)((q + 1) - buf) < buf_size - 1U)
	{
		const int prev_dir_present = (q != buf - 1 && *q == '/');
		if(skip_dotdir_if_any(&p, prev_dir_present))
		{
			/* skip_dotdir_if_any() function did all the job for us. */
		}
		else if(prev_dir_present &&
				(strncmp(p, "../", 3) == 0 || strcmp(p, "..") == 0) &&
				!(ends_with(buf, "/../") || strcmp(buf, "../") == 0))
		{
			/* Remove the last path component added. */
#ifdef _WIN32
			/* Special handling of Windows disk name. */
			if(*(q - 1) != ':')
#endif
			{
				++p;
				--q;
				while(q >= buf && *q != '/')
				{
					--q;
				}
			}
		}
		else if(*p == '/')
		{
			/* Don't add more than one slash between path components.  And don't add
			 * leading slash if initial path is relative. */
			if(!prev_dir_present && !(q == buf - 1 && *directory != '/'))
			{
				*++q = '/';
			}
		}
		else
		{
			/* Copy current path component till the end (slash or end of
			 * line/buffer). */
			*++q = *p;
			while(p[1] != '\0' && p[1] != '/' &&
					(size_t)((q + 1) - buf) < buf_size - 1U)
			{
				*++q = *++p;
			}
		}

		++p;
	}

	/* If initial path is relative and we ended up with an empty path, produce
	 * "./". */
	if(*directory != '/' && q < buf && (size_t)((q + 1) - buf) < buf_size - 2U)
	{
		*++q = '.';
	}

	if((*directory != '\0' && q < buf) || (q >= buf && *q != '/'))
	{
		*++q = '/';
	}

	*++q = '\0';
}

/* Checks whether *path begins with current directory component ('./') and moves
 * *path to the last character of such component (to slash if present) or to the
 * previous of the last character depending on has_parent and following
 * contents of the path.  When has_parent is zero the function normalizes
 * '\.\.\.+/?' prefix on Windows to '\./?'.  Returns non-zero if a path
 * component was fully skipped. */
static int
skip_dotdir_if_any(const char *path[], int has_parent)
{
	const char *tail;

	size_t dot_count = 0U;
	while((*path)[dot_count] == '.')
	{
		++dot_count;
	}

#ifndef _WIN32
	if(dot_count != 1U)
#else
	if(dot_count == 0U || dot_count == 2U)
#endif
	{
		return 0;
	}

	tail = &(*path)[dot_count];
	if(tail[0] == '/' || tail[0] == '\0')
	{
		const int cut_off = (has_parent || (tail[0] == '/' && tail[1] == '.'));
		if(!cut_off)
		{
			--dot_count;
		}

		/* Possibly step back one character to do not fall out of the string. */
		*path += dot_count - ((*path)[dot_count] == '\0' ? 1 : 0);
		return cut_off;
	}
	return 0;
}

const char *
make_rel_path(const char path[], const char base[])
{
	static char buf[PATH_MAX + 1];

#ifdef _WIN32
	if(path[1] == ':' && base[1] == ':' && path[0] != base[0])
	{
		canonicalize_path(path, buf, sizeof(buf));
		return buf;
	}
#endif

	/* Attempt to resolve all symbolic links in paths, this should give both the
	 * shortest and valid relative path. */
	char path_real[PATH_MAX + 1], base_real[PATH_MAX + 1];
	if(os_realpath(path, path_real) == path_real)
	{
		path = path_real;
	}
	if(os_realpath(base, base_real) == base_real)
	{
		base = base_real;
	}

	const char *p = path, *b = base;
	int i;
	int nslashes;

	while(p[0] != '\0' && p[1] != '\0' && b[0] != '\0' && b[1] != '\0')
	{
		const char *op = p, *ob = b;
		if((p = strchr(p + 1, '/')) == NULL)
			p = path + strlen(path);
		if((b = strchr(b + 1, '/')) == NULL)
			b = base + strlen(base);
		if(p - path != b - base || strnoscmp(path, base, p - path) != 0)
		{
			p = op;
			b = ob;
			break;
		}
	}

	canonicalize_path(b, buf, sizeof(buf));
	chosp(buf);

	nslashes = 0;
	for(i = 0; buf[i] != '\0'; i++)
		if(buf[i] == '/')
			nslashes++;

	buf[0] = '\0';
	while(nslashes-- > 0)
		strcat(buf, "../");
	if(*p == '/')
		p++;
	if(*p != '\0')
	{
		canonicalize_path(p, buf + strlen(buf), sizeof(buf) - strlen(buf));
	}
	chosp(buf);

	if(buf[0] == '\0')
		strcpy(buf, ".");

	return buf;
}

int
is_path_absolute(const char *path)
{
#ifdef _WIN32
	if(isalpha(path[0]) && path[1] == ':')
		return 1;
	if(path[0] == '/' && path[1] == '/')
		return 1;
#endif
	return (path[0] == '/');
}

int
is_root_dir(const char *path)
{
#ifdef _WIN32
	if(isalpha(path[0]) && stroscmp(path + 1, ":/") == 0)
		return 1;

	if(path[0] == '/' && path[1] == '/' && path[2] != '\0')
	{
		char *p = strchr(path + 2, '/');
		if(p == NULL || p[1] == '\0')
			return 1;
	}
#endif
	return (path[0] == '/' && path[1] == '\0');
}

int
is_unc_root(const char *path)
{
#ifdef _WIN32
	if(is_unc_path(path) && path[2] != '\0')
	{
		char *p = strchr(path + 2, '/');
		if(p == NULL || p[1] == '\0')
			return 1;
	}
	return 0;
#else
	return 0;
#endif
}

char *
replace_home_part(const char path[])
{
	char *result = replace_home_part_strict(path);
	if(!is_root_dir(result))
	{
		chosp(result);
	}
	return result;
}

char *
replace_home_part_strict(const char path[])
{
	static char buf[PATH_MAX + 1];

	const char *home_tail = find_home_tail(path);
	if(home_tail == NULL)
	{
		copy_str(buf, sizeof(buf), path);
	}
	else
	{
		buf[0] = '~';
		buf[1] = '/';
		copy_str(buf + 2, sizeof(buf) - 2, home_tail);
	}

	return buf;
}

char *
make_tilde_path(const char path[])
{
	const char *home_tail = find_home_tail(path);
	if(home_tail == NULL)
	{
		return strdup(path);
	}

	return format_str("~/%s", home_tail);
}

/* Finds part of the path which follows home directory and any number of slashes
 * after it.  Returns pointer to the part or NULL if it's not present. */
static const char *
find_home_tail(const char path[])
{
	const size_t len = strlen(cfg.home_dir) - 1;

	/* Not using path_starts_with() because cfg.home_dir always ends with a slash
	 * and this way we can avoid repeating strlen(). */
	if(strnoscmp(path, cfg.home_dir, len) == 0 &&
			(path[len] == '\0' || path[len] == '/'))
	{
		return skip_char(path + len, '/');
	}

	return NULL;
}

char *
expand_tilde(const char path[])
{
	char *const expanded_path = try_replace_tilde(path);
	if(expanded_path == path)
	{
		return strdup(path);
	}
	return expanded_path;
}

char *
replace_tilde(char path[])
{
	char *const expanded_path = try_replace_tilde(path);
	if(expanded_path != path)
	{
		free(path);
	}
	return expanded_path;
}

/* Tries to expands tilde in the front of the path.  Returns the path or newly
 * allocated string without tilde. */
static char *
try_replace_tilde(const char path[])
{
	if(path[0] != '~')
	{
		return (char *)path;
	}

	if(path[1] == '\0' || path[1] == '/')
	{
		char *const result = format_str("%s%s", cfg.home_dir,
				(path[1] == '/') ? (path + 2) : "");
		return result;
	}

#ifndef _WIN32
	char user_name[NAME_MAX + 1];

	const char *p = until_first(path + 1, '/');
	int user_name_len = p - (path + 1);
	if(user_name_len + 1 > (int)sizeof(user_name))
	{
		return (char *)path;
	}

	copy_str(user_name, user_name_len + 1, path + 1);

	struct passwd *pw = getpwnam(user_name);
	if(pw == NULL)
	{
		return (char *)path;
	}

	return join_paths(pw->pw_dir, p);
#else
	return (char *)path;
#endif
}

char *
get_last_path_component(const char path[])
{
	char *slash = strrchr(path, '/');
	if(slash == NULL)
	{
		slash = (char *)path;
	}
	else if(slash[1] == '\0')
	{
		/* Skip slashes. */
		while(slash > path && slash[0] == '/')
		{
			--slash;
		}
		/* Skip until slash. */
		while(slash > path && slash[-1] != '/')
		{
			--slash;
		}
	}
	else
	{
		++slash;
	}
	return slash;
}

void
remove_last_path_component(char path[])
{
	char *slash;

	/* Get rid of trailing slashes, if any (in rather inefficient way). */
	while(ends_with_slash(path))
	{
		chosp(path);
	}

	slash = strrchr(path, '/');
	if(slash == NULL)
	{
		/* At most one item of path is left. */
		path[0] = '\0';
		return;
	}

	/* Take care to do not turn path in root into no path (should become just
	 * root). */
	slash[1] = '\0';
	if(!is_root_dir(path))
	{
		slash[0] = '\0';
	}
}

int
is_path_well_formed(const char *path)
{
#ifndef _WIN32
	return strchr(path, '/') != NULL;
#else
	return is_unc_path(path) || (strlen(path) >= 2 && path[1] == ':' &&
			drive_exists(path[0]));
#endif
}

void
ensure_path_well_formed(char *path)
{
	if(is_path_well_formed(path))
	{
		return;
	}

#ifndef _WIN32
	strcpy(path, "/");
#else
	strcpy(path, env_get("SYSTEMDRIVE"));
	strcat(path, "/");
#endif
}

void
to_canonic_path(const char path[], const char base[], char buf[],
		size_t buf_len)
{
	char normalized[PATH_MAX + 1];
	copy_str(normalized, sizeof(normalized), path);
	system_to_internal_slashes(normalized);

	if(!is_path_absolute(normalized))
	{
		char full_path[PATH_MAX*2 + 1];
		/* Assert is not level above to keep "." in curr_dir in tests, but this
		 * should be possible to change. */
		assert(is_path_absolute(base) && "Base path has to be absolute.");
		snprintf(full_path, sizeof(full_path), "%s/%s", base, normalized);
		canonicalize_path(full_path, buf, buf_len);
	}
	else
	{
		canonicalize_path(normalized, buf, buf_len);
	}

	if(!is_root_dir(buf))
	{
		chosp(buf);
	}
}

int
contains_slash(const char path[])
{
#ifndef _WIN32
	return (strchr(path, '/') != NULL);
#else
	return (strpbrk(path, "/\\") != NULL);
#endif
}

char *
find_slashr(const char *path)
{
	char *result = strrchr(path, '/');
#ifdef _WIN32
	if(result == NULL)
		result = strrchr(path, '\\');
#endif
	return result;
}

char *
cut_extension(char path[])
{
	char *e;
	char *ext;

	if((ext = strrchr(path, '.')) == NULL)
		return path + strlen(path);

	*ext = '\0';
	if((e = strrchr(path, '.')) != NULL && stroscmp(e + 1, "tar") == 0)
	{
		*ext = '.';
		ext = e;
	}
	*ext = '\0';

	return ext + 1;
}

void
split_ext(char path[], int *root_len, const char **ext_pos)
{
	char *const dot = find_ext_dot(path);

	if(dot == NULL)
	{
		const size_t len = strlen(path);

		*ext_pos = path + len;
		*root_len = len;

		return;
	}

	*dot = '\0';
	*ext_pos = dot + 1;
	*root_len = dot - path;
}

char *
get_ext(const char path[])
{
	char *const dot = find_ext_dot(path);

	if(dot == NULL)
	{
		return (char *)path + strlen(path);
	}

	return dot + 1;
}

/* Gets extension for file path.  Returns pointer to the dot or NULL if file
 * name has no extension. */
static char *
find_ext_dot(const char path[])
{
	const char *const basename = after_last(path, '/');
	char *const dot = strrchr(basename, '.');

	const int no_ext = dot == NULL
	                || dot == basename;
	return no_ext ? NULL : dot;
}

void
exclude_file_name(char path[])
{
	if(path_exists(path, DEREF) && !is_valid_dir(path))
	{
		remove_last_path_component(path);
	}
}

int
is_parent_dir(const char path[])
{
	return path[0] == '.' && path[1] == '.' &&
		((path[2] == '/' && path[3] == '\0') || path[2] == '\0');
}

int
is_builtin_dir(const char name[])
{
	return name[0] == '.'
	    && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
}

int
find_cmd_in_path(const char cmd[], size_t path_len, char path[])
{
	size_t i;
	size_t paths_count;
	char **paths;

	paths = get_paths(&paths_count);
	for(i = 0; i < paths_count; i++)
	{
		char tmp_path[PATH_MAX + 1];
		snprintf(tmp_path, sizeof(tmp_path), "%s/%s", paths[i], cmd);

		/* Need to check for executable, not just a file, as this additionally
		 * checks for path with different executable extensions on Windows. */
		if(executable_exists(tmp_path))
		{
			if(path != NULL)
			{
				copy_str(path, path_len, tmp_path);
			}
			return 0;
		}
	}
	return 1;
}

const char *
get_tmpdir(void)
{
	return env_get_one_of_def("/tmp/", "TMPDIR", "TEMP", "TEMPDIR", "TMP",
			(const char *)NULL);
}

void
build_path(char buf[], size_t buf_len, const char p1[], const char p2[])
{
	p2 = skip_char(p2, '/');
	if(p2[0] == '\0')
	{
		copy_str(buf, buf_len, p1);
	}
	else
	{
		snprintf(buf, buf_len, "%s%s%s", p1, ends_with_slash(p1) ? "" : "/", p2);
	}
}

char *
join_paths(const char p1[], const char p2[])
{
	const char *slash = (ends_with_slash(p1) ? "" : "/");
	p2 = skip_char(p2, '/');
	return format_str("%s%s%s", p1, slash, p2);
}

#ifdef _WIN32

int
is_unc_path(const char path[])
{
	return (path[0] == '/' && path[1] == '/' && path[2] != '/');
}

void
system_to_internal_slashes(char path[])
{
	replace_char(path, '\\', '/');
}

void
internal_to_system_slashes(char path[])
{
	replace_char(path, '/', '\\');
}

#endif

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
