#include "ent.hpp"
#include "io.hpp"
#include <fcntl.h>
#include <dirent.h>
#include <time.h>


#define REOPEN

static bool rmdir_r(int dfd, int fd, const char* name) {
    DIR* dp = fdopendir(fd);
    if (!dp) {
        LOG_ERRNO("fdopendir(%d)", fd);
        close(fd);
        return false;
    }

    bool rv = true;

    while (true) {
        struct dirent* de = readdir(dp);
        if (!de) {
            break;
        }
        if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) {
            continue;
        }
        if (de->d_type == DT_UNKNOWN) {
            struct stat ss;
            if (fstatat(fd, de->d_name, &ss, AT_SYMLINK_NOFOLLOW) == -1) {
                LOG_ERRNO("fstatat(%s)", de->d_name);
                rv = false;
                break;
            }
            if (S_ISDIR(ss.st_mode)) {
                de->d_type = DT_DIR;
            }
        }
        if (de->d_type == DT_DIR) {
            int fd_r = openat(fd, de->d_name, O_RDONLY|O_DIRECTORY|O_NOFOLLOW);
            if (fd_r == -1) {
                LOG_ERRNO("openat(%d, %s)", fd, de->d_name);
                rv = false;
                break;
            }
            if (!rmdir_r(fd, fd_r, de->d_name)) {
                rv = false;
                break;
            }
        } else {
            if (unlinkat(fd, de->d_name, 0) == -1) {
                LOG_ERRNO("unlinkat(%s)", de->d_name);
                rv = false;
                break;
            }
        }
    }

    if (rv) {
        if (unlinkat(dfd, name, AT_REMOVEDIR) == -1) {
            LOG_ERRNO("unlinkat(%s)", name);
            rv = false;
        }
    }
    closedir(dp); // closes fd
    return rv;
}


Ent::Ent(int d, int f, const char* n, Attr* a): dfd(d), fd(f), fd_ro(false), attr(a), type(a->is_dir()? TYPE_DIR: TYPE_REG), name(strdup(n)) {
    assert(dfd != -1);
    assert(strlen(name));
    assert(attr);
    assert(attr->is_dir() || attr->is_reg());
    #ifdef REOPEN
        if (fd != -1) {
            close(fd);
            fd = -1;
        }
    #else
        assert(fd != -1);
    #endif
}


Ent::Ent(const char* n): dfd(-1), fd(-1), fd_ro(false), attr(NULL), type(TYPE_NONE), name(strdup(n)) {
    assert(strlen(name));
}


Ent::~Ent() {
    if (fd != -1) close(fd);
    free((void*)name);
    if (attr) delete attr;
}


bool Ent::reopen(bool recreate, bool ro) {
    if (recreate && ro) return false;

    if (fd != -1) {
        if (recreate || (!ro && fd_ro)) {
            close(fd);
            fd = -1;
        } else {
            return true; // so r/o ones should be closed
        }
    }
    switch (type) {
        case TYPE_NONE:
            return false;
        case TYPE_DIR:
            if (recreate) return false;
            fd = openat(dfd, name, O_RDONLY|O_DIRECTORY|O_NOFOLLOW);
            break;
        case TYPE_REG:
            if (recreate) {
                fd = openat(dfd, name, O_RDWR|O_NOFOLLOW|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
                assert(!ro);
            } else {
                fd = openat(dfd, name, (ro? O_RDONLY: O_RDWR)|O_NOFOLLOW);
            }
            fd_ro = ro;
            break;
    }
    if (fd == -1) {
        LOG_ERRNO("(re-)openat(%d, %s)", dfd, name);
        return false;
    }
    Attr* new_attr = Attr::getInst(fd);
    if (!new_attr) {
        close(fd);
        fd = -1;
        return false;
    }
    assert(attr);
    if (!recreate && (new_attr->ino() != attr->ino() || new_attr->cmp(attr) != Attr::SAME)) {
        LOG("reopen apply mismatch for %s", name);
        close(fd);
        fd = -1;
        return false;
    }
    delete attr;
    attr = new_attr;
    return true;
}


Ent* Ent::getInst(int dfd, int fd, const char* name, const struct stat* ss) {
    Attr* a;
    if (ss) {
        a = Attr::getInst(*ss);
    } else if (fd != -1) {
        a = Attr::getInst(fd);
    } else {
        a = Attr::getInst(dfd, name);
    }
    if (!a) {
        close(fd);
        return NULL;
    }
    return new Ent(dfd, fd, name, a);
}


Ent* Ent::getInst(int dfd, const char* name, bool dir, const struct stat* ss) {
#ifdef REOPEN
    return getInst(dfd, -1, name, ss);
#else
    int fd = dir?
        openat(dfd, name, O_RDONLY|O_DIRECTORY|O_NOFOLLOW):
        #ifdef REOPEN
            openat(dfd, name, O_RDONLY|O_NOFOLLOW);
        #else
            openat(dfd, name, O_RDWR|O_NOFOLLOW);
        #endif
    if (fd == -1) {
        LOG_ERRNO("openat(%d, %s)", dfd, name);
        return NULL;
    }
    return getInst(dfd, fd, name, ss);
#endif
}


Ent* Ent::createInst(int dfd, const char* name, bool dir) {
    if (dir) {
        if (mkdirat(dfd, name, S_IRUSR|S_IWUSR) == -1) {
            LOG_ERRNO("mkdirat(%d, %s)", dfd, name);
            return NULL;
        }
        Ent* rv = getInst(dfd, name, dir);
        if (!rv) {
            if (unlinkat(dfd, name, 0) == -1) {
                LOG_ERRNO("unlinkat(%d, %s)", dfd, name);
            }
        }
        return rv;
    } else {
        int fd = openat(dfd, name, O_RDWR|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
        if (fd == -1) {
            LOG_ERRNO("openat(%d, %s)", dfd, name);
            return NULL;
        }
        return getInst(dfd, fd, name, NULL);
    }
}


bool Ent::read_from(Ent* o, int bupdfd) {
    if (type != TYPE_REG || o->type != TYPE_REG) {
        return false;
    }
    if (bupdfd != -1 && !delInst(bupdfd)) {
        return false;
    }
    if (!reopen(bupdfd != -1) || !o->reopen(false, true)) {
        return false;
    }
    if (lseek(fd, 0, SEEK_SET) == -1 || lseek(o->fd, 0, SEEK_SET) == -1) {
        LOG_ERRNO("lseek()");
        return false;
    }
    if (ftruncate(fd, o->attr->size()) == -1) {
        LOG_ERRNO("ftruncate(%d)", fd);
        return false;
    }
    return write_buffered(o->attr->size(), o->fd, fd);
}


bool Ent::apply(const Ent* o) {
    if (type == TYPE_NONE || type != o->type) {
        return false;
    }
    if (!reopen()) {
        return false;
    }
    if (!o->attr->apply_to(fd)) {
        return false;
    }

    Attr* new_attr = Attr::getInst(fd); // can somehow take o?
    if (!new_attr) {
        return false;
    }
    if (new_attr->cmp(o->attr) != Attr::SAME) {
        LOG("attr apply mismatch for %s", name);
    }

    if (attr) {
        delete attr;
    }
    attr = new_attr;

    return true;
}


bool Ent::delInst(int bupdfd) {
    if (bupdfd != -1) {
        if (type != TYPE_REG && type != TYPE_DIR) {
            return false;
        }
        #if 0
            if (renameat2(dfd, name, bupdfd, name, RENAME_NOREPLACE) == -1) { // needs kernel >= 3.15
                LOG_ERRNO("renameat(%s)", name); // TODO: suffix in case of EEXIST
                return false;
            }
        #else
            struct stat ss;
            errno = 0;
            if (fstatat(bupdfd, name, &ss, AT_SYMLINK_NOFOLLOW) != -1 || errno != ENOENT) { // race
                LOG_ERRNO("fstatat(%s)", name);
                return false;
            }
            if (renameat(dfd, name, bupdfd, name) == -1) {
                LOG_ERRNO("renameat(%s)", name);
                return false;
            }
        #endif
        if (fd != -1) {
            close(fd);
            fd = -1;
        }
        return true;
    }

    switch (type) {
        case TYPE_NONE:
            break;
        case TYPE_DIR:
            if (!reopen()) break;
            if (rmdir_r(dfd, fd, name)) {
                fd = -1;
                return true;
            }
            fd = -1;
            break;
        case TYPE_REG:
            if (unlinkat(dfd, name, 0) == -1) {
                LOG_ERRNO("unlinkat(%d, %s)", dfd, name);
                break;
            }
            if (fd != -1) {
                close(fd);
                fd = -1;
            }
            return true;
    }
    return false;
}


void Ent::print(const char* prefix, const char* suffix) const {
    assert(type != TYPE_NONE);
    char mt[32], ct[32];
    ctime_r(&attr->mtime().tv_sec, mt);
    ctime_r(&attr->ctime().tv_sec, ct);
    *strchrnul(mt, '\n') = '\0';
    *strchrnul(ct, '\n') = '\0';
    printf("%s" COLOR(YELLOW, "%c") "%04o %05d/%05d %s %s " COLOR(YELLOW, "%s") " %ld%s", prefix?:"", attr->is_dir()? 'd':'-', attr->mode(), attr->uid(), attr->gid(), mt, ct, name, attr->size(), suffix?:"");
}


void Ent::print(const Ent* e, const char* prefix, const char* suffix) {
    if (!e) {
        printf("%s" COLOR(RED, "(none)") "%s", prefix?:"", suffix?:"");
    } else if (e->type == TYPE_NONE) {
        printf("%s" COLOR(RED, "%s") "%s", prefix?:"", e->name, suffix?:"");
    } else {
        e->print(prefix, suffix);
    }
}