/**
 * Copyright © 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Universal Permissive License v 1.0 as shown at
 * http://oss.oracle.com/licenses/upl
 *
 */
#include <assert.h>
#include <sys/stat.h>
#include <pthread.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "putils.h"
#include "malloc.h"
/* parallel diff of a directory heirarchy that also checks ownership and attributes */

struct diff_task {
    char *relative_path;
    struct diff_task *next;
};

// Worker state
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;        ;
pthread_cond_t  cond  = PTHREAD_COND_INITIALIZER;
struct diff_task *tasks = NULL;
enum { n_workers = 32};
size_t n_working = 0; // When tasks==NULL and n_working==0, we are done
pthread_t workers[n_workers];

#define PMAX 4096
const char *src_dir;
const char *dst_dir;

int check_ownership = 1;

// From the qsort man page:
static int cmpstringp(const void *p1, const void *p2) {
    // The actual arguments to this function are "pointers to pointers
    // to char", but strcmp(3) arguments are "pointers to char", hence
    // the following cast plus dereference.
    return strcmp(* (char * const *) p1, * (char * const *) p2);
}

static void collect_and_sort_pathnames(const char *abs_path,
                                       // output:
                                       size_t *np,
                                       char ***paths_p)
// Collect all the pathnames in a directory, except for . and .., and
// return them in sorted order.
 {
    size_t n = 0;
    size_t limit = 2;
    char **MALLOC_N(paths, limit);
    DIR *dir = opendir(abs_path);
    assert(dir);
    struct dirent *dentry;
    while ((dentry = readdir(dir))) {
        if (strcmp(dentry->d_name, ".") == 0) continue;
        if (strcmp(dentry->d_name, "..") == 0) continue;
        if (n >= limit) {
            limit *= 2;
            REALLOC(paths, limit);
        }
        assert(n < limit);
        paths[n++] = strdup(dentry->d_name);
    }
    closedir(dir);
    qsort(paths, n, sizeof(char*), cmpstringp);
    *np      = n;
    *paths_p = paths;
}

static void diff_r(const char *relative_path) {
    char src_path[PMAX];
    {
        int n = snprintf(src_path, PMAX, "%s/%s", src_dir, relative_path);
        assert(n < PMAX);
    }
    char dst_path[PMAX];
    {
        int n = snprintf(dst_path, PMAX, "%s/%s", dst_dir, relative_path);
        assert(n < PMAX);
    }
    struct stat src_stat, dst_stat;
    {
        int src_r = lstat(src_path, &src_stat);
        assert (src_r == 0);
    }
    {
        int dst_r = lstat(dst_path, &dst_stat);
        assert(dst_r == 0);
    }
    assert((src_stat.st_mode & S_IFMT) == (dst_stat.st_mode & S_IFMT));
    {
        mode_t mask = 0777;
        if ((src_stat.st_mode & mask) != (dst_stat.st_mode & mask)) {
            fprintf(stderr, "masks differ %s and %s\n", src_path, dst_path);
            assert(0);
        }
    }
    if (check_ownership) {
        if (src_stat.st_uid != dst_stat.st_uid) {
            fprintf(stderr, "uid doesn't match for %s and %s\n", src_path, dst_path);
        }
        assert(src_stat.st_uid == dst_stat.st_uid);
        assert(src_stat.st_gid == dst_stat.st_gid);
    }
    if (S_ISREG(dst_stat.st_mode)) {
        assert(src_stat.st_size == dst_stat.st_size);
        FILE *src_f = fopen(src_path, "r");
        FILE *dst_f = fopen(dst_path, "r");
        if (src_f == NULL || dst_f == NULL) {
            fprintf(stderr, "Failed to open one or more files\n");
            return;
        }
        for (size_t i = 0 ; i < (size_t)(src_stat.st_size); i++) {
            int src_c = getc(src_f); assert(src_c != EOF);
            int dst_c = getc(dst_f); assert(dst_c != EOF);
            assert(src_c == dst_c);
        }
        fclose(src_f);
        fclose(dst_f);
    } else if (S_ISDIR(src_stat.st_mode)) {
        size_t sn, dn;
        char **sps, **dps;
        collect_and_sort_pathnames(src_path, &sn, &sps);
        collect_and_sort_pathnames(dst_path, &dn, &dps);
        assert(sn == dn);
        for (size_t i = 0; i < sn; i++) {
            assert(strcmp(sps[i], dps[i]) == 0);
            struct diff_task *MALLOC(task);
            char sub_rel_path[PMAX];
            {
                int n = snprintf(sub_rel_path, PMAX, "%s/%s", relative_path, sps[i]);
                assert(n < PMAX);
            }
            char *srp = strdup(sub_rel_path);
            pthread_mutex_lock(&mutex);
            *task = (struct diff_task){.relative_path = srp,
                                       .next          = tasks};
            tasks = task;
            pthread_cond_signal(&cond);
            pthread_mutex_unlock(&mutex);
        }
    } else if (S_ISLNK(src_stat.st_mode)) {
        char slname[PMAX];
        ssize_t slen = readlink(src_path, slname, PMAX);
        assert(slen >= 0 && slen < PMAX);
        char dlname[PMAX];
        ssize_t dlen = readlink(dst_path, dlname, PMAX);
        assert(dlen >= 0 && dlen < PMAX);
        assert(slen == dlen);
        slname[slen] = 0;
        dlname[dlen] = 0;
        if (0 != strcmp(slname, dlname)) {
            fprintf(stderr, "%s -> %s is not the same as %s -> %s\n", src_path, slname, dst_path, dlname);
            assert(0);
        }
    }
}

static void* run_worker(void *d) {
    pthread_mutex_lock(&mutex);
    while (1) {
        // Lock is held here.
        while (!tasks && n_working) {
            pthread_cond_wait(&cond, &mutex);
        }
        // Either there is a task or n_working==0
        if (tasks) {
            struct diff_task *task = tasks;
            tasks = task->next;
            n_working++;
            pthread_mutex_unlock(&mutex);
            diff_r(task->relative_path);
            free(task->relative_path);
            free(task);
            pthread_mutex_lock(&mutex);
            n_working--;
            if (tasks == NULL && n_working == 0) {
                pthread_cond_broadcast(&cond);
            }
        } else if (n_working == 0) {
            // No tasks and nothing to do.
            pthread_mutex_unlock(&mutex);
            return d;
        }
    }
}

static void run_workers(void) {
    MALLOC(tasks);
    *tasks = (struct diff_task){.relative_path = strdup(""),
                                .next = NULL};
    n_working = 0;
    for (size_t i = 0; i < n_workers; i++) {
        pthread_create(&workers[i], NULL, run_worker, &workers[i]);
    }
    for (size_t i = 0; i < n_workers; i++) {
        void *result;
        pthread_join(workers[i], &result);
    }
}

int main (int argc, const char *argv[]) {
    assert(argc == 3);
    src_dir = argv[1];
    dst_dir = argv[2];
    run_workers();
    return 0;
}
