/* vifm
 * Copyright (C) 2011 xaizek.
 *
 * This program 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.
 *
 * This program 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 "string_array.h"

#include <assert.h> /* assert() */
#include <stddef.h> /* NULL size_t */
#include <stdio.h> /* FILE SEEK_END SEEK_SET fclose() fprintf() fread()
                      ftell() fseek() */
#include <stdlib.h> /* free() malloc() realloc() */
#include <string.h> /* strcspn() */

#include "../compat/os.h"
#include "../compat/reallocarray.h"
#include "file_streams.h"

static char * read_whole_file(const char filepath[], size_t *read);
static char * read_seekable_stream(FILE *fp, size_t *read);
static char * read_stream(FILE *fp, size_t *read, int drop_bom, progress_cb cb,
		const void *arg);
static size_t get_remaining_stream_size(FILE *fp);
static char ** text_to_lines(char text[], size_t text_len, int *nlines,
		int null_sep);

int
add_to_string_array(char ***array, int len, const char item[])
{
	char **p = reallocarray(*array, len + 1, sizeof(*p));
	if(p == NULL)
	{
		return len;
	}
	*array = p;

	if(item == NULL)
	{
		p[len++] = NULL;
	}
	else if((p[len] = strdup(item)) != NULL)
	{
		++len;
	}

	return len;
}

int
put_into_string_array(char ***array, int len, char item[])
{
	char **const arr = reallocarray(*array, len + 1, sizeof(char *));
	if(arr != NULL)
	{
		*array = arr;
		arr[len++] = item;
	}
	return len;
}

void
remove_from_string_array(char **array, size_t len, int pos)
{
	free(array[pos]);
	memmove(array + pos, array + pos + 1, sizeof(char *)*((len - 1) - pos));
}

int
is_in_string_array(char *array[], size_t len, const char item[])
{
	const int pos = string_array_pos(array, len, item);
	return pos >= 0;
}

int
is_in_string_array_case(char *array[], size_t len, const char item[])
{
	const int pos = string_array_pos_case(array, len, item);
	return pos >= 0;
}

int
is_in_string_array_os(char *array[], size_t len, const char item[])
{
#ifndef _WIN32
	return is_in_string_array(array, len, item);
#else
	return is_in_string_array_case(array, len, item);
#endif
}

char **
copy_string_array(char **array, size_t len)
{
	char **result = reallocarray(NULL, len, sizeof(char *));
	size_t i;
	for(i = 0U; i < len; ++i)
	{
		result[i] = strdup(array[i]);
	}
	return result;
}

int
string_array_pos(char *array[], size_t len, const char item[])
{
	size_t i = len;
	if(item != NULL)
	{
		for(i = 0U; i < len; ++i)
		{
			if(strcmp(array[i], item) == 0)
			{
				break;
			}
		}
	}
	return (i < len) ? (int)i : -1;
}

int
string_array_pos_case(char *array[], size_t len, const char item[])
{
	size_t i = len;
	if(item != NULL)
	{
		for(i = 0; i < len; i++)
		{
			if(strcasecmp(array[i], item) == 0)
			{
				break;
			}
		}
	}
	return (i < len) ? (int)i : -1;
}

int
string_array_equal(char *first[], size_t first_len, char *second[],
		size_t second_len)
{
	if(first_len != second_len)
	{
		return 0;
	}

	size_t i;
	for(i = 0U; i < first_len; ++i)
	{
		if(strcmp(first[i], second[i]) != 0)
		{
			return 0;
		}
	}

	return 1;
}

void
free_string_array(char *array[], size_t len)
{
	if(array != NULL)
	{
		free_strings(array, len);
		free(array);
	}
}

void
free_strings(char *array[], size_t len)
{
	size_t i;
	for(i = 0; i < len; ++i)
	{
		free(array[i]);
	}
}

int
count_strings(char *array[])
{
	int count = 0;
	while(*array++ != NULL)
	{
		++count;
	}
	return count;
}

char **
read_file_of_lines(const char filepath[], int *nlines)
{
	size_t text_len;
	char *const text = read_whole_file(filepath, &text_len);
	if(text == NULL)
	{
		return NULL;
	}

	char **list = text_to_lines(text, text_len, nlines, 0);
	if(list == NULL)
	{
		list = malloc(sizeof(*list));
		*nlines = 0;
	}
	return list;
}

/* Reads file specified by filepath into null terminated string.  Returns
 * string of length *read to be freed by caller on success, otherwise NULL is
 * returned. */
static char *
read_whole_file(const char filepath[], size_t *read)
{
	char *content = NULL;
	FILE *fp;

	*read = 0U;

	if((fp = os_fopen(filepath, "rb")) != NULL)
	{
		content = read_seekable_stream(fp, read);
		fclose(fp);
	}

	return content;
}

char **
read_file_lines(FILE *f, int *nlines)
{
	size_t text_len;
	char *const text = read_seekable_stream(f, &text_len);
	return text_to_lines(text, text_len, nlines, 0);
}

char **
read_stream_lines(FILE *f, int *nlines, int null_sep_heuristic, progress_cb cb,
		const void *arg)
{
	int null;
	size_t text_len;
	char *const text = read_nonseekable_stream(f, &text_len, cb, arg);
	if(text == NULL)
	{
		return NULL;
	}
	null = (null_sep_heuristic && strlen(text) != text_len);
	return text_to_lines(text, text_len, nlines, null);
}

char *
read_nonseekable_stream(FILE *fp, size_t *read, progress_cb cb, const void *arg)
{
	return read_stream(fp, read, 1, cb, arg);
}

/* Reads content of the fp stream that supports seek operation (points to a
 * file) until end-of-file into null terminated string.  Returns string of
 * length *read to be freed by caller on success, otherwise NULL is returned and
 * *read is set to 0UL. */
static char *
read_seekable_stream(FILE *fp, size_t *read)
{
	char *content;
	size_t len;

	skip_bom(fp);
	len = get_remaining_stream_size(fp);

	if(len == 0)
	{
		/* Give this file another shot in case it's a virtual file that doesn't
		 * report its size ahead of time. */
		return read_stream(fp, read, 0, NULL, NULL);
	}

	*read = 0UL;

	if((content = malloc(len + 1U)) == NULL)
	{
		return NULL;
	}

	if(fread(content, len, 1U, fp) == 1U)
	{
		content[len] = '\0';
		*read = len;
	}
	else
	{
		free(content);
		content = NULL;
	}

	return content;
}

/* Reads content of the fp stream that doesn't support seek operation (e.g. it
 * points to a pipe) until end-of-file into null terminated string.  cb can be
 * NULL.  Returns string of length *read to be freed by caller on success,
 * otherwise NULL is returned. */
static char *
read_stream(FILE *fp, size_t *read, int drop_bom, progress_cb cb,
		const void *arg)
{
	enum { PIECE_LEN = 4096 };
	char *content = malloc(PIECE_LEN + 1);

	if(content != NULL)
	{
		char *last_allocated_block = content;
		size_t len = 0U, piece_len;
		if(drop_bom)
		{
			skip_bom(fp);
		}
		while((piece_len = fread(content + len, 1, PIECE_LEN, fp)) != 0U)
		{
			const size_t new_size = len + piece_len + PIECE_LEN + 1U;
			last_allocated_block = realloc(content, new_size);
			if(last_allocated_block == NULL)
			{
				break;
			}

			content = last_allocated_block;
			len += piece_len;

			if(cb != NULL)
			{
				cb(arg);
			}
		}
		content[len] = '\0';

		if(last_allocated_block == NULL)
		{
			free(content);
			content = NULL;
		}
		else
		{
			*read = len;
		}
	}

	if(content == NULL)
	{
		*read = 0U;
	}

	return content;
}

/* Calculates remaining size of the stream.  Assumes that the fp stream supports
 * seek operation.  Returns the size. */
static size_t
get_remaining_stream_size(FILE *fp)
{
	size_t remaining_size;
	const long pos = ftell(fp);
	assert(pos >= 0 && "Stream expected to support seek operation.");

	(void)fseek(fp, 0, SEEK_END);
	remaining_size = ftell(fp) - pos;
	(void)fseek(fp, pos, SEEK_SET);

	return remaining_size;
}

/* Converts text of length text_len into an array of strings.  Always frees
 * piece of memory pointed to by the text.  Returns non-NULL on success,
 * otherwise NULL is returned, *nlines is untouched.  For empty file non-NULL
 * will be returned, but *nlines will be zero. */
static char **
text_to_lines(char text[], size_t text_len, int *nlines, int null_sep)
{
	char **list = NULL;

	if(text != NULL)
	{
		list = break_into_lines(text, text_len, nlines, null_sep);
		free(text);
	}
	return list;
}

char **
break_into_lines(char text[], size_t text_len, int *nlines, int null_sep)
{
	const char *const seps = null_sep ? "" : "\n\r";
	const char *const end = text + text_len;
	char **list = NULL;

	*nlines = 0;
	while(text < end)
	{
		const size_t line_len = strcspn(text, seps);

		char *after_line = text + line_len;
		if(after_line[0] == '\n')
		{
			after_line += 1;
		}
		else if(after_line[0] == '\r')
		{
			after_line += (after_line[1] == '\n') ? 2 : 1;
		}
		else if(after_line[0] == '\0')
		{
			do
			{
				++after_line;
			}
			while(after_line < end && after_line[0] == '\0');
		}

		text[line_len] = '\0';
		*nlines = add_to_string_array(&list, *nlines, text);

		text = after_line;
	}

	return list;
}

int
write_file_of_lines(const char filepath[], char *strs[], size_t nstrs)
{
	FILE *const fp = os_fopen(filepath, "w");
	if(fp == NULL)
	{
		return 1;
	}

	write_lines_to_file(fp, strs, nstrs);
	fclose(fp);
	return 0;
}

void
write_lines_to_file(FILE *fp, char *strs[], size_t nstrs)
{
	size_t i;
	for(i = 0U; i < nstrs; ++i)
	{
		fputs(strs[i], fp);
		putc('\n', fp);
	}
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
