#include <sys/stat.h>

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

void
process_directory(const char* dirpath)
{
	DIR* dir = opendir(dirpath);
	if (dir == NULL)
	{
		if (errno != ENAMETOOLONG)
		{
			fprintf(stderr, "%s: Could not open directory: %s: %s\n",
			        getprogname(), dirpath, strerror(errno));
			return;
		}

		errno = 0;
		int status = 0;

		dir = opendir_cd_crawl(dirpath, &status);
		if (status == SYMLINK_ENCOUNTERED)
		{
			return;
		}

		if (dir == NULL)
		{
			fprintf(stderr,
			        "%s: Could not open directory %s via cd crawl: %s\n",
			        getprogname(), dirpath, strerror(errno));
			return;
		}
	}

	LinkedList* entries = list_create(free_ls_entry);
	if (entries == NULL)
	{
		fprintf(stderr, "%s: Could not create list for directory entries\n",
		        getprogname());
		if (closedir(dir) == -1)
		{
			fprintf(stderr, "%s: Warning - Could not close directory %s: %s\n",
			        getprogname(), dirpath, strerror(errno));
		}
		return;
	}

	struct dirent* dent;
	while ((dent = readdir(dir)) != NULL)
	{
		/* allow hidden and dot files */
		if (ls_options.include_all_files)
		{
			/* allow all files */
		}
		else if (ls_options.include_hidden_files)
		{
			/* allow only hidden files, no dots */
			if (strcmp(dent->d_name, ".") == 0 ||
			    strcmp(dent->d_name, "..") == 0)
			{
				continue;
			}
		}
		else /* don't allow either */
		{
			if (dent->d_name[0] == '.')
			{
				continue;
			}
		}

		int dirlen = strlen(dirpath);
		int is_slash = (dirpath[dirlen - 1] != '/');
		size_t fullpath_size =
			dirlen + strlen(dent->d_name) + (is_slash ? 2 : 1);
		char* fullpath = malloc(fullpath_size);

		if (fullpath == NULL)
		{
			fprintf(stderr,
			        "%s: Malloc failed while trying to allocate memory for "
			        "full path name: %s\n",
			        getprogname(), strerror(errno));
			continue;
		}
		/* solely to address the bug where / was printed twice when traversing
		 * over the root directory */
		if (!is_slash)
		{
			snprintf(fullpath, fullpath_size, "%s%s", dirpath, dent->d_name);
		}
		else
		{
			snprintf(fullpath, fullpath_size, "%s/%s", dirpath, dent->d_name);
		}

		struct stat st;
		/* do not follow symlinks */
		if (lstat(fullpath, &st) == -1)
		{
			if (errno != ENAMETOOLONG)
			{
				fprintf(stderr, "%s: Could not stat: %s: %s\n", getprogname(),
				        fullpath, strerror(errno));
				free(fullpath);
				continue;
			}
			/* Mark as not an error, for readability than anything */
			errno = 0;


			if (lstat_cd_crawl(fullpath, &st) == -1)
			{
				fprintf(stderr, "%s: Could not stat (after crawl): %s: %s\n",
				        getprogname(), fullpath, strerror(errno));
				free(fullpath);
				continue;
			}
		}

		if (dent->d_type == DT_LNK && S_ISDIR(st.st_mode))
		{
			/*
			 * Don't follow symlinks.
			 * Prevents a weird recursion bug where the traversing/printing,
			 * which looks like:
			 * a/
			 * a/a
			 * a/a/b
			 * a/a/ <- wrong
			 * a/ <- wrong
			 * Only applies to the cd_opendir_crawl though...
			 */

			free(fullpath);
			continue;
		}


		file_entry_t* fe = malloc(sizeof(file_entry_t));
		if (fe == NULL)
		{
			fprintf(stderr,
			        "%s: Malloc failed while trying to allocate memory for "
			        "file entry: %s\n",
			        getprogname(), strerror(errno));
			free(fullpath);
			continue;
		}

		fe->pathname = fullpath;
		fe->filename = strdup(dent->d_name);
		fe->statbuf = st;
		fe->is_cmdline_arg = 0;

		if (list_add_last(entries, fe))
		{
			fprintf(stderr, "%s: Could not add entry '%s' to the list.\n",
			        getprogname(), fe->filename);
			free_ls_entry(fe); /* also frees the fullpath malloced above */
			continue;
		}
	}

	if (closedir(dir) == -1)
	{
		fprintf(stderr, "%s: Warning - Could not close directory %s: %s\n",
		        getprogname(), dirpath, strerror(errno));
		/*
		 * We don't necessarily need to return here per say,
		 * as this directory should never be accessed again,
		 * by this process, but either way the user should
		 * probably be made aware just in case.
		 */
	}

	ls_sort_entries(entries);

	if (ls_options.long_format ||
	    (ls_options.show_blocks_used && isatty(STDOUT_FILENO)))
	{
		char nice_buf[BUFSIZ];
		long total = get_total_dir_blocks(entries);
		if (ls_options.report_size_nicely)
		{
			if (humanize_number(nice_buf, HUMANIZE_BUFSIZ, total, "",
			                    HN_AUTOSCALE, HUMANIZE_OPTIONS) == -1)
			{
				fprintf(stderr, "%s: Warning - could not humanize total.\n",
				        getprogname());
				goto humanize_fallback;
			}
		}
		else
		{
		humanize_fallback:
			snprintf(nice_buf, sizeof(nice_buf), "%ld", total);
		}
		printf("total %s\n", nice_buf);
	}

	/* Try recurse into directories */
	if (ls_options.recurse)
	{
		/*
		 * prints all files in that directory first before checking and
		 * traversing through subdirectories
		 */
		list_foreach(entries, reg_print_entry);
		list_foreach_where(entries, process_entry, f_entry_is_dir);
	}
	else
	{
		list_foreach(entries, process_entry);
	}

	/* Deallocate all the malloced memory */
	list_destroy(entries);
}

void
process_entry(void* ventry)
{
	file_entry_t* entry = (file_entry_t*)ventry;

	/* Prevent recursion into . and .. */
	if (!entry->is_cmdline_arg && (strcmp(entry->filename, ".") == 0 ||
	                               strcmp(entry->filename, "..") == 0))
	{
		if (!ls_options.recurse)
		{
			print_entry(entry->pathname, &entry->statbuf);
		}
		return;
	}

	if (S_ISDIR(entry->statbuf.st_mode) &&
	    !ls_options.directories_as_files_and_no_symlink_indir)
	{
		if (entry->is_cmdline_arg)
		{
			/* The ls -R command specifically acts different by showing
			 * the first directory name followed by a colon and newline
			 * if and only if there are > 1 valid arguments passed.
			 */
			if (ls_options.valid_arguments_passed > 1 &&
			    S_ISDIR(entry->statbuf.st_mode))
			{
				force_printf(entry->pathname, strlen(entry->pathname));
				printf(":\n");
				process_directory(entry->pathname);
				printf("\n");
			}
			else
			{
				process_directory(entry->pathname);
			}
		}
		else if (ls_options.recurse)
		{
			printf("\n\n");
			force_printf(entry->pathname, strlen(entry->pathname));
			printf(":\n");
			process_directory(entry->pathname);
		}
		else
		{
			print_entry(entry->pathname, &entry->statbuf);
		}
	}
	else
	{
		print_entry(entry->pathname, &entry->statbuf);
	}
}