/*
 * options.c
 *
 * Copyright © 2020 Emmanuel Lepage-Vallee <elv1313@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#include "options.h"
#include "common/version.h"

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <getopt.h>
#include <basedir_fs.h>

#define KEY_VALUE_BUF_MAX 64
#define READ_BUF_MAX 127

static void
set_api_level(char *value)
{
    if (!value)
        return;

    char *ptr;
    int ret = strtol(value, &ptr, 10);

    /* There is no valid number at all */
    if (value == ptr) {
        fprintf(stderr, "Invalid API level %s\n", value);
        return;
    }

    /* There is a number, but also letters, this is invalid */
    if (ptr[0] != '\0' && ptr[0] != '.') {
        fprintf(stderr, "Invalid API level %s\n", value);
        return;
    }

    /* This API level doesn't exist, fallback to v4 */
    if (ret < 4)
        ret = 4;

    globalconf.api_level = ret;
}

static void
push_arg(string_array_t *args, char *value, size_t *len)
{
    value[*len] = '\0';
    string_array_append(args, a_strdup(value));
    (*len) = 0;
}

/*
 * Support both shebang and modeline modes.
 */
bool
options_init_config(xdgHandle *xdg, char *execpath, char *configpath, int *init_flags, string_array_t *paths)
{
    /* The different state the parser can have. */
    enum {
        MODELINE_STATE_INIT       , /* Start of line        */
        MODELINE_STATE_NEWLINE    , /* Start of line        */
        MODELINE_STATE_COMMENT    , /* until --             */
        MODELINE_STATE_MODELINE   , /* until awesome_mode:  */
        MODELINE_STATE_SHEBANG    , /* until !              */
        MODELINE_STATE_KEY_DELIM  , /* until the key begins */
        MODELINE_STATE_KEY        , /* until '='            */
        MODELINE_STATE_VALUE_DELIM, /* after ':'            */
        MODELINE_STATE_VALUE      , /* until ',' or '\n'    */
        MODELINE_STATE_COMPLETE   , /* Parsing is done      */
        MODELINE_STATE_INVALID    , /* note a modeline      */
        MODELINE_STATE_ERROR        /* broken modeline      */
    } state = MODELINE_STATE_INIT;

    /* The parsing mode */
    enum {
        MODELINE_MODE_NONE   , /* No modeline */
        MODELINE_MODE_LINE   , /* modeline    */
        MODELINE_MODE_SHEBANG, /* #! shebang  */
    } mode = MODELINE_MODE_NONE;

    const unsigned char name[] = "awesome_mode:";
    char key_buf [KEY_VALUE_BUF_MAX+1] = {'\0'};
    char file_buf[READ_BUF_MAX+1     ] = {'\0'};
    size_t pos = 0;

    string_array_t argv = {};
    string_array_append(&argv, a_strdup(execpath));

    FILE *fp = NULL;

    /* It is too early to know which config works. So we assume
     * the first one found is the one to use for the modeline. This
     * may or may not end up being the config that works. However since
     * knowing if the config works requires to load it, and loading must
     * be done only after the modeline is parsed, this is the best we can
     * do
     */
    if (!configpath) {
        const char *xdg_confpath = xdgConfigFind("awesome/rc.lua", xdg);

        /* xdg_confpath is "string1\0string2\0string3\0\0" */
        if (xdg_confpath && *xdg_confpath)
            fp = fopen(xdg_confpath, "r");
        else
            fp = fopen(AWESOME_DEFAULT_CONF, "r");

        p_delete(&xdg_confpath);
    } else
        fp = fopen(configpath, "r");

    /* Share the error codepath with parsing errors */
    if (!fp)
        return false;

    /* Try to read the first line */
    if (!fgets(file_buf, READ_BUF_MAX, fp)) {
        fclose(fp);
        return false;
    }

    unsigned char c;

    /* Simple state machine to translate both modeline and shebang into argv */
    for (int i = 0; (c = file_buf[i++]) != '\0';) {
        /* Be very permissive, skip the unknown, UTF is not allowed */
        if ((c > 126 || c < 32) && c != 10 && c != 13 && c != 9) {
            static bool once = true;
            /* Print a warning once */
            if (once) {
                fprintf(stderr, "WARNING: modelines must use ASCII\n");
                once = false;
            }
            continue;
        }

        switch (state) {
            case MODELINE_STATE_INIT:
                switch (c) {
                    case '#':
                        state = MODELINE_STATE_SHEBANG;
                        break;
                    case ' ': case '-':
                        state = MODELINE_STATE_COMMENT;
                        break;
                    default:
                        state = MODELINE_STATE_INVALID;
                }
                break;
            case MODELINE_STATE_NEWLINE:
                switch (c) {
                    case ' ': case '-':
                        state = MODELINE_STATE_COMMENT;
                        break;
                    default:
                        state = MODELINE_STATE_INVALID;
                }
                break;
            case MODELINE_STATE_COMMENT:
                switch (c) {
                    case '-':
                        state = MODELINE_STATE_MODELINE;
                        break;
                    default:
                        state = MODELINE_STATE_INVALID;
                }
                break;
            case MODELINE_STATE_MODELINE:
                if (c == ' ')
                    break;
                else if (c != name[pos++]) {
                    state = MODELINE_STATE_INVALID;
                    pos   = 0;
                }

                if (pos == 13) {
                    pos   = 0;
                    state = MODELINE_STATE_KEY_DELIM;
                    mode  = MODELINE_MODE_LINE;
                }

                break;
            case MODELINE_STATE_SHEBANG:
                switch(c) {
                    case '!':
                        mode  = MODELINE_MODE_SHEBANG;
                        state = MODELINE_STATE_KEY_DELIM;
                        break;
                    default:
                        state = MODELINE_STATE_INVALID;
                }
                break;
            case MODELINE_STATE_KEY_DELIM:
                switch (c) {
                    case ' ': case '\t': case ':': case '=':
                        break;
                    case '\n': case '\r':
                        state = MODELINE_STATE_ERROR;
                        break;
                    default:
                        /* In modeline mode, assume all keys are the long name */
                        switch(mode) {
                            case MODELINE_MODE_LINE:
                                strcpy(key_buf, "--");
                                pos = 2;
                                break;
                            case MODELINE_MODE_SHEBANG:
                            case MODELINE_MODE_NONE:
                                break;
                        };
                        state = MODELINE_STATE_KEY;
                        key_buf[pos++] = c;
                }
                break;
            case MODELINE_STATE_KEY:
                switch (c) {
                    case '=':
                        push_arg(&argv, key_buf, &pos);
                        state = MODELINE_STATE_VALUE_DELIM;
                        break;
                    case ' ': case '\t': case ':':
                        push_arg(&argv, key_buf, &pos);
                        state = MODELINE_STATE_KEY_DELIM;
                        break;
                    default:
                        key_buf[pos++] = c;
                }
                break;
            case MODELINE_STATE_VALUE_DELIM:
                switch (c) {
                    case ' ': case '\t':
                        break;
                    case '\n': case '\r':
                        state = MODELINE_STATE_ERROR;
                        break;
                    case ':':
                        state = MODELINE_STATE_KEY_DELIM;
                        break;
                    default:
                        state = MODELINE_STATE_VALUE;
                        key_buf[pos++] = c;
                }
                break;
            case MODELINE_STATE_VALUE:
                switch(c) {
                    case ',': case ' ': case ':': case '\t':
                        push_arg(&argv, key_buf, &pos);
                        state = MODELINE_STATE_KEY_DELIM;
                        break;
                    case '\n': case '\r':
                        push_arg(&argv, key_buf, &pos);
                        state = MODELINE_STATE_COMPLETE;
                        break;
                    default:
                        key_buf[pos++] = c;
                }
                break;
            case MODELINE_STATE_INVALID:
                /* This cannot happen, the `if` below should prevent it */
                state = MODELINE_STATE_ERROR;
                break;
            case MODELINE_STATE_COMPLETE:
            case MODELINE_STATE_ERROR:
                break;
        }

        /* No keys or values are that large */
        if (pos >= KEY_VALUE_BUF_MAX)
            state = MODELINE_STATE_ERROR;

        /* Stop parsing when completed */
        if (state == MODELINE_STATE_ERROR || state == MODELINE_STATE_COMPLETE)
            break;

        /* Try the next line */
        if (((i == READ_BUF_MAX || file_buf[i] == '\0') && !feof(fp)) || state == MODELINE_STATE_INVALID) {
            if (state == MODELINE_STATE_KEY || state == MODELINE_STATE_VALUE)
                push_arg(&argv, key_buf, &pos);

            /* Skip empty lines */
            do {
                if (fgets(file_buf, READ_BUF_MAX, fp))
                    state = MODELINE_STATE_NEWLINE;
                else {
                    state = argv.len ? MODELINE_STATE_COMPLETE : MODELINE_STATE_ERROR;
                    break;
                }

                i = 0; /* Always reset `i` to avoid an unlikely invalid read */
            } while (i == 0 && file_buf[0] == '\n');
        }
    }

    fclose(fp);

    /* Reset the global POSIX args counter */
    optind = 0;

    /* Be future proof, allow let unknown keys live, let the Lua code decide */
    (*init_flags) |= INIT_FLAG_ALLOW_FALLBACK;

    options_check_args(argv.len, argv.tab, init_flags, paths);

    /* Cleanup */
    string_array_wipe(&argv);

    return state == MODELINE_STATE_COMPLETE;
}


char *
options_detect_shebang(int argc, char **argv)
{
    /* There is no cross-platform ways to check if it is *really* called by a
     * shebang. There is a couple Linux specific hacks which work with the
     * most common C libraries, but they won't work on *BSD.
     *
     * On some platforms, the argv is going to be parsed by the OS, in other
     * they will be concatenated in one big string. There is some ambiguities
     * caused by that. For example, `awesome -s foo` and and `#!/bin/awesome -s`
     * are both technically valid if `foo` is a directory in the first and
     * lua file (without extension) in the second. While `-s` with a file
     * won't work, it is hard to know by looking at the string.
     *
     * The trick to avoid any ambiguity is to just read the file and see if
     * the args match. `options_init_config` will be called later and the args
     * will be parsed properly.
     */

    /* On WSL and some other *nix this isn't true, but it is true often enough */
    if (argc > 3 || argc == 1)
        return NULL;

    /* Check if it is executable */
    struct stat inf;
    if (stat(argv[argc-1], &inf) || !(inf.st_mode & S_IXUSR))
        return NULL;

    FILE *fp = fopen(argv[argc-1], "r");

    if (!fp)
        return NULL;

    char buf[3];

    if (!fgets(buf, 2, fp)) {
        fclose(fp);
        return NULL;
    }

    fclose(fp);

    if (!strcmp(buf, "#!"))
        return NULL;

    /* Ok, good enough, this is a shebang script, assume it called `awesome` */
    return a_strdup(argv[argc-1]);
}

/** Print help and exit(2) with given exit_code.
 * \param exit_code The exit code.
 */
static void __attribute__ ((noreturn))
exit_help(int exit_code)
{
    FILE *outfile = (exit_code == EXIT_SUCCESS) ? stdout : stderr;
    fprintf(outfile,
"Usage: awesome [OPTION]\n\
  -h, --help             show help\n\
  -v, --version          show version\n\
  -c, --config FILE      configuration file to use\n\
  -f, --force            ignore modelines and apply the command line arguments\n\
  -s, --search DIR       add a directory to the library search path\n\
  -k, --check            check configuration file syntax\n\
  -a, --no-argb          disable client transparency support\n\
  -l  --api-level LEVEL  select a different API support level than the current version \n\
  -m, --screen on|off    enable or disable automatic screen creation (default: on)\n\
  -r, --replace          replace an existing window manager\n");
    exit(exit_code);
}

#define ARG 1
#define NO_ARG 0

char *
options_check_args(int argc, char **argv, int *init_flags, string_array_t *paths)
{

    const struct option long_options[] =
    {
        { "help"      , NO_ARG, NULL, 'h'  },
        { "version"   , NO_ARG, NULL, 'v'  },
        { "config"    , ARG   , NULL, 'c'  },
        { "force"     , NO_ARG, NULL, 'f'  },
        { "check"     , NO_ARG, NULL, 'k'  },
        { "search"    , ARG   , NULL, 's'  },
        { "no-argb"   , NO_ARG, NULL, 'a'  },
        { "replace"   , NO_ARG, NULL, 'r'  },
        { "screen"    , ARG   , NULL, 'm'  },
        { "api-level" , ARG   , NULL, 'l'  },
        { "reap"      , ARG   , NULL, '\1' },
        { NULL        , NO_ARG, NULL, 0    }
    };

    char *confpath = NULL;
    int opt;

    while((opt = getopt_long(argc, argv, "vhfkc:arms:l:",
                             long_options, NULL)) != -1) {
        switch(opt)
        {
          case 'v':
            eprint_version();
            break;
          case 'h':
            if (! ((*init_flags) & INIT_FLAG_ALLOW_FALLBACK))
                exit_help(EXIT_SUCCESS);
            break;
          case 'f':
            (*init_flags) |= INIT_FLAG_FORCE_CMD_ARGS;
            break;
          case 'k':
            (*init_flags) |= INIT_FLAG_RUN_TEST;
            break;
          case 'c':
            if (confpath != NULL)
                fatal("--config may only be specified once");
            confpath = a_strdup(optarg);

            /* Make sure multi-file config works */
            string_array_append(paths, g_path_get_dirname(optarg));
            break;
          case 'm':
            /* Validation */
            if ((!optarg) || !(A_STREQ(optarg, "off") || A_STREQ(optarg, "on")))
                fatal("The possible values of -m/--screen are \"on\" or \"off\"");

            globalconf.no_auto_screen = A_STREQ(optarg, "off");

            (*init_flags) &= ~INIT_FLAG_AUTO_SCREEN;

            break;
          case 's':
            globalconf.have_searchpaths = true;
            string_array_append(paths, a_strdup(optarg));
            break;
          case 'a':
            globalconf.had_overriden_depth = true;
            (*init_flags) &= ~INIT_FLAG_ARGB;
            break;
          case 'r':
            (*init_flags) |= INIT_FLAG_REPLACE_WM;
            break;
          case 'l':
              set_api_level(optarg);
              break;
          case '\1':
            /* Silently ignore --reap and its argument */
            break;
          default:
            if (! ((*init_flags) & INIT_FLAG_ALLOW_FALLBACK))
                exit_help(EXIT_FAILURE);
            break;
        }}

    return confpath;
}

#undef AR
#undef NO_ARG
#undef KEY_VALUE_BUF_MAX
#undef READ_BUF_MAX
