#include <sys/stat.h>
#include <sys/types.h>

#include <ctype.h>
#include <errno.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "dir_management.h"
#include "ls.h"
#include "print.h"
#include "utils.h"

/* Calls print_entry with a void* argument to a file_entry_t */
void
reg_print_entry(void* ventry)
{
	file_entry_t* entry = (file_entry_t*)ventry;
	print_entry(entry->pathname, &entry->statbuf);
	return;
}

/*
 * Prints the string, replacing unprintable characters with '?' when
 * appropriate, otherwise prints raw
 */
void
force_printf(const char* str, int size)
{
	if (str == NULL || size <= 0)
	{
		return;
	}

	char buf[size];
	for (int i = 0; i < size; i++)
	{
		/* isprint yells if not unsigned */
		unsigned char c = (unsigned char)str[i];
		buf[i] = (ls_options.nonprint_mode == 'q' && !isprint(c)) ? '?' : c;
	}

	for (int i = 0; i < size; i++)
	{
		printf("%c", buf[i]);
	}

	return;
}

/*
 * Classifies a file based on its mode_t and returns the appropriate
 * suffix character, or '\0' if no suffix is appropriate.
 */

char
classify(mode_t mode)
{
	if (S_ISDIR(mode))
	{
		return '/';
	}
	if (S_ISLNK(mode))
	{
		return '@';
	}
	if (S_ISSOCK(mode))
	{
		return '=';
	}
	if (S_ISFIFO(mode))
	{
		return '|';
	}
#ifdef S_WHITEOUT
	if (S_ISWHT(mode))
	{
		return '%';
	}
#endif
	if (S_ISREG(mode) && (mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
	{
		return '*';
	}
	return '\0';
}

/*
 * Shows the mode_t permission string and other information like links, owner,
 * group, size (or major/minor for device files), modification time, name,
 * and classification suffix. Also prints the target of symlinks if applicable.
 */

void
print_entry(const char* name, struct stat* st)
{
	char perms[11]; /* type + rwxrwxrwx + null terminator for printing */

	/*
	 * the format string being used (" %b %d %H:%M:%S %Y") can produce
	 * characters in the 25-30 character range (more or less depending
	 * on locale), so a buffer of 64 is about double that
	 * (and a multiple of 2!), so it should be safe.
	 */

	char timebuf[64];
	struct tm lt;
	char suf;

	if (ls_options.show_inode_number)
	{
		printf("%lu ", (unsigned long)st->st_ino);
	}
	if (ls_options.show_blocks_used)
	{
		char nice_size_buf[BUFSIZ];
		/*
		 * ls -shl SPECIFICALLY has this type of logic,
		 * which is "display bytes used on disk"
		 */
		long blocks_or_bytes =
			ls_options.long_format && ls_options.report_size_nicely
				? (st->st_blocks * ls_options.block_size)
				: get_blocks_from_sectors(st);
		if (ls_options.report_size_nicely)
		{

			if (humanize_number(nice_size_buf, HUMANIZE_BUFSIZ, blocks_or_bytes,
			                    "", HN_AUTOSCALE, HUMANIZE_OPTIONS) == -1)
			{
				fprintf(stderr,
				        "%s: Warning - could not humanize entry's blocks / "
				        "bytes.\n",
				        getprogname());
				goto humanize_fallback;
			}
		}
		else
		{
		humanize_fallback:
			snprintf(nice_size_buf, sizeof(nice_size_buf), "%ld",
			         blocks_or_bytes);
		}

		printf("%s ", nice_size_buf);
	}

	/* Long format case handled seperately down below */
	if (!ls_options.long_format)
	{
		/*
		 * Manually stripping just in case basename affects the source
		 * string, which according to the man page it doesn't, but still,
		 * just in case.
		 */
		const char* basename = strrchr(name, '/');
		basename = basename ? basename + 1 : name;

		force_printf(basename, strlen(basename));

		if (ls_options.classify_special_files)
		{
			suf = classify(st->st_mode);
			if (suf)
			{
				printf("%c", suf);
			}
		}

		printf("\n");
		return;
	}

	strmode(st->st_mode, perms);

	/* skips leading ./ for -l */
	const char* displayname = name;
	const char* base = strrchr(name, '/');
	displayname = base ? base + 1 : name;

	/* Start checking for group names */
	struct passwd* pw = NULL;
	struct group* gr = NULL;

	if (ls_options.long_format == 'l')
	{
		pw = getpwuid(st->st_uid);
		gr = getgrgid(st->st_gid);
	}

	long gu_name_bufsiz = sysconf(_SC_LOGIN_NAME_MAX);
	if (gu_name_bufsiz == -1)
	{
		gu_name_bufsiz = BUFSIZ; /* Fallback value just to be safe */
	}
	char uname_buf[gu_name_bufsiz];
	char gname_buf[gu_name_bufsiz];

	const char* uname =
		pw ? pw->pw_name
		   : (snprintf(uname_buf, sizeof(uname_buf), "%u", st->st_uid),
	          uname_buf);

	const char* gname =
		gr ? gr->gr_name
		   : (snprintf(gname_buf, sizeof(gname_buf), "%u", st->st_gid),
	          gname_buf);
	/* End checking for group names */

	time_t display_time;
	switch (ls_options.time_field)
	{
	case 'c':
		display_time = st->st_ctime;
		break;
	case 'u':
		display_time = st->st_atime;
		break;
	case 'm':
	default:
		display_time = st->st_mtime;
		break;
	}

	if (localtime_r(&display_time, &lt) == NULL)
	{
		fprintf(stderr, "%s: localtime_r failed: %s\n", getprogname(),
		        strerror(errno));
		return;
	}

	if (strftime(timebuf, sizeof timebuf, " %b %d %H:%M", &lt) == 0)
	{
		fprintf(stderr, "%s: strftime failed: %s\n", getprogname(),
		        strerror(errno));
		return;
	}

	/* remove leading space from the error check */
	memmove(timebuf, timebuf + 1, strlen(timebuf));

	char size_or_devnums[BUFSIZ];

	if (S_ISCHR(st->st_mode) || S_ISBLK(st->st_mode))
	{
		snprintf(size_or_devnums, sizeof(size_or_devnums), "%u,%u",
		         major(st->st_rdev), minor(st->st_rdev));
	}
	else
	{
		if (ls_options.report_size_nicely)
		{
			/*
			 * We want the function to think the array is max
			 * HUMANIZE_BUFSIZ, not BUFSIZ, bytes so it condenses the number
			 * accordingly. It has been my experience that the output is
			 * dependent on the passed size.
			 */
			if (humanize_number(size_or_devnums, HUMANIZE_BUFSIZ, st->st_size,
			                    "", HN_AUTOSCALE, HUMANIZE_OPTIONS) == -1)
			{
				fprintf(stderr,
				        "%s: Warning - could not humanize entry's blocks / "
				        "bytes.\n",
				        getprogname());
				goto humanize_fallback2;
			}
		}
		else
		{
		humanize_fallback2:
			snprintf(size_or_devnums, sizeof(size_or_devnums), "%ld",
			         st->st_size);
		}
	}

	printf("%s %lu %s %s %s %s ", perms, (unsigned long)st->st_nlink, uname,
	       gname, size_or_devnums, timebuf);

	force_printf(displayname, strlen(displayname));


	if (ls_options.classify_special_files)
	{
		suf = classify(st->st_mode);
		if (suf)
		{
			printf("%c", suf);
		}
	}

	/* if symlink, show " -> target" */
	if (S_ISLNK(st->st_mode))
	{
		char target[PATH_MAX + 1];
		ssize_t len = readlink(name, target, sizeof(target) - 1);
		if (len < 0)
		{
			if (errno != ENAMETOOLONG)
			{
				fprintf(stderr, "%s: could not readlink '%s': %s\n",
				        getprogname(), name, strerror(errno));
			}
			else
			{
				errno = 0;
				len = read_link_crawl(name, target, sizeof(target) - 1);
			}
		}

		if (len >= 0)
		{
			target[len] = '\0';
			printf(" -> ");
			force_printf(target, len);
		}
	}

	printf("\n");
}