/*
    SPDX-FileCopyrightText: 2008-2017 jerome DOT laurens AT u-bourgogne DOT fr
    SPDX-License-Identifier: X11

    This file is part of the __SyncTeX__ package.

    [//]: # (Latest Revision: Fri Jul 14 16:20:41 UTC 2017)
    [//]: # (Version: 1.19)

    See `synctex_parser_readme.md` for more details
*/

/*  In this file, we find all the functions that may depend on the operating system. */

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <synctex_parser_utils.h>

#include <ctype.h>
#include <limits.h>
#include <string.h>

#include <sys/stat.h>

#if defined(_WIN32) || defined(__WIN32__) || defined(__TOS_WIN__) || defined(__WINDOWS__)
#define SYNCTEX_WINDOWS 1
#endif

#if defined(__OS2__)
#define SYNCTEX_OS2 1
#endif

#if defined(_WIN32)
#define SYNCTEX_RECENT_WINDOWS 1
#endif

#ifdef SYNCTEX_WINDOWS
#include <shlwapi.h> /* Use shlwapi.lib */
#include <windows.h>
#endif

void *_synctex_malloc(size_t size)
{
    void *ptr = malloc(size);
    if (ptr) {
        memset(ptr, 0, size); /* ensures null termination of strings */
    }
    return (void *)ptr;
}

void _synctex_free(void *ptr)
{
    if (ptr) {
        free(ptr);
    }
}

#if !defined(_WIN32)
#include <syslog.h>
#endif

int _synctex_log(int level, const char *prompt, const char *reason, ...) SYNCTEX_PRINTF_FORMAT(3, 4);
int _synctex_log(int level, const char *prompt, const char *reason, ...)
{
    va_list arg;
    int result;
    va_start(arg, reason);
#ifdef SYNCTEX_RECENT_WINDOWS
    { /*	This code is contributed by William Blum.
         As it does not work on some older computers,
         the _WIN32 conditional here is replaced with a SYNCTEX_RECENT_WINDOWS one.
         According to http://msdn.microsoft.com/en-us/library/aa363362(VS.85).aspx
         Minimum supported client	Windows 2000 Professional
         Minimum supported server	Windows 2000 Server
         People running Windows 2K standard edition will not have OutputDebugStringA.
         JL.*/
        char *buff;
        size_t len;
        OutputDebugStringA(prompt);
#ifdef _MSC_VER
        len = _vscprintf(reason, arg) + 1;
        buff = (char *)malloc(len * sizeof(char));
#else /* MinGW */
        size_t buffersize = 1024;
        size_t max_buffersize = 1024 * buffersize;
        int result;
        buff = (char *)malloc(buffersize * sizeof(char));
        result = _vsnprintf(buff, buffersize - 1, reason, arg);
        while (-1 == result && buffersize <= max_buffersize) {
            buffersize = buffersize * 2;
            buff = (char *)realloc(buff, buffersize * sizeof(char));
            result = _vsnprintf(buff, buffersize - 1, reason, arg);
        }
        if (-1 == result) {
            // could not make the buffer big enough or simply could not write to it
            free(buff);
            return -1;
        }
#endif
        result = vsprintf(buff, reason, arg) + strlen(prompt);
        OutputDebugStringA(buff);
        OutputDebugStringA("\n");
        free(buff);
    }
#elif defined SYNCTEX_USE_SYSLOG
    char *buffer1 = NULL;
    char *buffer2 = NULL;
    openlog("SyncTeX", LOG_CONS | LOG_PID | LOG_PERROR | LOG_NDELAY, LOG_LOCAL0);
    if (vasprintf(&buffer1, reason, arg) >= 0 && asprintf(&buffer2, "%s%s", prompt, buffer1) >= 0) {
        syslog(level, "%s", buffer2);
        result = (int)strlen(buffer2);
    } else {
        syslog(level, "%s", prompt);
        vsyslog(level, reason, arg);
        result = (int)strlen(prompt);
    }
    free(buffer1);
    free(buffer2);
    closelog();
#else
    FILE *where = level == LOG_ERR ? stderr : stdout;
    result = fputs(prompt, where);
    result += vfprintf(where, reason, arg);
    result += fprintf(where, "\n");
#endif
    va_end(arg);
    return result;
}

int _synctex_error(const char *reason, ...)
{
    va_list arg;
    int result;
    va_start(arg, reason);
#if defined(SYNCTEX_RECENT_WINDOWS) /* LOG_ERR is not used */
    result = _synctex_log(0, "! SyncTeX Error : ", reason, arg);
#else
    result = _synctex_log(LOG_ERR, "! SyncTeX Error : ", reason, arg);
#endif
    va_end(arg);
    return result;
}

int _synctex_debug(const char *reason, ...)
{
    va_list arg;
    int result;
    va_start(arg, reason);
#if defined(SYNCTEX_RECENT_WINDOWS) /* LOG_DEBUG is not used */
    result = _synctex_log(0, "! SyncTeX Error : ", reason, arg);
#else
    result = _synctex_log(LOG_DEBUG, "! SyncTeX Error : ", reason, arg);
#endif
    va_end(arg);
    return result;
}

/*  strip the last extension of the given string, this string is modified! */
void _synctex_strip_last_path_extension(char *string)
{
    if (NULL != string) {
        char *last_component = NULL;
        char *last_extension = NULL;
#if defined(SYNCTEX_WINDOWS)
        last_component = PathFindFileNameA(string);
        last_extension = PathFindExtensionA(string);
        if (last_extension == NULL)
            return;
        if (last_component == NULL)
            last_component = string;
        if (last_extension > last_component) { /* filter out paths like "my/dir/.hidden" */
            last_extension[0] = '\0';
        }
#else
        char *next = NULL;
        /*  first we find the last path component */
        if (NULL == (last_component = strstr(string, "/"))) {
            last_component = string;
        } else {
            ++last_component;
            while ((next = strstr(last_component, "/"))) {
                last_component = next + 1;
            }
        }
#if defined(SYNCTEX_OS2)
        /*  On OS2, the '\' is also a path separator. */
        while ((next = strstr(last_component, "\\"))) {
            last_component = next + 1;
        }
#endif /* SYNCTEX_OS2 */
        /*  then we find the last path extension */
        if ((last_extension = strstr(last_component, "."))) {
            ++last_extension;
            while ((next = strstr(last_extension, "."))) {
                last_extension = next + 1;
            }
            --last_extension;                      /*  back to the "." */
            if (last_extension > last_component) { /*  filter out paths like ....my/dir/.hidden"*/
                last_extension[0] = '\0';
            }
        }
#endif /* SYNCTEX_WINDOWS */
    }
}

synctex_bool_t synctex_ignore_leading_dot_slash_in_path(const char **name_ref)
{
    if (SYNCTEX_IS_DOT((*name_ref)[0]) && SYNCTEX_IS_PATH_SEPARATOR((*name_ref)[1])) {
        do {
            (*name_ref) += 2;
            while (SYNCTEX_IS_PATH_SEPARATOR((*name_ref)[0])) {
                ++(*name_ref);
            }
        } while (SYNCTEX_IS_DOT((*name_ref)[0]) && SYNCTEX_IS_PATH_SEPARATOR((*name_ref)[1]));
        return synctex_YES;
    }
    return synctex_NO;
}

/*  The base name is necessary to deal with the 2011 file naming convention...
 *  path is a '\0' terminated string
 *  The return value is the trailing part of the argument,
 *  just following the first occurrence of the regexp pattern "[^|/|\].[\|/]+".*/
const char *_synctex_base_name(const char *path)
{
    const char *ptr = path;
    do {
        if (synctex_ignore_leading_dot_slash_in_path(&ptr)) {
            return ptr;
        }
        do {
            if (!*(++ptr)) {
                return path;
            }
        } while (!SYNCTEX_IS_PATH_SEPARATOR(*ptr));
    } while (*(++ptr));
    return path;
}

/*  Compare two file names, windows is sometimes case insensitive... */
synctex_bool_t _synctex_is_equivalent_file_name(const char *lhs, const char *rhs)
{
    /*  Remove the leading regex '(\./+)*' in both rhs and lhs */
    synctex_ignore_leading_dot_slash_in_path(&lhs);
    synctex_ignore_leading_dot_slash_in_path(&rhs);
next_character:
    if (SYNCTEX_IS_PATH_SEPARATOR(*lhs)) {      /*  lhs points to a path separator */
        if (!SYNCTEX_IS_PATH_SEPARATOR(*rhs)) { /*  but not rhs */
            return synctex_NO;
        }
        ++lhs;
        ++rhs;
        synctex_ignore_leading_dot_slash_in_path(&lhs);
        synctex_ignore_leading_dot_slash_in_path(&rhs);
        goto next_character;
    } else if (SYNCTEX_IS_PATH_SEPARATOR(*rhs)) { /*  rhs points to a path separator but not lhs */
        return synctex_NO;
    } else if (SYNCTEX_ARE_PATH_CHARACTERS_EQUAL(*lhs, *rhs)) { /*  uppercase do not match */
        return synctex_NO;
    } else if (!*lhs) { /*  lhs is at the end of the string */
        return *rhs ? synctex_NO : synctex_YES;
    } else if (!*rhs) { /*  rhs is at the end of the string but not lhs */
        return synctex_NO;
    }
    ++lhs;
    ++rhs;
    goto next_character;
}

synctex_bool_t _synctex_path_is_absolute(const char *name)
{
    if (!strlen(name)) {
        return synctex_NO;
    }
#if defined(SYNCTEX_WINDOWS) || defined(SYNCTEX_OS2)
    if (strlen(name) > 2) {
        return (name[1] == ':' && SYNCTEX_IS_PATH_SEPARATOR(name[2])) ? synctex_YES : synctex_NO;
    }
    return synctex_NO;
#else
    return SYNCTEX_IS_PATH_SEPARATOR(name[0]) ? synctex_YES : synctex_NO;
#endif
}

/*  We do not take care of UTF-8 */
const char *_synctex_last_path_component(const char *name)
{
    const char *c = name + strlen(name);
    if (c > name) {
        if (!SYNCTEX_IS_PATH_SEPARATOR(*c)) {
            do {
                --c;
                if (SYNCTEX_IS_PATH_SEPARATOR(*c)) {
                    return c + 1;
                }
            } while (c > name);
        }
        return c; /* the last path component is the void string*/
    }
    return c;
}

int _synctex_copy_with_quoting_last_path_component(const char *src, char **dest_ref, size_t size)
{
    if (src && dest_ref) {
        const char *lpc;
#define dest (*dest_ref)
        dest = NULL; /*	Default behavior: no change and success. */
        lpc = _synctex_last_path_component(src);
        if (strlen(lpc)) {
            if (strchr(lpc, ' ') && lpc[0] != '"' && lpc[strlen(lpc) - 1] != '"') {
                /*	We are in the situation where adding the quotes is allowed.	*/
                /*	Time to add the quotes.	*/
                /*  Consistency test: we must have dest+size>dest+strlen(dest)+2
                 *	or equivalently: strlen(dest)+2<size (see below) */
                if (strlen(src) < size) {
                    if ((dest = (char *)malloc(size + 2))) {
                        char *dpc = dest + (lpc - src); /*	dpc is the last path component of dest.	*/
                        if (dest != strncpy(dest, src, size)) {
                            _synctex_error("!  _synctex_copy_with_quoting_last_path_component: Copy problem");
                            free(dest);
                            dest = NULL; /*  Don't forget to reinitialize. */
                            return -2;
                        }
                        memmove(dpc + 1, dpc, strlen(dpc) + 1); /*	Also move the null terminating character. */
                        dpc[0] = '"';
                        dpc[strlen(dpc) + 1] = '\0'; /*	Consistency test */
                        dpc[strlen(dpc)] = '"';
                        return 0; /*	Success. */
                    }
                    return -1; /*	Memory allocation error.	*/
                }
                _synctex_error("!  _synctex_copy_with_quoting_last_path_component: Internal inconsistency");
                return -3;
            }
            return 0; /*	Success. */
        }
        return 0; /*	No last path component. */
#undef dest
    }
    return 1; /*  Bad parameter, this value is subject to changes. */
}

/*  The client is responsible of the management of the returned string, if any. */
char *_synctex_merge_strings(const char *first, ...);

char *_synctex_merge_strings(const char *first, ...)
{
    va_list arg;
    size_t size = 0;
    const char *temp;
    /*   First retrieve the size necessary to store the merged string */
    va_start(arg, first);
    temp = first;
    do {
        size_t len = strlen(temp);
        if (UINT_MAX - len < size) {
            _synctex_error("!  _synctex_merge_strings: Capacity exceeded.");
            return NULL;
        }
        size += len;
    } while ((temp = va_arg(arg, const char *)) != NULL);
    va_end(arg);
    if (size > 0) {
        char *result = NULL;
        ++size;
        /*  Create the memory storage */
        if (NULL != (result = (char *)malloc(size))) {
            char *dest = result;
            va_start(arg, first);
            temp = first;
            do {
                if ((size = strlen(temp)) > 0) {
                    /*  There is something to merge */
                    if (dest != strncpy(dest, temp, size)) {
                        _synctex_error("!  _synctex_merge_strings: Copy problem");
                        free(result);
                        result = NULL;
                        return NULL;
                    }
                    dest += size;
                }
            } while ((temp = va_arg(arg, const char *)) != NULL);
            va_end(arg);
            dest[0] = '\0'; /*  Terminate the merged string */
            return result;
        }
        _synctex_error("!  _synctex_merge_strings: Memory problem");
        return NULL;
    }
    return NULL;
}

/*  The purpose of _synctex_get_name is to find the name of the synctex file.
 *  There is a list of possible filenames from which we return the most recent one and try to remove all the others.
 *  With two runs of pdftex or xetex we are sure the synctex file is really the most appropriate.
 */
int _synctex_get_name(const char *output, const char *build_directory, char **synctex_name_ref, synctex_io_mode_t *io_mode_ref)
{
    if (output && synctex_name_ref && io_mode_ref) {
        /*  If output is already absolute, we just have to manage the quotes and the compress mode */
        size_t size = 0;
        char *synctex_name = NULL;
        synctex_io_mode_t io_mode = *io_mode_ref;
        const char *base_name = _synctex_last_path_component(output); /*  do not free, output is the owner. base name of output*/
        /*  Do we have a real base name ? */
        if (strlen(base_name) > 0) {
            /*  Yes, we do. */
            const char *temp = NULL;
            char *core_name = NULL; /*  base name of output without path extension. */
            char *dir_name = NULL;  /*  dir name of output */
            char *quoted_core_name = NULL;
            char *basic_name = NULL;
            char *gz_name = NULL;
            char *quoted_name = NULL;
            char *quoted_gz_name = NULL;
            char *build_name = NULL;
            char *build_gz_name = NULL;
            char *build_quoted_name = NULL;
            char *build_quoted_gz_name = NULL;
            struct stat buf;
            time_t the_time = 0;
            /*  Create core_name: let temp point to the dot before the path extension of base_name;
             *  We start form the \0 terminating character and scan the string upward until we find a dot.
             *  The leading dot is not accepted. */
            if ((temp = strrchr(base_name, '.')) && (size = temp - base_name) > 0) {
                /*  There is a dot and it is not at the leading position    */
                if (NULL == (core_name = (char *)malloc(size + 1))) {
                    _synctex_error("!  _synctex_get_name: Memory problem 1");
                    return -1;
                }
                if (core_name != strncpy(core_name, base_name, size)) {
                    _synctex_error("!  _synctex_get_name: Copy problem 1");
                    free(core_name);
                    dir_name = NULL;
                    return -2;
                }
                core_name[size] = '\0';
            } else {
                /*  There is no path extension,
                 *  Just make a copy of base_name */
                core_name = _synctex_merge_strings(base_name);
            }
            /*  core_name is properly set up, owned by "self". */
            /*  creating dir_name. */
            size = strlen(output) - strlen(base_name);
            if (size > 0) {
                /*  output contains more than one path component */
                if (NULL == (dir_name = (char *)malloc(size + 1))) {
                    _synctex_error("!  _synctex_get_name: Memory problem");
                    free(core_name);
                    return -1;
                }
                if (dir_name != strncpy(dir_name, output, size)) {
                    _synctex_error("!  _synctex_get_name: Copy problem");
                    free(dir_name);
                    dir_name = NULL;
                    free(core_name);
                    dir_name = NULL;
                    return -2;
                }
                dir_name[size] = '\0';
            }
            /*  dir_name is properly set up. It ends with a path separator, if non void. */
            /*  creating quoted_core_name. */
            if (strchr(core_name, ' ')) {
                quoted_core_name = _synctex_merge_strings("\"", core_name, "\"");
            }
            /*  quoted_core_name is properly set up. */
            if (dir_name && strlen(dir_name) > 0) {
                basic_name = _synctex_merge_strings(dir_name, core_name, synctex_suffix, NULL);
                if (quoted_core_name && strlen(quoted_core_name) > 0) {
                    quoted_name = _synctex_merge_strings(dir_name, quoted_core_name, synctex_suffix, NULL);
                }
            } else {
                basic_name = _synctex_merge_strings(core_name, synctex_suffix, NULL);
                if (quoted_core_name && strlen(quoted_core_name) > 0) {
                    quoted_name = _synctex_merge_strings(quoted_core_name, synctex_suffix, NULL);
                }
            }
            if (!_synctex_path_is_absolute(output) && build_directory && (size = strlen(build_directory))) {
                temp = build_directory + size - 1;
                if (_synctex_path_is_absolute(temp)) {
                    build_name = _synctex_merge_strings(build_directory, basic_name, NULL);
                    if (quoted_core_name && strlen(quoted_core_name) > 0) {
                        build_quoted_name = _synctex_merge_strings(build_directory, quoted_name, NULL);
                    }
                } else {
                    build_name = _synctex_merge_strings(build_directory, "/", basic_name, NULL);
                    if (quoted_core_name && strlen(quoted_core_name) > 0) {
                        build_quoted_name = _synctex_merge_strings(build_directory, "/", quoted_name, NULL);
                    }
                }
            }
            if (basic_name) {
                gz_name = _synctex_merge_strings(basic_name, synctex_suffix_gz, NULL);
            }
            if (quoted_name) {
                quoted_gz_name = _synctex_merge_strings(quoted_name, synctex_suffix_gz, NULL);
            }
            if (build_name) {
                build_gz_name = _synctex_merge_strings(build_name, synctex_suffix_gz, NULL);
            }
            if (build_quoted_name) {
                build_quoted_gz_name = _synctex_merge_strings(build_quoted_name, synctex_suffix_gz, NULL);
            }
            /*  All the others names are properly set up... */
            /*  retain the most recently modified file */
#define TEST(FILENAME, COMPRESS_MODE)                                                                                                                                                                                                          \
    if (FILENAME) {                                                                                                                                                                                                                            \
        if (stat(FILENAME, &buf)) {                                                                                                                                                                                                            \
            free(FILENAME);                                                                                                                                                                                                                    \
            FILENAME = NULL;                                                                                                                                                                                                                   \
        } else if (buf.st_mtime > the_time) {                                                                                                                                                                                                  \
            the_time = buf.st_mtime;                                                                                                                                                                                                           \
            synctex_name = FILENAME;                                                                                                                                                                                                           \
            if (COMPRESS_MODE) {                                                                                                                                                                                                               \
                io_mode |= synctex_io_gz_mask;                                                                                                                                                                                                 \
            } else {                                                                                                                                                                                                                           \
                io_mode &= ~synctex_io_gz_mask;                                                                                                                                                                                                \
            }                                                                                                                                                                                                                                  \
        }                                                                                                                                                                                                                                      \
    }
            TEST(basic_name, synctex_DONT_COMPRESS);
            TEST(gz_name, synctex_COMPRESS);
            TEST(quoted_name, synctex_DONT_COMPRESS);
            TEST(quoted_gz_name, synctex_COMPRESS);
            TEST(build_name, synctex_DONT_COMPRESS);
            TEST(build_gz_name, synctex_COMPRESS);
            TEST(build_quoted_name, synctex_DONT_COMPRESS);
            TEST(build_quoted_gz_name, synctex_COMPRESS);
#undef TEST
            /*  Free all the intermediate filenames, except the one that will be used as returned value. */
#define CLEAN_AND_REMOVE(FILENAME)                                                                                                                                                                                                             \
    if (FILENAME && (FILENAME != synctex_name)) {                                                                                                                                                                                              \
        remove(FILENAME);                                                                                                                                                                                                                      \
        printf("synctex tool info: %s removed\n", FILENAME);                                                                                                                                                                                   \
        free(FILENAME);                                                                                                                                                                                                                        \
        FILENAME = NULL;                                                                                                                                                                                                                       \
    }
            CLEAN_AND_REMOVE(basic_name);
            CLEAN_AND_REMOVE(gz_name);
            CLEAN_AND_REMOVE(quoted_name);
            CLEAN_AND_REMOVE(quoted_gz_name);
            CLEAN_AND_REMOVE(build_name);
            CLEAN_AND_REMOVE(build_gz_name);
            CLEAN_AND_REMOVE(build_quoted_name);
            CLEAN_AND_REMOVE(build_quoted_gz_name);
#undef CLEAN_AND_REMOVE
            /* set up the returned values */
            *synctex_name_ref = synctex_name;
            /* synctex_name won't always end in .gz, even when compressed. */
            FILE *F = fopen(synctex_name, "r");
            if (F != NULL) {
                if (!feof(F) && 31 == fgetc(F) && !feof(F) && 139 == fgetc(F)) {
                    io_mode = synctex_compress_mode_gz;
                }
                fclose(F);
            }
            *io_mode_ref = io_mode;
            return 0;
        }
        return -1; /*  bad argument */
    }
    return -2;
}

const char *_synctex_get_io_mode_name(synctex_io_mode_t io_mode)
{
    static const char *synctex_io_modes[4] = {"r", "rb", "a", "ab"};
    unsigned index = ((io_mode & synctex_io_gz_mask) ? 1 : 0) + ((io_mode & synctex_io_append_mask) ? 2 : 0); /* bug pointed out by Jose Alliste */
    return synctex_io_modes[index];
}
