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

#include <ctype.h>
#include <dirent.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 "MyLinkedList.h"
#include "print.h"
#include "utils.h"

int
ls_count_dirs(LinkedList* entries)
{
	int count = 0;
	Node* node = entries->head;
	while (node != NULL)
	{
		file_entry_t* entry = (file_entry_t*)node->data;
		if (S_ISDIR(entry->statbuf.st_mode))
		{
			count++;
		}
		node = node->next;
	}
	return count;
}

void
ls_sort_entries(LinkedList* entries)
{
	if (ls_options.sort_none)
	{
		return;
	}

	if (ls_options.sort_size)
	{
		list_sort(entries, compare_file_size);
	}
	else if (ls_options.sort_time)
	{
		switch (ls_options.time_field)
		{
		case 'c':
			list_sort(entries, compare_by_status_change_time);
			break;

		case 'u':
			list_sort(entries, compare_by_access_time);
			break;

		case 'm':
		default:
			list_sort(entries, compare_by_modification_time);
			break;
		}
	}


	if (!ls_options.sort_size && !ls_options.sort_time)
	{
		list_sort(entries, compare_by_lexicographic);
	}

	if (ls_options.sort_reverse)
	{
		list_reverse(entries);
	}
}

int
compare_by_lexicographic(void* a, void* b)
{
	const file_entry_t* fa = (const file_entry_t*)a;
	const file_entry_t* fb = (const file_entry_t*)b;

	return strcmp(fa->filename, fb->filename);
}

int
compare_file_size(void* a, void* b)
{
	file_entry_t* fa = (file_entry_t*)a;
	file_entry_t* fb = (file_entry_t*)b;

	if (fa->statbuf.st_size > fb->statbuf.st_size)
	{
		return -1;
	}
	if (fa->statbuf.st_size < fb->statbuf.st_size)
	{
		return 1;
	}
	/* Man page does not explicitly specify that if two files are of 
	 * the same size that we should default to lexicographic. 
	 * So in line with the spec, we shant.
	 */
	return 0;
}

int
compare_by_access_time(void* a, void* b)
{
	file_entry_t* fa = (file_entry_t*)a;
	file_entry_t* fb = (file_entry_t*)b;

	if (fa->statbuf.st_atime > fb->statbuf.st_atime)
	{
		return -1;
	}
	if (fa->statbuf.st_atime < fb->statbuf.st_atime)
	{
		return 1;
	}
	return compare_by_lexicographic(a, b);
}

int
compare_by_status_change_time(void* a, void* b)
{
	file_entry_t* fa = (file_entry_t*)a;
	file_entry_t* fb = (file_entry_t*)b;

	if (fa->statbuf.st_ctime > fb->statbuf.st_ctime)
	{
		return -1;
	}
	if (fa->statbuf.st_ctime < fb->statbuf.st_ctime)
	{
		return 1;
	}
	return compare_by_lexicographic(a, b);
}

int
compare_by_modification_time(void* a, void* b)
{
	file_entry_t* fa = (file_entry_t*)a;
	file_entry_t* fb = (file_entry_t*)b;

	if (fa->statbuf.st_mtime > fb->statbuf.st_mtime)
	{
		return -1;
	}
	if (fa->statbuf.st_mtime < fb->statbuf.st_mtime)
	{
		return 1;
	}

	/* 
	 * -t "Sort by time modified (most recently modified first) 
	 * **BEFORE** sorting the operands by lexicographical order."
	 */
	return compare_by_lexicographic(a, b);
}

int
f_entry_is_dir(void* entry)
{
	file_entry_t* file_entry = (file_entry_t*)entry;
	return S_ISDIR(file_entry->statbuf.st_mode);
}

int
f_entry_isnt_dir(void* entry)
{
	file_entry_t* file_entry = (file_entry_t*)entry;
	return !S_ISDIR(file_entry->statbuf.st_mode);
}

void
free_ls_entry(void* entry)
{
	file_entry_t* file_entry = (file_entry_t*)entry;
	free(file_entry->pathname);
	free(file_entry->filename);
	free(file_entry);
}

long
get_blocks_from_sectors(struct stat* st)
{
	long blocks_used = st->st_blocks;
	long block_size_pref = ls_options.block_size;
	long actual_bytes_used = st->st_size;

	if (ls_options.reports_size_in_kilobytes)
	{
		block_size_pref = 1024;
	}

	/* -h flag specifies it should be in bytes */
	if (ls_options.report_size_nicely)
	{
		return actual_bytes_used;
	}
	return (((blocks_used * BLOCKSIZE_DEFAULT) + block_size_pref - 1) /
	        block_size_pref);
}

long
get_total_dir_blocks(LinkedList* entries)
{
	long total = 0;
	Node* node = entries->head;
	while (node != NULL)
	{
		file_entry_t* entry = (file_entry_t*)node->data;
		total += ls_options.report_size_nicely ? entry->statbuf.st_size
		                                       : entry->statbuf.st_blocks;
		node = node->next;
	}

	if (ls_options.report_size_nicely)
	{
		return total;
	}

	long block_size_pref = ls_options.block_size;
	if (ls_options.reports_size_in_kilobytes)
	{
		block_size_pref = 1024;
	}

	return (((total * BLOCKSIZE_DEFAULT) + block_size_pref - 1) /
	        block_size_pref);
}

ssize_t
read_link_crawl(const char* linkpath, char* target, size_t target_size)
{
	const char* ptr = linkpath;
	char next_dir_buf[PATH_MAX];
	char cwd[PATH_MAX];
	ssize_t len = -1;

	if (getcwd(cwd, sizeof(cwd)) == NULL)
	{
		fprintf(stderr, "%s: could not get current working directory: %s\n",
		        getprogname(), strerror(errno));
		return -1;
	}

	/* skip the initial ./ */
	if (ptr[0] == '.' && ptr[1] == '/')
	{
		ptr += 2;
	}

	while (*ptr != '\0')
	{
		const char* start = ptr;
		while (*ptr != '\0' && *ptr != '/')
		{
			ptr++;
		}

		/* build the next path */
		int count = ptr - start;
		next_dir_buf[0] = '\0';
		strncat(next_dir_buf, "./", strlen("./") + 1);
		strncat(next_dir_buf, start, count);

		if (*ptr == '\0')
		{
			len = readlink(next_dir_buf, target, target_size - 1);
			if (len == -1)
			{
				fprintf(stderr, "%s: readlink failed on '%s': %s\n",
				        getprogname(), next_dir_buf, strerror(errno));
				goto exit;
			}
			target[len] = '\0';
			goto exit;
		}

		if (chdir(next_dir_buf) == -1)
		{
			fprintf(stderr, "%s: could not chdir to '%s': %s\n", getprogname(),
			        next_dir_buf, strerror(errno));
			goto exit;
		}

		if (*ptr == '/')
		{
			ptr++;
		}
	}

exit:
	if (chdir(cwd) == -1)
	{
		fprintf(stderr,
		        "%s: could not return to original directory '%s': %s. Cannot "
		        "proceed.\n",
		        getprogname(), cwd, strerror(errno));
		exit(EXIT_FAILURE);
	}

	return len;
}

DIR*
opendir_cd_crawl(const char* dirpath, int* status)
{
	const char* ptr = dirpath;
	char next_dir_buf[PATH_MAX];
	char cwd[PATH_MAX];
	DIR* ret_val = NULL;
	struct stat st;

	*status = 0;

	if (getcwd(cwd, sizeof(cwd)) == NULL)
	{
		fprintf(stderr, "%s: could not get current working directory: %s\n",
		        getprogname(), strerror(errno));
		return NULL;
	}

	/* skip the initial ./ */
	if (ptr[0] == '.' && ptr[1] == '/')
	{
		ptr += 2;
	}

	while (*ptr != '\0')
	{
		const char* start = ptr;
		while (*ptr != '\0' && *ptr != '/')
		{
			ptr++;
		}

		/* build the next path */
		int count = ptr - start;
		next_dir_buf[0] = '\0';
		strncat(next_dir_buf, "./", strlen("./") + 1);
		strncat(next_dir_buf, start, count);

		if (lstat(next_dir_buf, &st) == -1)
		{
			fprintf(stderr, "%s: lstat failed on '%s': %s\n", getprogname(),
			        next_dir_buf, strerror(errno));
			goto exit;
		}

		if (S_ISLNK(st.st_mode))
		{
			*status = SYMLINK_ENCOUNTERED;
			goto exit;
		}

		if (chdir(next_dir_buf) == -1)
		{
			fprintf(stderr, "%s: could not chdir to '%s': %s\n", getprogname(),
			        next_dir_buf, strerror(errno));
			goto exit;
		}

		if (*ptr == '/')
		{
			ptr++;
		}
	}

	if ((ret_val = opendir(".")) == NULL)
	{
		fprintf(stderr, "%s: could not opendir at end of crawl: %s\n",
		        getprogname(), strerror(errno));
	}

exit:
	if (chdir(cwd) == -1)
	{
		fprintf(stderr,
		        "%s: could not return to original directory '%s': %s. Cannot "
		        "proceed.\n",
		        getprogname(), cwd, strerror(errno));
		exit(EXIT_FAILURE);
	}

	return ret_val;
}

int
lstat_cd_crawl(const char* dirpath, struct stat* st)
{
	char cwd[PATH_MAX];
	char next_dir_buf[PATH_MAX];
	char last_entry[NAME_MAX + 1];
	const char* ptr = dirpath;
	int ret_val = -1;

	if (getcwd(cwd, sizeof(cwd)) == NULL)
	{
		fprintf(stderr, "%s: could not get current working directory: %s\n",
		        getprogname(), strerror(errno));
		return ret_val;
	}

	/* skip the initial ./ */
	if (ptr[0] == '.' && ptr[1] == '/')
	{
		ptr += 2;
	}

	while (*ptr != '\0')
	{
		const char* start = ptr;
		while (*ptr != '\0' && *ptr != '/')
		{
			ptr++;
		}
		int count = ptr - start;

		if (*ptr == '\0')
		{
			if (count > NAME_MAX)
			{
				count = NAME_MAX;
			}
			strncpy(last_entry, start, count);
			last_entry[count] = '\0';
			break;
		}

		/* build the next path */
		next_dir_buf[0] = '\0';
		strncat(next_dir_buf, "./", strlen("./") + 1);
		strncat(next_dir_buf, start, count);

		if (chdir(next_dir_buf) == -1)
		{
			fprintf(stderr, "%s: could not chdir to %s: %s\n", getprogname(),
			        next_dir_buf, strerror(errno));
			goto exit;
		}

		if (*ptr == '/')
		{
			ptr++;
		}
	}

	if (lstat(last_entry, st) == -1)
	{
		fprintf(stderr, "%s: could not lstat '%s': %s\n", getprogname(),
		        last_entry, strerror(errno));
		goto exit;
	}
	else
	{
		/* Success */
		ret_val = 0;
	}

exit:
	if (chdir(cwd) == -1)
	{
		fprintf(stderr,
		        "%s: could not return to original directory '%s': %s. Cannot "
		        "proceed.\n",
		        getprogname(), cwd, strerror(errno));
		exit(EXIT_FAILURE);
	}
	return ret_val;
}