#include "lprintf.h"
// Must not remove debug.h  because it breaks llist
#include "bins.h"
#define DMID 14
#include "debug.h"
#define _BIN_PATH_SEP	'/'


/* msg.idx header record - element names as per DPOP, so they don't make much sense ;-) */
typedef struct {
	char		ffvrsn[8];	/* file format version number */
	int	nmsgs;				/* number of messages currently stored */
	int	hna;				/* highest message number accessed */
} _bin_idx_header;


/* stucture of msg index records - element names as per DPOP, so they don't make much sense ;-) */
typedef struct {
	int		offset;		/* start of message in bin file file */
	int		uidl;		/* offset to start of uidl record for message */
	int		size;		/* length of message */
	int		nlines;		/* number of lines in msg NOT including hdr lines; */
	int		hdrlines;	/* numer of hdr lines in this message */
	int		read;		/* message has been read */
	int		deleted;	/* message has been marked as deleted=1 or moved=-1 */
	int		s_read;		/* read status at start of session */
	int		s_deleted;	/* deleted status of message at start of session */
	int		hdrsize;	/* hdr size, not used any more - DHGB */
	int		bin;		/* bin file number msg is stored in */
} _bin_message_record;


/* message node */
typedef struct _BIN_MESSAGE_NODE 
{
	int		offset;						/* offset within .dat file, in bytes */
	char	bin_filename[BIN_SIZE];		/* filename of .dat file containing message */
	int		deleted;					/* true if message has been deleted */
	int		size;						/* message size, in bytes */
} _bin_message_node;


/*	_bin_make_filename() constructs a bin index filename and bin pathname from
 *	'user', 'path', 'domain_prefix', hashing on level 'hash_spool' and encoding
 *	invalid characters in 'invalid'.
 *
 *	Upon success, the index filename is placed in 'dest_filename', the path is 
 *	placed in 'dest_path', and 'dest_filename' is returned.
 *
 *	Upon failure, NULL is returned.
 */
char *_bin_make_filename(char *dest_filename, char *dest_path, char *user, char *path, const char *domain_prefix, char *invalid, int hash_spool, char *bin_name);


/* _bin_kill_directory() recursively removes the contents of 'directory'.
 *
 *	Upon success, 1 is returned.
 *
 *	Upon failure, 0 is returned, and if 'failure' is not NULL, *'failure'
 *	is set to errno.
 */
int _bin_kill_directory(char *directory, int *failure);


/*	_bin_ncpy copies string 'src' into string 'dst', to a maximum of 'len' chars.
 *	This is safer and more efficient than ncpy, which it replaces.
 */
char *_bin_ncpy(char *dst,const char *src, int len);


/*	_bin_ncat copies concatenates string 'src' to string 'dst', to a maximum of
 *	'len' chars. This is safer and more efficient than ncat, which it
 *	replaces.
 */
char *_bin_ncat(char *dst,const char *src, int len);


/* _bin_kill_directory() recursively removes the contents of 'directory'.
 *
 *	Upon success, 1 is returned.
 *
 *	Upon failure, 0 is returned, and if 'failure' is not NULL, *'failure'
 *	is set to errno.
 */
int _bin_kill_directory(char *directory, int *failure)
{
	WILD_HANDLE	*h;
	WILD_DATA	mydata;
	int			more;
	char		full_name[BIN_SIZE];
	int			kill_failure;
	
	more = wild_first(&h, directory, &mydata);

	do
	{
		lprintf(full_name, BIN_SIZE - 1, "%s%c%s", directory, _BIN_PATH_SEP, mydata.name);
	
		/* don't try to delete '.' or '..' */
		if ( strcmp(mydata.name,".") == 0 ) continue;
		if ( strcmp(mydata.name,"..") == 0 ) continue;
	
		if ( mydata.isdir && !mydata.islink )
		{
			/* delete the contents of any directories that aren't links */
			if ( _bin_kill_directory(full_name, &kill_failure) == 0 )
			{
				if ( failure ) *failure = errno;
				wild_end(&h);
				return 0;
			}
		}
		else
		{
			/* delete the contents of any file, link or not */
		//	imsg("bin: deleting(%s)\n",full_name);
			if ( unlink(full_name) == -1 )
			{
				if ( failure ) *failure = errno;
				wild_end(&h);
				return 0;
			}
		}		
	}
	while ( (more = wild_next(h, &mydata)) != 0 );
	wild_end(&h);
	
	/* remove the directory */
	if ( rmdir(directory) == -1 )
	{
		if ( failure ) *failure = errno;
		return 0;
	}
	
	return 1;
}


/*	bin_open() opens bins for user 'user', domain 'domain', in path 
 *	'path'.  If 'bin_name' is not NULL, then 'bin_name' is used
 *	rather than creating bin filenamesfrom the aforementioned components.
 * 
 *	If non-empty, 'domain_prefix' is prepended to the username 'user'.
 *	bin_open() hashes the specified path using 'user' as the username and 
 *	'hash_spool' as the hashing level.  When hashing, all characters in the
 *	string pointed to by 'invalid' are considered to be invalid.
 *
 *	bin_open() places all bin info in the bin structure pointed to by 'bin'.
 *
 *	bin_open() returns one of the following conditions:
 *
 *		BIN_OKAY				bins were successfully opened
 *		BIN_FAIL				miscellaneous failure
 *		BIN_FAIL_INIT			failed to initialise bin structure
 *		BIN_FAIL_FILE_IO		specified maildrop can't be opened
 *		BIN_FAIL_MALLOC			failed to allocate memory - this is *bad*
 *		
 *	If bin_open() succeeds, the bin structure pointed to by 'bin' will
 *	be initialised, and bin_open() will return BIN_OKAY.  Once initialised,
 *	the bin structure may not be initialised again until it has been
 *	closed with bin_close().
 *
 *	If bin_open() fails, bin_open() will return BIN_FAIL_FILE_IO.
 *
 *	If bin_open() returns BIN_FAIL_FILE_IO, and 'failure' is not NULL, 
 *	*'failure' will be set to errno, thus allowing the calling function to 
 *	determine the reason for failure in detail.
 *
 *	If 'bin', 'user', 'path', 'domain_prefix' or 'invalid' are NULL, 
 *	bin_open() will fail, returning BIN_FAIL_INIT.
 */
int	bin_open(bin_data *bin, char *user, char *path, const char *domain_prefix, char *invalid, int hash_spool, int *failure, char *bin_name, char *reason)
{
	char				bin_filename[BIN_SIZE];
	char tmp[BFSZ];
	FILE 				*index_file;
	_bin_idx_header		header_data;	
	_bin_message_record	record_data;
	_bin_message_node	*node;
	
	if ( _bin_make_filename(bin_filename, bin->bin_path, user, path, domain_prefix, invalid, hash_spool, bin_name) == NULL )
	{
		lprintf(reason,100,"Could not create bin file name for (%s) (%s)\n",user,path);
		return BIN_FAIL_INIT;
	}
		
	/* try to open the index file in binary mode */			
	index_file = fopen(bin_filename, "rb");
	if ( index_file == NULL )
	{
		*failure = errno;
		lprintf(reason,100,"Open(%s) failed %s\n",bin_filename,strerror(errno));
		return BIN_FAIL_FILE_IO;
	}

	/* read header */
	if ( fread(&header_data, sizeof(_bin_idx_header), 1, index_file) != 1 )
	{
		*failure = errno;
		lprintf(reason,100,"Read(%s) failed %s\n",bin_filename,strerror(errno));
		fclose(index_file);
		return BIN_FAIL;
	}

	/* initialise the linked list to hold message records */
	llist_init(&bin->message_list);
	
	while ( 1 )
	{														
		/* check there's another message record to read */	
		if ( fread(&record_data, sizeof(_bin_message_record), 1, index_file) != 1 )
		{		
			break;			
		}

		node = calloc(1,sizeof(_bin_message_node));
		if ( node == NULL ) {
			*failure = errno;
			lprintf(reason,100,"Malloc(%s) failed %s\n",bin_filename,strerror(errno));
			fclose(index_file);
			return BIN_FAIL_MALLOC;
		}

		/* generate information about message */
		//snprintf(node->bin_filename, BIN_SIZE - 1, "%sbin%05d.dat", lib_addsep(bin->bin_path,tmp), record_data.bin); 
		lprintf(node->bin_filename, BIN_SIZE - 1, "%sbin%05d.dat", lib_addsep(bin->bin_path, tmp), record_data.bin);
		node->offset = record_data.offset;
		node->deleted = record_data.s_deleted;
		node->size = record_data.size;

		/* store message record in linked list */					
		llist_add_tail(&bin->message_list, node);
	}							
	
	fclose(index_file);
	
	/* set status for bins so that user must call bin_next_message() before reading */
	bin->status = BIN_STATUS_EOM;
	bin->first = 1;
	bin->bin_file = NULL;
	strcpy(reason,"Bin read ok");
	return BIN_OKAY;
}


/*	bin_dele() deletes the bins for user 'user', domain 'domain', in path 
 *	'path'.  If 'bin_name' is not NULL, then 'bin_name' is used
 *	rather than creating bin filenames from the aforementioned components. 
 * 
 *	If bin_dele() fails, bin_dele() will return BIN_FAIL_FILE_IO.
 *
 *	If bin_dele() returns BIN_FAIL_FILE_IO, and 'failure' is not NULL, 
 *	*'failure' will be set to errno, thus allowing the calling function to 
 *	determine the reason for failure in detail.
 *
 *	If 'bin', 'user', 'path', 'domain_prefix' or 'invalid' are NULL, 
 *	bin_dele() will fail, returning BIN_FAIL.
 */
int	bin_dele(char *user, char *path, const char *domain_prefix, char *invalid, int hash_spool, int *failure, char *bin_name)
{
	char	bin_pathname[NEWHASH_BFSZ];
	char	bin_filename[BIN_SIZE];
	int		kill_failure;
	
	if ( _bin_make_filename(bin_filename, bin_pathname, user, path, domain_prefix, invalid, hash_spool, bin_name) == NULL )	
	{
		return BIN_FAIL;
	}
	
	/* try to kill it */
	if ( _bin_kill_directory(bin_pathname, &kill_failure) != 1 )
	{
		if ( failure != NULL ) *failure = kill_failure;
		return BIN_FAIL_FILE_IO;
	}
	
	return BIN_OKAY;
}


/*	bin_next_message() moves to the next message in the bins pointed to by 
 *	'bin'. 
 *	
 *	If bin_next_message() succeeds, bin_next_message() returns BIN_OKAY if
 *	there is another message, or BIN_FAIL_EOF if at the end of the last message.
 *
 *	If bin_next_message() fails, bin_next_message() will return BIN_FAIL_FILE_IO.
 *
 *	If bin_next_message() returns BIN_FAIL_FILE_IO, and 'failure' is not NULL, 
 *	*'failure' will be set to errno, thus allowing the calling function to 
 *	determine the reason for failure in detail.
 *
 *	bin_next_message() returns BIN_FAIL if 'bin' is NULL.
 */
int bin_next_message(bin_data *bin, int *failure)
{
	int					result;
	_bin_message_node	*node;
	*failure = 0;
		
	if ( !bin ) return BIN_FAIL;
	
	if ( bin->first == 1 )
	{
		bin->first = 0;
		result = llist_reset_current(&(bin->message_list), LLIST_HEAD);
	}
	else
	{
		result = llist_next(&(bin->message_list));
	}
	
	if ( !result || !bin->message_list.current || !bin->message_list.current->data ) 
	{
		bin->status = BIN_STATUS_EOF;
		return BIN_FAIL_EOF;
	}

	node = (_bin_message_node *)bin->message_list.current->data;
	
	/* if we get here, then current node contains file details */
/*	printf("message:\n\toffset %d\n\tfile '%s'\n\tdeletion %d\n\tsize %d\n", node->offset, node->bin_filename, node->deleted, node->size);*/

	/* if file is non-NULL, close it */
	if ( bin->bin_file ) fclose(bin->bin_file);
	
	bin->bin_file = fopen(node->bin_filename, "rb");
	if ( !bin->bin_file )
	{
		if ( failure ) *failure = errno;
		return BIN_FAIL_FILE_IO;
	}
	
	if ( fseek(bin->bin_file, node->offset, SEEK_SET) != 0 )
	{
		if ( failure ) *failure = errno;
		return BIN_FAIL_FILE_IO;
	}
	
	bin->bytes_read = 0;
	bin->status = BIN_STATUS_IN_MSG;
		
	return BIN_OKAY;
}


/*	bin_gets() attempts to read up to one character less than 'size' characters
 *	from bins 'bin'. 
 *	
 *	Reading stops at the end of the current message, or a newline.  Newlines are
 *	stored in the buffer; they are not discarded.
 *
 *	If bin_gets() succeeds, it returns 'buf', and returns NULL on failure or
 *	upon being passed a NULL 'bin'.
 */
char *bin_gets(char *buf, int size, bin_data *bin)
{
	_bin_message_node	*node;
	int					bytes_left;
	char				*return_value;
	char				*i;
	int					offset;

	if ( !buf || !bin ) return NULL;
	if ( bin->status != BIN_STATUS_IN_MSG ) return NULL;

	node = (_bin_message_node *)bin->message_list.current->data;
	bytes_left = node->size - bin->bytes_read;

	if ( size > bytes_left ) size = bytes_left;
			
	if ( bytes_left <= 1 )
	{
		bin->status = BIN_STATUS_EOM;
		return NULL;
	}
				
	return_value = fgets(buf, size, bin->bin_file);
	
	/* convert all line terminators to UNIX style */
	offset = 0;
	for ( i = buf; *i != '\0'; i ++ )
	{
		if ( *i == 13 )
		{
			*i = 10;
			*(i + 1) = '\0';
			offset = 1;

			break;
		}
	}
	
	bin->bytes_read += lib_strlen(buf) + offset;

	return return_value;
}


/*	bin_close() closes bin structure 'bin'.
 *
 *	If bin_close() succeeds, the bin structure pointed to by 'bin' will
 *	be closed, and bin_close() will return BIN_OKAY.  Any open files will be
 *	closed, and all allocated memory will be freed.  The bin structure 
 *	pointed to by 'bin' will then be free for use by bin_close().
 *
 *	If bin_close() fails, or if 'bin' is NULL, bin_close() will return 
 *	BIN_FAIL.
 */
int bin_close(bin_data *bin)
{
	if ( !bin || !bin->bin_file ) return BIN_FAIL;
	
	if ( fclose(bin->bin_file) == 0 )
	{
		bin->status = BIN_STATUS_CLOSED;
	}
	else
	{
		return BIN_FAIL;
	}	

	llist_delete_list(&(bin->message_list), 1);
	return BIN_OKAY;
}


/*	bin_get_path() returns a pointer to a string containing the path
 *	of the bins contained within 'bin'.
 *
 *	Note that the path is only set after a call to bin_open() - calling
 *	bin_get_path() on a bin structure which has not been opened, or
 *	had an attempt at opening fail, will invoke undefined behaviour.
 *
 *	bin_get_path() will return NULL if 'bin' is NULL.
 */
const char *bin_get_path(bin_data *bin)
{
	if ( !bin || !bin->bin_path )
	{
		return NULL;
	}
	
	return bin->bin_path;
}


/*	_bin_make_filename() constructs a bin index filename and bin pathname from
 *	'user', 'path', 'domain_prefix', hashing on level 'hash_spool' and encoding
 *	invalid characters in 'invalid'.
 *
 *	Upon success, the index filename is placed in 'dest_filename', the path is 
 *	placed in 'dest_path', and 'dest_filename' is returned.
 *
 *	Upon failure, NULL is returned.
 */
char *_bin_make_filename(char *dest_filename, char *dest_path, char *user, char *path, const char *domain_prefix, char *invalid, int hash_spool, char *bin_name)
{
	char encoded_username[NEWHASH_BFSZ];
	char bin_basepath[NEWHASH_BFSZ];
	char tmp[BFSZ];
	/* ensure we haven't been passed NULL strings */	
	if ( !user || !path || !domain_prefix || !invalid )
	{
		return NULL;
	}	

	/* generate an encoded username */
	_bin_ncpy(encoded_username, user, NEWHASH_BFSZ - 1);
	encode_invalid(encoded_username, invalid,tmp);
		
	if ( bin_name && (lib_strlen(bin_name)>0) ) 
	{
		/*_bin_ncpy(dest_filename, bin_name, BIN_SIZE - 1);*/
		lprintf(dest_path, BIN_SIZE - 1, "%s.bin", bin_name);
	}
	else
	{
		/* generate path to bin directory */	
		_bin_ncpy(bin_basepath, path, NEWHASH_BFSZ - 1);
		hash_new(bin_basepath, user, invalid, hash_spool, 0, NULL);
		lprintf(dest_path, BIN_SIZE - 1, "%s%s%s.bin", lib_addsep(bin_basepath,tmp), domain_prefix, encoded_username);
	}

	lprintf(dest_filename, BIN_SIZE - 1, "%s/msg.idx", dest_path);		
	
	return dest_filename;
}


/*	_bin_ncpy copies string 'src' into string 'dst', to a maximum of 'len' chars.
 *	This is safer and more efficient than ncpy, which it replaces.
 */
char *_bin_ncpy(char *dst,const char *src, int len)
{
	char *xdst = dst;	/* temporary pointer to destination */
	int xlen = len;		/* position of null terminator */

    /* copy source string over destination string */
	for (;(*src !=0) && (len>0); ) {
		*dst++  = *src++;
		len--;
	}

	/* enure that destination string is safely terminated */
	*dst++ = 0;
	xdst[xlen] = 0;

	return xdst;
}


/*	_bin_ncat copies concatenates string 'src' to string 'dst', to a maximum of
 *	'len' chars. This is safer and more efficient than ncat, which it
 *	replaces.
 */
char *_bin_ncat(char *dst,const char *src, int len)
{
	char *xdst = dst;	/* temporary pointer to destination */
	int xlen = len;		/* position of null terminator */

	/* find end of destination string */
	for (;(*dst !=0) && (len>0); dst++) {
		len--;
	}

    /* append source string to end of destination string */
	for (;(*src !=0) && (len>0); ) {
		*dst++  = *src++;
		len--;
	}

	/* enure that destination string is safely terminated */
	*dst++ = 0;
    xdst[xlen] = 0;

	return xdst;
}


/*	The following main() function is only included if _BIN_TESTING is defined.
 *	It produces an executable that dumps all messages in a users bins to console,
 *	separated by lines of dashes.  The syntax is:
 *
 *		bins username path domain_prefix hashing_level
 *
 *	For example:
 *
 * 		./bins test0 /var/spool/mail "" 2
 */																
#ifdef _BIN_TESTING																															
int main(int argc, char *argv[])
{
	bin_data			bin;
	int					result;
	int					error;
	char				buffer[1024];	

	if ( argc < 5 )
	{
		fprintf(stderr, "insufficient arguments\n");
		return EXIT_FAILURE;
	}
	
	/* try to open bin file */
	result = bin_open(&bin, argv[1], argv[2], argv[3], ".\\/", atoi(argv[4]), &error, NULL, NULL);
	if ( result != BIN_OKAY )
	{
		fprintf(stderr, "maildrop '%s' failed to open", bin_get_path(&bin));
		if ( result == BIN_FAIL_FILE_IO )
		{
			fprintf(stderr, ": %s", strerror(error));
		}
		fprintf(stderr, "\n");
		return EXIT_FAILURE;
	}
	else
	{		
		/* display success */	
//		printf("maildrop '%s' opened\n", bin_get_path(&bin));
	}
	
	/* print contents of all messages */
	while ( 1 )
	{
		if ( bin_next_message(&bin, &error) != BIN_OKAY )
		{
			fprintf(stderr, "failed to get next message in bins - %s\n", strerror(error));
			break;
		}

		printf("--------------------------------------------------------\n");
		while ( bin_gets(buffer, 1023, &bin) != NULL )
		{
			printf("%s", buffer);
		}	
		printf("--------------------------------------------------------\n");
	}
	
	/* try to close bins */
	if ( bin_close(&bin) == BIN_OKAY )
	{		
		printf("bin closed\n");
	}
	else
	{
		fprintf(stderr, "bin '%s' failed to close\n", bin_get_path(&bin));
		return EXIT_FAILURE;
	}
	
	/* if specified, delete bins */
	if ( argc > 5 )
	{
		if ( strcmp(argv[5], "dele") == 0 )
		{
			result = bin_dele(argv[1], argv[2], argv[3], ".\\/", atoi(argv[4]), &error, NULL);
			if ( result != BIN_OKAY )
			{
				fprintf(stderr, "bin failed to delete");
				if ( result == BIN_FAIL_FILE_IO )
				{
					fprintf(stderr, ": %s", strerror(error));
				}
				fprintf(stderr, "\n");
				return EXIT_FAILURE;
			}
			else
			{		
				/* display success */	
				printf("bins deleted\n");
			}
		}
	}

	return EXIT_SUCCESS;
}
#endif

