/*
 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#include <shlwapi.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>

#define JAVA_EXECUTABLE_NAME "java.exe"

#ifndef LAUNCHER_ARGS
#error LAUNCHER_ARGS must be defined
#endif

static char* launcher_args[] = LAUNCHER_ARGS;

char* quote_argument(char* arg) {
    // See https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
    // for an explanation of how to properly quote command lines for CreateProcess
    size_t arg_length = strlen(arg);

    if (strcspn(arg, " \t\n\v\"") == arg_length) {
        // No quoting is needed
        return arg;
    }

    // Worst-case buffer size: all characters need a backslash, and starting + end quotes
    size_t buffer_size = arg_length * 2 + 3;
    char* buffer = malloc(buffer_size);
    if (buffer == NULL) {
        return NULL;
    }

    int backslashes = 0;
    char* write_pos = buffer;
    char* read_pos = arg;

    // Start with a quote character
    *write_pos++ = '"';

    while (*read_pos) {
        while (*read_pos == '\\') {
            read_pos++;
            backslashes++;
        }

        if (*read_pos == '"') {
            // Any potential backslashes before a quote needs to be doubled,
            // and the quote needs to be escaped with an additional backslash
            for (int i = 0; i < backslashes * 2 + 1; i++) {
                *write_pos++ = '\\';
            }
            *write_pos++ = *read_pos++;
            backslashes = 0;
        } else {
            // Backslashes not preceeding a quote are copied without escaping
            for (int i = 0; i < backslashes; i++) {
                *write_pos++ = '\\';
            }
            if (*read_pos) {
                *write_pos++ = *read_pos++;
                backslashes = 0;
            }
        }
    }

    // If the string ended with backslashes, they need to be doubled before
    // the final quote character
    for (int i = 0; i < backslashes; i++) {
        *write_pos++ = '\\';
    }
    *write_pos++ = '"';
    *write_pos = '\0';

    return buffer;
}

int main(int argc, char* argv[]) {
    ////////////////////////////////////////////////////////////////////////////
    // Create a fully qualified path to the java executable in the same
    // directory as this file resides in.

    // Calculate path length first
    DWORD our_full_path_len = GetFullPathName(argv[0], 0, NULL, NULL);
    if (our_full_path_len == 0) {
        fprintf(stderr, "failed to get the full path of the executable: %lu\n", GetLastError());
        return 1;
    }

    char* our_full_path = malloc(our_full_path_len + 1);
    if (our_full_path == NULL) {
        perror("malloc failed");
        return 1;
    }

    if (GetFullPathName(argv[0], our_full_path_len + 1, our_full_path, NULL) == 0) {
        fprintf(stderr, "failed to get the full path of the executable: %lu\n", GetLastError());
        return 1;
    }

    char *last_slash_pos = strrchr(our_full_path, '\\');
    if (last_slash_pos == NULL) {
        fprintf(stderr, "no '\\' found in the full path of the executable\n");
        return 1;
    }

    size_t base_length = last_slash_pos - our_full_path + 1;
    size_t java_path_length = base_length + strlen(JAVA_EXECUTABLE_NAME) + 1;

    char *java_path = malloc(java_path_length);
    if (java_path == NULL) {
        perror("malloc failed");
        return 1;
    }

    memcpy(java_path, our_full_path, base_length);
    strcpy(java_path + base_length, JAVA_EXECUTABLE_NAME);

    ////////////////////////////////////////////////////////////////////////////
    // Build the argument list: our executable name + launcher args + users args

    int launcher_argsc = sizeof(launcher_args) / sizeof(char *);

    char **java_args = malloc((launcher_argsc + argc + 1) * sizeof(char *));
    if (java_args == NULL) {
        perror("malloc failed");
        return 1;
    }

    // Our executable name
    java_args[0] = quote_argument(argv[0]);
    if (java_args[0] == NULL) {
        perror("malloc failed");
        return 1;
    }

    // Launcher arguments
    for (int i = 0; i < launcher_argsc; i++) {
        char* quoted = quote_argument(launcher_args[i]);
        if (quoted == NULL) {
            perror("malloc failed");
            return 1;
        }
        java_args[i + 1] = quoted;
    }

    // User arguments
    for (int i = 1; i < argc; i++) {
        char* quoted = quote_argument(argv[i]);
        if (quoted == NULL) {
            perror("malloc failed");
            return 1;
        }
        java_args[launcher_argsc + i] = quoted;
    }

    java_args[launcher_argsc + argc] = NULL;

    // Windows needs the command line as a single string, not as an array of char*
    size_t total_length = 0;
    for (int i = 0; java_args[i] != NULL; i++) {
        char* arg = java_args[i];
        total_length += strlen(java_args[i]) + 1;
    }

    char* command_line = malloc(total_length);
    if (command_line == NULL) {
        perror("malloc failed");
        return 1;
    }

    // Concatenate the quoted arguments with a space between them
    char* write_pos = command_line;
    for (int i = 0; java_args[i] != NULL; i++) {
        size_t arg_len = strlen(java_args[i]);
        memcpy(write_pos, java_args[i], arg_len);
        write_pos += arg_len;

        // Append a space
        *write_pos++ = ' ';
    }

    // Replace the last space with a null terminator
    write_pos--;
    *write_pos = '\0';

    ////////////////////////////////////////////////////////////////////////////
    // Finally execute the real java process with the constructed arguments

    if (GetEnvironmentVariable("_JAVA_LAUNCHER_DEBUG", NULL, 0)) {
        char *program_name = PathFindFileName(argv[0]);

        fprintf(stderr, "%s: executing: '%s' '%s'\n", program_name, java_path, command_line);
    }

    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    memset(&si, 0, sizeof(si));
    memset(&pi, 0, sizeof(pi));

    // Windows has no equivalent of exec, so start the process and wait for it
    // to finish, to be able to return the same exit code
    if (!CreateProcess(java_path, command_line, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        fprintf(stderr, "CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }

    if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED) {
        fprintf(stderr, "WaitForSingleObject failed: %lu\n", GetLastError());
        return 1;
    }

    DWORD exit_code;
    if (!GetExitCodeProcess(pi.hProcess, &exit_code)) {
        fprintf(stderr, "GetExitCodeProcess failed: %lu\n", GetLastError());
        return 1;
    }
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return exit_code;
}
