/*
 * Copyright (C) 2012-2023 Robin Haberkorn
 *
 * 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 3 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, see .
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include 
#include 
#include 
#include 
#include 
#include 
#ifdef HAVE_WINDOWS_H
#define WIN32_LEAN_AND_MEAN
#include 
#endif
#include 
#include 
#include "sciteco.h"
#include "qreg.h"
#include "glob.h"
#include "interface.h"
#include "string-utils.h"
#include "file-utils.h"
#ifdef G_OS_WIN32
/*
 * NOTE: File attributes are represented as DWORDs in the Win32 API
 * which should be equivalent to guint32.
 */
G_STATIC_ASSERT(sizeof(DWORD) == sizeof(teco_file_attributes_t));
/*
 * NOTE: Invalid file attributes should be represented by 0xFFFFFFFF.
 */
G_STATIC_ASSERT(INVALID_FILE_ATTRIBUTES == TECO_FILE_INVALID_ATTRIBUTES);
teco_file_attributes_t
teco_file_get_attributes(const gchar *filename)
{
	return GetFileAttributes((LPCTSTR)filename);
}
void
teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs)
{
	SetFileAttributes((LPCTSTR)filename, attrs);
}
gchar *
teco_file_get_absolute_path(const gchar *path)
{
	TCHAR buf[MAX_PATH];
	return path && GetFullPathName(path, sizeof(buf), buf, NULL) ? g_strdup(buf) : NULL;
}
gboolean
teco_file_is_visible(const gchar *path)
{
	return !(GetFileAttributes((LPCTSTR)path) & FILE_ATTRIBUTE_HIDDEN);
}
#else /* !G_OS_WIN32 */
teco_file_attributes_t
teco_file_get_attributes(const gchar *filename)
{
	struct stat buf;
	return g_stat(filename, &buf) ? TECO_FILE_INVALID_ATTRIBUTES : buf.st_mode;
}
void
teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs)
{
	g_chmod(filename, attrs);
}
#ifdef G_OS_UNIX
gchar *
teco_file_get_absolute_path(const gchar *path)
{
	gchar buf[PATH_MAX];
	if (!path)
		return NULL;
	if (realpath(path, buf))
		return g_strdup(buf);
	if (g_path_is_absolute(path))
		return g_strdup(path);
	g_autofree gchar *cwd = g_get_current_dir();
	return g_build_filename(cwd, path, NULL);
}
gboolean
teco_file_is_visible(const gchar *path)
{
	g_autofree gchar *basename = g_path_get_basename(path);
	return *basename != '.';
}
#else /* !G_OS_UNIX */
#if GLIB_CHECK_VERSION(2,58,0)
/*
 * FIXME: This should perhaps be preferred on any platform.
 * But it will complicate preprocessing.
 */
gchar *
teco_file_get_absolute_path(const gchar *path)
{
	return g_canonicalize_filename(path, NULL);
}
#else /* !GLIB_CHECK_VERSION(2,58,0) */
/*
 * This will never canonicalize relative paths.
 * I.e. the absolute path will often contain
 * relative components, even if `path` exists.
 * The only exception would be a simple filename
 * not containing any "..".
 */
gchar *
teco_file_get_absolute_path(const gchar *path)
{
	if (!path)
		return NULL;
	if (g_path_is_absolute(path))
		return g_strdup(path);
	g_autofree gchar *cwd = g_get_current_dir();
	return g_build_filename(cwd, path, NULL);
}
#endif /* !GLIB_CHECK_VERSION(2,58,0) */
/*
 * There's no platform-independent way to determine if a file
 * is visible/hidden, so we just assume that all files are
 * visible.
 */
gboolean
teco_file_is_visible(const gchar *path)
{
	return TRUE;
}
#endif /* !G_OS_UNIX */
#endif /* !G_OS_WIN32 */
/**
 * Perform tilde expansion on a file name or path.
 *
 * This supports only strings with a "~" prefix.
 * A user name after "~" is not supported.
 * The $HOME environment variable/register is used to retrieve
 * the current user's home directory.
 */
gchar *
teco_file_expand_path(const gchar *path)
{
	if (!path)
		return g_strdup("");
	if (path[0] != '~' || (path[1] && !G_IS_DIR_SEPARATOR(path[1])))
		return g_strdup(path);
	/*
	 * $HOME should not have a trailing directory separator since
	 * it is canonicalized to an absolute path at startup,
	 * but this ensures that a proper path is constructed even if
	 * it does (e.g. $HOME is changed later on).
	 *
	 * FIXME: In the future, it might be possible to remove the entire register.
	 */
	teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "$HOME", 5);
	g_assert(qreg != NULL);
	/*
	 * Getting the string should not possible to fail.
	 * The $HOME register should not contain any null-bytes on startup,
	 * but it may have been changed later on.
	 */
	g_auto(teco_string_t) home = {NULL, 0};
	if (!qreg->vtable->get_string(qreg, &home.data, &home.len, NULL) ||
	    teco_string_contains(&home, '\0'))
		return g_strdup(path);
	g_assert(home.data != NULL);
	return g_build_filename(home.data, path+1, NULL);
}
/**
 * Auto-complete a filename/directory.
 *
 * @param filename The filename to auto-complete or NULL.
 * @param file_test Restrict completion to files matching the test.
 *                  If G_FILE_TEST_EXISTS, both files and directories are completed.
 *                  If G_FILE_TEST_IS_DIR, only directories will be completed.
 * @param insert String to initialize with the autocompletion.
 * @return TRUE if the completion was unambiguous (eg. command can be terminated).
 */
gboolean
teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_t *insert)
{
	memset(insert, 0, sizeof(*insert));
	if (teco_globber_is_pattern(filename))
		return FALSE;
	g_autofree gchar *filename_expanded = teco_file_expand_path(filename);
	gsize filename_len = strlen(filename_expanded);
	/*
	 * Derive base and directory names.
	 * We do not use g_path_get_basename() or g_path_get_dirname()
	 * since we need strict suffixes and prefixes of filename
	 * in order to construct paths of entries in dirname
	 * that are suitable for auto completion.
	 */
	gsize dirname_len = teco_file_get_dirname_len(filename_expanded);
	g_autofree gchar *dirname = g_strndup(filename_expanded, dirname_len);
	gchar *basename = filename_expanded + dirname_len;
	g_autoptr(GDir) dir = g_dir_open(dirname_len ? dirname : ".", 0, NULL);
	if (!dir)
		return FALSE;
	/*
	 * On Windows, both forward and backslash
	 * directory separators are allowed in directory
	 * names passed to glib.
	 * To imitate glib's behaviour, we use
	 * the last valid directory separator in `filename_expanded`
	 * to generate new separators.
	 * This also allows forward-slash auto-completion
	 * on Windows.
	 */
	const gchar *dir_sep = dirname_len ? dirname + dirname_len - 1
	                                   : G_DIR_SEPARATOR_S;
	GSList *files = NULL;
	guint files_len = 0;
	gsize prefix_len = 0;
	const gchar *cur_basename;
	while ((cur_basename = g_dir_read_name(dir))) {
		if (!g_str_has_prefix(cur_basename, basename))
			continue;
		/*
		 * NOTE: `dirname` contains any directory separator, so strcat() works here.
		 * Reserving one byte at the end of the filename ensures we can easily
		 * append the directory separator without reallocations.
		 */
		gchar *cur_filename = g_malloc(strlen(dirname)+strlen(cur_basename)+2);
		strcat(strcpy(cur_filename, dirname), cur_basename);
		/*
		 * NOTE: This avoids g_file_test() for G_FILE_TEST_EXISTS
		 * since the file we process here should always exist.
		 */
		if ((!*basename && !teco_file_is_visible(cur_filename)) ||
		    (file_test != G_FILE_TEST_EXISTS &&
		     !g_file_test(cur_filename, file_test))) {
			g_free(cur_filename);
			continue;
		}
		if (file_test == G_FILE_TEST_IS_DIR ||
		    g_file_test(cur_filename, G_FILE_TEST_IS_DIR))
			strcat(cur_filename, dir_sep);
		files = g_slist_prepend(files, cur_filename);
		if (g_slist_next(files)) {
			teco_string_t other_file;
			other_file.data = (gchar *)g_slist_next(files)->data + filename_len;
			other_file.len = strlen(other_file.data);
			gsize len = teco_string_diff(&other_file, cur_filename + filename_len,
			                             strlen(cur_filename) - filename_len);
			if (len < prefix_len)
				prefix_len = len;
		} else {
			prefix_len = strlen(cur_filename + filename_len);
		}
		files_len++;
	}
	if (prefix_len > 0) {
		teco_string_init(insert, (gchar *)files->data + filename_len, prefix_len);
	} else if (files_len > 1) {
		files = g_slist_sort(files, (GCompareFunc)g_strcmp0);
		for (GSList *file = files; file; file = g_slist_next(file)) {
			teco_popup_entry_type_t type = TECO_POPUP_DIRECTORY;
			gboolean is_buffer = FALSE;
			if (!teco_file_is_dir((gchar *)file->data)) {
				type = TECO_POPUP_FILE;
				/* FIXME: inefficient */
				is_buffer = teco_ring_find((gchar *)file->data) != NULL;
			}
			teco_interface_popup_add(type, (gchar *)file->data,
			                         strlen((gchar *)file->data), is_buffer);
		}
		teco_interface_popup_show();
	}
	/*
	 * FIXME: If we are completing only directories,
	 * we can theoretically insert the completed character
	 * after directories without subdirectories.
	 */
	gboolean unambiguous = files_len == 1 && !teco_file_is_dir((gchar *)files->data);
	g_slist_free_full(files, g_free);
	return unambiguous;
}