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

#include <dirent.h>
#include <errno.h>
#include <libgen.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 "utils.h"


ls_options_t ls_options = {
	.include_hidden_files = 0,
	.include_all_files = 0,
	.long_format = '\0',
	.directories_as_files_and_no_symlink_indir = 0,
	.reports_size_in_kilobytes = 0,
	.report_size_nicely = 0,
	.sort_size = 0,
	.sort_time = 0,
	.sort_reverse = 0,
	.sort_none = 0,
	.recurse = 0,
	.show_inode_number = 0,
	.show_blocks_used = 0,
	.block_size = BLOCKSIZE_DEFAULT,
	.time_field = '\0',
	.nonprint_mode = '\0',
	.classify_special_files = 0,
	.valid_arguments_passed = 0
};

/*  ls [ −AacdFfhiklnqRrSstuw] [file ...] */
int
main(int argc, char* argv[])
{
	int opt;
	int any_flags_set = 0;

	/* Set defaults for print mode */
	ls_options.nonprint_mode = isatty(STDOUT_FILENO) ? 'q' : 'w';

	/* ':' supresses getopt's error handling (iirc from CS 392!!!) */
	while ((opt = getopt(argc, argv, ":AacdFfhiklnqRrSstuw")) != -1)
	{
		any_flags_set = 1;
		switch (opt)
		{
		/* List all entries other than '.' and '..'. Always active for super
		 * user */
		case 'A':
			ls_options.include_hidden_files = 1;
			break;

		/* Include entries whose names begin with '.' */
		case 'a':
			ls_options.include_all_files = 1;
			break;

		/* Use time when file status was last changed, not when it was last
		 * modified (for -t and -l)*/
		case 'c':
			ls_options.time_field = 'c';
			break;

		/*
		 * Directories are listed as plain files (not searched recursively) AND
		 * symbolic links in the argument list are not indirected through.
		 */
		case 'd':
			ls_options.directories_as_files_and_no_symlink_indir = 1;
			ls_options.recurse = 0;
			break;

		/* Classify files with suffixes (/ @ * etc.) */
		case 'F':
			ls_options.classify_special_files = 1;
			break;
		/* Output is not sorted. */
		case 'f':
			ls_options.sort_none = 1;
			break;

		/* Makes -s and -l reporting size in bytes. Overrides -k*/
		case 'h':
			ls_options.reports_size_in_kilobytes = 0;
			ls_options.report_size_nicely = 1;
			break;

		/* Include inode numbers in the output */
		case 'i':
			ls_options.show_inode_number = 1;
			break;

		/* Modifies -s, reports size in kilobytes. */
		case 'k':
			ls_options.reports_size_in_kilobytes = 1;
			ls_options.report_size_nicely = 0;
			break;

		/* list in long format */
		case 'l':
			ls_options.long_format = 'l';
			break;

		/* -l but owner and group ids are numbers and not strings */
		case 'n':
			ls_options.long_format = 'n';
			break;

		/* force printing of non-printable characters as '?' (default when
		 * output is not a terminal)*/
		case 'q':
			ls_options.nonprint_mode = 'q';
			break;

		/* Recurse into directories*/
		case 'R':
			ls_options.recurse = 1;
			ls_options.directories_as_files_and_no_symlink_indir = 0;
			break;

		/* Reverse the order of the sort */
		case 'r':
			ls_options.sort_reverse = 1;
			break;

		/* Sort by file size */
		case 'S':
			ls_options.sort_size = 1;
			ls_options.sort_time = 0;
			break;

		/*
		 * display system blocks used by each file in 512 bytes or BLOCKSIZE.
		 * If the output is to a terminal, a total sum for all the file sizes
		 * is output on a line before the listing.
		 */
		case 's':
			ls_options.show_blocks_used = 1;
			break;

		/* Sort by file modification time before sorting by alphabetical order
		 */
		case 't':
			ls_options.sort_size = 0;
			ls_options.sort_time = 1;
			ls_options.time_field = 'm';
			break;

		/*  Use time of last access, instead of last modification of the file
		 * for sorting ( −t ) orprinting ( −l) */
		case 'u':
			ls_options.time_field = 'u';
			break;

		/* force printing of non-printable characters (default when output is
		 * not a terminal)*/
		case 'w':
			ls_options.nonprint_mode = 'w';
			break;

		default:
			/* ls(1) seems to propagate errors in this format */
			fprintf(stderr, "%s: invalid option -- %c\n", getprogname(),
			        optopt); /* optopt to get what actually caused the error
			        and not '?' sometimes */
			return EXIT_FAILURE;
		}
	}

	/*
	 * If neither -h nor -k were specified, BLOCKSIZE enviornmental variable
	 * should be set. (Inspired by how ls(1) works)
	 */
	if (ls_options.reports_size_in_kilobytes == 0 &&
	    ls_options.report_size_nicely == 0)
	{
		int b_len;
		long block_size = 0;

		(void)getbsize(&b_len, &block_size);
		ls_options.block_size = (int)block_size;
	}


	/* TZ=US/Eastern, US/Pacific, etc.*/
	const char* tz = getenv("TZ");
	if (tz != NULL)
	{
		tzset();
	}

	/* "-A [...] Always set for the super-user" */
	if (geteuid() == 0)
	{
		ls_options.include_hidden_files = 1;
	}

	/*
	 * This linked list implementation takes a function pointer for freeing
	 * entries in the create function for what is hopefully nice and easy
	 * clean up.
	 */
	LinkedList* original_entries = list_create(free_ls_entry);
	if (original_entries == NULL)
	{
		return EXIT_FAILURE;
	}

	int failed_parse = 0;
	for (int i = any_flags_set + 1; i < argc; i++)
	{
		struct stat path_stat;

		if ((ls_options.directories_as_files_and_no_symlink_indir
		         ? lstat(argv[i], &path_stat)
		         : stat(argv[i], &path_stat)) == -1)
		{
			fprintf(stderr, "%s: '%s': %s\n", getprogname(), argv[i],
			        strerror(errno));
			failed_parse = 1;
			continue;
		}


		file_entry_t* entry = (file_entry_t*)malloc(sizeof(file_entry_t));
		if (entry == NULL)
		{
			fprintf(stderr,
			        "%s: could not allocate memory for new file entry\n",
			        getprogname());

			goto error_exit;
		}

		/*
		 * malloced memory from strdup will be
		 * cleaned up by the free_ls_entry function
		 */
		entry->pathname = strdup(argv[i]);
		entry->filename = strdup(basename(argv[i]));

		entry->statbuf = path_stat;
		entry->is_cmdline_arg = 1;

		if (entry->pathname == NULL || entry->filename == NULL)
		{
			fprintf(stderr, "%s: error duplicating strings for file entry\n",
			        getprogname());
			/*
			 * Doesn't matter that much since we're exiting, but just in
			 * case one succeeded and the other failed, free the memory
			 */
			if (entry->pathname != NULL)
			{
				free(entry->pathname);
			}

			if (entry->filename != NULL)
			{
				free(entry->filename);
			}
			/* Entry itself needs to be freed */
			free(entry);
			goto error_exit;
		}

		if (list_add_last(original_entries, entry))
		{
			fprintf(stderr, "%s: could not add file entry to list\n",
			        getprogname());

			free_ls_entry(entry);
			goto error_exit;
		}
	}

	/* If no files or directories are found, we list the current directory */
	if (list_size(original_entries) == 0)
	{
		/*
		 * If the user provided command line arguments and none of them
		 * succeeded, the error message is output enough.
		 * (Inspired by how ls(1) behaves).
		 */
		if (failed_parse)
		{
			goto error_exit;
		}

		struct stat path_stat;
		if (lstat(".", &path_stat) == -1)
		{
			fprintf(stderr, "%s: '%s': %s\n", getprogname(), ".",
			        strerror(errno));

			goto error_exit;
		}

		file_entry_t* entry = (file_entry_t*)malloc(sizeof(file_entry_t));
		if (entry == NULL)
		{
			fprintf(stderr,
			        "%s: could not allocate memory for new file entry\n",
			        getprogname());

			goto error_exit;
		}

		entry->pathname = strdup(".");
		entry->filename = strdup(".");

		entry->statbuf = path_stat;
		entry->is_cmdline_arg = 1;

		if (entry->pathname == NULL || entry->filename == NULL)
		{
			fprintf(stderr, "%s: error duplicating strings for file entry\n",
			        getprogname());

			if (entry->pathname != NULL)
			{
				free(entry->pathname);
			}

			if (entry->filename != NULL)
			{
				free(entry->filename);
			}
			/* Entry itself needs to be freed */
			free(entry);
			goto error_exit;
		}

		if (list_add_last(original_entries, entry))
		{
			fprintf(stderr, "%s: could not add file entry to list\n",
			        getprogname());

			/* Frees the pathnames from strdup(3) and the entry itself */
			free_ls_entry(entry);
			goto error_exit;
		}
	}
	ls_options.valid_arguments_passed = list_size(original_entries);

	ls_sort_entries(original_entries);

	/*
	 * Nice formatting case where ls puts regular files before directories
	 * as to not randomly insert files from the commandline in between
	 * recursive listings, giving the impression a file is located within a
	 * sub-directory, when it is not.
	 */
	list_foreach_where(original_entries, process_entry, f_entry_isnt_dir);

	/*
	 * If files and directories, we should list the contents of were provided,
	 * there should be a newline present after the regular files to indicate
	 * we are now listing contents of a directory.
	 * If there are no directories, or there are only regular files,
	 * there is no need for a newline. (There are of course only
	 * "regular files" if the -d flag is specified).
	 */
	int dir_count = ls_count_dirs(original_entries);
	if (ls_options.valid_arguments_passed != dir_count && dir_count > 0 &&
	    !ls_options.directories_as_files_and_no_symlink_indir)
	{
		printf("\n");
	}
	list_foreach_where(original_entries, process_entry, f_entry_is_dir);


	(void)list_destroy(original_entries);
	return EXIT_SUCCESS;

/* Normally unreachable; for errors only */
error_exit:
	(void)list_destroy(original_entries);
	return EXIT_FAILURE;
}