#!/usr/bin/env python3
# Generate front-end.
# TODO: When we can assume gengetopt 2.23, convert this back to C.
#
# Copyright (c) 1995-2020 Mit authors
#
# The package is distributed under the MIT/X11 License.
#
# THIS PROGRAM IS PROVIDED AS IS, WITH NO WARRANTY. USE IS AT THE USER’S
# RISK.

from code_util import Code, copyright_banner
from spec import MitErrorCode
from features.trap_errors import TrapErrorCode


GENERATOR_PROGRAM = 'gen-main'
PURPOSE = 'Command-line front end for Mit.'
COPYRIGHT_YEARS = '1995-2020'


# Command line

class Doc:
    '''
    Free-form documentation.

     - docstring - str or list of str - if the string (or first string)
       starts and ends with a double quote, it is printed literally and may
       thus contain interpolations. Otherwise, it is output in quotes. For a
       list, the other strings are printed literally; they are intended to
       be arguments for `printf` substitutions.
    '''
    elements = []
    def __init__(self, docstring):
        self.elements.append(self)
        if not isinstance(docstring, list):
            docstring = [docstring]
        if docstring[0][:1] != '"' or docstring[0][-1:] != '"':
            docstring[0] = f'"{repr(docstring[0])[1:-1]}"'
        self._docstring = docstring

    @property
    def docstring(self):
        return ', '.join(self._docstring)

class Option(Doc):
    '''
    A command-line option.

     - long_name - str - long option name
     - docstring - str - argument docstring as for Doc class.
     - short_name - str - short option char
     - arg - str - 'no_argument' (default), 'required_argument' or
       'optional_argument'
     - arg_name - str - name for the argument, if any
     - parse_code - str - code run when the option is parsed
    '''
    options = []
    def __init__(self, long_name, docstring, short_name=None,
                 arg=None, arg_name=None, parse_code=None):
        super().__init__(docstring)
        self.options.append(self)
        self.long_name = long_name
        self.short_name = short_name
        self.arg = arg or 'no_argument'
        self.arg_name = arg_name
        self.parse_code = parse_code

class Arg(Doc):
    '''
    A command-line argument.

     - arg - str - argument name
     - docstring - str - argument docstring as for Doc class.
    '''
    def __init__(self, arg, docstring):
        super().__init__(docstring)
        self.arg = arg

Doc('Execution:')
Option('memory',
       ['size of memory in words [default %zu]', '(size_t)memory_words'],
       short_name='m',
       arg='required_argument', arg_name='N',
       parse_code=Code('''\
           memory_words = parse_num_words("memory size must be a positive number up to %zu");
       ''')
)

Option('stack',
       ['size of stack frame in words [default %zu]', '(size_t)mit_stack_words'],
       short_name='s',
       arg='required_argument', arg_name='N',
       parse_code=Code('''\
           mit_stack_words = parse_num_words("stack frame size must be a positive number up to %zu");
       ''')
)

Option('no-optimize',
       'use the naive interpreter',
       parse_code=Code('mit_run = mit_run_simple;'),
)

Doc('\nMiscellaneous:')
Option('help',
       'display this help message and exit',
       parse_code=Code('''\
           usage();
           exit(EXIT_SUCCESS);
       '''),
)

Option('version',
       'display version information and exit',
       parse_code=Code('''\
               printf(PACKAGE_NAME " " VERSION " (%zu-byte word, %s-endian)\\n"
                      "Copyright (c) Mit authors 1994-2020.\\n"
                      PACKAGE_NAME " comes with ABSOLUTELY NO WARRANTY.\\n"
                      "You may redistribute copies of " PACKAGE_NAME "\\n"
                      "under the terms of the MIT/X11 License.\\n",
                      sizeof(mit_word_t), ENDISM_STRING);
               exit(EXIT_SUCCESS);
       ''')
)

Arg('OBJECT-FILE', 'load and run object OBJECT-FILE')
Doc('')
Doc('"The ARGUMENTs are passed to OBJECT-FILE."')
Doc('''
If an error occurs during execution, the exit status is the error code; if
execution halts normally, the exit status is 0. Note that the VM code may
cause an exit with a different status.''')


# Generate output

# Errors
def enum_to_error_table(error_enum):
    code = Code()
    for error in error_enum:
        code.append('{{{}, "{}"}},'.format(
            error.value,
            error.name.lower().translate(str.maketrans('_', ' ')),
        ))
    return code

error_enums = Code()
error_enums.extend(enum_to_error_table(MitErrorCode))
error_enums.extend(enum_to_error_table(TrapErrorCode))

# Command-line documentation
help_code = Code()
for element in Doc.elements:
    if isinstance(element, Option):
        arg = ''
        if element.arg_name: arg = element.arg_name
        if element.arg == 'optional_argument':
            arg = f'[{arg}]'
        help_code.append('printf("  %-26s", "--{long_name}{short_opt} {arg}");'.format(
            long_name=element.long_name,
            short_opt=f', -{element.short_name}' if element.short_name else '',
            arg=arg,
        ))
        help_code.append(f'printf({element.docstring});')
        help_code.append('putchar(\'\\n\');')
    elif isinstance(element, Arg):
        help_code.append(f'printf("  %-26s%s\\n", "{element.arg}", {element.docstring});')
    elif isinstance(element, Doc):
        help_code.append(f'printf({element.docstring}"\\n");')

# Command-line options
longopts_struct = Code()
shortopts = ''
short_options_code = Code()
long_options_code = Code('')
else_code = ''
for i, option in enumerate(Option.options):
    longopts_struct.append('{{"{long_name}", {arg}, NULL, \'{short_name}\'}},'.format(
        long_name=option.long_name, arg=option.arg or '""',
        short_name=option.short_name or "\\0"))
    if option.short_name:
        shortopts += option.short_name
        if option.arg == 'required_argument':
            shortopts += ':'
        elif option.arg == 'optional_argument':
            shortopts += '::'
        short_options_code.append(f'''\
            else if (c == '{option.short_name}')
                longindex = {i};'''
        )
    if option.parse_code:
        long_options_code.append(f'{else_code}if (longindex == {i}) {{')
        long_options_code.append(option.parse_code)
        else_code = '} else '
long_options_code.append('}')

longopts_struct.append('{0, 0, 0, 0}')
short_options_assignment = Code(f'const char *shortopts = "+:{shortopts}";')

# main.c
code = copyright_banner(GENERATOR_PROGRAM, PURPOSE, COPYRIGHT_YEARS)
code.extend(Code('''

#include "config.h"

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <getopt.h>
#include <unistd.h>

#include "getprogname.h"

#include "mit/mit.h"

#include "warn.h"


// Error messages
struct {
    int code;
    const char *msg;
} error_msg[] = {
''',
    error_enums,
    '''\
};

static const char *error_to_msg(int code)
{
    for (size_t i = 0; i < sizeof(error_msg) / sizeof(error_msg[0]); i++) {
        if (error_msg[i].code == code)
            return error_msg[i].msg;
    }
    return "unknown error";
}


// Command-line options
#ifdef WORDS_BIGENDIAN
#define ENDISM_STRING "big"
#else
#define ENDISM_STRING "little"
#endif

struct option longopts[] = {''',
    longopts_struct,
    '''\
};

// Parse an integer representing a size measured in words
static mit_uword_t parse_num_words(const char *errfmt)
{
    size_t max = SIZE_MAX / sizeof(mit_word_t);
    char *endptr;
    uintmax_t size = strtoumax(optarg, &endptr, 10);
    if (*optarg == '\\0' || *endptr != '\\0' || size == 0 || size > (uintmax_t)max)
        die(errfmt, max);
    return (mit_uword_t)size;
}

// Return the length of a seekable stream, or `-1` if not seekable
static off_t fleno(FILE *fp)
{
    off_t pos = ftello(fp);
    if (pos != -1 && fseeko(fp, 0, SEEK_END) == 0) {
        off_t end = ftello(fp);
        if (end != -1 && fseeko(fp, pos, SEEK_SET) == 0)
            return end - pos;
    }
    return -1;
}

// Skip any #! header
static int skip_hashbang(FILE *fp)
{
    if (getc(fp) != '#' || getc(fp) != '!')
        return fseeko(fp, 0, SEEK_SET);
    for (int res; (res = getc(fp)) != '\\n'; )
        if (res == EOF)
            return -1;
    return 0;
}

// Global state
mit_uword_t memory_words;
const char *program_name;

static void usage(void)
{
    printf("Usage: %s [OPTION...] OBJECT-FILE [ARGUMENT...]\\n"
           "\\n"
           "Run " PACKAGE_NAME " virtual machine code.\\n"
           "\\n",
           program_name);''',
    help_code,
    '''
}

int main(int argc, char *argv[])
{
    program_name = getprogname();

    memory_words = 0x100000U;
    mit_run = mit_run_simple;

    // Options string starts with '+' to stop option processing at first
    // non-option, then leading ':' so as to return ':' for a missing arg,
    // not '?'
    ''',
    short_options_assignment,
    Code('''
    for (;;) {
        int this_optind = optind, longindex = -1;
        int c = getopt_long(argc, argv, shortopts, longopts, &longindex);

        if (c == -1)
            break;
        else if (c == ':')
            die("option '%s' requires an argument", argv[this_optind]);
        else if (c == '?')
            die("unrecognised option '%s'\\nTry '%s --help' for more information.", argv[this_optind], program_name);
'''),
    Code(short_options_code),
    Code(long_options_code),
    Code('}'),
    '''
    // Give the remaining command-line arguments to the VM
    mit_argc = argc - optind;
    mit_argv = &argv[optind];
    const char *object_file = mit_argv[0];

    // If invoked without an object file, give usage message and exit with error
    if (mit_argc < 1) {
        usage();
        exit(EXIT_FAILURE);
    }

    // Set up VM
    mit_word_t *memory = calloc(memory_words, sizeof(mit_word_t));
    if (memory == NULL)
        die("could not allocate virtual machine memory");
    mit_word_t *stack = calloc(mit_stack_words, sizeof(mit_word_t));
    if (stack == NULL)
        die("could not allocate virtual machine stack");
    mit_uword_t stack_depth = 0;

    // Load object file and report any error
    {
        FILE *fp = fopen(object_file, "rb");
        if (fp == NULL)
            die("cannot not open file '%s'", object_file);
        int ret = skip_hashbang(fp);
        if (ret == -1)
            die("error reading file '%s'", object_file);
        off_t len = fleno(fp);
        if (len == -1)
            die("error finding size of file '%s'", object_file);
        if ((size_t)len > memory_words * sizeof(mit_word_t))
            die("file '%s' is too big to fit in memory", object_file);
        if ((off_t)fread(memory, 1, len, fp) != len)
            die("error reading file '%s'", object_file);
        if (fclose(fp) == EOF)
            die("error closing file '%s'", object_file);
    }

    // Run
    mit_word_t error = mit_run(memory, 0, stack, mit_stack_words, &stack_depth);
    if (error < 0 && error >= -127) {
        warn("error %zd: %s", error, error_to_msg(error));
        // Translate `error` into a process exit code
        error = 127 - error;
    }

    free(memory);
    return error;
}'''
))

print(code)
