"""Generate C wrapper functions.
"""

# Copyright The Mbed TLS Contributors
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later

### WARNING: the code in this file has not been extensively reviewed yet.
### We do not think it is harmful, but it may be below our normal standards
### for robustness and maintainability.

import os
import re
import sys
from typing import Dict, NamedTuple, List, Optional, Tuple

from .c_parsing_helper import ArgumentInfo, FunctionInfo
from . import typing_util


def c_declare(prefix: str, name: str, suffix: str) -> str:
    """Format a declaration of name with the given type prefix and suffix."""
    if not prefix.endswith('*'):
        prefix += ' '
    return prefix + name + suffix


WrapperInfo = NamedTuple('WrapperInfo', [
    ('argument_names', List[str]),
    ('guard', Optional[str]),
    ('wrapper_name', str),
])

def strip_indentation(in_str: str, new_lines: int = 1, indent_lv: int = 0) -> str:
    """Return a whitespace stripped str, with configurable whitespace in output.

    The method will remove space-character indentation from input string.
    It will also remove all new-lines around the text-block as well as
    trailing whitespace.
    The output indentation can be configured by indent_lv, and will use blocks
    of 4 spaces.
    At the end of the string a `new_lines` amount of empty lines will be added.
    """

    _ret_string = in_str.lstrip('\n').rstrip()
    # Count empty spaces in beggining of each line. The smallest non-zero entry
    # will be used to clean up input indentation.
    indents = [len(n)-1 for n in re.findall(r'(?m)^ +\S', in_str)]

    if indents:
        _ret_string = re.sub(r'(?m)^ {{{indent}}}'.format(indent=min(indents)),
                             '', _ret_string)
    if indent_lv:
        _ret_string = '\n'.join([' ' * indent_lv * 4 + s
                                 for s in _ret_string.splitlines()])
    return _ret_string + ('\n' * (new_lines + 1))

class Base:
    """Generate a C source file containing wrapper functions."""

    # This class is designed to have many methods potentially overloaded.
    # Tell pylint not to complain about methods that have unused arguments:
    # child classes are likely to override those methods and need the
    # arguments in question.
    #pylint: disable=no-self-use,unused-argument

    # Prefix prepended to the function's name to form the wrapper name.
    _WRAPPER_NAME_PREFIX = ''
    # Suffix appended to the function's name to form the wrapper name.
    _WRAPPER_NAME_SUFFIX = '_wrap'

    _INCLUDES = ['<mbedtls/build_info.h>']

    # Functions with one of these qualifiers are skipped.
    _SKIP_FUNCTION_WITH_QUALIFIERS = frozenset(['inline', 'static'])

    def __init__(self):
        """Construct a wrapper generator object.
        """
        self.program_name = os.path.basename(sys.argv[0])
        # To be populated in a derived class
        self.functions = {} #type: Dict[str, FunctionInfo]
        self._function_guards = {} #type: Dict[str, str]
        # Preprocessor symbol used as a guard against multiple inclusion in the
        # header. Must be set before writing output to a header.
        # Not used when writing .c output.
        self.header_guard = None #type: Optional[str]

    def _write_prologue(self, out: typing_util.Writable, header: bool) -> None:
        """Write the prologue of a C file.

        This includes a description comment and some include directives.
        """
        prologue = strip_indentation(f'''
            /* Automatically generated by {self.program_name}, do not edit! */

            /* Copyright The Mbed TLS Contributors
             * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
             */

        ''')

        if header:
            prologue += strip_indentation(f'''
                #ifndef {self.header_guard}
                #define {self.header_guard}

                #ifdef __cplusplus
                extern "C" {{
                #endif

            ''')

        if not header:
            # On Mingw-w64, force the use of a C99-compliant printf() and friends.
            # This is necessary on older versions of Mingw and/or Windows runtimes
            # where snprintf does not always zero-terminate the buffer, and does
            # not support formats such as "%zu" for size_t and "%lld" for long long.
            prologue += strip_indentation(f'''
                #if !defined(__USE_MINGW_ANSI_STDIO)
                #define __USE_MINGW_ANSI_STDIO 1
                #endif
            ''')

        for include in self._INCLUDES:
            prologue += "#include {}\n".format(include)

        # Make certain there is an empty line at the end of this section.
        prologue += '\n' if self._INCLUDES else '\n\n'

        out.write(prologue)

    def _write_epilogue(self, out: typing_util.Writable, header: bool) -> None:
        """Write the epilogue of a C file."""
        epilogue = ""
        if header:
            epilogue += strip_indentation(f'''
                #ifdef __cplusplus
                }}
                #endif

                #endif /* {self.header_guard} */

            ''')

        epilogue += ('/* End of automatically generated file. */\n')
        out.write(epilogue)

    def _wrapper_function_name(self, original_name: str) -> str:
        """The name of the wrapper function.

        By default, this adds a suffix.
        """
        return (self._WRAPPER_NAME_PREFIX +
                original_name +
                self._WRAPPER_NAME_SUFFIX)

    def _wrapper_declaration_start(self,
                                   function: FunctionInfo,
                                   wrapper_name: str) -> str:
        """The beginning of the wrapper function declaration.

        This ends just before the opening parenthesis of the argument list.

        This is a string containing at least the return type and the
        function name. It may start with additional qualifiers or attributes
        such as `static`, `__attribute__((...))`, etc.
        """
        return c_declare(function.return_type, wrapper_name, '')

    def _argument_name(self,
                       function_name: str,
                       num: int,
                       arg: ArgumentInfo) -> str:
        """Name to use for the given argument in the wrapper function.

        Argument numbers count from 0.
        """
        name = 'arg' + str(num)
        if arg.name:
            name += '_' + arg.name
        return name

    def _wrapper_declaration_argument(self,
                                      function_name: str,
                                      num: int, name: str,
                                      arg: ArgumentInfo) -> str:
        """One argument definition in the wrapper function declaration.

        Argument numbers count from 0.
        """
        return c_declare(arg.type, name, arg.suffix)

    def _underlying_function_name(self, function: FunctionInfo) -> str:
        """The name of the underlying function.

        By default, this is the name of the wrapped function.
        """
        return function.name

    def _return_variable_name(self, function: FunctionInfo) -> str:
        """The name of the variable that will contain the return value."""
        return 'retval'

    def _write_function_call(self, out: typing_util.Writable,
                             function: FunctionInfo,
                             argument_names: List[str]) -> None:
        """Write the call to the underlying function.
        """
        # Note that the function name is in parentheses, to avoid calling
        # a function-like macro with the same name, since in typical usage
        # there is a function-like macro with the same name which is the
        # wrapper.
        call = '({})({})'.format(self._underlying_function_name(function),
                                 ', '.join(argument_names))
        if function.returns_void():
            out.write('    {};\n'.format(call))
        else:
            ret_name = self._return_variable_name(function)
            ret_decl = c_declare(function.return_type, ret_name, '')
            out.write('    {} = {};\n'.format(ret_decl, call))

    def _write_function_return(self, out: typing_util.Writable,
                               function: FunctionInfo,
                               if_void: bool = False) -> None:
        """Write a return statement.

        If the function returns void, only write a statement if if_void is true.
        """
        if function.returns_void():
            if if_void:
                out.write('    return;\n')
        else:
            ret_name = self._return_variable_name(function)
            out.write('    return {};\n'.format(ret_name))

    def _write_function_body(self, out: typing_util.Writable,
                             function: FunctionInfo,
                             argument_names: List[str]) -> None:
        """Write the body of the wrapper code for the specified function.
        """
        self._write_function_call(out, function, argument_names)
        self._write_function_return(out, function)

    def _skip_function(self, function: FunctionInfo) -> bool:
        """Whether to skip this function.

        By default, static or inline functions are skipped.
        """
        if not self._SKIP_FUNCTION_WITH_QUALIFIERS.isdisjoint(function.qualifiers):
            return True
        return False

    def _function_guard(self, function: FunctionInfo) -> Optional[str]:
        """A preprocessor condition for this function.

        The wrapper will be guarded with `#if` on this condition, if not None.
        """
        return self._function_guards.get(function.name)

    def _wrapper_info(self, function: FunctionInfo) -> Optional[WrapperInfo]:
        """Information about the wrapper for one function.

        Return None if the function should be skipped.
        """
        if self._skip_function(function):
            return None
        argument_names = [self._argument_name(function.name, num, arg)
                          for num, arg in enumerate(function.arguments)]
        return WrapperInfo(
            argument_names=argument_names,
            guard=self._function_guard(function),
            wrapper_name=self._wrapper_function_name(function.name),
        )

    def _write_function_prototype(self, out: typing_util.Writable,
                                  function: FunctionInfo,
                                  wrapper: WrapperInfo,
                                  header: bool) -> None:
        """Write the prototype of a wrapper function.

        If header is true, write a function declaration, with a semicolon at
        the end. Otherwise just write the prototype, intended to be followed
        by the function's body.
        """
        declaration_start = self._wrapper_declaration_start(function,
                                                            wrapper.wrapper_name)
        arg_indent = '    '
        terminator = ';\n' if header else '\n'
        if function.arguments:
            out.write(declaration_start + '(\n')
            for num in range(len(function.arguments)):
                arg_def = self._wrapper_declaration_argument(
                    function.name,
                    num, wrapper.argument_names[num], function.arguments[num])
                arg_terminator = \
                    (')' + terminator if num == len(function.arguments) - 1 else
                     ',\n')
                out.write(arg_indent + arg_def + arg_terminator)
        else:
            out.write(declaration_start + '(void)' + terminator)

    def _write_c_function(self, out: typing_util.Writable,
                          function: FunctionInfo) -> None:
        """Write wrapper code for one function.

        Do nothing if the function is skipped.
        """
        wrapper = self._wrapper_info(function)
        if wrapper is None:
            return
        out.write('/* Wrapper for {} */\n'.format(function.name))

        if wrapper.guard is not None:
            out.write('#if {}\n'.format(wrapper.guard))
        self._write_function_prototype(out, function, wrapper, False)
        out.write('{\n')
        self._write_function_body(out, function, wrapper.argument_names)
        out.write('}\n')
        if wrapper.guard is not None:
            out.write('#endif /* {} */\n'.format(wrapper.guard))
        out.write('\n')

    def _write_h_function_declaration(self, out: typing_util.Writable,
                                      function: FunctionInfo,
                                      wrapper: WrapperInfo) -> None:
        """Write the declaration of one wrapper function.
        """
        self._write_function_prototype(out, function, wrapper, True)

    def _write_h_macro_definition(self, out: typing_util.Writable,
                                  function: FunctionInfo,
                                  wrapper: WrapperInfo) -> None:
        """Write the macro definition for one wrapper.
        """
        arg_list = ', '.join(wrapper.argument_names)
        out.write('#define {function_name}({args}) \\\n    {wrapper_name}({args})\n'
                  .format(function_name=function.name,
                          wrapper_name=wrapper.wrapper_name,
                          args=arg_list))

    def _write_h_function(self, out: typing_util.Writable,
                          function: FunctionInfo) -> None:
        """Write the complete header content for one wrapper.

        This is the declaration of the wrapper function, and the
        definition of a function-like macro that calls the wrapper function.

        Do nothing if the function is skipped.
        """
        wrapper = self._wrapper_info(function)
        if wrapper is None:
            return
        if wrapper.guard is not None:
            out.write('#if {}\n'.format(wrapper.guard))
        self._write_h_function_declaration(out, function, wrapper)
        self._write_h_macro_definition(out, function, wrapper)
        if wrapper.guard is not None:
            out.write('#endif /* {} */\n'.format(wrapper.guard))
        out.write('\n')

    def write_c_file(self, filename: str) -> None:
        """Output a whole C file containing function wrapper definitions."""
        with open(filename, 'w', encoding='utf-8') as out:
            self._write_prologue(out, False)
            for name in sorted(self.functions):
                self._write_c_function(out, self.functions[name])
            self._write_epilogue(out, False)

    def _header_guard_from_file_name(self, filename: str) -> str:
        """Preprocessor symbol used as a guard against multiple inclusion."""
        # Heuristic to strip irrelevant leading directories
        filename = re.sub(r'.*include[\\/]', r'', filename)
        return re.sub(r'[^0-9A-Za-z]', r'_', filename, flags=re.A).upper()

    def write_h_file(self, filename: str) -> None:
        """Output a header file with function wrapper declarations and macro definitions."""
        self.header_guard = self._header_guard_from_file_name(filename)
        with open(filename, 'w', encoding='utf-8') as out:
            self._write_prologue(out, True)
            for name in sorted(self.functions):
                self._write_h_function(out, self.functions[name])
            self._write_epilogue(out, True)


class UnknownTypeForPrintf(Exception):
    """Exception raised when attempting to generate code that logs a value of an unknown type."""

    def __init__(self, typ: str) -> None:
        super().__init__("Unknown type for printf format generation: " + typ)


class Logging(Base):
    """Generate wrapper functions that log the inputs and outputs."""

    def __init__(self) -> None:
        """Construct a wrapper generator including logging of inputs and outputs.

        Log to stdout by default. Call `set_stream` to change this.
        """
        super().__init__()
        self.stream = 'stdout'

    def set_stream(self, stream: str) -> None:
        """Set the stdio stream to log to.

        Call this method before calling `write_c_output` or `write_h_output`.
        """
        self.stream = stream

    def _write_prologue(self, out: typing_util.Writable, header: bool) -> None:
        super()._write_prologue(out, header)
        if not header:
            out.write("""
#if defined(MBEDTLS_FS_IO) && defined(MBEDTLS_TEST_HOOKS)
#include <stdio.h>
#include <inttypes.h>
#include <mbedtls/platform.h> // for mbedtls_fprintf
#endif /* defined(MBEDTLS_FS_IO) && defined(MBEDTLS_TEST_HOOKS) */
""")

    _PRINTF_SIMPLE_FORMAT = {
        'int': '%d',
        'long': '%ld',
        'long long': '%lld',
        'size_t': '%zu',
        'unsigned': '0x%08x',
        'unsigned int': '0x%08x',
        'unsigned long': '0x%08lx',
        'unsigned long long': '0x%016llx',
    }

    def _printf_simple_format(self, typ: str) -> Optional[str]:
        """Use this printf format for a value of typ.

        Return None if values of typ need more complex handling.
        """
        return self._PRINTF_SIMPLE_FORMAT.get(typ)

    _PRINTF_TYPE_CAST = {
        'int32_t': 'int',
        'uint32_t': 'unsigned',
        'uint64_t': 'unsigned long long',
    } #type: Dict[str, str]

    def _printf_type_cast(self, typ: str) -> Optional[str]:
        """Cast values of typ to this type before passing them to printf.

        Return None if values of the given type do not need a cast.
        """
        return self._PRINTF_TYPE_CAST.get(typ)

    _POINTER_TYPE_RE = re.compile(r'\s*\*\Z')

    def _printf_parameters(self, typ: str, var: str) -> Tuple[str, List[str]]:
        """The printf format and arguments for a value of type typ stored in var.
        """
        expr = var
        base_type = typ
        # For outputs via a pointer, get the value that has been written.
        # Note: we don't support pointers to pointers here.
        pointer_match = self._POINTER_TYPE_RE.search(base_type)
        if pointer_match:
            base_type = base_type[:pointer_match.start(0)]
            expr = '*({})'.format(expr)
        # Maybe cast the value to a standard type.
        cast_to = self._printf_type_cast(base_type)
        if cast_to is not None:
            expr = '({}) {}'.format(cast_to, expr)
            base_type = cast_to
        # Try standard types.
        fmt = self._printf_simple_format(base_type)
        if fmt is not None:
            return '{}={}'.format(var, fmt), [expr]
        raise UnknownTypeForPrintf(typ)

    def _write_function_logging(self, out: typing_util.Writable,
                                function: FunctionInfo,
                                argument_names: List[str]) -> None:
        """Write code to log the function's inputs and outputs."""
        formats, values = '%s', ['"' + function.name + '"']
        for arg_info, arg_name in zip(function.arguments, argument_names):
            fmt, vals = self._printf_parameters(arg_info.type, arg_name)
            if fmt:
                formats += ' ' + fmt
                values += vals
        if not function.returns_void():
            ret_name = self._return_variable_name(function)
            fmt, vals = self._printf_parameters(function.return_type, ret_name)
            if fmt:
                formats += ' ' + fmt
                values += vals
        out.write("""\
#if defined(MBEDTLS_FS_IO) && defined(MBEDTLS_TEST_HOOKS)
    if ({stream}) {{
        mbedtls_fprintf({stream}, "{formats}\\n",
                        {values});
    }}
#endif /* defined(MBEDTLS_FS_IO) && defined(MBEDTLS_TEST_HOOKS) */
"""
                  .format(stream=self.stream,
                          formats=formats,
                          values=', '.join(values)))

    def _write_function_body(self, out: typing_util.Writable,
                             function: FunctionInfo,
                             argument_names: List[str]) -> None:
        """Write the body of the wrapper code for the specified function.
        """
        self._write_function_call(out, function, argument_names)
        self._write_function_logging(out, function, argument_names)
        self._write_function_return(out, function)
