#!/usr/bin/env python
#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
#

#
# header_wrappers.py: Generates SWIG proxy wrappers around Subversion
#                     header files
#

import os, re, sys, glob, shutil, tempfile
if __name__ == "__main__":
  parent_dir = os.path.dirname(os.path.abspath(os.path.dirname(sys.argv[0])))
  sys.path[0:0] = [ parent_dir, os.path.dirname(parent_dir) ]
from gen_base import unique, native_path, build_path_basename, build_path_join
import generator.swig

class Generator(generator.swig.Generator):
  """Generate SWIG proxy wrappers around Subversion header files"""

  def __init__(self, conf, swig_path):
    """Initialize Generator object"""
    generator.swig.Generator.__init__(self, conf, swig_path)

    # Build list of header files
    self.header_files = list(map(native_path, self.includes))
    self.header_basenames = list(map(os.path.basename, self.header_files))

  # Ignore svn_repos_parse_fns_t because SWIG can't parse it
  _ignores = ["svn_repos_parse_fns_t",
              "svn_auth_gnome_keyring_unlock_prompt_func_t",
              ]

  def write_makefile_rules(self, makefile):
    """Write makefile rules for generating SWIG wrappers for Subversion
    header files."""
    wrapper_fnames = []
    python_script = '$(abs_srcdir)/build/generator/swig/header_wrappers.py'
    makefile.write('GEN_SWIG_WRAPPER = cd $(top_srcdir) && $(PYTHON)' +
                   ' %s build.conf $(SWIG)\n\n'  % python_script)
    for fname in self.includes:
      wrapper_fname = build_path_join(self.proxy_dir,
        self.proxy_filename(build_path_basename(fname)))
      wrapper_fnames.append(wrapper_fname)
      makefile.write(
        '%s: %s %s\n' % (wrapper_fname, fname, python_script) +
        '\t$(GEN_SWIG_WRAPPER) %s\n\n' % fname
      )
    makefile.write('SWIG_WRAPPERS = %s\n\n' % ' '.join(wrapper_fnames))
    for short_name in self.short.values():
      # swig-pl needs the '.swig_checked' target here; swig-rb and swig-py
      # already reach it via a different dependency chain:
      #
      #    In build-outputs.mk, swig-py and swig-rb targets depend on *.la
      #    targets, which depend on *.lo targets, which depend on *.c targets,
      #    which depend on .swig_checked target.
      makefile.write('autogen-swig-%s: .swig_checked $(SWIG_WRAPPERS)\n' % short_name)
    makefile.write('\n\n')

  def proxy_filename(self, include_filename):
    """Convert a .h filename into a _h.swg filename"""
    return include_filename.replace(".h","_h.swg")

  def _write_nodefault_calls(self, structs):
    """Write proxy definitions to a SWIG interface file"""
    self.ofile.write("\n/* No default constructors for opaque structs */\n")
    self.ofile.write('#ifdef SWIGPYTHON\n');
    for structName, structDefinition in structs:
      if not structDefinition:
        self.ofile.write('%%nodefault %s;\n' % structName)
    self.ofile.write('#endif\n');

  def _write_includes(self, includes, base_fname):
    """Write includes to a SWIG interface file"""

    self.ofile.write('\n/* Includes */\n')
    self.ofile.write('%%{\n#include "%s"\n%%}\n' % base_fname)
    if base_fname not in self._ignores:
      self.ofile.write('%%include %s\n' % base_fname)


  def _write_callback(self, type, return_type, module, function, params,
                      callee):
    """Write out an individual callback"""

    # Get rid of any extra spaces or newlines
    return_type = ' '.join(return_type.split())
    params = ' '.join(params.split())

    # Calculate parameters
    if params == "void":
      param_names = ""
      params = "%s _obj" % type
    else:
      param_names = ", ".join(self._re_param_names.findall(params))
      params = "%s _obj, %s" % (type, params)

    invoke_callback = "%s(%s)" % (callee, param_names)
    if return_type != "void":
      invoke_callback = "return %s" % (invoke_callback)

    # Write out the declaration
    self.ofile.write(
      "static %s %s_invoke_%s(\n" % (return_type, module, function) +
      "  %s) {\n" % params +
      "  %s;\n" % invoke_callback +
      "}\n\n")


  def _write_callback_typemaps(self, callbacks):
    """Apply the CALLABLE_CALLBACK typemap to all callbacks"""

    self.ofile.write('\n/* Callback typemaps */\n')
    types = [];
    for match in callbacks:
      if match[0] and match[1]:
        # Callbacks declared as a typedef
        return_type, module, function, params = match
        type = "%s_%s_t" % (module, function)
        types.append(type)

    if types:
      self.ofile.write(
        "#ifdef SWIGPYTHON\n"
        "%%apply CALLABLE_CALLBACK {\n"
        "  %s\n"
        "};\n"
        "%%apply CALLABLE_CALLBACK * {\n"
        "  %s *\n"
        "};\n"
        "#endif\n" % ( ",\n  ".join(types), " *,\n  ".join(types) )
      );


  def _write_baton_typemaps(self, batons):
    """Apply the PY_AS_VOID typemap to all batons"""

    self.ofile.write('\n/* Baton typemaps */\n')

    if batons:
      self.ofile.write(
        "#ifdef SWIGPYTHON\n"
        "%%apply void *PY_AS_VOID {\n"
        "  void *%s\n"
        "};\n"
        "#endif\n" % ( ",\n  void *".join(batons) )
      )


  def _write_callbacks(self, callbacks):
    """Write invoker functions for callbacks"""
    self.ofile.write('\n/* Callbacks */\n')
    self.ofile.write("\n%inline %{\n")

    struct = None
    for match in callbacks:
      if match[0] and not match[1]:
        # Struct definitions
        struct = match[0]
      elif not match[0] and struct not in self._ignores:
        # Struct member callbacks
        return_type, name, params = match[1:]
        type = "%s *" % struct

        self._write_callback(type, return_type, struct[:-2], name, params,
                             "(_obj->%s)" % name)
      elif match[0] and match[1]:
        # Callbacks declared as a typedef
        return_type, module, function, params = match
        type = "%s_%s_t" % (module, function)

        if type not in self._ignores:
          self._write_callback(type, return_type, module, function, params,
                               "_obj")

    self.ofile.write("%}\n")

    self.ofile.write("\n#ifdef SWIGPYTHON\n")
    for match in callbacks:

      if match[0] and not match[1]:
        # Struct definitions
        struct = match[0]
      elif not match[0] and struct not in self._ignores:
        # Using funcptr_member_proxy, add proxy methods to anonymous
        # struct member callbacks, so that they can be invoked directly.
        return_type, name, params = match[1:]
        self.ofile.write('%%funcptr_member_proxy(%s, %s, %s_invoke_%s);\n'
          % (struct, name, struct[:-2], name))
      elif match[0] and match[1]:
        # Using funcptr_proxy, create wrapper objects for each typedef'd
        # callback, so that they can be invoked directly. The
        # CALLABLE_CALLBACK typemap (used in _write_callback_typemaps)
        # ensures that these wrapper objects are actually used.
        return_type, module, function, params = match
        self.ofile.write('%%funcptr_proxy(%s_%s_t, %s_invoke_%s);\n'
          % (module, function, module, function))
    self.ofile.write("\n#endif\n")

  def _write_proxy_definitions(self, structs):
    """Write proxy definitions to a SWIG interface file"""
    self.ofile.write('\n/* Structure definitions */\n')
    self.ofile.write('#ifdef SWIGPYTHON\n');
    for structName, structDefinition in structs:
      if structDefinition:
        self.ofile.write('%%proxy(%s);\n' % structName)
      else:
        self.ofile.write('%%opaque_proxy(%s);\n' % structName)
    self.ofile.write('#endif\n');

  """Regular expression for parsing includes from a C header file"""
  _re_includes = re.compile(r'#\s*include\s*[<"]([^<">;\s]+)')

  """Regular expression for parsing structs from a C header file"""
  _re_structs = re.compile(r'\btypedef\s+(?:struct|union)\s+'
                           r'(svn_[a-z_0-9]+)\b\s*(\{?)')

  """Regular expression for parsing callbacks declared inside structs
     from a C header file"""
  _re_struct_callbacks = re.compile(r'\btypedef\s+(?:struct|union)\s+'
                                    r'(svn_[a-z_0-9]+)\b|'
                                    r'\n[ \t]+((?!typedef)[a-z_0-9\s*]+)'
                                    r'\(\*(\w+)\)'
                                    r'\s*\(([^)]+)\);')


  """Regular expression for parsing callbacks declared as a typedef
     from a C header file"""
  _re_typed_callbacks = re.compile(r'typedef\s+([a-z_0-9\s*]+)'
                                   r'\(\*(svn_[a-z]+)_([a-z_0-9]+)_t\)\s*'
                                   r'\(([^)]+)\);');

  """Regular expression for parsing batons"""
  _re_batons = re.compile(r'void\s*\*\s*(\w*baton\w*)');

  """Regular expression for parsing parameter names from a parameter list"""
  _re_param_names = re.compile(r'\b(\w+)\s*\)*\s*(?:,|$)')

  """Regular expression for parsing comments"""
  _re_comments = re.compile(r'/\*.*?\*/')

  def _write_swig_interface_file(self, base_fname, batons, includes, structs,
                                 callbacks):
    """Convert a header file into a SWIG header file"""

    # Calculate output filename from base filename
    output_fname = os.path.join(self.proxy_dir,
      self.proxy_filename(base_fname))

    # Open a temporary output file
    if sys.version_info[0] >= 3:
      self.ofile = tempfile.TemporaryFile(dir=self.proxy_dir,
                                          mode="w+",
                                          encoding="utf8")
    else:
      self.ofile = tempfile.TemporaryFile(dir=self.proxy_dir)

    self.ofile.write('/* Proxy classes for %s\n' % base_fname)
    self.ofile.write(' * DO NOT EDIT -- AUTOMATICALLY GENERATED\n')
    self.ofile.write(' * BY build/generator/swig/header_wrappers.py */\n')

    # Write list of structs for which we shouldn't define constructors
    # by default
    self._write_nodefault_calls(structs)

    # Write typemaps for the callbacks
    self._write_callback_typemaps(callbacks)

    # Write typemaps for the batons
    self._write_baton_typemaps(batons)

    # Write includes into the SWIG interface file
    self._write_includes(includes, base_fname)

    # Write proxy definitions into the SWIG interface file
    self._write_proxy_definitions(structs)

    # Write callback definitions into the SWIG interface file
    self._write_callbacks(callbacks)

    # Copy the temporary file over to the result file.
    # Ideally we'd simply rename the temporary file to output_fname,
    # but NamedTemporaryFile() only supports its 'delete' parameter
    # in python 2.6 and above, and renaming the file while it's opened
    # exclusively is probably not a good idea.
    outputfile = open(output_fname, 'w')
    self.ofile.seek(0)
    shutil.copyfileobj(self.ofile, outputfile)

    # Close our temporary file.
    # It will also be deleted automatically.
    self.ofile.close()

    # Close our output file, too.
    outputfile.close()

  def process_header_file(self, fname):
    """Generate a wrapper around a header file"""

    # Read the contents of the header file
    contents = open(fname).read()

    # Remove comments
    contents = self._re_comments.sub("", contents)

    # Get list of includes
    includes = unique(self._re_includes.findall(contents))

    # Get list of structs
    structs = unique(self._re_structs.findall(contents))

    # Get list of batons
    batons = unique(self._re_batons.findall(contents))

    # Get list of callbacks
    callbacks = (self._re_struct_callbacks.findall(contents) +
                 self._re_typed_callbacks.findall(contents))

    # Get the location of the output file
    base_fname = os.path.basename(fname)

    # Write the SWIG interface file
    self._write_swig_interface_file(base_fname, batons, includes, structs,
                                    callbacks)

  def write(self):
    """Generate wrappers for all header files"""

    for fname in self.header_files:
      self.process_header_file(fname)

if __name__ == "__main__":
  if len(sys.argv) < 3:
    print("""Usage: %s build.conf swig [ subversion/include/header_file.h ]
Generates SWIG proxy wrappers around Subversion header files. If no header
files are specified, generate wrappers for subversion/include/*.h. """ % \
    os.path.basename(sys.argv[0]))
  else:
    gen = Generator(sys.argv[1], sys.argv[2])
    if len(sys.argv) > 3:
      for fname in sys.argv[3:]:
        gen.process_header_file(fname)
    else:
      gen.write()
