#!/usr/bin/python3

# SPDX-License-Identifier: Apache-2.0
# Copyright 2019 Charles University

import sys
import os
import argparse
import logging

KERNEL_TEST_EXTRAS = {
    'basic/probe_memory': {
        'CFLAGS': [ '-DKERNEL_TEST_PROBE_MEMORY_MAINMEM_SIZE_KB={mainmem_size}']
    }
}

CONFIG_MK_FMT = '''

TARGET = {toolchain_target}
TOOLCHAIN_DIR = {toolchain_dir}

CC = $(TOOLCHAIN_DIR)/bin/$(TARGET)-gcc
LD = $(TOOLCHAIN_DIR)/bin/$(TARGET)-ld
OBJCOPY = $(TOOLCHAIN_DIR)/bin/$(TARGET)-objcopy
OBJDUMP = $(TOOLCHAIN_DIR)/bin/$(TARGET)-objdump
GDB = $(TOOLCHAIN_DIR)/bin/$(TARGET)-gdb
AR = $(TOOLCHAIN_DIR)/bin/$(TARGET)-ar

KERNEL_TEST_SOURCES = {kernel_test_sources}
KERNEL_EXTRA_CFLAGS = {kernel_extra_cflags}

SHARED_EXTRA_CFLAGS = {extra_cflags}

USERSPACE_TEST_SOURCES = {userspace_test_sources}

'''

BUILD_DIR_MAKEFILE_FMT = '''

SRC_BASE = {src_base}/

vpath %.S $(SRC_BASE)
vpath %.c $(SRC_BASE)
vpath %.h $(SRC_BASE)

include $(SRC_BASE)/Makefile

'''

MSIM_CONF_FMT = '''

# Generated by configure.py

# Single processor machine
add dcpu cpu0

# Setup main memory for kernel
add rwm mainmem 0
mainmem generic {ram_size}K
mainmem load "kernel/kernel.bin"

# Setup memory for the loader
# (R4000 starts at hardwired address 0x1FC00000).
add rom loadermem 0x1FC00000
loadermem generic 4K
loadermem load "kernel/loader.bin"

# Console printer
add dprinter printer 0x10000000

# Setup userspace application
add rom app 0x1FB00000
app generic 128K
app load "userspace/app.bin"

'''

MIPSEL_TARGETS = ['mipsel-linux-gnu', 'mipsel-unknown-linux-gnu']

def copy_to_file(inp_fd, out_name, out_mode):
    with open(out_name, out_mode) as out:
        while True:
            buffer = inp_fd.read(512)
            if buffer == '':
                break
            out.write(buffer)

def has_gcc_installed(prefix, target):
    gcc_path = os.path.join(prefix, 'bin', '{}-gcc'.format(target))
    return os.path.exists(gcc_path)

def main(argv):
    args = argparse.ArgumentParser(description='Configure the build')
    args.add_argument('--verbose',
        default=False,
        dest='verbose',
        action='store_true',
        help='Be verbose.'
    )
    args.add_argument('-f', '--force',
        default=False,
        dest='force',
        action='store_true',
        help='Overwrite existing files.'
    )
    args.add_argument('--toolchain',
        default=None,
        dest='toolchain_dir',
        help='Toolchain directory.'
    )
    args.add_argument('--target',
        default=None,
        dest='toolchain_target',
        help='Toolchain target triplet.'
    )
    args.add_argument('--debug',
        default=False,
        dest='debug',
        action='store_true',
        help='Build kernel in debug mode.'
    )
    args.add_argument('-D',
        default=[],
        dest='defines',
        action='append',
        help='Extra defines'
    )
    args.add_argument('--memory-size',
        default=None,
        dest='memory_size',
        type=int,
        help='Base memory size (msim.conf) in KB'
    )
    tests = args.add_mutually_exclusive_group(required=False)
    tests.add_argument('--kernel-test',
        default=None,
        dest='kernel_test',
        help='Kernel test (name).'
    )
    tests.add_argument('--userspace-test',
        default=None,
        dest='userspace_test',
        help='Userspace test (name).'
    )
    config = args.parse_args(argv[1:])
    
    if config.verbose:
        config.logging_level = logging.DEBUG
    else:
        config.logging_level = logging.INFO
    
    logging.basicConfig(format='[%(asctime)s %(name)-6s %(levelname)7s] %(message)s', level=config.logging_level)
    logger = logging.getLogger('main')
    
    # Get basic paths
    src_base = os.path.realpath(os.path.dirname(argv[0]))
    cwd = os.path.realpath(os.getcwd())
    is_out_of_tree_build = src_base != cwd

    # Cannot set memory size in in-tree build
    # (we do not want to modify msim.conf in place).
    if (config.memory_size is not None) and (not is_out_of_tree_build):
        logger.critical('Cannot change memory size when building in source tree.')
        sys.exit(1)
    if config.memory_size is None:
        config.memory_size = 1024

    # Detect toolchain
    if config.toolchain_dir is None:
        for tc_dir in ['/opt/mff-nswi004/', os.path.expanduser("~/.local/"), '/usr/']:
            if config.toolchain_target is None:
                tc_possible_targets = MIPSEL_TARGETS
            else:
                tc_possible_targets = [config.toolchain_target]
            for tc_target in tc_possible_targets:
                if has_gcc_installed(tc_dir, tc_target):
                    config.toolchain_dir = tc_dir
                    if config.toolchain_target is None:
                        config.toolchain_target = tc_target
    else:
        config.toolchain_dir = os.path.realpath(config.toolchain_dir)
        if config.toolchain_target is None:
            for tc_target in MIPSEL_TARGETS:
                if has_gcc_installed(config.toolchain_dir, tc_target):
                    config.toolchain_target = tc_target

    if config.toolchain_dir is None:
        logger.critical("Failed to locate cross-compiler toolchain.")
        logger.critical("Have you installed the toolchain and/or have you specified --toolchain?")
        sys.exit(1)
    if config.toolchain_target is None:
        logger.critical("Failed to determine cross-compiler target.")
        logger.critical("Have you installed the toolchain and/or have you specified --target?")
        sys.exit(1)

    if not has_gcc_installed(config.toolchain_dir, config.toolchain_target):
        logger.critical("GCC for {} not found in {}.".format(config.toolchain_target, config.toolchain_dir))
        logger.critical("Have you installed the toolchain?")
        sys.exit(1)

    # Always create config.mk
    logger.debug('Creating config.mk')
    with open('config.mk', 'w') as f:
        kernel_test_sources = ''
        kernel_extra_cflags = []
        userspace_test_sources = ''
        if config.debug:
            kernel_extra_cflags.append('-DKERNEL_DEBUG')
        if not (config.kernel_test is None):
            kernel_test_sources = 'tests/{}/test.c'.format(config.kernel_test)
            kernel_extra_cflags.append('-DKERNEL_TEST')
            if config.kernel_test in KERNEL_TEST_EXTRAS:
                test_extras = KERNEL_TEST_EXTRAS[config.kernel_test]
                if 'CFLAGS' in test_extras:
                    for i in test_extras['CFLAGS']:
                        kernel_extra_cflags.append(i.format(
                            mainmem_size=config.memory_size
                        ))
        if not (config.userspace_test is None):
            userspace_test_sources = 'tests/{}/test.c'.format(config.userspace_test)
            kernel_extra_cflags.append('-DUSERSPACE_TEST')
        extra_cflags = " ".join("-D{}".format(i) for i in config.defines)
        f.write(CONFIG_MK_FMT.format(
            toolchain_dir=config.toolchain_dir,
            toolchain_target=config.toolchain_target,
            kernel_test_sources=kernel_test_sources,
            kernel_extra_cflags=' '.join(kernel_extra_cflags),
            userspace_test_sources=userspace_test_sources,
            extra_cflags=extra_cflags,
        ))

    # Create rest of files only when building in different directory
    if is_out_of_tree_build:
        # Create Makefiles
        for sub in ['.', 'kernel', 'userspace', 'userspace/libc']:
            logger.debug('Creating Makefile inside {}/'.format(sub))
            os.makedirs(sub, exist_ok=True)
            with open(os.path.join(cwd, sub, 'Makefile'), 'w') as f:
                f.write(BUILD_DIR_MAKEFILE_FMT.format(
                    src_base=os.path.join(src_base, sub)
                ))

        # Do not overwrite these
        for f in ['gdbinit']:
            logger.debug('Copying {}'.format(f))
            with open(os.path.join(src_base, f), 'r') as inp:
                try:
                    copy_to_file(inp, f, 'x')
                except FileExistsError:
                    if config.force:
                        logger.warning('Overwriting {}'.format(f))
                        copy_to_file(inp, f, 'w')
                    else:
                        logger.error('Refusing to overwrite {}'.format(f))

        # Write msim.conf
        msim_conf_data = MSIM_CONF_FMT.format(
            ram_size=config.memory_size
        )
        msim_conf_path = os.path.join(cwd, 'msim.conf')
        try:
            with open(msim_conf_path, 'x') as f:
                f.write(msim_conf_data)
        except FileExistsError:
            if config.force:
                logger.warning('Overwriting msim.conf')
                with open(msim_conf_path, 'w') as f:
                    f.write(msim_conf_data)
            else:
                logger.error('Refusing to overwrite msim.conf')

if __name__ == "__main__":
    main(sys.argv)
