#!/usr/bin/env python3
# Copyright (C) 1996-2022 Free Software Foundation, Inc.
#
# This file is part of the GNU simulators.
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

"""Helper to generate nltvals.def.

nltvals.def is a file that describes various newlib/libgloss target values used
by the host/target interface.  This needs to be rerun whenever the newlib source
changes.  Developers manually run it.

If the path to newlib is not specified, it will be searched for in:
- the root of this source tree
- alongside this source tree
"""

import argparse
from pathlib import Path
import re
import subprocess
import sys
from typing import Iterable, List, TextIO


PROG = Path(__file__).name

# Unfortunately, many newlib/libgloss ports have seen fit to define their own
# syscall.h file.  This means that system call numbers can vary for each port.
# Support for all this crud is kept here, rather than trying to get too fancy.
# If you want to try to improve this, please do, but don't break anything.
#
# If a target isn't listed here, it gets the standard syscall.h file (see
# libgloss/syscall.h) which hopefully new targets will use.
#
# NB: New ports should use libgloss, not newlib.
TARGET_DIRS = {
    'cr16': 'libgloss/cr16/sys',
    'd10v': 'newlib/libc/sys/d10v/sys',
    'i960': 'libgloss/i960',
    'mcore': 'libgloss/mcore',
    'riscv': 'libgloss/riscv/machine',
    'sh': 'newlib/libc/sys/sh/sys',
    'v850': 'libgloss/v850/sys',
}


# The header for the generated def file.
FILE_HEADER = f"""\
/* Newlib/libgloss macro values needed by remote target support.  */
/* This file is machine generated by {PROG}.  */\
"""


def gentvals(output: TextIO, cpp: str, srctype: str, srcdir: Path,
             headers: Iterable[str],
             pattern: str,
             filter: str = r'^$',
             target: str = None):
    """Extract constants from the specified files using a regular expression.

    We'll run things through the preprocessor.
    """
    headers = tuple(headers)

    # Require all files exist in order to regenerate properly.
    for header in headers:
        fullpath = srcdir / header
        assert fullpath.exists(), f'{fullpath} does not exist'

    if target is not None:
        print(f'#ifdef NL_TARGET_{target}', file=output)
    print(f'#ifdef {srctype}_defs', file=output)

    print('\n'.join(f'/* from {x} */' for x in headers), file=output)

    if target is None:
        print(f'/* begin {srctype} target macros */', file=output)
    else:
        print(f'/* begin {target} {srctype} target macros */', file=output)

    # Extract all the symbols.
    srcfile = ''.join(f'#include <{x}>\n' for x in headers)
    syms = set()
    define_pattern = re.compile(r'^#\s*define\s+(' + pattern + ')')
    filter_pattern = re.compile(filter)
    for header in headers:
        with open(srcdir / header, 'r', encoding='utf-8') as fp:
            data = fp.read()
        for line in data.splitlines():
            m = define_pattern.match(line)
            if m and not filter_pattern.search(line):
                syms.add(m.group(1))
    for sym in sorted(syms):
        srcfile += f'#ifdef {sym}\nDEFVAL {{ "{sym}", {sym} }},\n#endif\n'

    result = subprocess.run(
        f'{cpp} -E -I"{srcdir}" -', shell=True, check=True, encoding='utf-8',
        input=srcfile, capture_output=True)
    for line in result.stdout.splitlines():
        if line.startswith('DEFVAL '):
            print(line[6:].rstrip(), file=output)

    print(f'#undef {srctype}_defs', file=output)
    if target is None:
        print(f'/* end {srctype} target macros */', file=output)
    else:
        print(f'/* end {target} {srctype} target macros */', file=output)
        print('#endif', file=output)
    print('#endif', file=output)


def gen_common(output: TextIO, newlib: Path, cpp: str):
    """Generate the common C library constants.

    No arch should override these.
    """
    gentvals(output, cpp, 'errno', newlib / 'newlib/libc/include',
             ('errno.h', 'sys/errno.h'), 'E[A-Z0-9]*')

    gentvals(output, cpp, 'signal', newlib / 'newlib/libc/include',
             ('signal.h', 'sys/signal.h'), r'SIG[A-Z0-9]*', filter=r'SIGSTKSZ')

    gentvals(output, cpp, 'open', newlib / 'newlib/libc/include',
             ('fcntl.h', 'sys/fcntl.h', 'sys/_default_fcntl.h'), r'O_[A-Z0-9]*')


def gen_targets(output: TextIO, newlib: Path, cpp: str):
    """Generate the target-specific lists."""
    for target, subdir in sorted(TARGET_DIRS.items()):
        gentvals(output, cpp, 'sys', newlib / subdir, ('syscall.h',),
                 r'SYS_[_a-zA-Z0-9]*', target=target)

    # Then output the common syscall targets.
    gentvals(output, cpp, 'sys', newlib / 'libgloss', ('syscall.h',),
             r'SYS_[_a-zA-Z0-9]*')


def gen(output: TextIO, newlib: Path, cpp: str):
    """Generate all the things!"""
    print(FILE_HEADER, file=output)
    gen_common(output, newlib, cpp)
    gen_targets(output, newlib, cpp)


def get_parser() -> argparse.ArgumentParser:
    """Get CLI parser."""
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        '-o', '--output', type=Path,
        help='write to the specified file instead of stdout')
    parser.add_argument(
        '--cpp', type=str, default='cpp',
        help='the preprocessor to use')
    parser.add_argument(
        '--srcroot', type=Path,
        help='the root of this source tree')
    parser.add_argument(
        'newlib', nargs='?', type=Path,
        help='path to the newlib+libgloss source tree')
    return parser


def parse_args(argv: List[str]) -> argparse.Namespace:
    """Process the command line & default options."""
    parser = get_parser()
    opts = parser.parse_args(argv)

    if opts.srcroot is None:
        opts.srcroot = Path(__file__).resolve().parent.parent.parent

    if opts.newlib is None:
        # Try to find newlib relative to our source tree.
        if (opts.srcroot / 'newlib').is_dir():
            # If newlib is manually in the same source tree, use it.
            if (opts.srcroot / 'libgloss').is_dir():
                opts.newlib = opts.srcroot
            else:
                opts.newlib = opts.srcroot / 'newlib'
        elif (opts.srcroot.parent / 'newlib').is_dir():
            # Or see if it's alongside the gdb/binutils repo.
            opts.newlib = opts.srcroot.parent / 'newlib'
    if opts.newlib is None or not opts.newlib.is_dir():
        parser.error('unable to find newlib')

    return opts


def main(argv: List[str]) -> int:
    """The main entry point for scripts."""
    opts = parse_args(argv)

    if opts.output is not None:
        output = open(opts.output, 'w', encoding='utf-8')
    else:
        output = sys.stdout

    gen(output, opts.newlib, opts.cpp)
    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
