#include <stdio.h>
#include <string.h>
#include <sys/dirent.h>
#include <cache.h>
#include <core.h>
#include <disk.h>
#include <fs.h>
#include "fat_fs.h"

static struct inode * new_fat_inode(struct fs_info *fs)
{
    struct inode *inode = alloc_inode(fs, 0, sizeof(sector_t));
    if (!inode)
	malloc_error("inode structure");		
    return inode;
}


static void vfat_close_file(struct file *file)
{
    if (file->inode) {
	file->offset = 0;
	free_inode(file->inode);
    }
}


/*
 * Check for a particular sector in the FAT cache
 */
static struct cache_struct * get_fat_sector(struct fs_info *fs, sector_t sector)
{
    return get_cache_block(fs->fs_dev, FAT_SB(fs)->fat + sector);
}

static uint32_t get_next_cluster(struct fs_info *fs, uint32_t clust_num)
{
    uint32_t next_cluster;
    sector_t fat_sector;
    uint32_t offset;
    int lo, hi;
    struct cache_struct *cs;
    uint32_t sector_mask = SECTOR_SIZE(fs) - 1;
    
    switch(FAT_SB(fs)->fat_type) {
    case FAT12:
	offset = clust_num + (clust_num >> 1);
	fat_sector = offset >> SECTOR_SHIFT(fs);
	offset &= sector_mask;
	cs = get_fat_sector(fs, fat_sector);
	if (offset == sector_mask) {
	    /* 
	     * we got the end of the one fat sector, 
	     * but we have just one byte and we need two,
	     * so store the low part, then read the next fat
	     * sector, read the high part, then combine it.
	     */
	    lo = *(uint8_t *)(cs->data + offset);
	    cs = get_fat_sector(fs, fat_sector + 1);
	    hi = *(uint8_t *)cs->data;
	    next_cluster = (hi << 8) + lo;
	} else {
	    next_cluster = *(uint16_t *)(cs->data + offset);
	}
	
	if (clust_num & 0x0001)
	    next_cluster >>= 4;         /* cluster number is ODD */
	else
	    next_cluster &= 0x0fff;     /* cluster number is EVEN */
	break;
	
    case FAT16:
	offset = clust_num << 1;
	fat_sector = offset >> SECTOR_SHIFT(fs);
	offset &= sector_mask;
	cs = get_fat_sector(fs, fat_sector);
	next_cluster = *(uint16_t *)(cs->data + offset);
	break;
	
    case FAT32:
	offset = clust_num << 2;
	fat_sector = offset >> SECTOR_SHIFT(fs);
	offset &= sector_mask;
	cs = get_fat_sector(fs, fat_sector);
	next_cluster = *(uint32_t *)(cs->data + offset);
	next_cluster &= 0x0fffffff;
	break;
    }
    
    return next_cluster;
}


static sector_t get_next_sector(struct fs_info* fs, uint32_t sector)
{
    struct fat_sb_info *sbi = FAT_SB(fs);
    sector_t data_area = sbi->data;
    sector_t data_sector;
    uint32_t cluster;
    int clust_shift = sbi->clust_shift;
    
    if (sector < data_area) {
	/* Root directory sector... */
	sector++;
	if (sector >= data_area)
	    sector = 0; /* Ran out of root directory, return EOF */
	return sector;
    }
    
    data_sector = sector - data_area;
    if ((data_sector + 1) & sbi->clust_mask)  /* Still in the same cluster */
	return sector + 1;		      /* Next sector inside cluster */

    /* get a new cluster */
    cluster = data_sector >> clust_shift;
    cluster = get_next_cluster(fs, cluster + 2) - 2;

    if (cluster >= sbi->clusters)
	return 0;
    
    /* return the start of the new cluster */
    sector = (cluster << clust_shift) + data_area;
    return sector;
}

/*
 * Here comes the place I don't like VFAT fs most; if we need seek
 * the file to the right place, we need get the right sector address
 * from begining everytime! Since it's a kind a signle link list, we
 * need to traver from the head-node to find the right node in that list.
 *
 * What a waste of time!
 */
static sector_t get_the_right_sector(struct file *file)
{
    int i = 0;
    int sector_pos  = file->offset >> SECTOR_SHIFT(file->fs);
    sector_t sector = *(sector_t *)file->inode->pvt;
    
    for (; i < sector_pos; i++) 
	sector = get_next_sector(file->fs, sector);
    
    return sector;
}

/**
 * __getfssec:
 *
 * get multiple sectors from a file
 *
 * This routine makes sure the subransfers do not cross a 64K boundary
 * and will correct the situation if it does, UNLESS *sectos* cross
 * 64K boundaries.
 *
 */
static void __getfssec(struct fs_info *fs, char *buf, 
                       struct file *file, uint32_t sectors)
{
    sector_t curr_sector = get_the_right_sector(file);
    sector_t frag_start , next_sector;
    uint32_t con_sec_cnt;
    struct disk *disk = fs->fs_dev->disk;
    
    while (sectors) {
        /* get fragment */
        con_sec_cnt = 0;
        frag_start = curr_sector;
        
        do {
            /* get consective sector  count */
            con_sec_cnt ++;
            sectors --;
            if (sectors == 0)
                break;
            
            next_sector = get_next_sector(fs, curr_sector);
            if (!next_sector)
                break;                        
        }while(next_sector == (++curr_sector));
        
#if 0   
        printf("You are reading data stored at sector --0x%x--0x%x\n", 
               frag_start, frag_start + con_sec_cnt -1);
#endif 
                        
        /* do read */
        disk->rdwr_sectors(disk, buf, frag_start, con_sec_cnt, 0);
        buf += con_sec_cnt << SECTOR_SHIFT(fs);/* adjust buffer pointer */
        
        if (!sectors)
            break;
        
        curr_sector = next_sector;
    }
    
}



/**
 * get multiple sectors from a file 
 *
 * @param: buf, the buffer to store the read data
 * @param: file, the file structure pointer
 * @param: sectors, number of sectors wanna read
 * @param: have_more, set one if has more
 *
 * @return: number of bytes read
 *
 */
static uint32_t vfat_getfssec(struct file *file, char *buf, int sectors,
			      bool *have_more)
{
    struct fs_info *fs = file->fs;
    uint32_t bytes_left = file->inode->size - file->offset;
    uint32_t bytes_read = sectors << fs->sector_shift;
    int sector_left;
        
    sector_left = (bytes_left + SECTOR_SIZE(fs) - 1) >> fs->sector_shift;
    if (sectors > sector_left)
        sectors = sector_left;
    
    __getfssec(fs, buf, file, sectors);
    
    if (bytes_read >= bytes_left) {
        bytes_read = bytes_left;
        *have_more = 0;
    } else {
        *have_more = 1;
    }    
    file->offset += bytes_read;
    
    return bytes_read;
}

/*
 * Mangle a filename pointed to by src into a buffer pointed to by dst; 
 * ends on encountering any whitespace.
 *
 */
static void vfat_mangle_name(char *dst, const char *src)
{
    char *p = dst;
    char c;
    int i = FILENAME_MAX -1;
    
    /*
     * Copy the filename, converting backslash to slash and
     * collapsing duplicate separators.
     */
    while (not_whitespace(c = *src)) {
        if (c == '\\')
            c = '/';
        
        if (c == '/') {
            if (src[1] == '/' || src[1] == '\\') {
                src++;
                i--;
                continue;
            }
        }        
        i--;
        *dst++ = *src++;
    }

    /* Strip terminal slashes or whitespace */
    while (1) {
        if (dst == p)
            break;
		if (*(dst-1) == '/' && dst-1 == p) /* it's the '/' case */
			break;
        if ((*(dst-1) != '/') && (*(dst-1) != '.'))
            break;
        
        dst--;
        i++;
    }

    i++;
    for (; i > 0; i --)
        *dst++ = '\0';
}

/*
 * Mangle a normal style string to DOS style string.
 */
static void mangle_dos_name(char *mangle_buf, char *src)
{       
    char *dst = mangle_buf;
    int i = 0;
    unsigned char c;        
    
    for (; i < 11; i ++)
	mangle_buf[i] = ' ';
    
    for (i = 0; i < 11; i++) {
	c = *src ++;
	
	if ((c <= ' ') || (c == '/')) 
	    break;
	
	if (c == '.') {
	    dst = &mangle_buf[8];
	    i = 7;
	    continue;
	}
	
	if (c >= 'a' && c <= 'z')
	    c -= 32;
	if ((c == 0xe5) && (i == 11))
	    c = 0x05;
	
	*dst++ = c;
    }
    mangle_buf[11] = '\0';
}


/* try with the biggest long name */
static char long_name[0x40 * 13];
static char entry_name[14];

static void unicode_to_ascii(char *entry_name, uint16_t *unicode_buf)
{
    int i = 0;
    
    for (; i < 13; i++) {
	if (unicode_buf[i] == 0xffff) {
	    entry_name[i] = '\0';
	    return;
	}
	entry_name[i] = (char)unicode_buf[i];
    }
}

/*
 * get the long entry name
 *
 */
static void long_entry_name(struct fat_long_name_entry *dir)
{
    uint16_t unicode_buf[13];
    
    memcpy(unicode_buf,      dir->name1, 5 * 2);
    memcpy(unicode_buf + 5,  dir->name2, 6 * 2);
    memcpy(unicode_buf + 11, dir->name3, 2 * 2);
    
    unicode_to_ascii(entry_name, unicode_buf);    
}


static uint8_t get_checksum(char *dir_name)
{
    int  i;
    uint8_t sum = 0;
    
    for (i = 11; i; i--)
	sum = ((sum & 1) << 7) + (sum >> 1) + *dir_name++;
    return sum;
}


/* compute the first sector number of one dir where the data stores */
static inline sector_t first_sector(struct fs_info *fs,
				    struct fat_dir_entry *dir)
{
    struct fat_sb_info *sbi = FAT_SB(fs);
    uint32_t first_clust;
    sector_t sector;
    
    first_clust = (dir->first_cluster_high << 16) + dir->first_cluster_low;
    sector = ((first_clust - 2) << sbi->clust_shift) + sbi->data;
    
    return sector;
}

static inline int get_inode_mode(uint8_t attr)
{
    if (attr == FAT_ATTR_DIRECTORY)
	return I_DIR;
    else
	return I_FILE;
}

 
static struct inode *vfat_find_entry(char *dname, struct inode *dir)
{
    struct fs_info *fs = dir->fs;
    struct inode *inode = new_fat_inode(fs);
    struct fat_dir_entry *de;
    struct fat_long_name_entry *long_de;
    struct cache_struct *cs;
    
    char mangled_name[12];
    sector_t dir_sector = *(sector_t *)dir->pvt;
    
    uint8_t vfat_init, vfat_next, vfat_csum = 0;
    uint8_t id;
    int slots;
    int entries;
    int checksum;
    int long_match = 0;
    
    slots = (strlen(dname) + 12) / 13 ;
    slots |= 0x40;
    vfat_init = vfat_next = slots;
    
    /* Produce the shortname version, if appropriate. */
    mangle_dos_name(mangled_name, dname);

    while (1) {
	cs = get_cache_block(fs->fs_dev, dir_sector);
	de = (struct fat_dir_entry *)cs->data;
	entries = 1 << (fs->sector_shift - 5);
	
	while(entries--) {
	    if (de->name[0] == 0)
		return NULL;
	    
	    if (de->attr == 0x0f) {
		/*
		 * It's a long name entry.
		 */
		long_de = (struct fat_long_name_entry *)de;
		id = long_de->id;
		if (id != vfat_next)
		    goto not_match;
		
		if (id & 0x40) {
		    /* get the initial checksum value */
		    vfat_csum = long_de->checksum;
		    id &= 0x3f;

		    /* ZERO the long_name buffer */
		    memset(long_name, 0, sizeof long_name);
		} else {
		    if (long_de->checksum != vfat_csum)
			goto not_match;
		}
		
		vfat_next = --id;
		
		/* got the long entry name */
		long_entry_name(long_de);
		memcpy(long_name + id * 13, entry_name, 13);
				
		/* 
		 * If we got the last entry, check it.
		 * Or, go on with the next entry.
		 */
		if (id == 0) {
		    if (strcmp(long_name, dname))
			goto not_match;
		    long_match = 1;
		}
		
		de++;
		continue;     /* Try the next entry */
	    } else {
		/*
		 * It's a short entry 
		 */
		if (de->attr & 0x08) /* ignore volume labels */
		    goto not_match;
		
		if (long_match == 1) {
		    /* 
		     * We already have a VFAT long name match. However, the 
		     * match is only valid if the checksum matches.
		     *
		     * Well, let's trun the long_match flag off first.
		     */
		     long_match = 0;
		    checksum = get_checksum(de->name);
		    if (checksum == vfat_csum)
			goto found;  /* Got it */
		} else {
		    if (!strncmp(mangled_name, de->name, 11))
			goto found;
		}
	    }
	    
	not_match:
	    vfat_next = vfat_init;
	    
	    de++;
	}
	
	/* Try with the next sector */
	dir_sector = get_next_sector(fs, dir_sector);
	if (!dir_sector)
	    return NULL;
    }
    
found:
    inode->size = de->file_size;
    *(sector_t *)inode->pvt = first_sector(fs, de);
    inode->mode = get_inode_mode(de->attr);
    
    return inode;
}

static struct inode *vfat_iget_root(struct fs_info *fs)
{
    struct inode *inode = new_fat_inode(fs);
    int root_size = FAT_SB(fs)->root_size;
    
    /* 
     * For FAT32, the only way to get the root directory size is to
     * follow the entire FAT chain to the end... which seems pointless.
     */
    inode->size = root_size ? root_size << fs->sector_shift : ~0;
    *(sector_t *)inode->pvt = FAT_SB(fs)->root;
    inode->mode = I_DIR;
    
    return inode;
}

static struct inode *vfat_iget(char *dname, struct inode *parent)
{
    return vfat_find_entry(dname, parent);
}

static struct dirent * vfat_readdir(struct file *file)
{
    struct fs_info *fs = file->fs;
    struct dirent *dirent;
    struct fat_dir_entry *de;
    struct fat_long_name_entry *long_de;
    struct cache_struct *cs;
    
    sector_t sector = get_the_right_sector(file);
    
    uint8_t vfat_init, vfat_next, vfat_csum;
    uint8_t id;
    int entries_left;
    int checksum;
    int long_entry = 0;
    int sec_off = file->offset & ((1 << fs->sector_shift) - 1);
    
    cs = get_cache_block(fs->fs_dev, sector);
    de = (struct fat_dir_entry *)(cs->data + sec_off);
    entries_left = ((1 << fs->sector_shift) - sec_off) >> 5;

    vfat_next = vfat_csum = 0xff;
    
    while (1) {
	while(entries_left--) {
	    if (de->name[0] == 0)
		return NULL;
	    if ((uint8_t)de->name[0] == 0xe5)
		goto invalid;
	    
	    if (de->attr == 0x0f) {
		/*
		 * It's a long name entry.
		 */
		long_de = (struct fat_long_name_entry *)de;
		id = long_de->id;
		
		if (id & 0x40) {
		    /* init vfat_csum and vfat_init */
		    vfat_csum = long_de->checksum;
		    id &= 0x3f;
		    vfat_init = id;
		    
		    /* ZERO the long_name buffer */
		    memset(long_name, 0, sizeof long_name);
		} else {
		    if (long_de->checksum != vfat_csum ||
			id != vfat_next)
			goto invalid;
		}
		
		vfat_next = --id;
		
		/* got the long entry name */
		long_entry_name(long_de);
		memcpy(long_name + id * 13, entry_name, 13);
		
		if (id == 0) 
		    long_entry = 1;
		
		de++;
		file->offset += sizeof(struct fat_dir_entry);
		continue;     /* Try the next entry */
	    } else {
		/*
		 * It's a short entry 
		 */
		if (de->attr & 0x08) /* ignore volume labels */
		    goto invalid;
		
		if (long_entry == 1) {
		    /* Got a long entry */
		    checksum = get_checksum(de->name);
		    if (checksum == vfat_csum)
			goto got;
		} else {
		    /* Use the long_name buffer to store a short one. */
		    int i;
		    char *p = long_name;
		    
		    for (i = 0; i < 8; i++) {
			if (de->name[i] == ' ')
			    break;
			*p++ = de->name[i];
		    }
		    *p++ = '.';
		    if (de->name[8] == ' ') {
			*--p = '\0';
		    } else {
			for (i = 8; i < 11; i++) {
			    if (de->name[i] == ' ')
				break;
			    *p++ = de->name[i];
			}
			*p = '\0';
		    }
		    
		    goto got;
		}
	    }
	    
	invalid:
	    de++;
	    file->offset += sizeof(struct fat_dir_entry);
	}
	
	/* Try with the next sector */
	sector = get_next_sector(fs, sector);
	if (!sector)
	    return NULL;
	cs = get_cache_block(fs->fs_dev, sector);
	de = (struct fat_dir_entry *)cs->data;
	entries_left = 1 << (fs->sector_shift - 5);
    }
    
got:
    if (!(dirent = malloc(sizeof(*dirent)))) {
	malloc_error("dirent structure in vfat_readdir");
	return NULL;
    }
    dirent->d_ino = 0;           /* Inode number is invalid to FAT fs */
    dirent->d_off = file->offset;
    dirent->d_reclen = 0;
    dirent->d_type = get_inode_mode(de->attr);
    strcpy(dirent->d_name, long_name);
    
    file->offset += sizeof(*de);  /* Update for next reading */
    
    return dirent;
}

/* Load the config file, return 1 if failed, or 0 */
static int vfat_load_config(void)
{
    const char * const syslinux_cfg[] = {
	"/boot/syslinux/syslinux.cfg",
	"/syslinux/syslinux.cfg",
	"/syslinux.cfg"
    };
    com32sys_t regs;
    char *p;
    int i = 0;

    /* 
     * we use the ConfigName to pass the config path because
     * it is under the address 0xffff
     */
    memset(&regs, 0, sizeof regs);
    regs.edi.w[0] = OFFS_WRT(ConfigName, 0);
    for (; i < 3; i++) {
        strcpy(ConfigName, syslinux_cfg[i]);
        call16(core_open, &regs, &regs);

        /* if zf flag set, then failed; try another */
        if (! (regs.eflags.l & EFLAGS_ZF))
            break;
    }
    if (i == 3) {
        printf("no config file found\n");
        return 1;  /* no config file */
    }
    
    strcpy(ConfigName, "syslinux.cfg");
    strcpy(CurrentDirName, syslinux_cfg[i]);
    p = strrchr(CurrentDirName, '/');
    *(p + 1) = '\0';        /* In case we met '/syslinux.cfg' */
    
    return 0;
}
 
static inline __constfunc uint32_t bsr(uint32_t num)
{
    asm("bsrl %1,%0" : "=r" (num) : "rm" (num));
    return num;
}

/* init. the fs meta data, return the block size in bits */
static int vfat_fs_init(struct fs_info *fs)
{
    struct fat_bpb fat;
    struct fat_sb_info *sbi;
    struct disk *disk = fs->fs_dev->disk;
    int sectors_per_fat;
    uint32_t clusters;
    sector_t total_sectors;
    
    fs->sector_shift = fs->block_shift = disk->sector_shift;
    fs->sector_size  = 1 << fs->sector_shift;
    fs->block_size   = 1 << fs->block_shift;

    disk->rdwr_sectors(disk, &fat, 0, 1, 0);
    
    sbi = malloc(sizeof(*sbi));
    if (!sbi)
	malloc_error("fat_sb_info structure");
    fs->fs_info = sbi;
    
    sectors_per_fat = fat.bxFATsecs ? : fat.fat32.bxFATsecs_32;
    total_sectors   = fat.bxSectors ? : fat.bsHugeSectors;
    
    sbi->fat       = fat.bxResSectors;	
    sbi->root      = sbi->fat + sectors_per_fat * fat.bxFATs;
    sbi->root_size = root_dir_size(fs, &fat);
    sbi->data      = sbi->root + sbi->root_size;
    
    sbi->clust_shift      = bsr(fat.bxSecPerClust);
    sbi->clust_byte_shift = sbi->clust_shift + fs->sector_shift;
    sbi->clust_mask       = fat.bxSecPerClust - 1;
    sbi->clust_size       = fat.bxSecPerClust << fs->sector_shift;
    
    clusters = (total_sectors - sbi->data) >> sbi->clust_shift;
    if (clusters <= 0xff4) {
	sbi->fat_type = FAT12;
    } else if (clusters <= 0xfff4) {
	sbi->fat_type = FAT16;
    } else {
	sbi->fat_type = FAT32;

	if (clusters > 0x0ffffff4)
	    clusters = 0x0ffffff4; /* Maximum possible */

	if (fat.fat32.extended_flags & 0x80) {
	    /* Non-mirrored FATs, we need to read the active one */
	    sbi->fat += (fat.fat32.extended_flags & 0x0f) * sectors_per_fat;
	}

	/* FAT32: root directory is a cluster chain */
	sbi->root = sbi->data
	    + ((fat.fat32.root_cluster-2) << sbi->clust_shift);
    }
    sbi->clusters = clusters;
    
    /* for SYSLINUX, the cache is based on sector size */
    return fs->sector_shift;
}
        
const struct fs_ops vfat_fs_ops = {
    .fs_name       = "vfat",
    .fs_flags      = FS_USEMEM | FS_THISIND,
    .fs_init       = vfat_fs_init,
    .searchdir     = NULL,
    .getfssec      = vfat_getfssec,
    .close_file    = vfat_close_file,
    .mangle_name   = vfat_mangle_name,
    .unmangle_name = generic_unmangle_name,
    .load_config   = vfat_load_config,
    .readdir       = vfat_readdir,
    .iget_root     = vfat_iget_root,
    .iget_current  = NULL,
    .iget          = vfat_iget,
};
