/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define C_LUCY_COMPOUNDFILEREADER
#define C_LUCY_CFREADERDIRHANDLE
#include "Lucy/Util/ToolSet.h"

#include "charmony.h"

#include "Lucy/Store/CompoundFileReader.h"
#include "Lucy/Store/CompoundFileWriter.h"
#include "Lucy/Store/FileHandle.h"
#include "Lucy/Store/InStream.h"
#include "Lucy/Util/IndexFileNames.h"
#include "Lucy/Util/Json.h"

CompoundFileReader*
CFReader_open(Folder *folder) {
    CompoundFileReader *self
        = (CompoundFileReader*)Class_Make_Obj(COMPOUNDFILEREADER);
    return CFReader_do_open(self, folder);
}

CompoundFileReader*
CFReader_do_open(CompoundFileReader *self, Folder *folder) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    String *cfmeta_file = SSTR_WRAP_C("cfmeta.json");
    Hash *metadata = (Hash*)Json_slurp_json((Folder*)folder, cfmeta_file);
    Err *error = NULL;

    Folder_init((Folder*)self, Folder_Get_Path(folder));

    // Parse metadata file.
    if (!metadata || !Hash_is_a(metadata, HASH)) {
        error = Err_new(Str_newf("Can't read '%o' in '%o'", cfmeta_file,
                                 Folder_Get_Path(folder)));
    }
    else {
        Obj *format = Hash_Fetch_Utf8(metadata, "format", 6);
        ivars->format = format ? (int32_t)Json_obj_to_i64(format) : 0;
        ivars->records = (Hash*)INCREF(Hash_Fetch_Utf8(metadata, "files", 5));
        if (ivars->format < 1) {
            error = Err_new(Str_newf("Corrupt %o file: Missing or invalid 'format'",
                                     cfmeta_file));
        }
        else if (ivars->format > CFWriter_current_file_format) {
            error = Err_new(Str_newf("Unsupported compound file format: %i32 "
                                     "(current = %i32", ivars->format,
                                     CFWriter_current_file_format));
        }
        else if (!ivars->records) {
            error = Err_new(Str_newf("Corrupt %o file: missing 'files' key",
                                     cfmeta_file));
        }
    }
    DECREF(metadata);
    if (error) {
        Err_set_error(error);
        DECREF(self);
        return NULL;
    }

    // Open an instream which we'll clone over and over.
    String *cf_file = SSTR_WRAP_C("cf.dat");
    ivars->instream = Folder_Open_In(folder, cf_file);
    if (!ivars->instream) {
        ERR_ADD_FRAME(Err_get_error());
        DECREF(self);
        return NULL;
    }

    // Assign.
    ivars->real_folder = (Folder*)INCREF(folder);

    // Strip directory name from filepaths for old format.
    if (ivars->format == 1) {
        Vector *files = Hash_Keys(ivars->records);
        String *folder_name = IxFileNames_local_part(Folder_Get_Path(folder));
        size_t folder_name_len = Str_Length(folder_name);

        for (size_t i = 0, max = Vec_Get_Size(files); i < max; i++) {
            String *orig = (String*)Vec_Fetch(files, i);
            if (Str_Starts_With(orig, folder_name)) {
                Obj *record = Hash_Delete(ivars->records, orig);
                size_t offset = folder_name_len + sizeof(CHY_DIR_SEP) - 1;
                size_t len    = Str_Length(orig) - offset;
                String *filename = Str_SubString(orig, offset, len);
                Hash_Store(ivars->records, filename, (Obj*)record);
                DECREF(filename);
            }
        }

        DECREF(folder_name);
        DECREF(files);
    }

    return self;
}

void
CFReader_Destroy_IMP(CompoundFileReader *self) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    DECREF(ivars->real_folder);
    DECREF(ivars->instream);
    DECREF(ivars->records);
    SUPER_DESTROY(self, COMPOUNDFILEREADER);
}

Folder*
CFReader_Get_Real_Folder_IMP(CompoundFileReader *self) {
    return CFReader_IVARS(self)->real_folder;
}

void
CFReader_Set_Path_IMP(CompoundFileReader *self, String *path) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    Folder_Set_Path(ivars->real_folder, path);
    CFReader_Set_Path_t super_set_path
        = (CFReader_Set_Path_t)SUPER_METHOD_PTR(COMPOUNDFILEREADER,
                                                LUCY_CFReader_Set_Path);
    super_set_path(self, path);
}

FileHandle*
CFReader_Local_Open_FileHandle_IMP(CompoundFileReader *self,
                                   String *name, uint32_t flags) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    Hash *entry = (Hash*)Hash_Fetch(ivars->records, name);
    FileHandle *fh = NULL;

    if (entry) {
        Err_set_error(Err_new(Str_newf("Can't open FileHandle for virtual file %o in '%o'",
                                       name, ivars->path)));
    }
    else {
        fh = Folder_Local_Open_FileHandle(ivars->real_folder, name, flags);
        if (!fh) {
            ERR_ADD_FRAME(Err_get_error());
        }
    }

    return fh;
}

bool
CFReader_Local_Delete_IMP(CompoundFileReader *self, String *name) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    Hash *record = (Hash*)Hash_Delete(ivars->records, name);
    DECREF(record);

    if (record == NULL) {
        return Folder_Local_Delete(ivars->real_folder, name);
    }
    else {
        // Once the number of virtual files falls to 0, remove the compound
        // files.
        if (Hash_Get_Size(ivars->records) == 0) {
            String *cf_file = SSTR_WRAP_C("cf.dat");
            if (!Folder_Delete(ivars->real_folder, cf_file)) {
                return false;
            }
            String *cfmeta_file = SSTR_WRAP_C("cfmeta.json");
            if (!Folder_Delete(ivars->real_folder, cfmeta_file)) {
                return false;

            }
        }
        return true;
    }
}

InStream*
CFReader_Local_Open_In_IMP(CompoundFileReader *self, String *name) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    Hash *entry = (Hash*)Hash_Fetch(ivars->records, name);

    if (!entry) {
        InStream *instream = Folder_Local_Open_In(ivars->real_folder, name);
        if (!instream) {
            ERR_ADD_FRAME(Err_get_error());
        }
        return instream;
    }
    else {
        Obj *len    = Hash_Fetch_Utf8(entry, "length", 6);
        Obj *offset = Hash_Fetch_Utf8(entry, "offset", 6);
        if (!len || !offset) {
            Err_set_error(Err_new(Str_newf("Malformed entry for '%o' in '%o'",
                                           name, Folder_Get_Path(ivars->real_folder))));
            return NULL;
        }
        else if (Str_Get_Size(ivars->path)) {
            String *fullpath = Str_newf("%o/%o", ivars->path, name);
            InStream *instream = InStream_Reopen(ivars->instream, fullpath,
                                                 Json_obj_to_i64(offset), Json_obj_to_i64(len));
            DECREF(fullpath);
            return instream;
        }
        else {
            return InStream_Reopen(ivars->instream, name, Json_obj_to_i64(offset),
                                   Json_obj_to_i64(len));
        }
    }
}

bool
CFReader_Local_Exists_IMP(CompoundFileReader *self, String *name) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    if (Hash_Fetch(ivars->records, name))              { return true; }
    if (Folder_Local_Exists(ivars->real_folder, name)) { return true; }
    return false;
}

bool
CFReader_Local_Is_Directory_IMP(CompoundFileReader *self,
                                String *name) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    if (Hash_Fetch(ivars->records, name))                    { return false; }
    if (Folder_Local_Is_Directory(ivars->real_folder, name)) { return true; }
    return false;
}

void
CFReader_Close_IMP(CompoundFileReader *self) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    InStream_Close(ivars->instream);
}

bool
CFReader_Local_MkDir_IMP(CompoundFileReader *self, String *name) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    if (Hash_Fetch(ivars->records, name)) {
        Err_set_error(Err_new(Str_newf("Can't MkDir: '%o' exists", name)));
        return false;
    }
    else {
        bool result = Folder_Local_MkDir(ivars->real_folder, name);
        if (!result) { ERR_ADD_FRAME(Err_get_error()); }
        return result;
    }
}

Folder*
CFReader_Local_Find_Folder_IMP(CompoundFileReader *self,
                               String *name) {
    CompoundFileReaderIVARS *const ivars = CFReader_IVARS(self);
    if (Hash_Fetch(ivars->records, name)) { return false; }
    return Folder_Local_Find_Folder(ivars->real_folder, name);
}

DirHandle*
CFReader_Local_Open_Dir_IMP(CompoundFileReader *self) {
    return (DirHandle*)CFReaderDH_new(self);
}

/****************************************************************************/

CFReaderDirHandle*
CFReaderDH_new(CompoundFileReader *cf_reader) {
    CFReaderDirHandle *self
        = (CFReaderDirHandle*)Class_Make_Obj(CFREADERDIRHANDLE);
    return CFReaderDH_init(self, cf_reader);
}

CFReaderDirHandle*
CFReaderDH_init(CFReaderDirHandle *self, CompoundFileReader *cf_reader) {
    DH_init((DirHandle*)self, CFReader_Get_Path(cf_reader));
    CFReaderDirHandleIVARS *const ivars = CFReaderDH_IVARS(self);
    ivars->cf_reader = (CompoundFileReader*)INCREF(cf_reader);

    Hash *cf_records = CFReader_IVARS(ivars->cf_reader)->records;
    ivars->elems  = Hash_Keys(cf_records);
    ivars->tick   = -1;
    // Accumulate entries from real Folder.
    Folder *real_folder = CFReader_Get_Real_Folder(ivars->cf_reader);
    DirHandle *dh = Folder_Local_Open_Dir(real_folder);
    while (DH_Next(dh)) {
        String *entry = DH_Get_Entry(dh);
        Vec_Push(ivars->elems, (Obj*)Str_Clone(entry));
        DECREF(entry);
    }
    DECREF(dh);
    return self;
}

bool
CFReaderDH_Close_IMP(CFReaderDirHandle *self) {
    CFReaderDirHandleIVARS *const ivars = CFReaderDH_IVARS(self);
    if (ivars->elems) {
        DECREF(ivars->elems);
        ivars->elems = NULL;
    }
    if (ivars->cf_reader) {
        DECREF(ivars->cf_reader);
        ivars->cf_reader = NULL;
    }
    return true;
}

bool
CFReaderDH_Next_IMP(CFReaderDirHandle *self) {
    CFReaderDirHandleIVARS *const ivars = CFReaderDH_IVARS(self);
    if (ivars->elems) {
        ivars->tick++;
        if (ivars->tick < (int32_t)Vec_Get_Size(ivars->elems)) {
            String *path = (String*)CERTIFY(
                                Vec_Fetch(ivars->elems, (size_t)ivars->tick), STRING);
            DECREF(ivars->entry);
            ivars->entry = (String*)INCREF(path);
            return true;
        }
        else {
            ivars->tick--;
            return false;
        }
    }
    return false;
}

bool
CFReaderDH_Entry_Is_Dir_IMP(CFReaderDirHandle *self) {
    CFReaderDirHandleIVARS *const ivars = CFReaderDH_IVARS(self);
    if (ivars->elems) {
        String *name = (String*)Vec_Fetch(ivars->elems, (size_t)ivars->tick);
        if (name) {
            return CFReader_Local_Is_Directory(ivars->cf_reader, name);
        }
    }
    return false;
}


