/* 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_FSFOLDER
#include "Lucy/Util/ToolSet.h"

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>

#ifdef CHY_HAS_SYS_TYPES_H
  #include <sys/types.h>
#endif

// For rmdir, (hard) link.
#ifdef CHY_HAS_UNISTD_H
  #include <unistd.h>
#endif

// For mkdir, rmdir.
#ifdef CHY_HAS_DIRECT_H
  #include <direct.h>
#endif

#include "Lucy/Store/FSFolder.h"
#include "Lucy/Store/CompoundFileReader.h"
#include "Lucy/Store/CompoundFileWriter.h"
#include "Lucy/Store/FSDirHandle.h"
#include "Lucy/Store/FSFileHandle.h"
#include "Lucy/Store/InStream.h"
#include "Lucy/Store/OutStream.h"
#include "Lucy/Util/IndexFileNames.h"

// Return a CharBuf containing a platform-specific absolute filepath.
static CharBuf*
S_fullpath(FSFolder *self, const CharBuf *path);

// Return true if the supplied path is a directory.
static bool_t
S_dir_ok(const CharBuf *path);

// Create a directory, or set Err_error and return false.
bool_t
S_create_dir(const CharBuf *path);

// Return true unless the supplied path contains a slash.
bool_t
S_is_local_entry(const CharBuf *path);

// Create a hard link.
bool_t
S_hard_link(CharBuf *from_path, CharBuf *to_path);

FSFolder*
FSFolder_new(const CharBuf *path) {
    FSFolder *self = (FSFolder*)VTable_Make_Obj(FSFOLDER);
    return FSFolder_init(self, path);
}

FSFolder*
FSFolder_init(FSFolder *self, const CharBuf *path) {
    CharBuf *abs_path = FSFolder_absolutify(path);
    Folder_init((Folder*)self, abs_path);
    DECREF(abs_path);
    return self;
}

void
FSFolder_initialize(FSFolder *self) {
    if (!S_dir_ok(self->path)) {
        if (!S_create_dir(self->path)) {
            RETHROW(INCREF(Err_get_error()));
        }
    }
}

bool_t
FSFolder_check(FSFolder *self) {
    return S_dir_ok(self->path);
}

FileHandle*
FSFolder_local_open_filehandle(FSFolder *self, const CharBuf *name,
                               uint32_t flags) {
    CharBuf      *fullpath = S_fullpath(self, name);
    FSFileHandle *fh = FSFH_open(fullpath, flags);
    if (!fh) { ERR_ADD_FRAME(Err_get_error()); }
    DECREF(fullpath);
    return (FileHandle*)fh;
}

bool_t
FSFolder_local_mkdir(FSFolder *self, const CharBuf *name) {
    CharBuf *dir = S_fullpath(self, name);
    bool_t result = S_create_dir(dir);
    if (!result) { ERR_ADD_FRAME(Err_get_error()); }
    DECREF(dir);
    return result;
}

DirHandle*
FSFolder_local_open_dir(FSFolder *self) {
    DirHandle *dh = (DirHandle*)FSDH_open(self->path);
    if (!dh) { ERR_ADD_FRAME(Err_get_error()); }
    return dh;
}

bool_t
FSFolder_local_exists(FSFolder *self, const CharBuf *name) {
    if (Hash_Fetch(self->entries, (Obj*)name)) {
        return true;
    }
    else if (!S_is_local_entry(name)) {
        return false;
    }
    else {
        struct stat stat_buf;
        CharBuf *fullpath = S_fullpath(self, name);
        bool_t retval = false;
        if (stat((char*)CB_Get_Ptr8(fullpath), &stat_buf) != -1) {
            retval = true;
        }
        DECREF(fullpath);
        return retval;
    }
}

bool_t
FSFolder_local_is_directory(FSFolder *self, const CharBuf *name) {
    // Check for a cached object, then fall back to a system call.
    Obj *elem = Hash_Fetch(self->entries, (Obj*)name);
    if (elem && Obj_Is_A(elem, FOLDER)) {
        return true;
    }
    else {
        CharBuf *fullpath = S_fullpath(self, name);
        bool_t result = S_dir_ok(fullpath);
        DECREF(fullpath);
        return result;
    }
}

bool_t
FSFolder_rename(FSFolder *self, const CharBuf* from, const CharBuf *to) {
    CharBuf *from_path = S_fullpath(self, from);
    CharBuf *to_path   = S_fullpath(self, to);
    bool_t   retval    = !rename((char*)CB_Get_Ptr8(from_path),
                                 (char*)CB_Get_Ptr8(to_path));
    if (!retval) {
        Err_set_error(Err_new(CB_newf("rename from '%o' to '%o' failed: %s",
                                      from_path, to_path, strerror(errno))));
    }
    DECREF(from_path);
    DECREF(to_path);
    return retval;
}

bool_t
FSFolder_hard_link(FSFolder *self, const CharBuf *from,
                   const CharBuf *to) {
    CharBuf *from_path = S_fullpath(self, from);
    CharBuf *to_path   = S_fullpath(self, to);
    bool_t   retval    = S_hard_link(from_path, to_path);
    DECREF(from_path);
    DECREF(to_path);
    return retval;
}

bool_t
FSFolder_local_delete(FSFolder *self, const CharBuf *name) {
    CharBuf *fullpath = S_fullpath(self, name);
    char    *path_ptr = (char*)CB_Get_Ptr8(fullpath);
#ifdef CHY_REMOVE_ZAPS_DIRS
    bool_t result = !remove(path_ptr);
#else
    bool_t result = !rmdir(path_ptr) || !remove(path_ptr);
#endif
    DECREF(Hash_Delete(self->entries, (Obj*)name));
    DECREF(fullpath);
    return result;
}

void
FSFolder_close(FSFolder *self) {
    Hash_Clear(self->entries);
}

Folder*
FSFolder_local_find_folder(FSFolder *self, const CharBuf *name) {
    Folder *subfolder = NULL;
    if (!name || !CB_Get_Size(name)) {
        // No entity can be identified by NULL or empty string.
        return NULL;
    }
    else if (!S_is_local_entry(name)) {
        return NULL;
    }
    else if (CB_Starts_With_Str(name, ".", 1)) {
        // Don't allow access outside of the main dir.
        return NULL;
    }
    else if (NULL != (subfolder = (Folder*)Hash_Fetch(self->entries, (Obj*)name))) {
        if (Folder_Is_A(subfolder, FOLDER)) {
            return subfolder;
        }
        else {
            return NULL;
        }
    }

    CharBuf *fullpath = S_fullpath(self, name);
    if (S_dir_ok(fullpath)) {
        subfolder = (Folder*)FSFolder_new(fullpath);
        if (!subfolder) {
            DECREF(fullpath);
            THROW(ERR, "Failed to open FSFolder at '%o'", fullpath);
        }
        // Try to open a CompoundFileReader. On failure, just use the
        // existing folder.
        CharBuf *cfmeta_file = (CharBuf*)ZCB_WRAP_STR("cfmeta.json", 11);
        if (Folder_Local_Exists(subfolder, cfmeta_file)) {
            CompoundFileReader *cf_reader = CFReader_open(subfolder);
            if (cf_reader) {
                DECREF(subfolder);
                subfolder = (Folder*)cf_reader;
            }
        }
        Hash_Store(self->entries, (Obj*)name, (Obj*)subfolder);
    }
    DECREF(fullpath);

    return subfolder;
}

static CharBuf*
S_fullpath(FSFolder *self, const CharBuf *path) {
    CharBuf *fullpath = CB_newf("%o%s%o", self->path, DIR_SEP, path);
    if (DIR_SEP[0] != '/') {
        CB_Swap_Chars(fullpath, '/', DIR_SEP[0]);
    }
    return fullpath;
}

static bool_t
S_dir_ok(const CharBuf *path) {
    struct stat stat_buf;
    if (stat((char*)CB_Get_Ptr8(path), &stat_buf) != -1) {
        if (stat_buf.st_mode & S_IFDIR) { return true; }
    }
    return false;
}

bool_t
S_create_dir(const CharBuf *path) {
    if (-1 == chy_makedir((char*)CB_Get_Ptr8(path), 0777)) {
        Err_set_error(Err_new(CB_newf("Couldn't create directory '%o': %s",
                                      path, strerror(errno))));
        return false;
    }
    return true;
}

bool_t
S_is_local_entry(const CharBuf *path) {
    ZombieCharBuf *scratch = ZCB_WRAP(path);
    uint32_t code_point;
    while (0 != (code_point = ZCB_Nip_One(scratch))) {
        if (code_point == '/') { return false; }
    }
    return true;
}

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

#if (defined(CHY_HAS_WINDOWS_H) && !defined(__CYGWIN__))

// Windows.h defines INCREF and DECREF, so we include it only at the end of
// this file and undef those symbols.
#undef INCREF
#undef DECREF

#include <windows.h>

bool_t
S_hard_link(CharBuf *from_path, CharBuf *to_path) {
    char *from8 = (char*)CB_Get_Ptr8(from_path);
    char *to8   = (char*)CB_Get_Ptr8(to_path);

    if (CreateHardLink(to8, from8, NULL)) {
        return true;
    }
    else {
        char *win_error = Err_win_error();
        Err_set_error(Err_new(CB_newf("CreateHardLink for new file '%o' from '%o' failed: %s",
                                      to_path, from_path, win_error)));
        FREEMEM(win_error);
        return false;
    }
}

#elif (defined(CHY_HAS_UNISTD_H))

bool_t
S_hard_link(CharBuf *from_path, CharBuf *to_path) {
    char *from8 = (char*)CB_Get_Ptr8(from_path);
    char *to8   = (char*)CB_Get_Ptr8(to_path);

    if (-1 == link(from8, to8)) {
        Err_set_error(Err_new(CB_newf("hard link for new file '%o' from '%o' failed: %s",
                                      to_path, from_path, strerror(errno))));
        return false;
    }
    else {
        return true;
    }
}

#else
  #error "Need either windows.h or unistd.h"
#endif /* CHY_HAS_UNISTD_H vs. CHY_HAS_WINDOWS_H */


