/*
 * Ouroboros - Copyright (C) 2016 - 2026
 *
 * RIB export using FUSE
 *
 *    Dimitri Staessens <dimitri@ouroboros.rocks>
 *    Sander Vrijders   <sander@ouroboros.rocks>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., http://www.fsf.org/about/contact/.
 */

#define _POSIX_C_SOURCE 200112L

#include "config.h"

#include <ouroboros/errno.h>
#include <ouroboros/list.h>
#include <ouroboros/rib.h>
#include <ouroboros/utils.h>

#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef HAVE_FUSE
#define _FILE_OFFSET_BITS 64
#define FUSE_USE_VERSION  26
#if defined (__linux__)
#define __USE_XOPEN
#elif defined (__FreeBSD__)
#define __XSI_VISIBLE 500
#endif /* __linux__ */
#include <sys/stat.h>
#include <fuse.h>

#ifndef CLOCK_REALTIME_COARSE
#define CLOCK_REALTIME_COARSE CLOCK_REALTIME
#endif

struct reg_comp {
        struct list_head next;

        char             path[RIB_PATH_LEN + 1];
        struct rib_ops * ops;
};

struct {
        struct list_head   reg_comps;

        char               mnt[RIB_PATH_LEN + 1];

        struct fuse *      fuse;
        struct fuse_chan * ch;

        pthread_rwlock_t   lock;

        pthread_t          fuse_thr;
} rib;

static int rib_open(const char *            path,
                    struct fuse_file_info * info)
{
        (void) path;

        info->nonseekable = 1;

        return 0;
}

static int rib_opendir(const char *         path,
                    struct fuse_file_info * info)
{
        (void) path;
        (void) info;

        return 0;
}

static int rib_read(const char *            path,
                    char *                  buf,
                    size_t                  size,
                    off_t                   offset,
                    struct fuse_file_info * info)
{
        struct list_head * p;
        char               comp[RIB_PATH_LEN + 1];
        char *             c;

        if (strlen(path) > RIB_PATH_LEN)
                return -1;

        strcpy(comp, path + 1);

        c = strstr(comp, RIB_SEPARATOR);
        if (c != NULL)
                *c = '\0';

        (void) info;
        (void) offset;

        pthread_rwlock_rdlock(&rib.lock);

        list_for_each(p, &rib.reg_comps) {
                struct reg_comp * r = list_entry(p, struct reg_comp, next);
                if (strcmp(comp, r->path) == 0) {
                        struct rib_ops * ops = r->ops;
                        pthread_rwlock_unlock(&rib.lock);
                        return ops->read(path + 1, buf, size);
                }
        }

        pthread_rwlock_unlock(&rib.lock);

        return -1;
}

static int rib_readdir(const char *            path,
                       void *                  buf,
                       fuse_fill_dir_t         filler,
                       off_t                   offset,
                       struct fuse_file_info * info)
{
        struct list_head * p;

        (void) offset;
        (void) info;

        /* Fix ls calling readdir in an infinite loop on raspbian. */
        if (info != NULL && info->nonseekable != 0)
                return -ENOENT;

        filler(buf, ".", NULL, 0);
        filler(buf, "..", NULL, 0);

        pthread_rwlock_rdlock(&rib.lock);

        if (strcmp(path, RIB_SEPARATOR) == 0) {
                list_for_each(p, &rib.reg_comps) {
                        struct reg_comp * c;
                        c = list_entry(p, struct reg_comp, next);
                        filler(buf, c->path, NULL, 0);
                }
        } else {
                list_for_each(p, &rib.reg_comps) {
                        char **           dir_entries;
                        ssize_t           len;
                        ssize_t           i;
                        struct reg_comp * c;
                        struct rib_ops *  ops;
                        c = list_entry(p, struct reg_comp, next);

                        if (strcmp(path + 1, c->path) != 0)
                                continue;

                        ops = c->ops;

                        assert(ops->readdir != NULL);

                        pthread_rwlock_unlock(&rib.lock);

                        len = ops->readdir(&dir_entries);
                        if (len < 0)
                                return 0;
                        for (i = 0; i < len; ++i)
                                filler(buf, dir_entries[i], NULL, 0);
                        freepp(char, dir_entries, len);
                        return 0;
                }
        }

        pthread_rwlock_unlock(&rib.lock);

        return 0;
}

static size_t __getattr(const char *  path,
                        struct stat * st)
{
        struct list_head * p;
        char               comp[RIB_PATH_LEN + 1];
        char *             c;
        struct rib_attr    attr;

        if (strlen(path) > RIB_PATH_LEN)
                return -1;

        strcpy(comp, path + 1);

        c = strstr(comp, RIB_SEPARATOR);
        if (c != NULL)
                *c = '\0';

        memset(&attr, 0, sizeof(attr));

        pthread_rwlock_rdlock(&rib.lock);

        list_for_each(p, &rib.reg_comps) {
                struct reg_comp * r = list_entry(p, struct reg_comp, next);
                if (strcmp(comp, r->path) == 0) {
                        size_t ret = r->ops->getattr(path + 1, &attr);
                        pthread_rwlock_unlock(&rib.lock);
                        st->st_mode   = S_IFREG | 0644;
                        st->st_blocks = 1;
                        st->st_nlink  = 1;
                        st->st_uid    = getuid();
                        st->st_gid    = getgid();
                        st->st_size   = attr.size;
                        st->st_atime  = attr.mtime;
                        st->st_mtime  = attr.mtime;
                        st->st_ctime  = attr.mtime;
                        return ret;
                }
        }

        pthread_rwlock_unlock(&rib.lock);

        return -1;
}

static int rib_getattr(const char *  path,
                       struct stat * st)
{
        struct list_head * p;
        struct timespec    now;

        memset(st, 0, sizeof(*st));

        if (strcmp(path, RIB_SEPARATOR) == 0)
                goto finish_dir;

        pthread_rwlock_rdlock(&rib.lock);

        list_for_each(p, &rib.reg_comps) {
                struct reg_comp * rc = list_entry(p, struct reg_comp, next);
                if (strcmp(path + 1, rc->path) == 0) {
                        pthread_rwlock_unlock(&rib.lock);
                        goto finish_dir;
                }
        }

        pthread_rwlock_unlock(&rib.lock);

        assert(st->st_mode == 0);

        return __getattr(path, st);

 finish_dir:
        clock_gettime(CLOCK_REALTIME_COARSE, &now);
        st->st_mode  = S_IFDIR | 0755;
        st->st_nlink = 2;
        st->st_uid   = getuid();
        st->st_gid   = getgid();
        st->st_mtime = now.tv_sec;
        st->st_atime = now.tv_sec;
        st->st_ctime = now.tv_sec;
        return 0;
}

static struct fuse_operations r_ops = {
        .getattr = rib_getattr,
        .open    = rib_open,
        .opendir = rib_opendir,
        .read    = rib_read,
        .readdir = rib_readdir
};

static void * fuse_thr(void * o)
{
        if (fuse_loop((struct fuse *) o) < 0)
                return (void *) -1;

        return (void *) 0;
}
#endif /* HAVE_FUSE */

int rib_init(const char * mountpt)
{
#ifdef HAVE_FUSE
        struct stat      st;
        char *           argv[] = {"-f",
                                   "-o",
                                   "ro,"
                                   "default_permissions,"
                                   "allow_other,"
                                   "fsname=rib",
                                   NULL};
        struct fuse_args args = FUSE_ARGS_INIT(3, argv);

        if (access("/dev/fuse", R_OK))
                goto fail;

        if (stat(FUSE_PREFIX, &st) == -1)
                goto fail;

        sprintf(rib.mnt, FUSE_PREFIX "/%s", mountpt);

        if (stat(rib.mnt, &st) == -1)
                switch(errno) {
                case ENOENT:
                        if (mkdir(rib.mnt, 0777))
                                goto fail_mnt;
                        break;
                case ENOTCONN:
                        fuse_unmount(rib.mnt, rib.ch);
                        break;
                default:
                        goto fail_mnt;
                }

        fuse_opt_parse(&args, NULL, NULL, NULL);

        list_head_init(&rib.reg_comps);

        rib.ch = fuse_mount(rib.mnt,  &args);
        if (rib.ch == NULL)
                goto fail_mount;

        rib.fuse = fuse_new(rib.ch, &args, &r_ops, sizeof(r_ops), NULL);
        if (rib.fuse == NULL)
                goto fail_fuse;

        if (pthread_rwlock_init(&rib.lock, NULL))
                goto fail_rwlock_init;

        if (pthread_create(&rib.fuse_thr, NULL, fuse_thr, rib.fuse))
                goto fail_fuse_thr;

        fuse_opt_free_args(&args);

        return 0;

 fail_fuse_thr:
        pthread_rwlock_destroy(&rib.lock);
 fail_rwlock_init:
        fuse_destroy(rib.fuse);
 fail_fuse:
        fuse_unmount(rib.mnt, rib.ch);
 fail_mount:
        fuse_opt_free_args(&args);
        rmdir(rib.mnt);
 fail_mnt:
        memset(rib.mnt, 0, sizeof(rib.mnt));
 fail:
        return -1;
#else
        (void) mountpt;
        return 0;
#endif
}

void rib_fini(void)
{
#ifdef HAVE_FUSE
        struct list_head * p;
        struct list_head * h;

        if (strlen(rib.mnt) == 0)
                return;

        fuse_exit(rib.fuse);

        fuse_unmount(rib.mnt, rib.ch);

        pthread_join(rib.fuse_thr, NULL);

        fuse_destroy(rib.fuse);

        rmdir(rib.mnt);

        pthread_rwlock_wrlock(&rib.lock);

        list_for_each_safe(p, h, &rib.reg_comps) {
                struct reg_comp * c = list_entry(p, struct reg_comp, next);
                list_del(&c->next);
                free(c);
        }

        pthread_rwlock_unlock(&rib.lock);

        pthread_rwlock_destroy(&rib.lock);

        memset(rib.mnt, 0, sizeof(rib.mnt));
#endif
}

void rib_cleanup(const char * mnt)
{
#ifdef HAVE_FUSE
        fuse_unmount(mnt, NULL);
        rmdir(mnt);
#else
        (void) mnt;
#endif
}

int rib_reg(const char *     path,
            struct rib_ops * ops)
{
#ifdef HAVE_FUSE
        struct reg_comp *  rc;
        struct list_head * p;

        if (strlen(rib.mnt) == 0)
                return 0;

        pthread_rwlock_wrlock(&rib.lock);

        list_for_each(p, &rib.reg_comps) {
                struct reg_comp * r = list_entry(p, struct reg_comp, next);
                if (strcmp(r->path, path) == 0) {
                        pthread_rwlock_unlock(&rib.lock);
                        return -EPERM;
                }

                if (strcmp(r->path, path) > 0)
                        break;
        }

        rc = malloc(sizeof(*rc));
        if (rc == NULL) {
                pthread_rwlock_unlock(&rib.lock);
                return -ENOMEM;
        }

        if (strlen(path) > RIB_PATH_LEN) {
                pthread_rwlock_unlock(&rib.lock);
                free(rc);
                return -1;
        }

        strcpy(rc->path, path);
        rc->ops = ops;

        list_add_tail(&rc->next, p);

        pthread_rwlock_unlock(&rib.lock);
#else
        (void) path;
        (void) ops;
#endif
        return 0;
}

void rib_unreg(const char * path)
{
#ifdef HAVE_FUSE
        struct list_head * p;
        struct list_head * h;

        if (strlen(rib.mnt) == 0)
                return;

        pthread_rwlock_wrlock(&rib.lock);

        list_for_each_safe(p, h, &rib.reg_comps) {
                struct reg_comp * r = list_entry(p, struct reg_comp, next);
                if (strcmp(r->path, path) == 0) {
                        list_del(&r->next);
                        free(r);
                        break;
                }
        }

        pthread_rwlock_unlock(&rib.lock);
#else
        (void) path;
#endif
}
