/*
 *      Copyright (C) 2005-2013 Team XBMC
 *      http://xbmc.org
 *
 *  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 2, 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 XBMC; see the file COPYING.  If not, see
 *  <http://www.gnu.org/licenses/>.
 *
 */

import groovy.xml.XmlParser
import groovy.xml.XmlUtil
import org.apache.commons.text.StringEscapeUtils

import groovy.text.SimpleTemplateEngine
import java.util.regex.Pattern

/**
 * This class contains a series of helper methods for parsing a xbmc addon spec xml file. It is intended to be
 * used from a bindings template.
 *
 * @author jim
 *
 */
public class Helper
{
   private static List classes
   private static Map outTypemap = [:]
   private static def defaultOutTypeConversion = null
   private static Map inTypemap = [:]
   private static def defaultInTypeConversion = null
   private static def doxygenXmlDir = null
   public static String newline = System.getProperty("line.separator");
   public static File curTemplateFile = null;

   public static void setTemplateFile(File templateFile) { curTemplateFile = templateFile }

   /**
    * In order to use any of the typemap helper features, the Helper class needs to be initialized with
    * this information.
    * @param pclasses is the list of all class nodes from the module
    * @param poutTypemap is the typemap table for output return values to the scripting language
    * @param defaultOutTypemap is the default typemap to use when the type conversion is unknown
    * @param pinTypemap is the typemap table for input parameters from the scripting language
    * @param defaultInTypemap is the default typemap for the input parameters from the scripting language
    */
    public static void setup(def template,List pclasses, Map poutTypemap, def defaultOutTypemap,
                             Map pinTypemap, def defaultInTypemap)
    {
      setTemplateFile(template.binding.templateFile)
      classes = pclasses ? pclasses : []
      if (poutTypemap) outTypemap.putAll(poutTypemap)
      if (defaultOutTypemap) defaultOutTypeConversion = defaultOutTypemap
      if (pinTypemap) inTypemap.putAll(pinTypemap)
      if (defaultInTypemap) defaultInTypeConversion = defaultInTypemap
    }

   public static class Sequence
   {
     private long cur = 0;

     public long increment() { return ++cur }
   }

  private static ThreadLocal<Sequence> curSequence = new ThreadLocal<Sequence>();

  public static void setDoxygenXmlDir(File dir) { doxygenXmlDir = dir }

  private static String retrieveDocStringFromDoxygen(Node methodOrClass)
  {
    if (doxygenXmlDir == null)
      return null

    Node doc = null
    def ret = ''

    // make the class name or namespace
    String doxygenId = findFullClassName(methodOrClass,'_1_1',true)
    boolean isInClass = doxygenId != null
    if (!doxygenId)
      doxygenId = findNamespace(methodOrClass,'_1_1',false,true)
    doxygenId = (isInClass ? 'class' : 'namespace') + doxygenId

    String doxygenFilename = doxygenId + '.xml'
    File doxygenXmlFile = new File(doxygenXmlDir,doxygenFilename)
    if (! doxygenXmlFile.exists())
    {
      System.out.println("WARNING: Cannot find doxygen file for ${methodOrClass.toString()} which should be \"${doxygenXmlFile}\"")
      return null
    }

    Node docspec = (new XmlParser().parse(doxygenXmlFile))
    if (methodOrClass.name() == 'class')
      doc = docspec.compounddef[0].detaileddescription[0]
    else // it's a method of some sort ... or it better be
    {
      Node memberdef = docspec.depthFirst().find {
        return (it instanceof String) ? false :
          ((it.name() == 'memberdef' && (it.@kind == 'function' || it.@kind == 'variable') && it.@id.startsWith(doxygenId)) &&
           (it.name != null && it.name.text().trim() == methodOrClass.@sym_name))
      }

      doc = memberdef != null ? memberdef.detaileddescription[0] : null
    }

    if (doc != null)
    {
      def indent = '    '
      def curIndent = ''
      def prevIndent = ''

      def handleDoc
      handleDoc = {
        if (it instanceof String)
          ret += it
        else // it's a Node
        {
          if (it.name() == 'detaileddescription')
            it.children().each handleDoc
          else if (it.name() == 'para')
          {
            it.children().each handleDoc
            ret += (it.parent()?.name() == 'listitem') ? newline : (newline + newline)
          }
          else if (it.name() == 'ref' || it.name() == "ulink")
            ret += (it.text() + ' ')
          else if (it.name() == 'verbatim')
            ret += it.text().denormalize()
          else if (it.name() == 'itemizedlist')
          {
            ret += newline
            prevIndent = curIndent
            curIndent += indent
            it.children().each handleDoc
            curIndent = prevIndent
          }
          else if (it.name() == 'listitem')
          {
            ret += (curIndent + '- ')
            it.children().each handleDoc
          }
          else if (it.name() == 'linebreak')
            ret += newline
          else if (it.name() == 'ndash')
            ret += "--"
          else if (it.name() == 'emphasis')
          {
            ret += '*'
            it.children().each handleDoc
          }
          else
            System.out.println("WARNING: Cannot parse the following as part of the doxygen processing:" + XmlUtil.serialize(it))
        }
      }

      doc.children().each handleDoc
    }

    return ret.denormalize()
  }

   /**
    * <p>This method uses the previously set outTypemap and defaultOutTypemap to produce the chunk of code
    * that will be used to return the method invocation result to the scripting language. For example, in
    * python, if the return type from the method is a long, then the OutConversion could look something like:</p>
    * <code>
    *    result = PyInt_FromLong(thingReturnedFromMethod);
    * </code>
    * <p>This could have resulted from a mini-template stored as the way to handle 'long's in the outTypemap:</p>
    * <code>
    *    ${result} = PyInt_FromLong(${api});
    * </code>
    * @param apiType - is the Swig typecode that describes the return type from the native method
    * @param method - is the node from the module xml that contains the method description
    * @return the code chunk as a string ready to be placed into the generated code.
    */
   public static String getOutConversion(String apiType, String apiName, Node method, Map overrideBindings = null, boolean recurse = true)
   {
      def convertTemplate = outTypemap[apiType]

      //    String apiLType = SwigTypeParser.convertTypeToLType(apiType)
      //    if (convertTemplate == null) convertTemplate = outTypemap[apiLType]

      // is the returns a pointer to a known class
      String className = null
      if (convertTemplate == null && apiType.startsWith('p.'))
      {
        Node classNode = findClassNodeByName(parents(method)[0], SwigTypeParser.getRootType(apiType),method)
        if (classNode)
        {
          className = findFullClassName(classNode)
          convertTemplate = defaultOutTypeConversion
        }
      }

      if (convertTemplate == null)
      {
        // Look for Pattern for keys that might fit
        convertTemplate = outTypemap.find({ key, value -> (key instanceof Pattern && key.matcher(apiType).matches()) })?.value
      }

      if (!convertTemplate)
      {
        String knownApiType = isKnownApiType(apiType,method)
        if (knownApiType)
        {
          convertTemplate = defaultOutTypeConversion
          className = knownApiType
        }
      }

      if (!convertTemplate)
      {
        // check the typedef resolution
        String apiTypeResolved = SwigTypeParser.SwigType_resolve_all_typedefs(apiType)
        if (!apiTypeResolved.equals(apiType))
          return getOutConversion(apiTypeResolved, apiName, method, overrideBindings, recurse)

        if (recurse)
          return getOutConversion(SwigTypeParser.SwigType_ltype(apiType),apiName,method,overrideBindings,false)
        else if (!isKnownApiType(apiType,method))
           throw new RuntimeException("WARNING: Cannot convert the return value of swig type ${apiType} for the call ${Helper.findFullClassName(method) + '::' + Helper.callingName(method)}")
      }

      boolean seqSetHere = false
      Sequence seq = curSequence.get()
      if (seq == null)
      {
        seqSetHere = true
        seq = new Sequence()
        curSequence.set(seq)
      }

      Map bindings = ['result' : apiName, 'api' : 'apiResult', 'type' : "${apiType}",
                      'method' : method, 'helper' : Helper.class,
                      'swigTypeParser' : SwigTypeParser.class,
                      'sequence' : seq ]
      if (className) bindings['classname'] = className

      if (overrideBindings) bindings.putAll(overrideBindings)

      if (convertTemplate instanceof List) /// then we expect the template string/file to be the first entry
      {
        Map additionalBindings = convertTemplate.size() > 1 ? convertTemplate[1] : [:]
        bindings.putAll(additionalBindings)
        convertTemplate = convertTemplate[0]
      }

      if (File.class.isAssignableFrom(convertTemplate.getClass()))
      {
        File cur = (File)convertTemplate
        if (!cur.exists()) // see if the file is relative to the template file
        {
          File parent = curTemplateFile.getParentFile()
          // find the relative path to the convertTemplate
          File cwd = new File('.').getCanonicalFile()
          String relative = cwd.getAbsoluteFile().toURI().relativize(convertTemplate.getAbsoluteFile().toURI()).getPath()
          convertTemplate = new File(parent,relative)

          // This is a fallback case which is hit occasionally on OSX as a result
          // of case mismatches between the two paths in the relativize call above.
          if (!convertTemplate.exists())
            convertTemplate = new File(parent,cur.toString())
        }
      }

      if (seqSetHere) curSequence.set(null)
      return new SimpleTemplateEngine().createTemplate(convertTemplate).make(bindings).toString()
   }

   /**
    * <p>This method uses the previously set inTypemap and defaultInTypemap to produce the chunk of code
    * that will be used to convert input parameters from the scripting language to the native method invocation
    * parameters. For example, if the native call takes a 'String' then the InConversion could look something like:</p>
    * <code>
    *    if (pythonStringArgument) PyXBMCGetUnicodeString(cArgument,pythonStringArgument,"cArgumentName");
    * </code>
    * <p>This could have resulted from a mini-template stored as the way to handle 'long's in the outTypemap:</p>
    * <code>
    *    if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},"${api}");
    * </code>
    * @param apiType - is the Swig typecode that describes the parameter type from the native method
    * @param apiName - is the name of the parameter from the method parameter list in the api
    * @param slName - is the name of the variable that holds the parameter from the scripting language
    * @param method - is the node from the module xml that contains the method description
    * @return the code chunk as a string ready to be placed into the generated code.
    */
   public static String getInConversion(String apiType, String apiName, String slName, Node method, Map overrideBindings = null)
   {
     return getInConversion(apiType, apiName, apiName, slName, method, overrideBindings);
   }

   /**
    * <p>This method uses the previously set inTypemap and defaultInTypemap to produce the chunk of code
    * that will be used to convert input parameters from the scripting language to the native method invocation
    * parameters. For example, if the native call takes a 'String' then the InConversion could look something like:</p>
    * <code>
    *    if (pythonStringArgument) PyXBMCGetUnicodeString(cArgument,pythonStringArgument,"cArgumentName");
    * </code>
    * <p>This could have resulted from a mini-template stored as the way to handle 'long's in the outTypemap:</p>
    * <code>
    *    if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},"${api}");
    * </code>
    * @param apiType - is the Swig typecode that describes the parameter type from the native method
    * @param apiName - is the name of the variable that holds the api parameter
    * @param paramName - is the name of the parameter from the method parameter list in the api
    * @param slName - is the name of the variable that holds the parameter from the scripting language
    * @param method - is the node from the module xml that contains the method description
    * @return the code chunk as a string ready to be placed into the generated code.
    */
   public static String getInConversion(String apiType, String apiName, String paramName, String slName, Node method, Map overrideBindings = null)
   {
      def convertTemplate = inTypemap[apiType]

      String apiLType = SwigTypeParser.convertTypeToLTypeForParam(apiType)

      if (convertTemplate == null) convertTemplate = inTypemap[apiLType]

      // is the returns a pointer to a known class
      if (convertTemplate == null && apiType.startsWith('p.'))
      {
         // strip off rval qualifiers
         String thisNamespace = Helper.findNamespace(method)
         Node clazz = classes.find { Helper.findFullClassName(it) == apiLType.substring(2) ||
            (it.@sym_name == apiLType.substring(2) && thisNamespace == Helper.findNamespace(it)) }
         if (clazz != null) convertTemplate = defaultInTypeConversion
      }

      // Look for Pattern for keys that might fit
      if (convertTemplate == null)
        convertTemplate = inTypemap.find({ key, value -> (key instanceof Pattern && key.matcher(apiType).matches()) })?.value

      // Try the LType
      if (convertTemplate == null)
        convertTemplate = inTypemap.find({ key, value -> (key instanceof Pattern && key.matcher(apiLType).matches()) })?.value

      if (!convertTemplate)
      {
         // check the typedef resolution
         String apiTypeResolved = SwigTypeParser.SwigType_resolve_all_typedefs(apiType)
         if (!apiTypeResolved.equals(apiType))
           return getInConversion(apiTypeResolved, apiName, paramName, slName, method, overrideBindings)

         // it's ok if this is a known type
         if (!isKnownApiType(apiType,method) && !isKnownApiType(apiLType,method))
           System.out.println("WARNING: Unknown parameter type: ${apiType} (or ${apiLType}) for the call ${Helper.findFullClassName(method) + '::' + Helper.callingName(method)}")
         convertTemplate = defaultInTypeConversion
      }

      if (convertTemplate)
      {
         boolean seqSetHere = false
         Sequence seq = curSequence.get()
         if (seq == null)
         {
           seqSetHere = true
           seq = new Sequence()
           curSequence.set(seq)
         }

         Map bindings = [
                  'type': "${apiType}", 'ltype': "${apiLType}",
                  'slarg' : "${slName}", 'api' : "${apiName}",
                  'param' : "${paramName}",
                  'method' : method, 'helper' : Helper.class,
                  'swigTypeParser' : SwigTypeParser.class,
                  'sequence' : seq
               ]

         if (overrideBindings) bindings.putAll(overrideBindings)

         if (convertTemplate instanceof List) /// then we expect the template string/file to be the first entry
         {
            Map additionalBindings = convertTemplate.size() > 1 ? convertTemplate[1] : [:]
            bindings.putAll(additionalBindings)
            convertTemplate = convertTemplate[0]
         }

         if (File.class.isAssignableFrom(convertTemplate.getClass()))
         {
           File cur = (File)convertTemplate
           if (!cur.exists()) // see if the file is relative to the template file
           {
             File parent = curTemplateFile.getParentFile()
             // find the relative path to the convertTemplate
             File cwd = new File('.').getCanonicalFile()
             String relative = cwd.getAbsoluteFile().toURI().relativize(convertTemplate.getAbsoluteFile().toURI()).getPath()
             convertTemplate = new File(parent,relative)

             // This is a fallback case which is hit occasionally on OSX as a result
             // of case mismatches between the two paths in the relativize call above.
             if (!convertTemplate.exists())
               convertTemplate = new File(parent,cur.toString())
           }
         }

         if (seqSetHere) curSequence.set(null);
         return new SimpleTemplateEngine().createTemplate(convertTemplate).make(bindings).toString()
      }

      return ''
   }

   static def ignoreAttributes = ['classes', 'symtab', 'sym_symtab',
      'sym_overname', 'options', 'sym_nextSibling', 'csym_nextSibling',
      'sym_previousSibling' ]

   /**
    * <p>Transform a Swig generated xml file into something more manageable. For the most part this method will:</p>
    *
    * <li>1) Make all pertinent 'attributelist' elements actually be attributes of the parent element while
    * an attribute with the name 'name' will become that parent element name.</li>
    * <li>2) Filter out unused attributes</li>
    * <li>3) Filter out the automatically included 'swig'swg'</li>
    * <li>4) Flatten out the remaining 'include' elements</li>
    * <li>5) Removes extraneous default argument function/method Node</li>
    * <li>6) Converts all type tables to a single entry under the main module node removing all 1-1 mappings.</li>
    * <li>7) Removes all non-public non-constructor methods.</li>
    * @param swigxml is the raw swig output xml document
    * @return the transformed document
    */
   public static Node transformSwigXml(Node swigxml)
   {
      Node node = transform(swigxml,
            {
               // do not include the 'include' entry that references the default swig.swg file.
               !(it.name() == 'include' &&
                     // needs to also contain an attribute list with an attribute 'name' that matches the swig.swg file
                     it.find {
                        it.name() == 'attributelist' && it.find {
                           it.name() == 'attribute' && it.@name == 'name' && it.@value =~ /swig\.swg$/
                        }
                     } ||
                     // also don't include any typescope entries
                     it.name() == 'typescopesitem' || it.name() == 'typetabsitem'
                     )
            },{ key, value -> !ignoreAttributes.contains(key) })

      // now make sure the outer most node is an include and there's only one
      assert node.include?.size() == 1 && node.include[0].module?.size() == 1 && node.include[0].module[0]?.@name != null,
      "Invalid xml doc result. Expected a single child node of the root node call 'include' with a single 'module' child node but got " + XmlUtil.serialize(node)

      // create an outermost 'module' node with the correct name
      Node ret = new Node(null, 'module', [ 'name':node.include[0].module[0].@name] )
      node.include[0].children().each { if (it.name() != 'module') ret.append(it) }

      // flatten out all other 'include' elements, parmlists, and typescopes
      flatten(ret,['include', 'parmlist', 'typescope' ])

      // remove any function nodes with default arguments
      List tmpl = []
      tmpl.addAll(ret.depthFirst())
      for (Node cur : tmpl)
      {
         if ((cur.name() == 'function' || cur.name() == 'constructor') && cur.@defaultargs != null)
            cur.parent().remove(cur)
      }

      // now validate that no other methods are overloaded since we can't handle those right now.
      functionNodesByOverloads(ret).each { key, value -> assert value.size() == 1, "Cannot handle overloaded methods unless simply using defaulting: " + value }

      // now gather up all of the typetabs and add a nice single
      // typetab entry with a better format in the main module
      List allTypetabs = ret.depthFirst().findAll { it.name() == 'typetab' }
      Node typenode = new Node(ret,'typetab')
      allTypetabs.each {
         it.attributes().each { key, value ->
            if (key != 'id' && key != value)
            {
               Node typeentry = new Node(null,'entry')
               String namespace = findNamespace(it)
               typeentry.@namespace = namespace != null ? namespace.trim() : ''
               typeentry.@type = key
               typeentry.@basetype = value

               if (typenode.find({ it.@basetype == typeentry.@basetype && it.@namespace == typeentry.@namespace }) == null)
                  typenode.append(typeentry)
            }
         }
         it.parent().remove(it)
      }

      // now remove all non-public methods, but leave constructors
      List allMethods = ret.depthFirst().findAll({ it.name() == 'function' || it.name() == 'destructor' || it.name() == 'constructor'})
      allMethods.each {
         if (it.@access != null && it.@access != 'public' && it.name() != 'constructor')
            it.parent().remove(it)
         else
         {
           def doc = retrieveDocStringFromDoxygen(it)
           if (doc != null && doc != '' && doc.trim() != ' ')
             new Node(it,'doc',['value' : doc])
         }
      }

      // now remove all non-public variables
      List allVariables = ret.depthFirst().findAll({ it.name() == 'variable' })
      allVariables.each {
         if (it.@access != null && it.@access != 'public')
            it.parent().remove(it)
         else
         {
           def doc = retrieveDocStringFromDoxygen(it)
           if (doc != null && doc != '' && doc.trim() != ' ')
             new Node(it,'doc',['value' : doc])
         }
      }

      // add the doc string to the classes
      List allClasses = ret.depthFirst().findAll({ it.name() == 'class'})
      allClasses.each {
        def doc = retrieveDocStringFromDoxygen(it)
        if (doc != null && doc != '' && doc.trim() != ' ')
          new Node(it,'doc',['value' : doc])
      }

      return ret
   }

   /**
    * @return true if the class node has a defined constructor. false otherwise.
    */
   public static boolean hasDefinedConstructor(Node clazz)
   {
      return (clazz.constructor != null && clazz.constructor.size() > 0)
   }

   /**
    * @return true id this Node has a docstring associated with it.
    */
   public static boolean hasDoc(Node methodOrClass)
   {
     return methodOrClass.doc != null && methodOrClass.doc[0] != null && methodOrClass.doc[0].@value != null
   }

   /**
    * @return true of the class node has a constructor but it's hidden (not 'public'). false otherwise.
    */
   public static boolean hasHiddenConstructor(Node clazz)
   {
      return (hasDefinedConstructor(clazz) && clazz.constructor[0].@access != null && clazz.constructor[0].@access != 'public')
   }

   /**
    * <p>This will look through the entire module and look up a class node by name. It will return null if
    * that class node isn't found. It's meant to be used to look up base classes from a base class list
    * so it's fairly robust. It goes through the following rules:</p>
    *
    * <li>Does the FULL classname (considering the namespace) match the name provided.</li>
    * <li>Does the FULL classname match the reference nodes namespace + '::' + the provided classname.</li>
    * <li>Does the class node's name (which may contain the full classname) match the classname provided.</li>
    *
    * <p>Note, this method is not likely to find the classnode if you just pass a simple name and
    * no referenceNode in the case where namespaces are used extensively.</p>
    */
   public static Node findClassNodeByName(Node module, String classname, Node referenceNode = null)
   {
      return module.depthFirst().findAll({ it.name() == 'class' }).find {
         // first check to see if this FULL class name matches
         if (findFullClassName(it).trim() == classname.trim()) return true

         // now check to see if it matches the straight name considering the reference node
         if (referenceNode != null && (findNamespace(referenceNode) + classname) == findFullClassName(it)) return true

         // now just see if it matches the straight name
         if (it.@name == classname) return true

         return false
      }
   }

   /**
    * Find me the class node that this node either is, or is within.
    * If this node is not within a class node then it will return null.
    */
   public static Node findClassNode(Node node)
   {
      if (node.name() == 'class') return node
      return node.parent() == null ? null : findClassNode(node.parent())
   }

   /**
    * If this node is a class node, or a child of a class name (for example, a method) then
    * the full classname, with the namespace will be returned. Otherwise, null.
    */
   public static String findFullClassName(Node node, String separator = '::', boolean filename = false)
   {
      String ret = null
      List rents = parents(node, { it.name() == 'class' })
      if (node.name() == 'class') rents.add(node)
      rents.each {
         if (ret == null)
            ret = it.@sym_name
         else
            ret += separator + it.@sym_name
      }

      return ret ? findNamespace(node,separator,true,filename) + ret : null
   }

   /**
    * Given the Node this method looks to see if it occurs within namespace and returns
    * the namespace as a String. It includes the trailing '::'
    */
   public static String findNamespace(Node node, String separator = '::', boolean endingSeparator = true, boolean filename = false)
   {
      String ret = null
      parents(node, { it.name() == 'namespace' }).each {
         String data = it.@name
         if (filename)
            data = data.replaceAll("_", "__")
         if (ret == null)
            ret = data
         else
            ret += separator + data
      }

      return ret == null ? '' : (ret + (endingSeparator ? separator : ''))
   }

   /**
    * Gather up all of the parent nodes in a list ordered from the top node, down to the
    * node that's passed as a parameter.
    */
   public static List parents(Node node, Closure filter = null, List ret = null)
   {
      Node parent = node.parent()
      if (parent != null)
      {
         ret = parents(parent,filter,ret)
         if (filter == null || filter.call(parent))
            ret += parent
      }
      else if (ret == null)
         ret = []

      return ret
   }

   /**
    * Group together overloaded methods into a map keyed by the first method's id. Each
    * entry in this map contains a list of nodes that represent overloaded versions of
    * the same method.
    */
   public static Map functionNodesByOverloads(Node module)
   {
      // find function nodes
      Map ret = [:]
      module.depthFirst().each {
         if (it.name() == 'function' || it.name() == 'constructor' || it.name() == 'destructor')
         {
            String id = it.@sym_overloaded != null ? it.@sym_overloaded : it.@id
            if (ret[id] == null) ret[id] = [it]
            else ret[id] += it
         }
      }
      return ret
   }

   /**
    * Because the return type of a property is a combination of the function
    * 'decl' and the function 'type,' this method will construct a valid Swig
    * typestring from the two.
    */
   public static String getPropertyReturnSwigType(Node method)
   {
      // we're going to take a shortcut here because it appears only the pointer indicator
      // ends up attached to the decl.
      String prefix = (method.@decl != null && method.@decl == 'p.') ? 'p.' : ''
      return method.@type != null ? prefix + method.@type : 'void'
   }

   /**
    * Because the return type is a combination of the function 'decl' and the
    * function 'type,' this method will construct a valid Swig typestring from
    * the two.
    */
   public static String getReturnSwigType(Node method)
   {
      // we're going to take a shortcut here because it appears only the pointer indicator
      // ends up attached to the decl.
      String prefix = (method.@decl != null && method.@decl.endsWith('.p.')) ? 'p.' : ''
      return method.@type != null ? prefix + method.@type : 'void'
   }

   /**
    * Given the method node this will produce the actual name of the method as if
    * it's being called. In the case of a constructor it will do a 'new.' In the
    * case of a destructor it will produce a 'delete.'
    */
   public static String callingName(Node method)
   {
      // if we're not in a class we need the fully qualified name
      String clazz = findFullClassName(method)
      // if we're in a class then we are going to assume we have a 'self' pointer
      // that we are going to invoke this on.
      if (clazz == null)
         return method.@name

      if (method.name() == 'constructor')
         return "new ${findNamespace(method)}${method.@sym_name}"

      if (method.name() == 'destructor')
         return 'delete'

      // otherwise it's just a call on a class being invoked on an instance
      return method.@name
   }

   /**
    * Swig has 'insert' nodes in it's parse tree that contain code chunks that are
    * meant to be inserted into various positions in the generated code. This method
    * will extract the nodes that refer to the specific section asked for. See the
    * Swig documentation for more information.
    */
   public static List getInsertNodes(Node module, String section)
   {
      return module.insert.findAll { section == it.@section || (section == 'header' && it.@section == null) }
   }

   public static String unescape(Node insertSection) { return unescape(insertSection.@code) }

   public static String unescape(String insertSection) { return StringEscapeUtils.unescapeHtml4(insertSection) }

   public static boolean isDirector(Node method)
   {
     Node clazz = findClassNode(method)
     if (!clazz || !clazz.@feature_director)
       return false
     if (method.name() == 'destructor')
       return false
     if (method.name() == 'constructor')
       return false
     return method.@storage && method.@storage == 'virtual'
   }

   /**
    * This method will search from the 'searchFrom' Node up to the root
    *  looking for a %feature("knownbasetypes") declaration that the given 'type' is
    *  known for 'searchFrom' Node.
    */
   public static boolean isKnownBaseType(String type, Node searchFrom)
   {
     return hasFeatureSetting(type,searchFrom,'feature_knownbasetypes',{ it.split(',').find({ it.trim() == type }) != null })
   }

   /**
    * This method will search from the 'searchFrom' Node up to the root
    *  looking for a %feature("knownapitypes") declaration that the given 'type' is
    *  known for 'searchFrom' Node.
    */
   public static String isKnownApiType(String type, Node searchFrom)
   {
     String rootType = SwigTypeParser.getRootType(type)
     String namespace = findNamespace(searchFrom,'::',false)
     String lastMatch = null
     hasFeatureSetting(type,searchFrom,'feature_knownapitypes',{ it.split(',').find(
       {
         if (it.trim() == rootType)
         {
           lastMatch = rootType
           return true
         }
         // we assume the 'type' is defined within namespace and
         //  so we can walk up the namespace appending the type until
         //  we find a match.
         while (namespace != '')
         {
//           System.out.println('checking ' + (namespace + '::' + rootType))
           if ((namespace + '::' + rootType) == it.trim())
           {
             lastMatch = it.trim()
             return true
           }
           // truncate the last namespace
           int chop = namespace.lastIndexOf('::')
           namespace = (chop > 0) ? namespace.substring(0,chop) : ''
         }
         return false
       }) != null })
     return lastMatch
   }

   private static String hasFeatureSetting(String type, Node searchFrom, String feature, Closure test)
   {
     if (!searchFrom)
       return null

     Object attr = searchFrom.attribute(feature)
     if (attr && test.call(attr))
       return attr.toString()

     return hasFeatureSetting(type,searchFrom.parent(),feature,test)
   }

   private static void flatten(Node node, List elementsToRemove)
   {
      for (boolean done = false; !done;)
      {
         done = true
         for (Node child : node.breadthFirst())
         {
            if (elementsToRemove.contains(child.name()))
            {
               Node parent = child.parent()
               parent.remove(child)
               child.each { parent.append(it) }
               done = false
               break
            }
         }
      }
   }

   private static Node transform(Node node, Closure nodeFilter, Closure attributeFilter)
   {
      // need to create a map and a list of nodes (which will be children) from the
      // attribute list.
      Map attributes = [:]
      List nodes = []
      node.each {
         if (nodeFilter == null || nodeFilter.call(it) == true)
         {
            if (it.name() == 'attributelist')
            {
               Tuple results = transformSwigAttributeList(it)
               attributes.putAll(results[0].findAll(attributeFilter))
               List childNodes = results[1]
               childNodes.each {
                  if (nodeFilter != null && nodeFilter.call(it) == true)
                     nodes.add(transform(it,nodeFilter,attributeFilter))
               }
            }
            else
               nodes.add(transform(it,nodeFilter,attributeFilter))
         }
      }

      // transfer the addr attribute from the original node over to the 'id' attribute of the
      // new node by adding it to the attributes map
      if (node.@addr)
      {
        // copy over the other attributes
        node.attributes().findAll { key,value -> if (key != 'addr' && key != 'id') attributes[key] = value }
        attributes['id'] = node.@addr
      }

      // In the case when the Node is a cdecl, we really want to replace the node name
      // with the 'kind' attribute value.
      Node ret
      if (node.name() == 'cdecl' && attributes.containsKey('kind'))
         ret = new Node(null, attributes['kind'], attributes.findAll({key, value -> key != 'kind' } ))
      else
         ret = new Node(null, node.name(), attributes)
      nodes.each { ret.append(it) }
      return ret
   }

   private static Tuple transformSwigAttributeList(Node attributeList)
   {
      Map attributes = [:]
      List nodes = []
      attributeList.each {
         if (it.name() == 'attribute')
            attributes[it.@name] = it.@value
         else
            nodes.add(it)
      }
      return [attributes, nodes]
   }
}

