/* sort.c -- functions used to sort files */

/*
 * This file is part of CliFM
 *
 * Copyright (C) 2016-2024, L. Abramovich <leo.clifm@outlook.com>
 * All rights reserved.

 * CliFM 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.
 *
 * CliFM 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 "helpers.h"

#include <string.h>
#include <unistd.h>
#include <strings.h> /* str(n)casecmp() */

#include "autocmds.h" /* update_autocmd_opts() */
#include "aux.h"      /* xatoi */
#include "checks.h"
#include "listing.h"
#include "messages.h" /* SORT_USAGE */

#define F_SORT(a, b)      ((a) == (b) ? 0 : ((a) > (b) ? 1 : -1))
#define F_SORT_DIRS(a, b) ((a) == (b) ? 0 : ((a) < (b) ? 1 : -1))

int
skip_files(const struct dirent *ent)
{
	/* In case a directory isn't reacheable, like a failed
	 * mountpoint... */
	/*  struct stat file_attrib;

	if (lstat(entry->d_name, &file_attrib) == -1) {
		fprintf(stderr, _("stat: cannot access '%s': %s\n"),
				entry->d_name, strerror(errno));
		return 0;
	} */

	if (SELFORPARENT(ent->d_name))
		return 0;

	/* Skip files matching FILTER */
	// ADD FILTER TYPE CHECK!
	if (filter.str
	&& regexec(&regex_exp, ent->d_name, 0, NULL, 0) == FUNC_SUCCESS)
		return 0;

	/* If not hidden files */
	if (conf.show_hidden == 0 && *ent->d_name == '.')
		return 0;

	return 1;
}

/* Return a pointer to the first alphanumeric character in NAME, or to the
 * first character if no alphanumeric character is found */
static inline void
skip_name_prefixes(char **name)
{
	char *s = *name;

	while (*s) {
		if (IS_ALNUM(*s))
			break;
		s++;
	}

	if (!*s)
		s = *name;

	*name = s;
}

/* Simple comparison routine for qsort()ing strings */
int
compare_strings(char **s1, char **s2)
{
#if defined(HAVE_STRCOLL)
	return strcoll(*s2, *s2);
#else
	int ret = **s1 - **s2;
	if (ret == 0)
		ret = strcmp(*s1, *s2);

	return ret;
#endif /* HAVE_STRCOLL */
}

static int
namecmp(char *s1, char *s2)
{
	if (conf.skip_non_alnum_prefix == 1) {
		skip_name_prefixes(&s1);
		skip_name_prefixes(&s2);
	}

	/* If both strings start with number, sort them as numbers, not as strings */
	if (IS_DIGIT(*s1) && IS_DIGIT(*s2)) {
		char *p1, *p2;
		const long long n1 = strtoll(s1, &p1, 10);
		const long long n2 = strtoll(s2, &p2, 10);
		if (n2 > n1)
			return -1;
		if (n2 < n1)
			return 1;
	}

	char ac = *s1, bc = *s2;

	if (!IS_UTF8_LEAD_BYTE(*s1) && !IS_UTF8_LEAD_BYTE(*s2)) {
	/* None of the strings starts with a unicode char: compare the first
	 * byte of both strings */
		if (!conf.case_sens_list) {
			ac = (char)TOLOWER(*s1);
			bc = (char)TOLOWER(*s2);
		}

		if (bc > ac)
			return -1;

		if (bc < ac)
			return 1;
	}

	if (!conf.case_sens_list || IS_UTF8_LEAD_BYTE(*s1) || IS_UTF8_LEAD_BYTE(*s2))
		return strcoll(s1, s2);

	return strcmp(s1, s2);
}

static inline int
sort_by_extension(struct fileinfo *pa, struct fileinfo *pb)
{
	char *e1 = (char *)NULL;
	char *e2 = (char *)NULL;

	if (pa->dir == 0) {
		if (pa->ext_name) {
			e1 = pa->ext_name + (pa->ext_name[1] != '\0');
		} else {
			char *p = strrchr(pa->name, '.');
			if (p && p[1])
				e1 = p + 1;
		}
	}

	if (pb->dir == 0) {
		if (pb->ext_name) {
			e2 = pb->ext_name + (pb->ext_name[1] != '\0');
		} else {
			char *p = strrchr(pb->name, '.');
			if (p && p[1])
				e2 = p + 1;
		}
	}

	if (e1 || e2) {
		if (!e1)
			return (-1);
		if (!e2)
			return 1;

		return strcasecmp(e1, e2);
	}

	return 0;
}

static inline int
sort_by_owner(struct fileinfo *pa, struct fileinfo *pb)
{
	if (pa->uid_i.name && pb->uid_i.name)
		return namecmp(pa->uid_i.name, pb->uid_i.name);

	return F_SORT(pa->uid, pb->uid);
}

static inline int
sort_by_group(struct fileinfo *pa, struct fileinfo *pb)
{
	if (pa->gid_i.name && pb->gid_i.name)
		return namecmp(pa->gid_i.name, pb->gid_i.name);

	return F_SORT(pa->gid, pb->gid);
}

static inline int
sort_by_type(struct fileinfo *pa, struct fileinfo *pb)
{
	const mode_t m1 = pa->type;
	const mode_t m2 = pb->type;

	const int e1 = (pa->type == DT_REG && pa->exec == 1);
	const int e2 = (pb->type == DT_REG && pb->exec == 1);

	if (e1 > e2)
		return 1;
	if (e2 > e1)
		return (-1);

	if (m1 > m2)
		return 1;
	if (m2 > m1)
		return (-1);

	return sort_by_extension(pa, pb);
}

int
entrycmp(const void *a, const void *b)
{
	struct fileinfo *pa = (struct fileinfo *)a;
	struct fileinfo *pb = (struct fileinfo *)b;
	int ret = 0;
	int st = conf.sort;

	if (conf.list_dirs_first == 1) {
		ret = F_SORT_DIRS(pa->dir, pb->dir);
		if (ret != 0)
			return ret;
	}

	if (conf.light_mode == 1 && !ST_IN_LIGHT_MODE(st))
		st = SNAME;

	switch (st) {
	case STSIZE: ret = F_SORT(pa->size, pb->size); break;
	case SATIME: /* fallthrough */
	case SBTIME: /* fallthrough */
	case SCTIME: /* fallthrough */
	case SMTIME: ret = F_SORT(pa->time, pb->time); break;
	case SVER: ret = xstrverscmp(pa->name, pb->name); break;
	case SEXT: ret = sort_by_extension(pa, pb); break;
	case SINO: ret = F_SORT(pa->inode, pb->inode); break;
	case SOWN: ret = sort_by_owner(pa, pb); break;
	case SGRP: ret = sort_by_group(pa, pb); break;
	case SBLK: ret = F_SORT(pa->blocks, pb->blocks); break;
	case SLNK: ret = F_SORT(pa->linkn, pb->linkn); break;
	case STYPE: ret = sort_by_type(pa, pb); break;
	default: break;
	}

	if (!ret)
		ret = namecmp(pa->name, pb->name);

	if (!conf.sort_reverse)
		return ret;

	return (-ret);
}

/* Same as alphasort, but is uses strcmp instead of sctroll, which is
 * slower. However, bear in mind that, unlike strcmp(), strcoll() is locale
 * aware. Use only with C and english locales */
int
xalphasort(const struct dirent **a, const struct dirent **b)
{
	int ret = 0;

	/* The if statements prevent strcmp from running in every
	 * call to the function (it will be called only if the first
	 * character of the two strings is the same), which makes the
	 * function faster */
	if ((*a)->d_name[0] > (*b)->d_name[0])
		ret = 1;
	else if ((*a)->d_name[0] < (*b)->d_name[0])
		ret = -1;
	else
		ret = strcmp((*a)->d_name, (*b)->d_name);

	if (!conf.sort_reverse)
		return ret;

	/* If sort_reverse, return the opposite value */
	return (-ret);
}

/* This is a modification of the alphasort function that makes it case
 * insensitive. It also sorts without taking the initial dot of hidden
 * files into account. Note that strcasecmp() isn't locale aware. Use
 * only with C and english locales */
int
alphasort_insensitive(const struct dirent **a, const struct dirent **b)
{
	const int ret = strcasecmp(((*a)->d_name[0] == '.') ? (*a)->d_name + 1
	: (*a)->d_name, ((*b)->d_name[0] == '.') ? (*b)->d_name + 1 : (*b)->d_name);

	if (!conf.sort_reverse)
		return ret;

	return (-ret);
}

char *
num_to_sort_name(const int n)
{
	switch (n) {
	case SNONE:	 return "none";
	case SNAME:  return "name";
	case STSIZE: return "size";
	case SATIME: return "atime";
	case SBTIME: return "btime";
	case SCTIME: return "ctime";
	case SMTIME: return "mtime";
	case SVER:   return "version";
	case SEXT:   return "extension";
	case SINO:   return "inode";
	case SOWN:   return "owner";
	case SGRP:   return "group";
	case SBLK:   return "blocks";
	case SLNK:   return "links";
	case STYPE:  return "type";
	default:     return "unknown";
	}
}

void
print_sort_method(void)
{
	char *name = num_to_sort_name(conf.sort);

	printf("%s%s%s%s", BOLD, name, NC,
		(conf.sort_reverse == 1) ? " [rev]" : "");

	if (conf.light_mode == 1 && !ST_IN_LIGHT_MODE(conf.sort))
		printf(_(" (not available in light mode: using %sname%s)\n"), BOLD, NC);
	else
		putchar('\n');
}

static inline void
toggle_sort_reverse(void)
{
	if (conf.sort_reverse)
		conf.sort_reverse = 0;
	else
		conf.sort_reverse = 1;
}

static inline int
re_sort_files_list(void)
{
	if (conf.autols == 0)
		return FUNC_SUCCESS;

	/* sort_switch just tells list_dir() to print a line with the current
	 * sorting order at the end of the files list. */
	sort_switch = 1;
	free_dirlist();
	const int ret = list_dir();
	sort_switch = 0;

	return ret;
}

/* If ARG is a string, write the corresponding integer to ARG itself.
 * Return zero if ARG corresponds to a valid sorting method or one
 * otherwise. */
static inline int
set_sort_by_name(char **arg)
{
	size_t i;
	for (i = 0; i <= SORT_TYPES; i++) {
		if (*(*arg) == *sort_methods[i].name
		&& strcmp(*arg, sort_methods[i].name) == 0) {
			if (conf.light_mode == 1
			&& !ST_IN_LIGHT_MODE(sort_methods[i].num)) {
				fprintf(stderr, _("st: '%s': Not available in light mode\n"),
					sort_methods[i].name);
				return FUNC_FAILURE;
			}

			*arg = xnrealloc(*arg, MAX_INT_STR, sizeof(char));
			snprintf(*arg, MAX_INT_STR, "%d", sort_methods[i].num);
			return FUNC_SUCCESS;
		}
	}

	fprintf(stdout, _("st: %s: No such sorting order\n"), *arg);
	return FUNC_FAILURE;
}

int
sort_function(char **arg)
{
	/* No argument: Just print current sorting order */
	if (!arg[1]) {
		fputs(_("Sorted by "), stdout);
		print_sort_method();
		return FUNC_SUCCESS;
	}

	/* Argument is alphanumerical string */
	if (!is_number(arg[1])) {
		if (*arg[1] == 'r' && strcmp(arg[1], "rev") == 0) {
			toggle_sort_reverse();
			return re_sort_files_list();
		}

		if (set_sort_by_name(&arg[1]) == FUNC_FAILURE)
			return FUNC_FAILURE;
	}

	/* Argument is a number */
	const int n = atoi(arg[1]);

	if (conf.light_mode == 1 && !ST_IN_LIGHT_MODE(n)) {
		fprintf(stderr, _("st: %d (%s): Not available in light mode\n"),
			n, num_to_sort_name(n));
		return FUNC_FAILURE;
	}

#ifndef ST_BTIME
	if (n == SBTIME) {
		fputs(_("st: Birth time is not available on this platform"), stderr);
		return FUNC_FAILURE;
	}
#endif /* !ST_BTIME */

	if (n >= 0 && n <= SORT_TYPES) {
		conf.sort = n;

		if (arg[2] && *arg[2] == 'r' && strcmp(arg[2], "rev") == 0)
			toggle_sort_reverse();

		update_autocmd_opts(AC_SORT);

		return re_sort_files_list();
	}

	/* If arg1 is a number but is not in the range 0-SORT_TYPES, error */
	fprintf(stderr, "%s\n", _(SORT_USAGE));
	return FUNC_FAILURE;
}
