#include "diskcache.hpp"
#include <fcntl.h>


DiskCache::DiskCache(const char* path) {
    dirfd = open(path, O_RDONLY|O_DIRECTORY);
    if (dirfd == -1) {
        log_errno(error, "open(%s, O_DIRECTORY)", path);
    }
}


DiskCache::~DiskCache() {
    if (dirfd != -1) close(dirfd);
}


void DiskCache::id2fn(const char* id, char*& tmp, char*& fn) {
    tmp = buf[0];
    fn = buf[1];

    cachekey_t h = hash(id);
    hash2str(h, fn);
    hash2str(h, tmp);
    strcpy(tmp + CACHEKEY_HEX_LEN, ".tmp");
}


ssize_t DiskCache::get(const char* id, char*& b) {
    unless (is_valid()) return -1;

    b = NULL;
    const size_t id_len = strlen(id);
    if (id_len > DISKCACHE_MAX_ID_LEN) return -1;
    char* fn;
    char* tmp;
    id2fn(id, tmp, fn);

    int fd = openat(dirfd, fn, O_RDONLY);
    if (fd == -1) {
        log_errno(debug, "openat(%d, %s)", dirfd, fn);
        return -1;
    }

    off_t size = file_size(fd);
    if (size <= (off_t)sizeof(diskcache_header_t)) {
        log(error, "truncated header, deleting '%s' (%s)", fn, id);
        if (unlinkat(dirfd, fn, 0) == -1) {
            log_errno(notice, "unlinkat(%d, %s)", dirfd, fn);
        }
        EINTR_RETRY(close(fd));
        return -1;
    }
    size -= sizeof(diskcache_header_t);

    diskcache_header_t header;
    if ((read(fd, &header, sizeof(header)) != sizeof(header)) || header.version != DISKCACHE_VERSION) {
        log(error, "cannot validate header, deleting '%s' (%s)", fn, id);
        if (unlinkat(dirfd, fn, 0) == -1) {
            log_errno(notice, "unlinkat(%d, %s)", dirfd, fn);
        }
        EINTR_RETRY(close(fd));
        return -1;
    }

    if (memcmp(id, header.id, id_len) != 0) {
        log(notice, "id mismatch/collision for '%s' (%s)", fn, id);
        EINTR_RETRY(close(fd));
        return -1;
    }

    b = (char*)pool.pop((size_t)size+1);
    if (read(fd, b, size) != size) {
        log_errno(notice, "cannot read from '%s' (%s)", fn, id); // errno might be incorrect
        pool.push(b);
        b = NULL;
        EINTR_RETRY(close(fd));
        return -1;
    }
    b[size] = '\0';

    log(info, "recalled %s for %s", fn, id);
    EINTR_RETRY(close(fd));
    return size;
}


bool DiskCache::set(const char* id, const iovec* iov, int iovcnt, size_t len) {
    unless (is_valid()) return false;

    if (strlen(id) > DISKCACHE_MAX_ID_LEN) return false;
    char* fn;
    char* tmp;
    id2fn(id, tmp, fn);

    int fd = openat(dirfd, tmp, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR);
    if (fd == -1) {
        log_errno(info, "openat(%d, %s)", dirfd, tmp);
        return false;
    }

    diskcache_header_t header;
    header.version = DISKCACHE_VERSION;
    header.time = NOW;
    strncpy(header.id, id, sizeof(header.id)); // writes additional null bytes to dest to ensure that a total of n bytes are written
    if (write(fd, &header, sizeof(header)) != sizeof(header)) {
        log_errno(error, "write(%s)", tmp); // errno might be incorrect
        EINTR_RETRY(close(fd));
        return false;
    }

    if (writev(fd, iov, iovcnt) != (ssize_t)len) {
        log_errno(error, "cannot write(%s)", tmp);
        EINTR_RETRY(close(fd));
        return false;
    }

    if (renameat(dirfd, tmp, dirfd, fn) == -1) {
        log_errno(error, "renameat(%d, %s, %s)", dirfd, tmp, fn);
        EINTR_RETRY(close(fd));
        return false;
    }

    log(info, "updated %s for %s", fn, id);
    EINTR_RETRY(close(fd));
    return true;
}


bool DiskCache::set(const char* id, const char* b, size_t len) {
    iovec iov;
    iov.iov_base = (void*)b;
    iov.iov_len = len;
    return set(id, &iov, 1, len);
}