/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdio.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/stat.h>

#ifndef true
    #define true 1
    #define false 0
#endif

#include "CFCUtil.h"

void
CFCUtil_null_check(const void *arg, const char *name, const char *file,
                   int line) {
    if (!arg) {
        CFCUtil_die("%s cannot be NULL at %s line %d", name, file, line);
    }
}

char*
CFCUtil_strdup(const char *string) {
    if (!string) { return NULL; }
    return CFCUtil_strndup(string, strlen(string));
}

char*
CFCUtil_strndup(const char *string, size_t len) {
    if (!string) { return NULL; }
    char *copy = (char*)MALLOCATE(len + 1);
    memcpy(copy, string, len);
    copy[len] = '\0';
    return copy;
}

char*
CFCUtil_cat(char *string, ...) {
    va_list args;
    char *appended;
    CFCUTIL_NULL_CHECK(string);
    size_t size = strlen(string) + 1;
    va_start(args, string);
    while (NULL != (appended = va_arg(args, char*))) {
        size += strlen(appended);
        string = (char*)REALLOCATE(string, size);
        strcat(string, appended);
    }
    va_end(args);
    return string;
}

void
CFCUtil_trim_whitespace(char *text) {
    if (!text) {
        return;
    }

    // Find start.
    char *ptr = text;
    while (*ptr != '\0' && isspace(*ptr)) { ptr++; }

    // Find end.
    size_t orig_len = strlen(text);
    char *limit = text + orig_len;
    for (; limit > text; limit--) {
        if (!isspace(*(limit - 1))) { break; }
    }

    // Modify string in place and NULL-terminate.
    while (ptr < limit) {
        *text++ = *ptr++;
    }
    *text = '\0';
}

void*
CFCUtil_wrapped_malloc(size_t count, const char *file, int line) {
    void *pointer = malloc(count);
    if (pointer == NULL && count != 0) {
        if (sizeof(long) >= sizeof(size_t)) {
            fprintf(stderr, "Can't malloc %lu bytes at %s line %d\n",
                    (unsigned long)count, file, line);
        }
        else {
            fprintf(stderr, "malloc failed at %s line %d\n", file, line);
        }
        exit(1);
    }
    return pointer;
}

void*
CFCUtil_wrapped_calloc(size_t count, size_t size, const char *file, int line) {
    void *pointer = calloc(count, size);
    if (pointer == NULL && count != 0) {
        if (sizeof(long) >= sizeof(size_t)) {
            fprintf(stderr,
                    "Can't calloc %lu elements of size %lu at %s line %d\n",
                    (unsigned long)count, (unsigned long)size, file, line);
        }
        else {
            fprintf(stderr, "calloc failed at %s line %d\n", file, line);
        }
        exit(1);
    }
    return pointer;
}

void*
CFCUtil_wrapped_realloc(void *ptr, size_t size, const char *file, int line) {
    void *pointer = realloc(ptr, size);
    if (pointer == NULL && size != 0) {
        if (sizeof(long) >= sizeof(size_t)) {
            fprintf(stderr, "Can't realloc %lu bytes at %s line %d\n",
                    (unsigned long)size, file, line);
        }
        else {
            fprintf(stderr, "realloc failed at %s line %d\n", file, line);
        }
        exit(1);
    }
    return pointer;
}

void
CFCUtil_wrapped_free(void *ptr) {
    free(ptr);
}

int
CFCUtil_current(const char *orig, const char *dest) {
    // If the destination file doesn't exist, we're not current.
    struct stat dest_stat;
    if (stat(dest, &dest_stat) == -1) {
        return false;
    }

    // If the source file is newer than the dest, we're not current.
    struct stat orig_stat;
    if (stat(orig, &orig_stat) == -1) {
        CFCUtil_die("Missing source file '%s'", orig);
    }
    if (orig_stat.st_mtime > dest_stat.st_mtime) {
        return false;
    }

    // Current!
    return 1;
}

void
CFCUtil_write_file(const char *filename, const char *content, size_t len) {
    FILE *fh = fopen(filename, "w+");
    if (fh == NULL) {
        CFCUtil_die("Couldn't open '%s': %s", filename, strerror(errno));
    }
    fwrite(content, sizeof(char), len, fh);
    if (fclose(fh)) {
        CFCUtil_die("Error when closing '%s': %s", filename, strerror(errno));
    }
}

char*
CFCUtil_slurp_text(const char *file_path, size_t *len_ptr) {
    FILE   *const file = fopen(file_path, "r");
    char   *contents;
    size_t  binary_len;
    long    text_len;

    /* Sanity check. */
    if (file == NULL) {
        CFCUtil_die("Error opening file '%s': %s", file_path, strerror(errno));
    }

    /* Find length; return NULL if the file has a zero-length. */
    binary_len = CFCUtil_flength(file);
    if (binary_len == 0) {
        *len_ptr = 0;
        return NULL;
    }

    /* Allocate memory and read the file. */
    contents = (char*)MALLOCATE(binary_len * sizeof(char) + 1);
    text_len = fread(contents, sizeof(char), binary_len, file);

    /* Weak error check, because CRLF might result in fewer chars read. */
    if (text_len <= 0) {
        CFCUtil_die("Tried to read %ld bytes of '%s', got return code %ld",
                    (long)binary_len, file_path, (long)text_len);
    }

    /* NULL-terminate. */
    contents[text_len] = '\0';

    /* Set length pointer for benefit of caller. */
    *len_ptr = text_len;

    /* Clean up. */
    if (fclose(file)) {
        CFCUtil_die("Error closing file '%s': %s", file_path, strerror(errno));
    }

    return contents;
}

void
CFCUtil_write_if_changed(const char *path, const char *content, size_t len) {
    FILE *f = fopen(path, "r");
    if (f) { // Does file exist?
        if (fclose(f)) {
            CFCUtil_die("Error closing file '%s': %s", path, strerror(errno));
        }
        size_t existing_len;
        char *existing = CFCUtil_slurp_text(path, &existing_len);
        int changed = true;
        if (existing_len == len && strcmp(content, existing) == 0) {
            changed = false;
        }
        FREEMEM(existing);
        if (changed == false) {
            return;
        }
    }
    CFCUtil_write_file(path, content, len);
}

long
CFCUtil_flength(void *file) {
    FILE *f = (FILE*)file;
    const long bookmark = (long)ftell(f);
    long check_val;
    long len;

    /* Seek to end of file and check length. */
    check_val = fseek(f, 0, SEEK_END);
    if (check_val == -1) { CFCUtil_die("fseek error : %s\n", strerror(errno)); }
    len = (long)ftell(f);
    if (len == -1) { CFCUtil_die("ftell error : %s\n", strerror(errno)); }

    /* Return to where we were. */
    check_val = fseek(f, bookmark, SEEK_SET);
    if (check_val == -1) { CFCUtil_die("fseek error : %s\n", strerror(errno)); }

    return len;
}

// Note: this has to be defined before including the Perl headers because they
// redefine stat() in an incompatible way on certain systems (Windows).
int
CFCUtil_is_dir(const char *path) {
    struct stat stat_buf;
    int stat_check = stat(path, &stat_buf);
    if (stat_check == -1) {
        return false;
    }
    return (stat_buf.st_mode & S_IFDIR) ? true : false;
}

int
CFCUtil_make_path(const char *path) {
    CFCUTIL_NULL_CHECK(path);
    char *target = CFCUtil_strdup(path);
    size_t orig_len = strlen(target);
    size_t len = orig_len;
    for (size_t i = 0; i <= len; i++) {
#ifndef WIN32
        if (target[i] == '\\') {
            i++;
            continue;
        }
#endif
        if (target[i] == CFCUTIL_PATH_SEP_CHAR || i == len) {
            target[i] = 0; // NULL-terminate.
            struct stat stat_buf;
            int stat_check = stat(target, &stat_buf);
            if (stat_check != -1) {
                if (!(stat_buf.st_mode & S_IFDIR)) {
                    CFCUtil_die("%s isn't a directory", target);
                }
            }
            else {
                int success = CFCUtil_make_dir(target);
                if (!success) {
                    FREEMEM(target);
                    return false;
                }
            }
            target[i] = CFCUTIL_PATH_SEP_CHAR;
        }
    }

    FREEMEM(target);
    return true;
}

/******************************** WINDOWS **********************************/
#ifdef _WIN32

#include <direct.h>
#include <windows.h>

typedef struct WinDH {
    HANDLE handle;
    WIN32_FIND_DATA *find_data;
    char path[MAX_PATH + 1];
    int first_time;
} WinDH;

int
CFCUtil_make_dir(const char *dir) {
    return !mkdir(dir);
}

void*
CFCUtil_opendir(const char *dir) {
    size_t dirlen = strlen(dir);
    if (dirlen >= MAX_PATH - 2) {
        CFCUtil_die("Exceeded MAX_PATH(%d): %s", (int)MAX_PATH, dir);
    }
    WinDH *dh = (WinDH*)CALLOCATE(1, sizeof(WinDH));
    dh->find_data = (WIN32_FIND_DATA*)MALLOCATE(sizeof(WIN32_FIND_DATA));

    // Tack on wildcard needed by FindFirstFile.
    sprintf(dh->path, "%s\\*", dir);

    dh->handle = FindFirstFile(dh->path, dh->find_data);
    if (dh->handle == INVALID_HANDLE_VALUE) {
        CFCUtil_die("Can't open dir '%s'", dh->path);
    }
    dh->first_time = true;

    return dh;
}

const char*
CFCUtil_dirnext(void *dirhandle) {
    WinDH *dh = (WinDH*)dirhandle;
    if (dh->first_time) {
        dh->first_time = false;
    }
    else {
        if ((FindNextFile(dh->handle, dh->find_data) == 0)) {
            if (GetLastError() != ERROR_NO_MORE_FILES) {
                CFCUtil_die("Error occurred while reading '%s'",
                            dh->path);
            }
            return NULL;
        }
    }
    return dh->find_data->cFileName;
}

void
CFCUtil_closedir(void *dirhandle, const char *dir) {
    WinDH *dh = (WinDH*)dirhandle;
    if (!FindClose(dh->handle)) {
        CFCUtil_die("Error occurred while closing dir '%s'", dir);
    }
    FREEMEM(dh->find_data);
    FREEMEM(dh);
}

/******************************** UNIXEN ***********************************/
#else

#include <dirent.h>

int
CFCUtil_make_dir(const char *dir) {
    return !mkdir(dir, 0777);
}


void*
CFCUtil_opendir(const char *dir) {
    DIR *dirhandle = opendir(dir);
    if (!dirhandle) {
        CFCUtil_die("Failed to opendir for '%s': %s", dir, strerror(errno));
    }
    return dirhandle;
}

const char*
CFCUtil_dirnext(void *dirhandle) {
    struct dirent *entry = readdir((DIR*)dirhandle);
    return entry ? entry->d_name : NULL;
}

void
CFCUtil_closedir(void *dirhandle, const char *dir) {
    if (closedir((DIR*)dirhandle) == -1) {
        CFCUtil_die("Error closing dir '%s': %s", dir, strerror(errno));
    }
}

#endif /* Windows vs. Unix. */

/***************************************************************************/

#ifdef CFCPERL

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"

void
CFCUtil_die(const char* format, ...) {
    SV *errsv = get_sv("@", 1);
    va_list args;
    va_start(args, format);
    sv_vsetpvf_mg(errsv, format, &args);
    va_end(args);
    croak(NULL);
}

void
CFCUtil_warn(const char* format, ...) {
    SV *mess = newSVpv("", 0);
    va_list args;
    va_start(args, format);
    sv_vsetpvf(mess, format, &args);
    va_end(args);
    fprintf(stderr, "%s\n", SvPV_nolen(mess));
    SvREFCNT_dec(mess);
}

#else

void
CFCUtil_die(const char* format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    fprintf(stderr, "\n");
    exit(1);
}

void
CFCUtil_warn(const char* format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    fprintf(stderr, "\n");
}

#endif /* CFCPERL */

