/*
 *  Copyright (c) 2009-2023, Peter Haag
 *  Copyright (c) 2004-2008, SWITCH - Teleinformatikdienste fuer Lehre und Forschung
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *   * Neither the name of the author nor the names of its contributors may be
 *     used to endorse or promote products derived from this software without
 *     specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/param.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include "config.h"

#ifndef HAVE_SEMUN
union semun {
    int val;               // value for SETVAL
    struct semid_ds *buf;  // buffer for IPC_STAT & IPC_SET
    u_short *array;        // array for GETALL & SETALL
};
#endif

#include "bookkeeper.h"
#include "util.h"

static bookkeeper_list_t *bookkeeper_list = NULL;

/* function prototypes */

static key_t hash(char *str, int flag);

static void sem_lock(int sem_set_id);

static void sem_unlock(int sem_set_id);

static inline bookkeeper_list_t *Get_bookkeeper_list_entry(bookkeeper_t *bookkeeper);

/* Create shared memory object and set its size */

/* hash: compute hash value of string */
#define MULTIPLIER 37
static key_t hash(char *str, int flag) {
    uint32_t h;
    unsigned char *p;
    char cleanPath[MAXPATHLEN];

    if (realpath(str, cleanPath) == NULL) {
        return -1;
    }

    h = 0;
    for (p = (unsigned char *)cleanPath; *p != '\0'; p++) h = MULTIPLIER * h + *p;

    if (flag) {
        h = MULTIPLIER * h + 'R';
    }
    // LogError("Bookeeper hash for path: '%s' -> '%s': %u flag: %i", str, cleanPath, h, flag);
    return (key_t)h;  // or, h % ARRAY_SIZE;

}  // End of hash

// locks the semaphore, for exclusive access to the bookkeeping record
static void sem_lock(int sem_set_id) {
    struct sembuf sem_op;

    /* wait on the semaphore, unless it's value is non-negative. */
    sem_op.sem_num = 0;
    sem_op.sem_op = -1;
    sem_op.sem_flg = 0;
    if (semop(sem_set_id, &sem_op, 1) == 0) return;

    LogError("semop() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));

}  // End of sem_lock

// sem_unlock. un-locks the semaphore.
static void sem_unlock(int sem_set_id) {
    struct sembuf sem_op;

    /* signal the semaphore - increase its value by one. */
    sem_op.sem_num = 0;
    sem_op.sem_op = 1;
    sem_op.sem_flg = 0;
    if (semop(sem_set_id, &sem_op, 1) == 0) return;

    LogError("semop() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));

}  // End of sem_unlock

static inline bookkeeper_list_t *Get_bookkeeper_list_entry(bookkeeper_t *bookkeeper) {
    bookkeeper_list_t *bookkeeper_list_entry;

    if (bookkeeper == NULL) return NULL;

    bookkeeper_list_entry = bookkeeper_list;
    while (bookkeeper_list_entry != NULL && bookkeeper_list_entry->bookkeeper != bookkeeper) bookkeeper_list_entry = bookkeeper_list_entry->next;

    return bookkeeper_list_entry;

}  // End of Get_bookkeeper_list_entry

int InitBookkeeper(bookkeeper_t **bookkeeper, char *path, pid_t nfcapd_pid) {
    int sem_key, shm_key, shm_id, sem_id;
    union semun sem_val;
    bookkeeper_list_t **bookkeeper_list_entry;

    *bookkeeper = NULL;

    shm_key = hash(path, 0);
    if (shm_key == -1) return ERR_PATHACCESS;

    // check if the shared memory is already allocated
    shm_id = shmget(shm_key, sizeof(bookkeeper_t), 0600);

    if (shm_id >= 0) {
        // the segment already exists. Either a running process is active
        // or an unclean shutdown happened

        // map the segement and check the record
        *bookkeeper = (bookkeeper_t *)shmat(shm_id, NULL, 0);
        if (*bookkeeper == (bookkeeper_t *)-1) {
            LogError("shmat() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
            return ERR_FAILED;
        }
        if ((*bookkeeper)->nfcapd_pid <= 0) {
            // rubbish or invalid pid of nfcapd process.
            // Assume unclean shutdown or something else. We clean up and take this record.
            memset((void *)(*bookkeeper), 0, sizeof(bookkeeper_t));
        } else {
            // check if the process created this record is still alive
            int ret = kill((*bookkeeper)->nfcapd_pid, 0);
            if (ret == -1) {
                switch (errno) {
                    case ESRCH:
                        // process does not exist, we can clean up this record and use it
                        memset((void *)(*bookkeeper), 0, sizeof(bookkeeper_t));
                        break;
                    case EPERM:
                        // A process exists, but we are not allowed to signal this process
                        LogError("Another collector with pid %i but different user ID is already running, and configured for '%s'",
                                 (*bookkeeper)->nfcapd_pid, path);
                        return ERR_EXISTS;
                        break;
                    default:
                        // This should never happen, but catch it anyway
                        LogError("semop() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
                        return ERR_FAILED;
                }
            } else {
                // process exists;
                LogError("Another collector with pid %i is already running, and configured for '%s'", (*bookkeeper)->nfcapd_pid, path);
                return ERR_EXISTS;
            }
            // if we pass this point, we have recycled an existing record
        }
    } else {
        // no valid shared segment was found
        switch (errno) {
            case ENOENT:
                // this is ok - no shared segemtn exists, we can create a new one below
                break;
            case EACCES:
                // there is such a segment, but we are not allowed to get it
                // Assume it's another nfcapd
                LogError("Access denied to collector bookkeeping record.");
                return ERR_EXISTS;
                break;
            default:
                // This should never happen, but catch it anyway
                LogError("semop() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
                return ERR_FAILED;
        }
        // we now create a new segment, this should not fail now
        shm_id = shmget(shm_key, sizeof(bookkeeper_t), IPC_CREAT | 0600);
        if (shm_id == -1) {
            // but did anyway - give up
            LogError("shmget() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
            return ERR_FAILED;
        }
        *bookkeeper = (bookkeeper_t *)shmat(shm_id, NULL, 0);
        if ((*bookkeeper) == (bookkeeper_t *)-1) {
            LogError("shmget() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
            return ERR_FAILED;
        }
        memset((void *)(*bookkeeper), 0, sizeof(bookkeeper_t));
    }
    // at this point we now have a valid record and can proceed
    (*bookkeeper)->nfcapd_pid = nfcapd_pid;
    (*bookkeeper)->sequence++;

    // create semaphore

    sem_key = hash(path, 1);
    // this should never fail, as we already got a key for the shared memory
    if (sem_key == -1) {
        // .. but catch it anyway .. and release shared memory. something is fishy
        struct shmid_ds buf;
        shmdt((void *)(*bookkeeper));
        shmctl(shm_id, IPC_RMID, &buf);
        return ERR_FAILED;
    }

    // get the semaphore
    sem_id = semget(sem_key, 1, IPC_CREAT | 0600);
    if (sem_id == -1) {
        struct shmid_ds buf;

        // this should not have failed
        LogError("semget() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));

        // release shared memory
        shmdt((void *)(*bookkeeper));
        shmctl(shm_id, IPC_RMID, &buf);
        return ERR_FAILED;
    }

    // initialize the semaphore
    sem_val.val = 1;
    if (semctl(sem_id, 0, SETVAL, sem_val) == -1) {
        struct shmid_ds buf;

        // this should not have failed
        LogError("semctl() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));

        // release shared memory
        shmdt((void *)(*bookkeeper));
        shmctl(shm_id, IPC_RMID, &buf);
        return ERR_FAILED;
    }

    bookkeeper_list_entry = &bookkeeper_list;
    while (*bookkeeper_list_entry != NULL) bookkeeper_list_entry = &((*bookkeeper_list_entry)->next);

    (*bookkeeper_list_entry) = (bookkeeper_list_t *)malloc(sizeof(bookkeeper_list_t));
    if (!*bookkeeper_list_entry) {
        struct shmid_ds buf;

        LogError("malloc() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
        shmdt((void *)(*bookkeeper));
        shmctl(shm_id, IPC_RMID, &buf);
        semctl(sem_id, 0, IPC_RMID);
        return ERR_FAILED;
    }
    memset((void *)*bookkeeper_list_entry, 0, sizeof(bookkeeper_list_t));

    (*bookkeeper_list_entry)->shm_id = shm_id;
    (*bookkeeper_list_entry)->sem_id = sem_id;
    (*bookkeeper_list_entry)->bookkeeper = *bookkeeper;
    (*bookkeeper_list_entry)->next = NULL;

    // we are done
    return BOOKKEEPER_OK;

}  // End of InitBookkeeper

int AccessBookkeeper(bookkeeper_t **bookkeeper, char *path) {
    bookkeeper_list_t **bookkeeper_list_entry;
    int sem_key, shm_key, shm_id, sem_id;

    *bookkeeper = NULL;

    shm_key = hash(path, 0);
    if (shm_key == -1) return ERR_PATHACCESS;

    // check if the shared memory is already allocated
    shm_id = shmget(shm_key, sizeof(bookkeeper_t), 0600);

    if (shm_id < 0) {
        // the segment does not exists. Check why

        switch (errno) {
            case ENOENT:
                // no shared segemtn exists.
                return ERR_NOTEXISTS;
                break;
            case EACCES:
                // there is such a segment, but we are not allowed to get it
                // Assume it's another nfcapd
                LogError("Access denied to collector bookkeeping record.");
                return ERR_FAILED;
                break;
            default:
                // This should never happen, but catch it anyway
                LogError("semop() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
                return ERR_FAILED;
        }
        // not reached
    }
    // at this point we now have a valid record and can proceed

    // create semaphore
    sem_key = hash(path, 1);
    // this should never fail, as we already got a key for the shared memory
    if (sem_key == -1) {
        // .. but catch it anyway .. and release shared memory. something is fishy
        return ERR_FAILED;
    }

    // get the semaphore
    sem_id = semget(sem_key, 1, 0600);
    if (sem_id == -1) {
        // this should not have failed
        LogError("semget() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));

        return ERR_FAILED;
    }

    // map the shared segment
    *bookkeeper = (bookkeeper_t *)shmat(shm_id, NULL, 0);
    if (*bookkeeper == (bookkeeper_t *)-1) {
        LogError("shmat() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
        return ERR_FAILED;
    }

    bookkeeper_list_entry = &bookkeeper_list;
    while (*bookkeeper_list_entry != NULL && (*bookkeeper_list_entry)->bookkeeper != NULL) bookkeeper_list_entry = &((*bookkeeper_list_entry)->next);

    // allocate new slot, else use unused slot
    if (*bookkeeper_list_entry == NULL) {
        (*bookkeeper_list_entry) = (bookkeeper_list_t *)malloc(sizeof(bookkeeper_list_t));
        if (!*bookkeeper_list_entry) {
            LogError("malloc() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
            return ERR_FAILED;
        }
        memset((void *)*bookkeeper_list_entry, 0, sizeof(bookkeeper_list_t));
    }

    (*bookkeeper_list_entry)->shm_id = shm_id;
    (*bookkeeper_list_entry)->sem_id = sem_id;
    (*bookkeeper_list_entry)->bookkeeper = *bookkeeper;
    (*bookkeeper_list_entry)->next = NULL;

    return BOOKKEEPER_OK;

}  // End of AccessBookkeeper

void ReleaseBookkeeper(bookkeeper_t *bookkeeper, int destroy) {
    bookkeeper_list_t *bookkeeper_list_entry;
    struct shmid_ds buf;

    if (!bookkeeper) return;

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return;
    }

    // detach from my process addr space memory
    if (shmdt((void *)bookkeeper) == -1) {
        // ups ..
        LogError("shmdt() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
    }
    bookkeeper = NULL;

    if (destroy == 0) {
        // Entry no longer valid
        bookkeeper_list_entry->bookkeeper = NULL;
        bookkeeper_list_entry->shm_id = 0;
        bookkeeper_list_entry->sem_id = 0;
        return;
    }

    // prevent other proceeses to access the share memory, while we are removing it
    // try to clean up.
    sem_lock(bookkeeper_list_entry->sem_id);
    if (shmctl(bookkeeper_list_entry->shm_id, IPC_RMID, &buf)) {
        // ups ..
        LogError("shmctl() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
    }
    sem_unlock(bookkeeper_list_entry->sem_id);

    if (semctl(bookkeeper_list_entry->sem_id, 0, IPC_RMID) == -1) {
        // ups ..
        LogError("semctl() error in %s line %d: %s", __FILE__, __LINE__, strerror(errno));
    }

    // Entry no longer valid
    bookkeeper_list_entry->bookkeeper = NULL;
    bookkeeper_list_entry->shm_id = 0;
    bookkeeper_list_entry->sem_id = 0;

}  // End of ReleaseBookkeeper

int LookBooks(bookkeeper_t *bookkeeper) {
    bookkeeper_list_t *bookkeeper_list_entry;

    if (!bookkeeper) return 0;

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return 1;
    }

    sem_lock(bookkeeper_list_entry->sem_id);

    return 0;

}  // End of LookBooks

int UnlookBooks(bookkeeper_t *bookkeeper) {
    bookkeeper_list_t *bookkeeper_list_entry;

    if (!bookkeeper) return 0;

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return 1;
    }

    sem_unlock(bookkeeper_list_entry->sem_id);

    return 0;

}  // End of UnlookBooks

void ClearBooks(bookkeeper_t *bookkeeper, bookkeeper_t *tmp_books) {
    bookkeeper_list_t *bookkeeper_list_entry;

    if (!bookkeeper) return;

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return;
    }

    sem_lock(bookkeeper_list_entry->sem_id);
    // backup copy
    if (tmp_books != NULL) {
        memcpy((void *)tmp_books, (void *)bookkeeper, sizeof(bookkeeper_t));
    }
    bookkeeper->first = 0;
    bookkeeper->last = 0;
    bookkeeper->numfiles = 0;
    bookkeeper->filesize = 0;
    bookkeeper->sequence++;
    sem_unlock(bookkeeper_list_entry->sem_id);

}  // End of ClearBooks

uint64_t BookSequence(bookkeeper_t *bookkeeper) {
    bookkeeper_list_t *bookkeeper_list_entry;
    uint64_t seq;

    if (!bookkeeper) return 0;

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return 0;
    }

    sem_lock(bookkeeper_list_entry->sem_id);
    seq = bookkeeper->sequence;
    sem_unlock(bookkeeper_list_entry->sem_id);

    return seq;

}  // End of BookSequence

void UpdateBooks(bookkeeper_t *bookkeeper, time_t when, uint64_t size) {
    bookkeeper_list_t *bookkeeper_list_entry;

    if (!bookkeeper) return;

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return;
    }

    sem_lock(bookkeeper_list_entry->sem_id);
    if (bookkeeper->first == 0) bookkeeper->first = when;

    bookkeeper->last = when;
    bookkeeper->numfiles++;
    bookkeeper->filesize += size;
    bookkeeper->sequence++;
    sem_unlock(bookkeeper_list_entry->sem_id);

}  // End of UpdateBooks

void UpdateBooksParam(bookkeeper_t *bookkeeper, time_t lifetime, uint64_t maxsize) {
    bookkeeper_list_t *bookkeeper_list_entry;

    if (!bookkeeper) return;

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return;
    }

    sem_lock(bookkeeper_list_entry->sem_id);
    bookkeeper->max_lifetime = lifetime;
    bookkeeper->max_filesize = maxsize;
    bookkeeper->sequence++;
    sem_unlock(bookkeeper_list_entry->sem_id);

}  // End of UpdateBooksParam

void PrintBooks(bookkeeper_t *bookkeeper) {
    bookkeeper_list_t *bookkeeper_list_entry;
    struct tm *ts;
    time_t t;
    char string[32];

    if (!bookkeeper) {
        printf("No bookkeeper record available!\n");
        return;
    }

    bookkeeper_list_entry = Get_bookkeeper_list_entry(bookkeeper);
    if (!bookkeeper_list_entry) {
        // this should never happen
        LogError("Software error in %s line %d: %s", __FILE__, __LINE__, "Entry not found in list");
        return;
    }

    sem_lock(bookkeeper_list_entry->sem_id);
    printf("Collector process: %lu\n", (unsigned long)bookkeeper->nfcapd_pid);
    printf("Record sequence  : %llu\n", (unsigned long long)bookkeeper->sequence);

    t = bookkeeper->first;
    ts = localtime(&t);
    strftime(string, 31, "%Y-%m-%d %H:%M:%S", ts);
    string[31] = '\0';
    printf("First           : %s\n", bookkeeper->first ? string : "<not set>");

    t = bookkeeper->last;
    ts = localtime(&t);
    strftime(string, 31, "%Y-%m-%d %H:%M:%S", ts);
    string[31] = '\0';
    printf("Last            : %s\n", bookkeeper->last ? string : "<not set>");
    printf("Number of files : %llu\n", (unsigned long long)bookkeeper->numfiles);
    printf("Total file size : %llu\n", (unsigned long long)bookkeeper->filesize);
    printf("Max file size   : %llu\n", (unsigned long long)bookkeeper->max_filesize);
    printf("Max life time   : %llu\n", (unsigned long long)bookkeeper->max_lifetime);
    sem_unlock(bookkeeper_list_entry->sem_id);

}  // End of PrintBooks
