/*
    FileHandler - General-purpose file handling library for Niotso
    farextract.c - Copyright (c) 2011 Niotso Project <http://niotso.org/>
    Author(s): Fatbag <X-Fi6@phppoll.org>

    Permission to use, copy, modify, and/or distribute this software for any
    purpose with or without fee is hereby granted, provided that the above
    copyright notice and this permission notice appear in all copies.

    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <time.h>
#include <sys/stat.h>
#include <errno.h>
#include "far.h"

#ifndef read_uint32
 #define read_uint32(x) (unsigned)(((x)[0]<<(8*0)) | ((x)[1]<<(8*1)) | ((x)[2]<<(8*2)) | ((x)[3]<<(8*3)))
#endif

#ifdef _WIN32
#define mkdir(path, x) mkdir(path)
#endif
static int mkpath(char * path){
    char * p;
    for(p = strpbrk(path+1, "/\\"); p; p = strpbrk(p+1, "/\\")){
        char c = *p;
        int value;
        *p = '\0'; value = mkdir(path, 0644); *p = c;
        if(value != 0 && errno != EEXIST)
            return -1;
    }
    return 0;
}

enum {
    profile_ts1 = 1,
    profile_tso,
    profile_sc4,
    profile_ts2,
    profile_spore,
    profile_ts3
};

typedef struct {
    uint32_t GroupID;
    const char * Directory;
} GroupMap_t;

typedef struct {
    uint32_t TypeID;
    const char * Extension;
} TypeMap_t;

typedef struct {
    uint8_t Size;
    const char * Header;
    const char * Extension;
} HeaderMap_t;

static const GroupMap_t GroupMap[] = {
    {0x0A3C55C7, "Music/Stations/Horror/"},
    {0x0A3C55CE, "Music/Stations/OldWorld/"},
    {0x0A3C55D3, "Music/Stations/SciFi/"},
    {0x1D6962CF, "SoundData/HitLabUI/"},
    {0x1D8A8B4F, "SoundData/HitLabTestSamples/"},
    {0x29D9359D, "SoundData/CustomTrks/"},
    {0x29DAA4A6, "SoundData/Custom/"},
    {0x29DD0888, "SoundData/Multiplayer/"},
    {0x69C6C943, "SoundData/tsov2/"},
    {0x8A6FCC30, "SoundData/EP5Samps/"},
    {0x9DBDBF74, "SoundData/HitLists/"},
    {0x9DBDBF89, "SoundData/Samples/"},
    {0x9DF26DAD, "Music/Stations/Country/"},
    {0x9DF26DAE, "Music/Stations/CountryD/"},
    {0x9DF26DB1, "Music/Stations/Latin/"},
    {0x9DF26DB3, "Music/Stations/Rap/"},
    {0x9DF26DB6, "Music/Stations/Rock/"},
    {0xA9C6C89A, "SoundData/Tracks/"},
    {0xBD6E5937, "SoundData/HitLabTest/"},
    {0xBDF26DB0, "Music/Stations/Disco/"},
    {0xC9C6C9B3, "SoundData/HitListsTemp/"},
    {0xDDBDBF8C, "SoundData/Stings/"},
    {0xDDE8F5C6, "SoundData/EP2/"},
    {0xDDF26DA9, "Music/Stations/Beach/"},
    {0xDDF26DB4, "Music/Stations/Rave/"},
    {0xFDBDBF87, "SoundData/TrackDefs/"},
    {0xFDF26DAB, "Music/Stations/Classica/"}
};

static const TypeMap_t TypeMap[] = {
    {0, ".dat"},
    {1, ".bmp"},
    {2, ".tga"},
    {5, ".skel"},
    {7, ".anim"},
    {9, ".mesh"},
    {11, ".bnd"},
    {12, ".apr"},
    {13, ".oft"},
    {14, ".png"},
    {15, ".po"},
    {16, ".col"},
    {18, ".hag"},
    {20, ".jpg"},
    {24, ".png"},
    {0x0A8B0E70, ".mad"},
    {0x1B6B9806, ".utk"},
    {0x1D07EB4B, ".xa"},
    {0x1D968538, ".xa"},
    {0x2026960B, ".dat"},
    {0x3CEC2B47, ".mp3"},
    {0x3D968536, ".mp3"},
    {0x5D73A611, ".trk"},
    {0x7B1ACFCD, ".hls"},
    {0x856DDBAC, ".bmp"},
    {0x9D796DB4, ".tlo"},
    {0x9D96853A, ".wav"},
    {0xA3CD96CF, ".tkd"},
    {0xBB7051F5, ".wav"}
};

static const HeaderMap_t AudioHeaders[] = {
    {2, "XA", ".xa"},
    {4, "RIFF", ".wav"},
    {4, "UTM0", ".utk"},
    {4, "\xFF\xFB\x90\x40", ".mp3"},
    {24, "# Generated by UI editor", ".scr"}
};

static const char * groupid_to_dir(uint32_t GroupID){
    size_t i;
    for(i=0; i<sizeof(GroupMap)/sizeof(GroupMap_t); i++){
        if(GroupMap[i].GroupID == GroupID)
            return GroupMap[i].Directory;
    }
    fprintf(stderr, "%sUnrecognized Group ID 0x%08X.\n", "farextract: warning: ", GroupID);
    return "./";
}

static const char * typeid_to_ext(uint32_t TypeID){
    size_t i;
    for(i=0; i<sizeof(TypeMap)/sizeof(TypeMap_t); i++){
        if(TypeMap[i].TypeID == TypeID)
            return TypeMap[i].Extension;
    }
    fprintf(stderr, "%sUnrecognized Type ID 0x%08X.\n", "farextract: warning: ", TypeID);
    return ".dat";
}

static const char * header_to_ext(const uint8_t * Buffer, size_t Size){
    size_t i;
    for(i=0; i<sizeof(AudioHeaders)/sizeof(HeaderMap_t); i++){
        if(Size >= AudioHeaders[i].Size && !memcmp(Buffer, AudioHeaders[i].Header, AudioHeaders[i].Size))
            return AudioHeaders[i].Extension;
    }
    
    return ".str";
}

int main(int argc, char *argv[]){
    int profile = 0, overwrite = 0;
    const char * InFile = "", * OutDirectory;
    FILE * hFile;
    size_t ArchiveSize;
    uint8_t * ArchiveData;
    int ArchiveType;
    clock_t BeginningTime, EndingTime;
    unsigned extracted = 0;
    int i;

    /****
    ** Check the arguments
    */

    if(argc == 1 || (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")))){
        printf("Usage: farextract [OPTIONS] [FILE] [OUTDIRECTORY]\n"
            "Extract the contents of a FAR, DBPF, or Persist file.\n"
            "With no FILE, or when FILE is -, read from standard input.\n"
            "\n"
            "Profile options:\n"
            "  -ts1, -tso, -sc4,       Select presets suitable for The Sims 1,\n"
            "    -ts2, -spore, -ts3    The Sims Online, SimCity 4, Spore, or The Sims 3\n"
            "\n");
        printf("Miscellaneous options:\n"
            "  -f, --force             Force overwriting of files without confirmation\n"
            "  -h, --help              Show this help and exit\n"
            "\n"
            "Report bugs to <X-Fi6@phppoll.org>.\n"
            "farextract and libfar are maintained by the Niotso project.\n"
            "Home page: <http://www.niotso.org/>\n");
        return 0;
    }

    for(i=1; !InFile[0] && i != argc-1; i++){
        /* Match for options */
        if(!profile){
            if(!strcmp(argv[i], "-ts1")){ profile = profile_ts1; continue; }
            if(!strcmp(argv[i], "-tso")){ profile = profile_tso; continue; }
            if(!strcmp(argv[i], "-sc4")){ profile = profile_sc4; continue; }
            if(!strcmp(argv[i], "-ts2")){ profile = profile_ts2; continue; }
            if(!strcmp(argv[i], "-spore")){ profile = profile_spore; continue; }
            if(!strcmp(argv[i], "-ts3")){ profile = profile_ts3; continue; }
        }
        if(!overwrite && (!strcmp(argv[i], "-f") || !strcmp(argv[i], "--force"))){
            overwrite = 1;
            continue;
        }
        /* Not an option */
        if(!strcmp(argv[i], "-")){
            fprintf(stderr, "%sReading from standard input is not yet implemented.", "farextract: error: ");
            return -1;
        }
        InFile = argv[i];
        continue;
    }
    /* We're left with the out directory */
    if(!InFile[0]){
        fprintf(stderr, "%sReading from standard input is not yet implemented.", "farextract: error: ");
        return -1;
    }
    OutDirectory = argv[i];

    /****
    ** Handle profile settings
    */
    if(!profile) profile = profile_tso;
    libfar_set_option(FAR_CONFIG_DEFAULT_TO_1A, (profile == profile_ts1));
    libfar_set_option(FAR_CONFIG_DBPF_COMPRESSED, (profile >= profile_sc4));
    libfar_set_option(FAR_CONFIG_REFPACK_HNSV, 0xFB);

    /****
    ** Open the file and read in the entire contents to memory
    */

    hFile = fopen(InFile, "rb");
    if(hFile == NULL){
        fprintf(stderr, "%sThe specified input file does not exist or could not be opened for reading.", "farextract: error: ");
        return -1;
    }
    fseek(hFile, 0, SEEK_END);
    ArchiveSize = ftell(hFile);
    if(ArchiveSize < 24){
        fprintf(stderr, "%sNot a valid archive.", "farextract: error: ");
        return -1;
    }
    fseek(hFile, 0, SEEK_SET);

    ArchiveData = malloc(ArchiveSize);
    if(ArchiveData == NULL){
        fprintf(stderr, "%sMemory for this archive could not be allocated.", "farextract: error: ");
        return -1;
    }
    if(fread(ArchiveData, 1, ArchiveSize, hFile) != ArchiveSize){
        fprintf(stderr, "%sThe input file could not be read.", "farextract: error: ");
        return -1;
    }
    fclose(hFile);

    /****
    ** Identify the type of archive
    */

    ArchiveType = far_identify(ArchiveData, ArchiveSize);
    if(ArchiveType == FAR_TYPE_INVALID){
        fprintf(stderr, "%sNot a valid archive.", "farextract: error: ");
        return -1;
    }

    if(chdir(OutDirectory) != 0){
        fprintf(stderr, "%sOutput directory '%s' does not exist.", "farextract: error: ", OutDirectory);
        return -1;
    }

    if(ArchiveType != FAR_TYPE_PERSIST){
        FAREntryNode * EntryNode;
        unsigned file = 0, filescount;

        /****
        ** Load header information
        */

        FARFile * FARFileInfo = far_create_archive(ArchiveType);
        if(FARFileInfo == NULL){
            fprintf(stderr, "%sMemory for this archive could not be allocated.", "farextract: error: ");
            return -1;
        }
        if(!far_read_header(FARFileInfo, ArchiveData, ArchiveSize)){
            fprintf(stderr, "%sNot a valid archive.", "farextract: error: ");
            return -1;
        }

        filescount = FARFileInfo->Files;
        printf("This archive contains %u files.\n\nExtracting\n", filescount);
        BeginningTime = clock();

        /****
        ** Load entry information
        */

        if(!far_enumerate_entries(FARFileInfo, ArchiveData+FARFileInfo->IndexOffset,
            ArchiveSize-FARFileInfo->IndexOffset, ArchiveSize)){
            fprintf(stderr, "%sEntry data is corrupt.", "farextract: error: ");
            return -1;
        }

        /****
        ** Extract each entry
        */
        for(EntryNode = FARFileInfo->FirstEntry; EntryNode; EntryNode = EntryNode->NextEntry){
            char destbuffer[256], * destination = destbuffer;

            file++;
            if(EntryNode->Entry.Filename)
                destination = EntryNode->Entry.Filename;
            else
                sprintf(destbuffer, "%s%08x%s", groupid_to_dir(EntryNode->Entry.GroupID),
                    EntryNode->Entry.FileID, typeid_to_ext(EntryNode->Entry.TypeID));

            if(far_read_entry_data(FARFileInfo, &(EntryNode->Entry), ArchiveData)){
                /* Decompression, if any, was successful */
                if(!EntryNode->Entry.Filename && EntryNode->Entry.TypeID == 0x2026960B)
                    sprintf(destbuffer, "%s%08x%s", groupid_to_dir(EntryNode->Entry.GroupID), EntryNode->Entry.FileID,
                        header_to_ext(EntryNode->Entry.DecompressedData, EntryNode->Entry.DecompressedSize));
            }else{
                printf(" (%u/%u) Skipped (%s): %s\n", file, filescount, "entry data is corrupt", destination);
                continue;
            }

            if(mkpath(destination) != 0){
                fprintf(stderr, "%sCould not create path '%s'.", "farextract: error: ", destination);
                return -1;
            }
            if(!overwrite){
                hFile = fopen(destination, "rb");
                if(hFile != NULL){
                    /* File exists */
                    fclose(hFile);
                    printf(" (%u/%u) Skipped (%s): %s\n", file, filescount, "file exists", destination);
                    if(EntryNode->Entry.DecompressedData != EntryNode->Entry.CompressedData)
                        libfar_free(EntryNode->Entry.DecompressedData);
                    continue;
                }
            }
            hFile = fopen(destination, "wb");
            if(hFile == NULL){
                printf(" (%u/%u) Skipped (%s): %s\n", file, filescount, "could not open", destination);
                if(EntryNode->Entry.DecompressedData != EntryNode->Entry.CompressedData)
                    libfar_free(EntryNode->Entry.DecompressedData);
                continue;
            }

            printf(" (%u/%u) %s (Group ID: 0x%08X, File ID: 0x%08X, Type ID: 0x%08X) (%u bytes)\n", file, filescount,
                destination, EntryNode->Entry.GroupID, EntryNode->Entry.FileID, EntryNode->Entry.TypeID,
                EntryNode->Entry.DecompressedSize);

            fwrite(EntryNode->Entry.DecompressedData, 1, EntryNode->Entry.DecompressedSize, hFile);
            fclose(hFile);

            if(EntryNode->Entry.DecompressedData != EntryNode->Entry.CompressedData)
                libfar_free(EntryNode->Entry.DecompressedData);
            extracted++;
        }
        printf("\nFinished extracting %u of %u files in %.2f seconds.", extracted, filescount,
            ((float) (clock() - BeginningTime))/CLOCKS_PER_SEC);
    }else{
        /* Persist file */
        PersistFile * PersistInfo;

        /****
        ** Load header information
        */

        PersistInfo = far_create_persist();
        if(PersistInfo == NULL){
            fprintf(stderr, "%sMemory for this archive could not be allocated.", "farextract: error: ");
            return -1;
        }
        if(!far_read_persist_header(PersistInfo, ArchiveData, ArchiveSize)){
            fprintf(stderr, "%sNot a valid archive.", "farextract: error: ");
            return -1;
        }

        /****
        ** Extract the data
        */
        printf("Extracting\n");
        BeginningTime = clock();
        if(!far_read_persist_data(PersistInfo, ArchiveData+18)){
            fprintf(stderr, "%sNot a valid archive.", "farextract: error: ");
            return -1;
        }
        EndingTime = clock();

        if(!overwrite){
            hFile = fopen(InFile, "rb");
            if(hFile != NULL){
                /* File exists */
                fclose(hFile);
                fprintf(stderr, "%sFile exists.", "farextract: error: ");
                libfar_free(PersistInfo->DecompressedData);
                return -1;
            }
        }
        hFile = fopen(InFile, "wb");
        if(hFile == NULL){
            fprintf(stderr, "%sCould not open.", "farextract: error: ");
            libfar_free(PersistInfo->DecompressedData);
            return -1;
        }

        fwrite(PersistInfo->DecompressedData, 1, PersistInfo->DecompressedSize, hFile);
        fclose(hFile);
        printf("Extracted %u bytes in %.2f seconds.\n", PersistInfo->DecompressedSize,
            ((float) (EndingTime - BeginningTime))/CLOCKS_PER_SEC);
    }
    return 0;
}