#!/usr/bin/env python

import sys
import fileinput
from cparser import *

# cache for "is this type a function?"
global FUNCTION_CACHE
FUNCTION_CACHE = {}
# statistics useful for sanity checking
global STATS
STATS = {}


def _has_function(seen, *args):
  """ Check if any member of `args` is a function, or will lead to a function
      For example, a typedef, a use or a pointer with many indirections
  """

  for arg in args:
    # Check if this argument has already been processed
    # if yes, exit to avoid infinite recursion
    if arg in seen:
      return False

    # we have now seen this argument, to prevent infinite recursion
    seen.add(arg)

    # If we find a pointer, check if it points to a function
    if isinstance(arg, CTypePointer):
      if _has_function(seen, arg.ctype):
        return True
    elif isinstance(arg, CTypeArray):
      # is it an array of functions (is that even a thing?)
      if _has_function(seen, arg.ctype):
        return True
    elif isinstance(arg, CTypeFunction):
      # its a function
      return True
    elif isinstance(arg, CTypeDefinition):
      # Maybe a typedef of function?
      if _has_function(seen, arg.ctype):
        return True
    elif isinstance(arg, CTypeUse):
      # Maybe a use of a function?
      if _has_function(seen, arg.ctype):
        return True
    elif isinstance(arg, CTypeStruct):
      # Does the struct have a member that may have a function?
      if _has_function(seen, *list(arg.fields())):
        return True
    elif isinstance(arg, CTypeUnion):
      # does the union have a member that may be a function?
      if _has_function(seen, *list(arg.fields())):
        return True
    elif isinstance(arg, tuple):
      if _has_function(seen, *arg):
        return True
    else:
      continue

  return False

def has_function(*args):

  # caching wraper for _has_function
  global FUNCTION_CACHE
  has_func = False
  # check if any of the arguments can lead to a function
  # if yes, then this call may have a function argument
  for arg in args:
    if arg not in FUNCTION_CACHE:
      FUNCTION_CACHE[arg] = _has_function(set(), arg)
    has_func = has_func or FUNCTION_CACHE[arg]
    if has_func:
      # no need to look through all arguments once we find a function
      break

  return has_func

def print_header():
  header = """
  // mcsema ABI library, automatically generated by cparser/make_abi-library.py
  // see: https://github.com/pgoodman/cparser/

__attribute__((used))
void *__mcsema_externs[] = {
"""
  sys.stdout.write(header)

def print_footer():
  global STATS

  footer = """
};
// end of mcsema ABI library
// automatically generated by cparser/make_abi-library.py
// see: https://github.com/pgoodman/cparser/
"""
  sys.stdout.write(footer)
  sys.stdout.write("// Total functions: {s[total]}\n".format(s=STATS))
  sys.stdout.write("// Total emitted in __mcsema_externs : {s[emitted]}\n".format(s=STATS))
  sys.stdout.write("// Excluded variadic functions: {s[variadic]}\n".format(s=STATS))
  sys.stdout.write("// Excluded due to callbacks: {s[callback]}\n".format(s=STATS))

def print_entry(fn):
  # emit C-style cast to be compatible with C
  sys.stdout.write("  (void *)({}),\n".format(fn))

if "__main__" == __name__:
  source_lines = []
  for line in fileinput.input():
    if not line.startswith("#"):
      source_lines.append(line)

  STATS['total'] = 0
  STATS['variadic'] = 0
  STATS['callback'] = 0
  STATS['emitted'] = 0

  units = CParser().parse_units(CTokenizer(source_lines))

  # ABI library header
  print_header()

  for decls, _, _ in units:
    for ctype, name in decls:
      ctype = ctype.unattributed_type()
      if isinstance(ctype, CTypeFunction):

        STATS['total'] += 1

        if ctype.is_variadic:
          # Skip variadic functions, it is not efficient to use --abi-library on them
          # and only stack-based arguments are properly handled
          sys.stdout.write("  // skipping because varargs function: {}\n".format(name))
          STATS['variadic'] += 1
        elif has_function(None, *ctype.param_types):
          # skip anything which may call back into lifted code, because the stack will be wrong
          sys.stdout.write("  // skipping because function pointer args: {}\n".format(name))
          STATS['callback'] += 1
        else:
          # normal functions are fine to call directly as library calls.
          STATS['emitted'] += 1
          args = ", ".join(map(repr, ctype.param_types))
          comment_decl = "  //{rt} {name}({args});\n".format(
                  rt=ctype.ret_type, name=name, args=args)
          sys.stdout.write(comment_decl)

          # write the ABI library entry
          print_entry(name)

  # ABI library footer
  print_footer()
