/*
 * Copyright (c) 2011 Oleg Romanenko
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @addtogroup fs
 * @{
 */ 

/**
 * @file	fat_directory.c
 * @brief	Functions that work with FAT directory.
 */

#include "fat_directory.h"
#include "fat_fat.h"
#include <libblock.h>
#include <errno.h>
#include <byteorder.h>
#include <mem.h>
#include <str.h>
#include <align.h>

int fat_directory_block_load(fat_directory_t *);


int fat_directory_open(fat_node_t *nodep, fat_directory_t *di)
{
	di->b = NULL;
	di->nodep = nodep;	
	if (di->nodep->type != FAT_DIRECTORY)
		return EINVAL;

	di->bs = block_bb_get(di->nodep->idx->devmap_handle);
	di->blocks = ROUND_UP(nodep->size, BPS(di->bs))/BPS(di->bs);
	di->pos = 0;
	di->bnum = 0;
	di->last = false;
	return EOK;
}

int fat_directory_close(fat_directory_t *di)
{
	int rc=EOK;
	
	if (di->b)
		rc = block_put(di->b);
	
	return rc;
}

int fat_directory_block_load(fat_directory_t *di)
{
	uint32_t i;
	int rc;

	i = (di->pos * sizeof(fat_dentry_t)) / BPS(di->bs);
	if (i < di->blocks) {
		if (di->b && di->bnum != i) {
			block_put(di->b);
			di->b = NULL;
		}
		if (!di->b) {
			rc = fat_block_get(&di->b, di->bs, di->nodep, i, BLOCK_FLAGS_NONE);
			if (rc != EOK) {
				di->b = NULL;
				return rc;
			}
			di->bnum = i;
		}
		return EOK;
	}
	return ENOENT;
}

int fat_directory_next(fat_directory_t *di)
{
	int rc;

	di->pos += 1;
	rc = fat_directory_block_load(di);
	if (rc!=EOK)
		di->pos -= 1;
	
	return rc;
}

int fat_directory_prev(fat_directory_t *di)
{
	int rc=EOK;
	
	if (di->pos > 0) {
		di->pos -= 1;
		rc=fat_directory_block_load(di);
	}
	else
		return ENOENT;
	
	if (rc!=EOK)
		di->pos += 1;
	
	return rc;
}

int fat_directory_seek(fat_directory_t *di, aoff64_t pos)
{
	aoff64_t _pos = di->pos;
	int rc;

	di->pos = pos;
	rc = fat_directory_block_load(di);
	if (rc!=EOK)
		di->pos = _pos;
	
	return rc;
}

int fat_directory_get(fat_directory_t *di, fat_dentry_t **d)
{
	int rc;
	
	rc = fat_directory_block_load(di);
	if (rc == EOK) {
		aoff64_t o = di->pos % (BPS(di->bs) / sizeof(fat_dentry_t));
		*d = ((fat_dentry_t *)di->b->data) + o;
	}
	
	return rc;
}

int fat_directory_read(fat_directory_t *di, char *name, fat_dentry_t **de)
{
	fat_dentry_t *d = NULL;
	uint16_t wname[FAT_LFN_NAME_SIZE];
	size_t lfn_offset, lfn_size;
	bool long_entry = false;
	int long_entry_count = 0;
	uint8_t checksum = 0;

	do {
		if (fat_directory_get(di, &d) == EOK) {
			switch (fat_classify_dentry(d)) {
			case FAT_DENTRY_LAST:
				long_entry_count = 0;
				long_entry = false;
				return ENOENT;
			case FAT_DENTRY_LFN:
				if (long_entry) {
					/* We found long entry */
					long_entry_count--;
					if ((FAT_LFN_ORDER(d) == long_entry_count) && 
						(checksum == FAT_LFN_CHKSUM(d))) {
						/* Right order! */
						fat_lfn_get_entry(d, wname, &lfn_offset);
					} else {
						/* Something wrong with order. Skip this long entries set */
						long_entry_count = 0;
						long_entry = false;
					}
				} else {
					if (FAT_IS_LFN(d)) {
						/* We found Last long entry! */
						if (FAT_LFN_COUNT(d) <= FAT_LFN_MAX_COUNT) {
							long_entry = true;
							long_entry_count = FAT_LFN_COUNT(d);
							lfn_size = (FAT_LFN_ENTRY_SIZE * 
								(FAT_LFN_COUNT(d) - 1)) + fat_lfn_size(d);
							lfn_offset = lfn_size;
							fat_lfn_get_entry(d, wname, &lfn_offset);
							checksum = FAT_LFN_CHKSUM(d);
						}
					}
				}
				break;
			case FAT_DENTRY_VALID:
				if (long_entry && 
					(checksum == fat_dentry_chksum(d->name))) {
					wname[lfn_size] = '\0';
					if (utf16_to_str(name, FAT_LFN_NAME_SIZE, wname) != EOK)
						fat_dentry_name_get(d, name);
				}
				else
					fat_dentry_name_get(d, name);
				
				*de = d;
				return EOK;
			default:
			case FAT_DENTRY_SKIP:
			case FAT_DENTRY_FREE:
				long_entry_count = 0;
				long_entry = false;
				break;
			}
		}
	} while (fat_directory_next(di) == EOK);
	
	return ENOENT;
}

int fat_directory_erase(fat_directory_t *di)
{
	int rc;
	fat_dentry_t *d;
	bool flag = false;
	uint8_t checksum;

	rc = fat_directory_get(di, &d);
	if (rc != EOK)
		return rc;
	checksum = fat_dentry_chksum(d->name);

	d->name[0] = FAT_DENTRY_ERASED;
	di->b->dirty = true;
	
	while (!flag && fat_directory_prev(di) == EOK) {
		if (fat_directory_get(di, &d) == EOK &&
			fat_classify_dentry(d) == FAT_DENTRY_LFN &&			
			checksum == FAT_LFN_CHKSUM(d)) {
				if (FAT_IS_LFN(d))
					flag = true;
				memset(d, 0, sizeof(fat_dentry_t));
				d->name[0] = FAT_DENTRY_ERASED;
				di->b->dirty = true;
		}
		else
			break;
	}

	return EOK;
}

int fat_directory_write(fat_directory_t *di, const char *name, fat_dentry_t *de)
{
	int rc;
	bool enable_lfn = true; /* We can use this variable to switch off LFN support */
	
	if (fat_valid_short_name(name)) {
		/* NAME could be directly stored in dentry without creating LFN */
		fat_dentry_name_set(de, name);
		if (fat_directory_is_sfn_exist(di, de))
			return EEXIST;
		rc = fat_directory_lookup_free(di, 1);
		if (rc != EOK)
			return rc;
		rc = fat_directory_write_dentry(di, de);
		return rc;
	} else if (enable_lfn && fat_valid_name(name)) {
		/* We should create long entries to store name */
		int long_entry_count;
		uint8_t checksum;
		uint16_t wname[FAT_LFN_NAME_SIZE];
		size_t lfn_size, lfn_offset;
		
		rc = str_to_utf16(wname, FAT_LFN_NAME_SIZE, name);
		if (rc != EOK)
			return rc;
		
		lfn_size = utf16_length(wname);
		long_entry_count = lfn_size / FAT_LFN_ENTRY_SIZE;
		if (lfn_size % FAT_LFN_ENTRY_SIZE)
			long_entry_count++;
		rc = fat_directory_lookup_free(di, long_entry_count+1);
		if (rc != EOK)
			return rc;
		aoff64_t start_pos = di->pos;

		/* Write Short entry */
		rc = fat_directory_create_sfn(di, de, name);
		if (rc != EOK)
			return rc;
		checksum = fat_dentry_chksum(de->name);

		rc = fat_directory_seek(di, start_pos+long_entry_count);
		if (rc != EOK)
			return rc;
		rc = fat_directory_write_dentry(di, de);
		if (rc != EOK)
			return rc;

		/* Write Long entry by parts */
		lfn_offset = 0;
		fat_dentry_t *d;
		size_t idx = 0;
		do {
			rc = fat_directory_prev(di);
			if (rc != EOK)
				return rc;
			rc = fat_directory_get(di, &d);
			if (rc != EOK)
				return rc;
			fat_lfn_set_entry(wname, &lfn_offset, lfn_size+1, d);
			FAT_LFN_CHKSUM(d) = checksum;
			FAT_LFN_ORDER(d) = ++idx;
			di->b->dirty = true;
		} while (lfn_offset < lfn_size);
		FAT_LFN_ORDER(d) |= FAT_LFN_LAST;

		rc = fat_directory_seek(di, start_pos+long_entry_count);
		return rc;
	}

	return ENOTSUP;
}

int fat_directory_create_sfn(fat_directory_t *di, fat_dentry_t *de, const char *lname)
{
	char name[FAT_NAME_LEN+1];
	char ext[FAT_EXT_LEN+1];
	char number[FAT_NAME_LEN+1];
	memset(name, FAT_PAD, FAT_NAME_LEN);
	memset(ext, FAT_PAD, FAT_EXT_LEN);
	memset(number, FAT_PAD, FAT_NAME_LEN);

	size_t name_len = str_size(lname);
	char *pdot = str_rchr(lname, '.');
	ext[FAT_EXT_LEN] = '\0';
	if (pdot) {
		pdot++;
		str_to_ascii(ext, pdot, FAT_EXT_LEN, FAT_SFN_CHAR);
		name_len = (pdot - lname - 1);
	}
	if (name_len > FAT_NAME_LEN)
		name_len = FAT_NAME_LEN;
	str_to_ascii(name, lname, name_len, FAT_SFN_CHAR);

	size_t idx;
	for (idx=1; idx <= FAT_MAX_SFN; idx++) {
		if (size_t_str(idx, 10, number, FAT_NAME_LEN-2)!=EOK)
			return EOVERFLOW;

		/* Fill de->name with FAT_PAD */
		memset(de->name, FAT_PAD, FAT_NAME_LEN+FAT_EXT_LEN);
		/* Copy ext */
		memcpy(de->ext, ext, str_size(ext));
		/* Copy name */
		memcpy(de->name, name, str_size(name));

		/* Copy number */
		size_t offset;
		if (str_size(name)+str_size(number)+1 >FAT_NAME_LEN)
			offset = FAT_NAME_LEN - str_size(number)-1;
		else
			offset = str_size(name);
		de->name[offset] = '~';
		offset++;
		memcpy(de->name+offset, number, str_size(number));

		if (!fat_directory_is_sfn_exist(di, de))
			return EOK;
	}
	return ERANGE;
}

int fat_directory_write_dentry(fat_directory_t *di, fat_dentry_t *de)
{
	fat_dentry_t *d;
	int rc;

	rc = fat_directory_get(di, &d);
	if (rc!=EOK)
		return rc;
	memcpy(d, de, sizeof(fat_dentry_t));
	di->b->dirty = true;
	return EOK;
}

int fat_directory_expand(fat_directory_t *di)
{
	int rc;
	fat_cluster_t mcl, lcl;

	if (!FAT_IS_FAT32(di->bs) && di->nodep->firstc == FAT_CLST_ROOT) {
		/* Can't grow the root directory on FAT12/16. */
		return ENOSPC;
	}
	rc = fat_alloc_clusters(di->bs, di->nodep->idx->devmap_handle, 1, &mcl, &lcl);
	if (rc != EOK)
		return rc;
	rc = fat_zero_cluster(di->bs, di->nodep->idx->devmap_handle, mcl);
	if (rc != EOK) {
		(void) fat_free_clusters(di->bs, di->nodep->idx->devmap_handle, mcl);
		return rc;
	}
	rc = fat_append_clusters(di->bs, di->nodep, mcl, lcl);
	if (rc != EOK) {
		(void) fat_free_clusters(di->bs, di->nodep->idx->devmap_handle, mcl);
		return rc;
	}
	di->nodep->size += BPS(di->bs) * SPC(di->bs);
	di->nodep->dirty = true;		/* need to sync node */
	di->blocks = di->nodep->size / BPS(di->bs);
	
	return EOK;
}

int fat_directory_lookup_free(fat_directory_t *di, size_t count)
{
	fat_dentry_t *d;
	size_t found;
	aoff64_t pos;
	
	do {
		found = 0;
		pos=0;
		fat_directory_seek(di, 0);
		do {
			if (fat_directory_get(di, &d) == EOK) {
				switch (fat_classify_dentry(d)) {
				case FAT_DENTRY_LAST:
				case FAT_DENTRY_FREE:
					if (found==0) pos = di->pos;
					found++;
					if (found == count) {
						fat_directory_seek(di, pos);
						return EOK;
					}
					break;
				case FAT_DENTRY_VALID:
				case FAT_DENTRY_LFN:
				case FAT_DENTRY_SKIP:
				default:
					found = 0;
					break;
				}
			}
		} while (fat_directory_next(di) == EOK);	
	} while (fat_directory_expand(di) == EOK);
	return ENOSPC;
}

int fat_directory_lookup_name(fat_directory_t *di, const char *name, fat_dentry_t **de)
{
	char entry[FAT_LFN_NAME_SIZE];
	fat_directory_seek(di, 0);
	while (fat_directory_read(di, entry, de) == EOK) {
		if (fat_dentry_namecmp(entry, name) == 0) {
			return EOK;
		} else {
			if (fat_directory_next(di) != EOK)
				break;
		}
	}
	return ENOENT;
}

bool fat_directory_is_sfn_exist(fat_directory_t *di, fat_dentry_t *de)
{
	fat_dentry_t *d;
	fat_directory_seek(di, 0);
	do {
		if (fat_directory_get(di, &d) == EOK) {
			switch (fat_classify_dentry(d)) {
			case FAT_DENTRY_LAST:
				return false;
			case FAT_DENTRY_VALID:
					if (bcmp(de->name, d->name, FAT_NAME_LEN+FAT_EXT_LEN)==0)
						return true;
					break;
			default:
			case FAT_DENTRY_LFN:
			case FAT_DENTRY_SKIP:
			case FAT_DENTRY_FREE:
				break;
			}
		}
	} while (fat_directory_next(di) == EOK);	
	return false;
}

/**
 * @}
 */ 
