#include <stdio.h>
#include <string.h>
#include "cache.h"
#include "core.h"
#include "disk.h"
#include "ext2_fs.h"
#include "fs.h"

#define FILENAME_MAX_LG2 8
#define FILENAME_MAX     (1 << FILENAME_MAX_LG2)
/* The size of open_file_t in extlinux is double of in others */
#define MAX_OPEN_LG2     (6 - 1)  
#define MAX_OPEN         (1 << MAX_OPEN_LG2)
#define MAX_SYMLINKS     64
#define SYMLINK_SECTORS  2

/* 
 * File structure, This holds the information for each currently open file 
 */
struct open_file_t {
    uint32_t file_bytesleft;  /* Number of bytes left (0 = free) */
    uint32_t file_sector;     /* Next linear sector to read */
    sector_t file_in_sec;     /* Sector where inode lives */
    uint16_t file_in_off;
    uint16_t file_mode;
    uint32_t pad[3];          /* pad to 2^5 == 0x20 bytes */
};

static struct open_file_t __bss16 Files[MAX_OPEN];

static char SymlinkBuf[SYMLINK_SECTORS * SECTOR_SIZE + 64];

static struct ext2_inode this_inode;
static struct ext2_super_block sb;

static uint16_t ClustByteShift,  ClustShift;
static uint32_t SecPerClust, ClustSize, ClustMask;
static uint32_t PtrsPerBlock1, PtrsPerBlock2, PtrsPerBlock3;
static int DescPerBlock, InodePerBlock;


/**
 * strecpy:
 *
 * just like the function strcpy(), except it returns non-zero if overflow.
 *
 * well, in Syslinux, strcpy() will advance both the dst and src string pointer.
 * 
 */
static int strecpy(char *dst, char *src, char *end)
{
    while (*src != '\0')
        *dst++ = *src++;
    *dst = '\0';
    
    if (dst > end)
        return 1;
    else 
        return 0;
}


/**
 * allocate_file:
 * 
 * Allocate a file structure
 *
 * @return: if successful return the file pointer, or return NULL
 *
 */
static struct open_file_t *allocate_file(void)
{
    struct open_file_t *file = Files;
    int i;
        
    for (i = 0; i < MAX_OPEN; i++) {
        if (file->file_bytesleft == 0) /* found it */
            return file;
        file++;
    }
    
    return NULL; /* not found */
}


/**
 * close_file:
 *
 * Deallocates a file structure point by FILE
 *
 * @param: file, the file structure we want deallocate
 *
 */
static void close_file(struct open_file_t *file)
{
    if (file)
        file->file_bytesleft = 0;
}


/**
 * mangle_name:
 * 
 * Mangle a filename pointed to by DS:SI(of regs) into a
 * buffer pointed to by ES:DI(of regs); ends on encountering
 * any whitespace.
 *
 */
static void ext2_mangle_name(char *dst, char *src)
{
    char *p = dst;
    int i = FILENAME_MAX -1;
    
    while(*src > ' ') {
        if (*src == '/') {
            if (*(src+1) == '/') {
                src ++;
                i --;
                continue;
            }
        }        
        i --;
        *dst++ = *src++;
    }

    while (1) {
        if (dst == p)
            break;        
        if (*(dst-1) != '/') 
            break;
        
        dst --;
        i ++;
    }

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

/*
 * Does the opposite of mangle_name; converts a DOS-mangled
 * filename to the conventional representation.  This is 
 * needed for the BOOT_IMAGE= parameter for the kernel.
 *
 * it returns the lenght of the filename.
 */
static int ext2_unmangle_name(char *dst, char *src)
{
    strcpy(dst, src);
    return strlen(src);
}


/**
 * get_group_desc:
 *
 * get the group's descriptor of group_num
 *
 * @param: group_num, the group number;
 * 
 * @return: the pointer of the group's descriptor
 *
 */ 
static struct ext2_group_desc *get_group_desc(struct fs_info *fs, uint32_t group_num)
{
    block_t block_num;
    uint32_t offset;
    struct ext2_group_desc *desc;
    struct cache_struct *cs;

    block_num = group_num / DescPerBlock;
    offset = group_num % DescPerBlock;

    block_num += sb.s_first_data_block + 1;
    cs = get_cache_block(fs->fs_dev, block_num);

    desc = (struct ext2_group_desc *)cs->data + offset;

    return desc;
}


/**
 * read_inode:
 *
 * read the right inode structure to _dst_.
 *
 * @param: inode_offset, the inode offset within a group;
 * @prarm: dst, wher we will store the inode structure;
 * @param: desc, the pointer to the group's descriptor
 * @param: block, a pointer used for retruning the blk number for file structure
 * @param: offset, same as block
 *
 */
static void read_inode(struct fs_info *fs, uint32_t inode_offset, 
                struct ext2_inode *dst, struct ext2_group_desc *desc,
                block_t *block, uint32_t *offset)
{
    struct cache_struct *cs;
    struct ext2_inode *inode;
    
    *block  = inode_offset / InodePerBlock + desc->bg_inode_table;
    *offset = inode_offset % InodePerBlock;
    
    cs = get_cache_block(fs->fs_dev, *block);
    
    /* well, in EXT4, the inode structure usually be 256 */
    inode = (struct ext2_inode *)(cs->data + (*offset * (sb.s_inode_size)));
    memcpy(dst, inode, EXT2_GOOD_OLD_INODE_SIZE);
    
    /* for file structure */
    *offset = (inode_offset * sb.s_inode_size) % ClustSize;
}


/**
 * open_inode:
 *
 * open a file indicated by an inode number in INR
 *
 * @param : inr, the inode number
 * @return: a open_file_t structure pointer
 *          file length in bytes
 *          the first 128 bytes of the inode, stores in ThisInode
 *
 */
static struct open_file_t * open_inode(struct fs_info *fs, uint32_t inr, uint32_t *file_len)
{
    struct open_file_t *file;
    struct ext2_group_desc *desc;
        
    uint32_t inode_group, inode_offset;
    block_t block_num;
    uint32_t block_off;    
    
    file = allocate_file();
    if (!file)
        return NULL;
    
    file->file_sector = 0;
    
    inr --;
    inode_group  = inr / sb.s_inodes_per_group;
    
    /* get the group desc */
    desc = get_group_desc(fs, inode_group);
    
    inode_offset = inr % sb.s_inodes_per_group;
    read_inode(fs, inode_offset, &this_inode, desc, &block_num, &block_off);
    
    /* Finally, we need to convet it to sector for now */
    file->file_in_sec = (block_num<<ClustShift) + (block_off>>SECTOR_SHIFT);
    file->file_in_off = block_off & (SECTOR_SIZE - 1);
    file->file_mode = this_inode.i_mode;
    *file_len = file->file_bytesleft = this_inode.i_size;
    
    if (*file_len == 0)
        return NULL;

    return file;
}



static struct ext4_extent_header * 
ext4_find_leaf (struct fs_info *fs, struct ext4_extent_header *eh, block_t block)
{
    struct ext4_extent_idx *index;
    struct cache_struct *cs;
    block_t blk;
    int i;
    
    while (1) {        
        if (eh->eh_magic != EXT4_EXT_MAGIC)
            return NULL;
        
        /* got it */
        if (eh->eh_depth == 0)
            return eh;
        
        index = EXT4_FIRST_INDEX(eh);        
        for (i = 0; i < eh->eh_entries; i++) {
            if (block < index[i].ei_block)
                break;
        }
        if (--i < 0)
            return NULL;
        
        blk = index[i].ei_leaf_hi;
        blk = (blk << 32) + index[i].ei_leaf_lo;
        
        /* read the blk to memeory */
        cs = get_cache_block(fs->fs_dev, blk);
        eh = (struct ext4_extent_header *)(cs->data);
    }
}

/* handle the ext4 extents to get the phsical block number */
static block_t linsector_extent(struct fs_info *fs, block_t block, struct ext2_inode *inode)
{
    struct ext4_extent_header *leaf;
    struct ext4_extent *ext;
    int i;
    block_t start;
    
    leaf = ext4_find_leaf(fs, (struct ext4_extent_header*)inode->i_block, block);
    if (! leaf) {
        printf("ERROR, extent leaf not found\n");
        return 0;
    }
    
    ext = EXT4_FIRST_EXTENT(leaf);
    for (i = 0; i < leaf->eh_entries; i++) {
        if (block < ext[i].ee_block)
            break;
    }
    if (--i < 0) {
        printf("ERROR, not find the right block\n");
        return 0;
    }
    
    
    /* got it */
    block -= ext[i].ee_block;
    if (block >= ext[i].ee_len)
        return 0;
    
    start = ext[i].ee_start_hi;
    start = (start << 32) + ext[i].ee_start_lo;
    
    return start + block;
}


/**
 * linsector_direct:
 * 
 * @param: block, the block index
 * @param: inode, the inode structure
 *
 * @return: the physic block number
 */
static block_t linsector_direct(struct fs_info *fs, uint32_t block, struct ext2_inode *inode)
{
    struct cache_struct *cs;
    
    /* direct blocks */
    if (block < EXT2_NDIR_BLOCKS) 
        return inode->i_block[block];
    

    /* indirect blocks */
    block -= EXT2_NDIR_BLOCKS;
    if (block < PtrsPerBlock1) {
        block_t ind_block = inode->i_block[EXT2_IND_BLOCK];
        cs = get_cache_block(fs->fs_dev, ind_block);
        
        return ((uint32_t *)cs->data)[block];
    }
    
    /* double indirect blocks */
    block -= PtrsPerBlock1;
    if (block < PtrsPerBlock2) {
        block_t dou_block = inode->i_block[EXT2_DIND_BLOCK];
        cs = get_cache_block(fs->fs_dev, dou_block);
        
        dou_block = ((uint32_t *)cs->data)[block / PtrsPerBlock1];
        cs = get_cache_block(fs->fs_dev, dou_block);
        
        return ((uint32_t*)cs->data)[block % PtrsPerBlock1];
    }
    
    /* triple indirect block */
    block -= PtrsPerBlock2;
    if (block < PtrsPerBlock3) {
        block_t tri_block = inode->i_block[EXT2_TIND_BLOCK];
        cs = get_cache_block(fs->fs_dev, tri_block);
        
        tri_block = ((uint32_t *)cs->data)[block / PtrsPerBlock2];
        cs = get_cache_block(fs->fs_dev, tri_block);
        
        tri_block = ((uint32_t *)cs->data)[block % PtrsPerBlock2];
        cs = get_cache_block(fs->fs_dev, tri_block);

        return ((uint32_t*)cs->data)[block % PtrsPerBlock1];
    }
    
    /* File too big, can not handle */
    printf("ERROR, file too big\n");
    return 0;
}


/**
 * linsector:
 *
 * Convert a linear sector index in a file to linear sector number
 *
 * well, alought this function converts a linear sector number to 
 * physic sector number, it uses block cache in the implemention.
 * 
 * @param: lin_sector, the lineral sector index
 * 
 * @return: physic sector number
 */
static sector_t linsector(struct fs_info *fs, uint32_t lin_sector)
{
    uint32_t block = lin_sector >> ClustShift;
    block_t ret;
    struct ext2_inode *inode;

    /* well, this is what I think the variable this_inode used for */
    inode = &this_inode;

    if (inode->i_flags & EXT4_EXTENTS_FLAG)
        ret = linsector_extent(fs, block, inode);
    else
        ret = linsector_direct(fs, block, inode);
    
    if (!ret) {
        printf("ERROR: something error happend at linsector..\n");
        return 0;
    }
    
    /* finally convert it to sector */
    return ((ret << ClustShift) + (lin_sector & ClustMask));
}


/*
 * NOTE! unlike strncmp, ext2_match_entry returns 1 for success, 0 for failure.
 *
 * len <= EXT2_NAME_LEN and de != NULL are guaranteed by caller.
 */
static inline int ext2_match_entry (const char * const name,
                                    struct ext2_dir_entry * de)
{
    if (!de->d_inode)
        return 0;
    return !strncmp(name, de->d_name, de->d_name_len);
}


/*
 * p is at least 6 bytes before the end of page
 */
static inline struct ext2_dir_entry *ext2_next_entry(struct ext2_dir_entry *p)
{
    return (struct ext2_dir_entry *)((char*)p + p->d_rec_len);
}

/**
 * getlinsec_ext:
 *
 * same as getlinsec, except load any sector from the zero
 * block as all zeros; use to load any data derived from
 * n ext2 block pointer, i.e. anything *except the superblock
 *
 */
static void getlinsec_ext(struct fs_info *fs, char *buf, 
                   sector_t sector, int sector_cnt)
{
    int ext_cnt = 0;
    struct disk *disk = fs->fs_dev->disk;
    
    if (sector < SecPerClust) {
        ext_cnt = SecPerClust - sector;
        memset(buf, 0, ext_cnt << SECTOR_SHIFT);
        buf += ext_cnt << SECTOR_SHIFT;
    }
    
    sector += ext_cnt;
    sector_cnt -= ext_cnt;
    disk->rdwr_sectors(disk, buf, sector, sector_cnt, 0);
}

/**
 * getfssec:
 *
 * Get multiple sectors from a file 
 *
 * Alought we have made the buffer data based on block size, 
 * we use sector for implemention; because reading multiple 
 * sectors (then can be multiple blocks) is what the function 
 * do. So, let it be based on sectors.
 *
 * This function can be called from C function, and either from
 * ASM function.
 * 
 * @param: ES:BX(of regs), the buffer to store data
 * @param: DS:SI(of regs), the pointer to open_file_t
 * @param: CX(of regs), number of sectors to read
 *
 * @return: ECX(of regs), number of bytes read
 *
 */
static uint32_t ext2_getfssec(struct fs_info *fs, char *buf, 
                       void *open_file, int sectors, int *have_more)
{
    int sector_left, next_sector, sector_idx;
    int frag_start, con_sec_cnt;
    int bytes_read = sectors << SECTOR_SHIFT;
    struct open_file_t *file = (struct open_file_t *)open_file;
    
    sector_left = (file->file_bytesleft + SECTOR_SIZE - 1) >> SECTOR_SHIFT;
    if (sectors > sector_left)
        sectors = sector_left;
    
    while (sectors) {
        /*
         * get the frament
         */
        sector_idx  = file->file_sector;
        next_sector = frag_start = linsector(fs, sector_idx);
        con_sec_cnt = 0;                
        
        /* get the consective sectors count */
        do {            
            con_sec_cnt ++;
            sectors --;
            if (sectors <= 0)
                break;
            
            /* if sectors >= the sectors left in the 64K block, break and read */
            if (sectors >= (((~(uint32_t)buf&0xffff)|((uint32_t)buf&0xffff0000)) + 1))
                break;
            
            sector_idx ++;
            next_sector ++;
        }while(next_sector == linsector(fs, sector_idx));                
        
#if 0   
        printf("You are reading data stored at sector --0x%x--0x%x\n", 
               frag_start, frag_start + con_sec_cnt -1);
#endif        
        getlinsec_ext(fs, buf, frag_start, con_sec_cnt);
        buf += con_sec_cnt << 9;
        file->file_sector += con_sec_cnt;  /* next sector index */
    }while(sectors);
    
    if (bytes_read >= file->file_bytesleft) { 
        bytes_read = file->file_bytesleft;
        *have_more = 0;
    } else
        *have_more = 1;    
    file->file_bytesleft -= bytes_read;

    return bytes_read;
}

   

/**
 * find_dir_entry:
 *
 * find a dir entry, if find return it or return NULL
 *
 */
static struct ext2_dir_entry* find_dir_entry(struct fs_info *fs, struct open_file_t *file,char *filename)
{
    int   have_more;
    char *EndBlock = trackbuf + (SecPerClust << SECTOR_SHIFT);;
    struct ext2_dir_entry *de;
    
    /* read a clust at a time */
    ext2_getfssec(fs, trackbuf, file, SecPerClust, &have_more);        
    de = (struct ext2_dir_entry *)trackbuf;        
    
    while (1) {
        if ((char *)de >= (char *)EndBlock) {
            if (!have_more) 
                return NULL;
            ext2_getfssec(fs, trackbuf, file,SecPerClust,&have_more);
            de = (struct ext2_dir_entry *)trackbuf;
        }
        
        /* Zero inode == void entry */
        if (de->d_inode == 0) {
            de = ext2_next_entry(de);       
            continue;
        }
        
        if (ext2_match_entry (filename, de)) {
            filename += de->d_name_len;
            if ((*filename == 0) || (*filename == '/'))
                return de;     /* got it */
            
            /* not match, restore the filename then try next */
            filename -= de->d_name_len;
        }
        
        de = ext2_next_entry(de);
    } 
}


static char* do_symlink(struct fs_info *fs, struct open_file_t *file, 
                 uint32_t file_len, char *filename)
{
    int  flag, have_more;
    
    char *SymlinkTmpBuf = trackbuf;
    char *lnk_end;
    char *SymlinkTmpBufEnd = trackbuf + SYMLINK_SECTORS * SECTOR_SIZE+64;  
    
    flag = this_inode.i_file_acl ? SecPerClust : 0;    
    if (this_inode.i_blocks == flag) {
        /* fast symlink */
        close_file(file);          /* we've got all we need */
        memcpy(SymlinkTmpBuf, this_inode.i_block, file_len);
        lnk_end = SymlinkTmpBuf + file_len;
        
    } else {                           
        /* slow symlink */
        ext2_getfssec(fs, SymlinkTmpBuf,file,SYMLINK_SECTORS,&have_more);
        lnk_end = SymlinkTmpBuf + file_len;
    }
    
    if (*filename != 0)
        *lnk_end++ = '/';
    
    if (strecpy(lnk_end, filename, SymlinkTmpBufEnd)) 
        return NULL; /* buffer overflow */
    
    /*
     * now copy it to the "real" buffer; we need to have
     * two buffers so we avoid overwriting the tail on 
     * the next copy.
     */
    strcpy(SymlinkBuf, SymlinkTmpBuf);
    
    /* return the new path */
    return SymlinkBuf;
}




/**
 * searchdir:
 *
 * Search the root directory for a pre-mangle filename in FILENAME.
 *
 * @param: filename, the filename we want to search.
 *
 * @out  : a file pointer, stores in DS:SI (NOTE, DS == 0)
 * @out  : file lenght in bytes, stores in eax
 *
 */
static void ext2_searchdir(char *filename, struct file *file)
{
    extern int CurrentDir;
    
    struct open_file_t *open_file;
    struct ext2_dir_entry *de;
    uint8_t  file_mode;
    uint8_t  SymlinkCtr = MAX_SYMLINKS;        
    uint32_t inr = CurrentDir;
    uint32_t ThisDir = CurrentDir;
    uint32_t file_len;
        
 begin_path:
    while (*filename == '/') { /* Absolute filename */
        inr = EXT2_ROOT_INO;
        filename ++;
    }
 open:
    if ((open_file = open_inode(file->fs, inr, &file_len)) == NULL)
        goto err_noclose;
        
    file_mode = open_file->file_mode >> S_IFSHIFT;
    
    /* It's a file */
    if (file_mode == T_IFREG) {
        if (*filename == '\0')
            goto done;
        else
            goto err;
    } 
    
    
    /* It's a directory */
    if (file_mode == T_IFDIR) {                
        ThisDir = inr;
        
        if (*filename == 0)
            goto err;
        while (*filename == '/')
            filename ++;
        
        de = find_dir_entry(file->fs, open_file, filename);
        if (!de) 
            goto err;
        
        inr = de->d_inode;
        filename += de->d_name_len;
        close_file(open_file);
        goto open;
    }    
    
        
    /*
     * It's a symlink.  We have to determine if it's a fast symlink
     * (data stored in the inode) or not (data stored as a regular
     * file.)  Either which way, we start from the directory
     * which we just visited if relative, or from the root directory
     * if absolute, and append any remaining part of the path.
     */
    if (file_mode == T_IFLNK) {
        if (--SymlinkCtr==0 || file_len>=SYMLINK_SECTORS*SECTOR_SIZE)
            goto err;    /* too many links or symlink too long */
        
        filename = do_symlink(file->fs, open_file, file_len, filename);
        if (!filename)    
            goto err_noclose;/* buffer overflow */
        
        inr = ThisDir;
        goto begin_path;     /* we got a new path, so search it again */
    }
    
    /* Otherwise, something bad ... */
 err:
    close_file(open_file);
 err_noclose:
    file_len = 0;
    open_file = NULL;
 done:        
    
    file->file_len = file_len;
    file->open_file = (void*)open_file;

#if 0
    if (open_file) {
        printf("file bytesleft: %d\n", open_file->file_bytesleft);
        printf("file sector   : %d\n", open_file->file_sector);
        printf("file in sector: %d\n", open_file->file_in_sec);
        printf("file offsector: %d\n", open_file->file_in_off);
    }
#endif

}

static void ext2_load_config(com32sys_t *regs)
{
    char *config_name = "extlinux.conf";
    com32sys_t out_regs;
    
    strcpy(ConfigName, config_name);
    *(uint32_t *)CurrentDirName = 0x00002f2e;  

    regs->edi.w[0] = OFFS_WRT(ConfigName, 0);
    memset(&out_regs, 0, sizeof out_regs);
    call16(core_open, regs, &out_regs);

    regs->eax.w[0] = out_regs.eax.w[0];

#if 0
    printf("the zero flag is %s\n", regs->eax.w[0] ?          \
           "CLEAR, means we found the config file" :
           "SET, menas we didn't find the config file");
#endif
}


/**
 * init. the fs meta data, return the block size bits.
 */
static int ext2_fs_init(struct fs_info *fs)
{
    struct disk *disk = fs->fs_dev->disk;
#if 0
    printf("super block@: %p\n", &sb);
#endif
    /* read the super block */
    disk->rdwr_sectors(disk, &sb, 2, 2, 0);
    
    ClustByteShift = sb.s_log_block_size + 10;
    ClustSize = 1 << ClustByteShift;
    ClustShift = ClustByteShift - SECTOR_SHIFT;
    
    DescPerBlock  = ClustSize >> ext2_group_desc_lg2size;
    InodePerBlock = ClustSize / sb.s_inode_size;
        
    SecPerClust = ClustSize >> SECTOR_SHIFT;
    ClustMask = SecPerClust - 1;
    
    PtrsPerBlock1 = 1 << (ClustByteShift - 2);
    PtrsPerBlock2 = 1 << ((ClustByteShift - 2) * 2);
    PtrsPerBlock3 = 1 << ((ClustByteShift - 2) * 3);
    
    return ClustByteShift;
}

const struct fs_ops ext2_fs_ops = {
    .fs_name       = "ext2",
    .fs_init       = ext2_fs_init,
    .searchdir     = ext2_searchdir,
    .getfssec      = ext2_getfssec,
    .mangle_name   = ext2_mangle_name,
    .unmangle_name = ext2_unmangle_name,
    .load_config   = ext2_load_config
};
