#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <openssl/sha.h>

#include "fat32.h"
#include "utils.h"

void printFsInfo(BootEntry *bootEntry) {

    printf("Number of FATs = %d\n"
        "Number of bytes per sector = %d\n"
        "Number of sectors per cluster = %d\n"
        "Number of reserved sectors = %d\n",
        bootEntry -> BPB_NumFATs, bootEntry -> BPB_BytsPerSec, bootEntry -> BPB_SecPerClus, bootEntry -> BPB_RsvdSecCnt);

}

char *formattedDirName(DirEntry *dirEntry) {

    char *formattedName = malloc(sizeof(char) * 13);
    int j = 0;
    /* Grab the main part of the file name */
    for (int i = 0; i < 8; i ++) {
        if (dirEntry -> DIR_Name[i] == 0x20) break;
        formattedName[j ++] = dirEntry -> DIR_Name[i];
    }

    /* The directory entry is a file container (i.e., another directory) */
    if (dirEntry -> DIR_Attr == 0x10) {
        formattedName[j ++] = '/';
    }
    /* The directory entry has a file extension */
    else if (dirEntry -> DIR_Name[8] != 0x20) {
        formattedName[j ++] = '.';
        for (int i = 8; i < 11; i ++) {
            if (dirEntry -> DIR_Name[i] == 0x20) break;
            formattedName[j ++] = dirEntry -> DIR_Name[i];
        }
    }
    return formattedName;

}

int printDirInfo(DirEntry *dirEntry) {

    /* The directory entry has a long file name or is freed */
    if (dirEntry -> DIR_Attr == (0x01 | 0x02 | 0x04 | 0x08)) return -1;
    if (dirEntry -> DIR_Name[0] == 0xE5 || dirEntry -> DIR_Name[0] == 0x05) return -1;
    
    char *formattedName = formattedDirName(dirEntry);
    printf("%s (size = %d, starting cluster = %d)\n",
        formattedName, dirEntry -> DIR_FileSize, (dirEntry -> DIR_FstClusHI << 16) | dirEntry -> DIR_FstClusLO);
    free(formattedName);
    return 0;

}

int delContEntryIndex(char *fileName, char *sCode, char *disk, BootEntry *bootEntry, DirEntry **rootDirEntries, int dirEntryCount) {

    int target = -1;
    char *formattedName;
    for (int i = 0; i < dirEntryCount; i ++) {
        /* The entry is marked as freed */
        if (rootDirEntries[i] -> DIR_Name[0] == 0xE5) {
            formattedName = formattedDirName(rootDirEntries[i]);
            if (strcmp(formattedName + 1, fileName + 1) == 0) {
                free(formattedName);
                if (sCode == NULL) {
                    /* Multiple freed entires with the same name */
                    if (target >= 0) return -2;
                    target = i;
                }
                else {
                    unsigned int curCluster = (rootDirEntries[i] -> DIR_FstClusHI << 16) | rootDirEntries[i] -> DIR_FstClusLO;
                    /* The target file is not empty */
                    if (curCluster != 0) {
                        SHA_CTX ctx;
                        SHA1_Init(&ctx);
                        unsigned int j = 0;
                        unsigned int BPB_BytsPerClus = bootEntry -> BPB_BytsPerSec * bootEntry -> BPB_SecPerClus;
                        /* SHA1 hash file contents for the next contiguous cluster */
                        while (rootDirEntries[i] -> DIR_FileSize > BPB_BytsPerClus * (j + 1)) {
                            SHA1_Update(&ctx, disk + followFAT(bootEntry, &curCluster, disk, 0), BPB_BytsPerClus);
                            j ++; curCluster ++;
                        }
                        /* SHA1 hash file contents for the last cluster */
                        SHA1_Update(&ctx, disk + followFAT(bootEntry, &curCluster, disk, 0),
                            rootDirEntries[i] -> DIR_FileSize - BPB_BytsPerClus * j);
                        unsigned char fSha1[SHA_DIGEST_LENGTH];
                        SHA1_Final(fSha1, &ctx);
                        /* File content hash matches the specified SHA1 code */
                        if (hashMatch(fSha1, sCode)) return i;
                    }
                    /* The target file is empty */
                    else if (strcmp(sCode, SHA1EMPTY) == 0) return i;
                }
            }
        }
    }
    return target;

}

int delNonContClusters(char *fileName, char *sCode, char *disk, BootEntry *bootEntry, DirEntry **rootDirEntries, int dirEntryCount, unsigned int *targetClusters) {

    /* The array in which the positions for the first cluster of some root directory entry are marked 1 */
    unsigned int occupied[22];
    for (int i = 0; i < 22; i ++) occupied[i] = 0;
    for (int i = 0; i < dirEntryCount; i ++) {
        occupied[(rootDirEntries[i] -> DIR_FstClusHI << 16) | rootDirEntries[i] -> DIR_FstClusLO] = 1;
    }
    /* The array of clusters that are not the first cluster of any root directory entry, within the range [2, 22) */
    int nonFirstClusterCount = 0;
    unsigned int nonFirstClusters[20];
    for (unsigned int i = 2; i < 22; i ++) {
        if (!occupied[i]) nonFirstClusters[nonFirstClusterCount ++] = i;
    }

    int target = -1;
    char *formattedName;
    for (int i = 0; i < dirEntryCount; i ++) {
        /* The entry is marked as freed */
        if (rootDirEntries[i] -> DIR_Name[0] == 0xE5) {
            formattedName = formattedDirName(rootDirEntries[i]);
            if (strcmp(formattedName + 1, fileName + 1) == 0) {
                free(formattedName);
                unsigned int firstCluster = (rootDirEntries[i] -> DIR_FstClusHI << 16) | rootDirEntries[i] -> DIR_FstClusLO;
                /* The target file is not empty */
                if (firstCluster != 0) {
                    unsigned int BPB_BytsPerClus = bootEntry -> BPB_BytsPerSec * bootEntry -> BPB_SecPerClus;
                    unsigned int targetClusterCount = rootDirEntries[i] -> DIR_FileSize / BPB_BytsPerClus + 1;
                    /* Except for the first cluster, traverse all possible permutations of other clusters */
                    unsigned int totalPermutations = 1;
                    for (unsigned int j = 0; j < targetClusterCount - 1; j ++) totalPermutations *= nonFirstClusterCount - j;
                    unsigned int **permutations = permute(nonFirstClusters, nonFirstClusterCount, targetClusterCount - 1);
                    for (unsigned int j = 0; j < totalPermutations; j ++) {
                        SHA_CTX ctx;
                        SHA1_Init(&ctx);
                        if (targetClusterCount == 1) {
                            SHA1_Update(&ctx, disk + followFAT(bootEntry, &firstCluster, disk, 0), rootDirEntries[i] -> DIR_FileSize);
                        }
                        else {
                            /* SHA1 hash contents for this permutation of clusters */
                            SHA1_Update(&ctx, disk + followFAT(bootEntry, &firstCluster, disk, 0), BPB_BytsPerClus);
                            for (unsigned int k = 0; k < targetClusterCount - 2; k ++) {
                                SHA1_Update(&ctx, disk + followFAT(bootEntry, &permutations[j][k], disk, 0), BPB_BytsPerClus);
                            }
                            /* SHA1 hash file contents for the last cluster */
                            SHA1_Update(&ctx, disk + followFAT(bootEntry, &permutations[j][targetClusterCount - 2], disk, 0),
                                rootDirEntries[i] -> DIR_FileSize - BPB_BytsPerClus * (targetClusterCount - 1));
                        }
                        unsigned char fSha1[SHA_DIGEST_LENGTH];
                        SHA1_Final(fSha1, &ctx);
                        if (hashMatch(fSha1, sCode)) {
                            targetClusters[0] = targetClusterCount;
                            targetClusters[1] = firstCluster;
                            for (unsigned int k = 0; k < targetClusterCount - 1; k ++) {
                                targetClusters[k + 2] = permutations[j][k];
                            }
                            /* Free the permutation array and its pointers */
                            for (unsigned int k = 0; k < totalPermutations; k ++) free(permutations[k]);
                            free(permutations);
                            return i;
                        }
                    }
                    /* Free the permutation array and its pointers */
                    for (unsigned int j = 0; j < totalPermutations; j ++) free(permutations[j]);
                    free(permutations);
                }
                /* The target file is empty */
                else if (strcmp(sCode, SHA1EMPTY) == 0) {
                    targetClusters[0] = 0;
                    return i;
                }
            }
        }
    }
    return target;

}

unsigned long followFAT(BootEntry *bootEntry, unsigned int *cluster, char *disk, int mode) {

    /* In FAT32, the cluster number starts with 2 */
    int curCluster = *cluster;
    assert(curCluster >= 2);
    /* FAT area, each taking 4 bytes, starting with cluster 0 */
    if (mode) memcpy(cluster, disk + bootEntry -> BPB_RsvdSecCnt * bootEntry -> BPB_BytsPerSec + curCluster * 4, 4);
    /* Data area, each taking 32 bytes, starting with cluster 2 */
    return (bootEntry -> BPB_RsvdSecCnt + bootEntry -> BPB_NumFATs * bootEntry -> BPB_FATSz32) * bootEntry -> BPB_BytsPerSec
        + (curCluster - 2) * bootEntry -> BPB_BytsPerSec * bootEntry -> BPB_SecPerClus;

}

void updateFATs(BootEntry *bootEntry, unsigned int cluster, char *disk, unsigned int DIR_NextClus) {

    unsigned long offset;
    /* Update all FATs */
    for (int i = 0; i < bootEntry -> BPB_NumFATs; i ++) {
        /* FAT area, each taking 4 bytes, starting with cluster 0 */
        offset = (bootEntry -> BPB_RsvdSecCnt + i * bootEntry -> BPB_FATSz32) * bootEntry -> BPB_BytsPerSec + cluster * 4;
        memcpy(disk + offset, &DIR_NextClus, 4);
    }

}
