/******************************* LICENSE **************************************
* Any code in this file may be redistributed or modified under the terms of
* the GNU General Public License as published by the Free Software
* Foundation; version 2 of the license.
****************************** END LICENSE ***********************************/

/******************************************************************************
* Author:
* Andrew Smith, http://littlesvr.ca/misc/contactandrew.php
*
* Copyright 2005-2007 Andrew Smith <andrew-smith@mail.ru>
*
* Contributors:
*
******************************************************************************/

#include <stdbool.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "bk.h"
#include "bkPath.h"
#include "bkAdd.h"
#include "bkError.h"
#include "bkGet.h"
#include "bkMangle.h"
#include "bkLink.h"
#include "bkMisc.h"
#include "bkSet.h"

int add(VolInfo* volInfo, const char* srcPathAndName, BkDir* destDir,
        const char* nameToUse)
{
    int rc;
    char lastName[NCHARS_FILE_ID_MAX_STORE];
    BkFileBase* oldHead; /* of the children list */
    struct stat statStruct;

    if (volInfo->stopOperation)
        return BKERROR_OPER_CANCELED_BY_USER;

    maybeUpdateProgress(volInfo);

    if (nameToUse == NULL) {
        rc = getLastNameFromPath(srcPathAndName, lastName);
        if (rc <= 0)
            return rc;
    } else {
        if (strlen(nameToUse) > NCHARS_FILE_ID_MAX_STORE - 1)
            return BKERROR_MAX_NAME_LENGTH_EXCEEDED;
        strcpy(lastName, nameToUse);
    }

    if (strcmp(lastName, ".") == 0 || strcmp(lastName, "..") == 0)
        return BKERROR_NAME_INVALID;

    if (!nameIsValid(lastName))
        return BKERROR_NAME_INVALID_CHAR;

    oldHead = destDir->children;

    if (volInfo->followSymLinks)
        rc = stat(srcPathAndName, &statStruct);
    else
        rc = lstat(srcPathAndName, &statStruct);
    if (rc == -1)
        return BKERROR_STAT_FAILED;

    if (IS_DIR(statStruct.st_mode)) {
        BkDir* newDir;

        newDir = malloc(sizeof(BkDir));
        if (newDir == NULL)
            return BKERROR_OUT_OF_MEMORY;

        memset(newDir, 0, sizeof(BkDir));

        strcpy(BK_BASE_PTR(newDir)->name, lastName);

        BK_BASE_PTR(newDir)->posixFileMode = statStruct.st_mode;

        BK_BASE_PTR(newDir)->next = oldHead;

        newDir->children = NULL;

        /* ADD dir contents */
        rc = addDirContents(volInfo, srcPathAndName, newDir);
        if (rc < 0) {
            free(newDir);
            return rc;
        }
        /* END ADD dir contents */

        destDir->children = BK_BASE_PTR(newDir);
    } else if (IS_REG_FILE(statStruct.st_mode)) {
        BkFile* newFile;

        if (statStruct.st_size > 0xFFFFFFFF)
            /* size won't fit in a 32bit variable on the iso */
            return BKERROR_ADD_FILE_TOO_BIG;

        newFile = malloc(sizeof(BkFile));
        if (newFile == NULL)
            return BKERROR_OUT_OF_MEMORY;

        memset(newFile, 0, sizeof(BkFile));

        strcpy(BK_BASE_PTR(newFile)->name, lastName);

        BK_BASE_PTR(newFile)->posixFileMode = statStruct.st_mode;

        BK_BASE_PTR(newFile)->next = oldHead;

        newFile->size = statStruct.st_size;

        newFile->onImage = false;

        newFile->position = 0;

        newFile->pathAndName = malloc(strlen(srcPathAndName) + 1);
        strcpy(newFile->pathAndName, srcPathAndName);

        if (volInfo->scanForDuplicateFiles) {
            BkHardLink* newLink;

            rc = findInHardLinkTable(volInfo, 0, newFile->pathAndName,
                                     statStruct.st_size, false, &newLink);
            if (rc < 0) {
                free(newFile);
                return rc;
            }

            if (newLink == NULL)
                /* not found */
            {
                rc = addToHardLinkTable(volInfo, 0, newFile->pathAndName,
                                        statStruct.st_size, false, &newLink);
                if (rc < 0) {
                    free(newFile);
                    return rc;
                }
            }

            newFile->location = newLink;
        }

        destDir->children = BK_BASE_PTR(newFile);
    } else if (IS_SYMLINK(statStruct.st_mode)) {
        BkSymLink* newSymLink;
        ssize_t numChars;

        newSymLink = malloc(sizeof(BkSymLink));
        if (newSymLink == NULL)
            return BKERROR_OUT_OF_MEMORY;

        memset(newSymLink, 0, sizeof(BkSymLink));

        strcpy(BK_BASE_PTR(newSymLink)->name, lastName);

        BK_BASE_PTR(newSymLink)->posixFileMode = statStruct.st_mode;

        BK_BASE_PTR(newSymLink)->next = oldHead;

        numChars = readlink(srcPathAndName, newSymLink->target,
                            NCHARS_SYMLINK_TARGET_MAX - 1);
        if (numChars == -1) {
            free(newSymLink);
            return BKERROR_OPEN_READ_FAILED;
        }
        newSymLink->target[numChars] = '\0';

        destDir->children = BK_BASE_PTR(newSymLink);
    } else
        return BKERROR_NO_SPECIAL_FILES;

    return 1;
}

int addDirContents(VolInfo* volInfo, const char* srcPath, BkDir* destDir)
{
    int rc;
    int srcPathLen;
    char* newSrcPathAndName;

    /* vars to read contents of a dir on fs */
    DIR* srcDir;
    struct dirent* dirEnt;

    srcPathLen = strlen(srcPath);

    /* including the new name and the possibly needed trailing '/' */
    newSrcPathAndName = malloc(srcPathLen + NCHARS_FILE_ID_MAX_STORE + 1);
    if (newSrcPathAndName == NULL)
        return BKERROR_OUT_OF_MEMORY;

    strcpy(newSrcPathAndName, srcPath);
    if (srcPath[srcPathLen - 1] != '/') {
        strcat(newSrcPathAndName, "/");
        srcPathLen++;
    }

    srcDir = opendir(srcPath);
    if (srcDir == NULL) {
        free(newSrcPathAndName);
        return BKERROR_OPENDIR_FAILED;
    }

    /* it may be possible but in any case very unlikely that readdir() will fail
    * if it does, it returns NULL (same as end of dir) */
    while ((dirEnt = readdir(srcDir)) != NULL) {
        if (strcmp(dirEnt->d_name, ".") == 0 || strcmp(dirEnt->d_name, "..") == 0)
            /* ignore "." and ".." */
            continue;

        if (strlen(dirEnt->d_name) > NCHARS_FILE_ID_MAX_STORE - 1) {
            closedir(srcDir);
            free(newSrcPathAndName);

            return BKERROR_MAX_NAME_LENGTH_EXCEEDED;
        }

        /* append file/dir name */
        strcpy(newSrcPathAndName + srcPathLen, dirEnt->d_name);

        rc = add(volInfo, newSrcPathAndName, destDir, NULL);
        if (rc <= 0 && rc != BKWARNING_OPER_PARTLY_FAILED) {
            bool goOn;

            if (volInfo->warningCbk != NULL && !volInfo->stopOperation)
                /* perhaps the user wants to ignore this failure */
            {
                snprintf(volInfo->warningMessage, BK_WARNING_MAX_LEN,
                         "Failed to add item '%s': '%s'",
                         dirEnt->d_name,
                         bk_get_error_string(rc));
                goOn = volInfo->warningCbk(volInfo->warningMessage);
                rc = BKWARNING_OPER_PARTLY_FAILED;
            } else
                goOn = false;

            if (goOn)
                continue;
            else {
                volInfo->stopOperation = true;
                closedir(srcDir);
                free(newSrcPathAndName);
                return rc;
            }
        }
    }

    free(newSrcPathAndName);

    rc = closedir(srcDir);
    if (rc != 0)
        /* exotic error */
        return BKERROR_EXOTIC;

    return 1;
}

int bk_add(VolInfo* volInfo, const char* srcPathAndName,
           const char* destPathStr, void(*progressFunction)(VolInfo*))
{
    return bk_add_as(volInfo, srcPathAndName, destPathStr, NULL,
                     progressFunction);
}

int bk_add_as(VolInfo* volInfo, const char* srcPathAndName,
              const char* destPathStr, const char* nameToUse,
              void(*progressFunction)(VolInfo*))
{
    int rc;
    NewPath destPath;
    char lastName[NCHARS_FILE_ID_MAX_STORE];

    /* vars to find the dir in the tree */
    BkDir* destDirInTree;
    bool dirFound;

    volInfo->progressFunction = progressFunction;

    rc = makeNewPathFromString(destPathStr, &destPath);
    if (rc <= 0) {
        freePathContents(&destPath);
        return rc;
    }

    rc = getLastNameFromPath(srcPathAndName, lastName);
    if (rc <= 0) {
        freePathContents(&destPath);
        return rc;
    }

    dirFound = findDirByNewPath(&destPath, &(volInfo->dirTree), &destDirInTree);
    if (!dirFound) {
        freePathContents(&destPath);
        return BKERROR_DIR_NOT_FOUND_ON_IMAGE;
    }

    freePathContents(&destPath);

    if (itemIsInDir(lastName, destDirInTree))
        return BKERROR_DUPLICATE_ADD;

    volInfo->stopOperation = false;

    rc = add(volInfo, srcPathAndName, destDirInTree, nameToUse);
    if (rc <= 0)
        return rc;

    return 1;
}

/*******************************************************************************
* bk_add_boot_record()
* Source boot file must be exactly the right size if floppy emulation requested.
* */
int bk_add_boot_record(VolInfo* volInfo, const char* srcPathAndName,
                       int bootMediaType)
{
    struct stat statStruct;
    int rc;

    if (bootMediaType != BOOT_MEDIA_NO_EMULATION &&
            bootMediaType != BOOT_MEDIA_1_2_FLOPPY &&
            bootMediaType != BOOT_MEDIA_1_44_FLOPPY &&
            bootMediaType != BOOT_MEDIA_2_88_FLOPPY) {
        return BKERROR_ADD_UNKNOWN_BOOT_MEDIA;
    }

    rc = stat(srcPathAndName, &statStruct);
    if (rc == -1)
        return BKERROR_STAT_FAILED;

    if ((bootMediaType == BOOT_MEDIA_1_2_FLOPPY &&
            statStruct.st_size != 1228800) ||
            (bootMediaType == BOOT_MEDIA_1_44_FLOPPY &&
             statStruct.st_size != 1474560) ||
            (bootMediaType == BOOT_MEDIA_2_88_FLOPPY &&
             statStruct.st_size != 2949120)) {
        return BKERROR_ADD_BOOT_RECORD_WRONG_SIZE;
    }

    volInfo->bootMediaType = bootMediaType;

    volInfo->bootRecordSize = statStruct.st_size;

    volInfo->bootRecordIsOnImage = false;

    /* make copy of the source path and name */
    if (volInfo->bootRecordPathAndName != NULL)
        free(volInfo->bootRecordPathAndName);
    volInfo->bootRecordPathAndName = malloc(strlen(srcPathAndName) + 1);
    if (volInfo->bootRecordPathAndName == NULL) {
        volInfo->bootMediaType = BOOT_MEDIA_NONE;
        return BKERROR_OUT_OF_MEMORY;
    }
    strcpy(volInfo->bootRecordPathAndName, srcPathAndName);

    /* this is the wrong function to use if you want a visible one */
    volInfo->bootRecordIsVisible = false;

    return 1;
}

/*******************************************************************************
* bk_create_dir()
*
* */
int bk_create_dir(VolInfo* volInfo, const char* destPathStr,
                  const char* newDirName)
{
    int nameLen;
    BkDir* destDir;
    int rc;
    BkFileBase* oldHead;
    BkDir* newDir;

    nameLen = strlen(newDirName);
    if (nameLen > NCHARS_FILE_ID_MAX_STORE - 1)
        return BKERROR_MAX_NAME_LENGTH_EXCEEDED;
    if (nameLen == 0)
        return BKERROR_BLANK_NAME;

    if (strcmp(newDirName, ".") == 0 || strcmp(newDirName, "..") == 0)
        return BKERROR_NAME_INVALID;

    if (!nameIsValid(newDirName))
        return BKERROR_NAME_INVALID_CHAR;

    rc = getDirFromString(&(volInfo->dirTree), destPathStr, &destDir);
    if (rc <= 0)
        return rc;

    if (itemIsInDir(newDirName, destDir))
        return BKERROR_DUPLICATE_CREATE_DIR;

    oldHead = destDir->children;

    newDir = malloc(sizeof(BkDir));
    if (newDir == NULL)
        return BKERROR_OUT_OF_MEMORY;

    strcpy(BK_BASE_PTR(newDir)->name, newDirName);

    BK_BASE_PTR(newDir)->posixFileMode = volInfo->posixDirDefaults;

    BK_BASE_PTR(newDir)->next = oldHead;

    newDir->children = NULL;

    destDir->children = BK_BASE_PTR(newDir);

    return 1;
}
