// vim: ts=4 sw=4 expandtab ft=c

// Copyright (C) 2001-2002, 2005-2006 The University of Melbourne.
// Copyright (C) 2014-2016, 2018 The Mercury team.
// This file is distributed under the terms specified in COPYING.LIB.

// mercury_trace_source.c
//
// This file implements routines to open and use a window to display the
// source code. Using these requires an X server and a version of
// vim compiled with '+clientserver'. If these are not available an
// error is returned.
//
// Main author: Mark Brown

#include "mercury_imp.h"
#include "mercury_trace_source.h"
#include "mercury_trace_internal.h"

#include <stdlib.h>
#include <stdio.h>

#ifdef MR_HAVE_UNISTD_H
  #include <unistd.h>
#endif

#ifdef MR_HAVE_SYS_TYPES_H
  #include <sys/types.h>    // for getpid()
#endif

#ifdef MR_HAVE_WINDOWS_H
#include <windows.h>
#endif

#define MR_DEFAULT_SOURCE_WINDOW_COMMAND    "xterm -e"
#define MR_DEFAULT_SOURCE_SERVER_COMMAND    "vim"

// When sent to a vim server, this string puts vim into "normal" mode.
// This has much the same effect as ESC, except that there is no bell if
// already in normal mode. We send this before most of the other
// commands to ensure that they are interpreted correctly.
//
// See the vim help page for ctrl-\_ctrl-n (type ':he ** ctrl-\\_ctrl-n'
// in vim).

#define MR_SOURCE_SERVER_RESET_STRING       "<C-\\><C-N>"

// These vim commands split the screen, move to the top window, and
// move down one window, respectively.

#define MR_SOURCE_SERVER_SPLIT_STRING       "<C-W>s"
#define MR_SOURCE_SERVER_TOP_STRING         "<C-W>t"
#define MR_SOURCE_SERVER_DOWN_STRING        "<C-W>j"

// This vim command centres the current window on the cursor.

#define MR_SOURCE_SERVER_CENTRE_STRING      "z."

// This is the command we use to quit the server. We have to close *all*
// windows, just in case we are in split screen mode. This won't quit if
// the user has modified any file, which is just as well.

#define MR_SOURCE_SERVER_QUIT_STRING        ":qall<CR>"

// The name used for the server will start with this basename.

#define MR_SOURCE_SERVER_BASENAME           "mdb_source_server"

// This defines the number of chars of the hostname to use to
// determine a unique server name.

#define MR_SERVER_HOSTNAME_CHARS        32

// The size of the buffer used to construct system calls in.

#define MR_SYSCALL_BUFFER_SIZE          512

// Checks whether $DISPLAY is set to something reasonable.
// If so, returns NULL, otherwise returns a warning message.

static const char   *MR_trace_source_check_display(void);

// Checks whether the server command is valid. If so, returns NULL,
// otherwise returns an error message.

static const char   *MR_trace_source_check_server_cmd(const char *server_cmd,
                        MR_bool verbose);

// Checks whether a server with the given name is accessible.
// If so, returns NULL, otherwise returns an error message.

static const char   *MR_trace_source_check_server(const char *server_cmd,
                        const char *server_name, MR_bool verbose);

// Tell the server to jump to the given file and line. If the server has
// multiple windows open, use the current window. If successful, returns NULL,
// otherwise returns an error message.

static const char   *MR_trace_source_jump(const char *server_cmd,
                        const char *server_name, const char *filename,
                        int lineno, MR_bool verbose);

// Send the given key sequence to the vim server. Returns the status code
// of the shell command.

static int          MR_trace_source_send(const char *server_cmd,
                        const char *server_name, const char *keys,
                        MR_bool verbose);

static int          MR_verbose_system_call(const char *system_call,
                        MR_bool verbose);

static const char *
MR_trace_source_check_display(void)
{
    if (getenv("DISPLAY") == NULL) {
        return "warning: DISPLAY environment variable is not set";
    } else {
        return NULL;
    }
}

static const char *
MR_trace_source_check_server_cmd(const char *server_cmd, MR_bool verbose)
{
    char    system_call[MR_SYSCALL_BUFFER_SIZE];
    int     status;

    // Try running the server with '--version', and see if the
    // output contains the string '+clientserver'.

    sprintf(system_call, "%s --version 2>&1 | fgrep -q '+clientserver' %s",
        server_cmd, (verbose ? "" : "> /dev/null 2>&1"));
    status = MR_verbose_system_call(system_call, verbose);
    if (status == 0) {
        return NULL;
    } else {
        return "error: source server command is not valid";
    }
}

static const char *
MR_trace_source_check_server(const char *server_cmd, const char *server_name,
    MR_bool verbose)
{
    char    system_call[MR_SYSCALL_BUFFER_SIZE];
    int     status;

    // Try running a server with '--serverlist', and see if the output
    // contains our server name. Server names are case insensitive.

    sprintf(system_call, "%s --serverlist | fgrep -iq '%s' %s",
        server_cmd, server_name, (verbose ? "" : "> /dev/null 2>&1"));
    status = MR_verbose_system_call(system_call, verbose);
    if (status == 0) {
        return NULL;
    } else {
        return "error: source server not found";
    }
}

const char *
MR_trace_source_open_server(MR_TraceSourceServer *server,
    const char *window_cmd, int timeout, MR_bool verbose)
{
    const char  *real_window_cmd;
    const char  *real_server_cmd;
    char        *name;
    const char  *msg;
    char        system_call[MR_SYSCALL_BUFFER_SIZE];
    int         status;
    size_t      base_len;
    size_t      i;

    if (window_cmd != NULL) {
        real_window_cmd = window_cmd;
    } else {
        real_window_cmd = MR_DEFAULT_SOURCE_WINDOW_COMMAND;
    }

    if (server->server_cmd != NULL) {
        real_server_cmd = server->server_cmd;
    } else {
        real_server_cmd = MR_DEFAULT_SOURCE_SERVER_COMMAND;
    }

    // 1) check that display is set;
    // 2) check that server is valid;
    // 3) start a server with a unique name;
    // 4) wait until the server is found;
    // 5) (if required) split the window.

    msg = MR_trace_source_check_display();
    if (msg != NULL) {
        return msg;
    }

    msg = MR_trace_source_check_server_cmd(real_server_cmd, verbose);
    if (msg != NULL) {
        return msg;
    }

    // If possible, we generate a unique name of the form
    // '<basename>.<hostname>.<pid>', but if the required functions
    // aren't available, we fall back to appending numbers to the
    // basename in sequence until one is found that is not being used.
    // This is quite a slow way of doing things, and there is also a
    // race condition, but it is difficult to see a better way.
    // We should let the server pick a unique name for itself, but
    // how would you communicate this name back to this process?

    base_len = strlen(MR_SOURCE_SERVER_BASENAME);

#if defined(MR_HAVE_GETPID) && defined(MR_HAVE_GETHOSTNAME)
    // Need to leave enough room for the pid, two '.'s and the
    // terminating zero.

    name = MR_malloc(base_len + MR_SERVER_HOSTNAME_CHARS + 32);
    strcpy(name, MR_SOURCE_SERVER_BASENAME);

    // Append the '.' and hostname.

    name[base_len] = '.';
    gethostname(name + base_len + 1, MR_SERVER_HOSTNAME_CHARS);
    // Do this just in case gethostname didn't terminate the string:
    name[base_len + 1 + MR_SERVER_HOSTNAME_CHARS] = '\0';

    // Find the end of the string and append the '.' and pid.

    i = base_len + 1 + strlen(name + base_len + 1);
    sprintf(name + i, ".%ld", (long) getpid());
#else
    i = 0;
    // Need to leave enough room for a '.', the integer and the
    // terminating zero.

    name = MR_malloc(base_len + 10);
    do {
        i++;
        sprintf(name, "%s.%lu", MR_SOURCE_SERVER_BASENAME, (unsigned long)i);
        // This should fail if there is no server with this name.
        msg = MR_trace_source_check_server(real_server_cmd, name, verbose);
    } while (msg == NULL);
#endif

    server->server_name = name;

    // Start the server in read-only mode, to discourage the user
    // from trying to edit the source.

    sprintf(system_call, "%s %s -R --servername \"%s\" %s &", real_window_cmd,
        real_server_cmd, name, (verbose ? "" : "> /dev/null 2>&1"));
    MR_verbose_system_call(system_call, verbose);

    // We need to wait until the source server has registered itself
    // with the X server. If we don't, we may execute remote commands
    // before the server has started up, and this will result in vim
    // (rather inconveniently) starting its own server on mdb's terminal.

    msg = MR_trace_source_attach(server, timeout, verbose);

    if (msg != NULL) {
        // Something went wrong, so we should free the server name
        // we allocated just above.

        MR_free(server->server_name);
        server->server_name = NULL;
        return msg;
    }

    if (server->split) {
        // Split the window.
        status = MR_trace_source_send(real_server_cmd, server->server_name,
            MR_SOURCE_SERVER_RESET_STRING MR_SOURCE_SERVER_SPLIT_STRING,
            verbose);
        if (status != 0) {
            server->split = MR_FALSE;
            return "warning: unable to split source window";
        }
    }

    return NULL;
}

const char *
MR_trace_source_attach(MR_TraceSourceServer *server, int timeout,
    MR_bool verbose)
{
    const char  *real_server_cmd;
    const char  *msg;
    int         i;

    if (server->server_cmd != NULL) {
        real_server_cmd = server->server_cmd;
    } else {
        real_server_cmd = MR_DEFAULT_SOURCE_SERVER_COMMAND;
    }

    msg = MR_trace_source_check_server(real_server_cmd, server->server_name,
        verbose);
    if (msg == NULL) {
        return NULL;
    }

    for (i = 0; i < timeout; i++) {
        // XXX This is an inaccurate way of keeping time.

#ifdef MR_HAVE_SLEEP
        sleep(1);
#elif MR_HAVE_CAPITAL_S_SLEEP
        Sleep(1000);
#else
        // Busy-wait for a few billion cycles, which should hopefully
        // take a second or more.

        volatile long i;
        for (i = 0; i < 100000000; i++) {
            /* empty */
            ;
        }
#endif
        msg = MR_trace_source_check_server(real_server_cmd,
            server->server_name, verbose);
        if (msg == NULL) {
            return NULL;
        }
    }

    return "timeout: source server not found";
}

const char *
MR_trace_source_sync(MR_TraceSourceServer *server, const char *filename,
    int lineno, const char *parent_filename, int parent_lineno,
    MR_bool verbose)
{
    const char  *real_server_cmd;
    const char  *msg;
    int         status;
    MR_bool     have_parent;
    MR_bool     have_current;

    have_parent = MR_strdiff(parent_filename, "") && parent_lineno != 0;
    have_current = MR_strdiff(filename, "") && lineno != 0;

    if (!have_parent && !have_current) {
        // No point in continuing.
        return NULL;
    }

    if (server->server_cmd != NULL) {
        real_server_cmd = server->server_cmd;
    } else {
        real_server_cmd = MR_DEFAULT_SOURCE_SERVER_COMMAND;
    }

    msg = MR_trace_source_check_server(real_server_cmd, server->server_name,
        verbose);
    if (msg != NULL) {
        return msg;
    }

    if (server->split) {
        // When in split mode, we open two windows on the vim server,
        // a primary window and the secondary window. The primary window
        // displays what would normally be shown in non-split mode.
        //
        // If there is no parent context (e.g. at internal events)
        // we leave the secondary window alone.
        //
        // If we have a parent context it will be displayed in the
        // primary window, so in this case we show the current context
        // in the secondary window.
        //
        // The primary window is the one second from the top (which will
        // usually be the bottom window). The secondary window is at the top.

        if (have_parent && have_current) {
            // Move to the secondary (top) window.
            status = MR_trace_source_send(real_server_cmd, server->server_name,
                MR_SOURCE_SERVER_RESET_STRING MR_SOURCE_SERVER_TOP_STRING,
                verbose);
            if (status != 0) {
                return "warning: source synchronisation failed";
            }

            msg = MR_trace_source_jump(real_server_cmd, server->server_name,
                filename, lineno, verbose);
            if (msg != NULL) {
                return msg;
            }

            // Move down one window to the primary.
            status = MR_trace_source_send(real_server_cmd, server->server_name,
                MR_SOURCE_SERVER_RESET_STRING MR_SOURCE_SERVER_DOWN_STRING,
                verbose);
            if (status != 0) {
                return "warning: source synchronisation failed";
            }
        } else {
            // Move to the primary (second from top) window.
            status = MR_trace_source_send(real_server_cmd, server->server_name,
                MR_SOURCE_SERVER_RESET_STRING MR_SOURCE_SERVER_TOP_STRING
                MR_SOURCE_SERVER_DOWN_STRING, verbose);
            if (status != 0) {
                return "warning: source synchronisation failed";
            }
        }
    }

    // We show the parent context if we can, since if both are present
    // the parent context is usually more interesting. Otherwise we
    // show the current context.

    if (have_parent) {
        msg = MR_trace_source_jump(real_server_cmd, server->server_name,
            parent_filename, parent_lineno, verbose);
        if (msg != NULL) {
            return msg;
        }
    } else {
        msg = MR_trace_source_jump(real_server_cmd, server->server_name,
            filename, lineno, verbose);
        if (msg != NULL) {
            return msg;
        }
    }

    return NULL;
}

static const char *
MR_trace_source_jump(const char *server_cmd, const char *server_name,
    const char *filename, int lineno, MR_bool verbose)
{
    char    system_call[MR_SYSCALL_BUFFER_SIZE];
    int     status;

    // Point the source server to the given context.

    sprintf(system_call, "%s --servername \"%s\" --remote '+%d' %s",
        server_cmd, server_name, lineno, filename);
    status = MR_verbose_system_call(system_call, verbose);
    if (status != 0) {
        return "warning: source synchronisation failed";
    }

    // Center the current line in the vim window. We need to put
    // the server in normal mode, just in case the user has changed
    // mode since the previous command was sent.

    status = MR_trace_source_send(server_cmd, server_name,
        MR_SOURCE_SERVER_RESET_STRING MR_SOURCE_SERVER_CENTRE_STRING, verbose);
    if (status != 0) {
        return "warning: source synchronisation failed";
    }

    return NULL;
}

const char *
MR_trace_source_close(MR_TraceSourceServer *server, MR_bool verbose)
{
    const char  *real_server_cmd;
    const char  *msg;

    if (server->server_cmd != NULL) {
        real_server_cmd = server->server_cmd;
    } else {
        real_server_cmd = MR_DEFAULT_SOURCE_SERVER_COMMAND;
    }

    msg = MR_trace_source_check_server(real_server_cmd, server->server_name,
        verbose);
    if (msg != NULL) {
        return msg;
    }

    MR_trace_source_send(real_server_cmd, server->server_name,
        MR_SOURCE_SERVER_RESET_STRING MR_SOURCE_SERVER_QUIT_STRING, verbose);

#if 0
    // XXX This doesn't work properly because the server takes some
    // time to close. Sometimes the server is still open when we do
    // the test, and this leads to a false warning.

    // We expect the following to fail. If it doesn't, the server
    // hasn't closed properly.

    msg = MR_trace_source_check_server(real_server_cmd, server->server_name,
        verbose);
    if (msg == NULL) {
        return "warning: failed to close source server";
    } else {
        return NULL;
    }
#else
    return NULL;
#endif
}

int MR_trace_source_send(const char *server_cmd, const char *server_name,
    const char *keys, MR_bool verbose)
{
    char    system_call[MR_SYSCALL_BUFFER_SIZE];

    sprintf(system_call, "%s --servername \"%s\" --remote-send '%s'",
        server_cmd, server_name, keys);
    return MR_verbose_system_call(system_call, verbose);
}

int MR_verbose_system_call(const char *system_call, MR_bool verbose)
{
    if (verbose) {
        fflush(MR_mdb_out);
        fprintf(MR_mdb_err, "+ %s\n", system_call);
    }
    return system(system_call);
}

