#!/usr/bin/env python
'''Generate code to translate an enum value into a string.

usage: genEnumToString {<srcFile> | <srcDir>} [<dstDir>] [basePrjDir] [--lang=c++|python]

For example:

  python common/IMyInterface.h gen/out_dir/

Running the command line above generates a source file named
IMyInterface_enum.cpp containing static data structures and toString
functions for each enum present in the input file. Each toString function
transforms the appropriate enum value into a string (const char*).
Typically, the toString functions are used to transform error code or input
parameters to human readable log or CLI output.

The generated function definition should be added where appropriate:

  const char* toString(ENUM_TYPE e, bool isComment = true);

If IMyInterface.h contains:

enum EFoo {
  FOO_OK = 0,        // "OK"
  FOO_UNKNOWN,       // Foo is in an unknown state. "Unknown"
  FOO_MISSING_PARAM, // "Missing parameter"
};

then
a. calling toString(FOO_UNKNOWN) will return "Unknown"
b. calling toString(FOO_MISSING_PARAM, false) will return "FOO_MISSING_PARAM" 

Since line comments on enum values are also used for doxygen comments, the
script tries to extract a quoted string to represent the "human-readable"
toString value.

(In the Voyager project, the file common/common.h contains a macro to define
the toString function for a given enum type.)

The generated python code defines a class with a dict mapping value to string and
to_string and to_enum class methods to perform the conversions. Typical use:

    from IMote_enum import Enum_motest_t

    raw_value = struct.unpack('B', buf[idx])
    enum_value = Enum_motest_t(raw_value)
    ...
    print 'Mote:', mac, 'State:', enum_value

'''

import sys
import re
import os
import glob

verbose = False

# ----------------------------------------------------------------------
# Templates

TEMPLATES = {
    'c++': {
        'FILE_HEADER': '''/*
 * IMPORTANT: This file is automatically generated. Any changes made here will be overwritten. 
 */

#include <stdexcept>
#include <map>
#include <string.h>
#include <boost/assign/list_of.hpp> 
#include "{INCLUDE_HDR}"

typedef std::pair<const char *, const char *> enumname_t;
''',
        'FILE_FOOTER': '''
''',
        'ENUM_HEADER': '''

// ------------------------------- {ENUM_NAME_PREFIX}{ENUM_NAME} --------------------------------------------
static std::map<{ENUM_NAME_PREFIX}{ENUM_NAME}, const enumname_t> _trTable_{ENUM_NAME} = boost::assign::map_list_of 
''',
        'ENUM_VALUE': '''   ( {}, enumname_t("{}", "{}") )''',
        'ENUM_FOOTER': ''';

const char * toString({ENUM_NAME_PREFIX}{ENUM_NAME} aVal, bool isComment) {{
   try {{
      if (isComment)
         return _trTable_{ENUM_NAME}.at(aVal).second;
      else
         return _trTable_{ENUM_NAME}.at(aVal).first;
   }} catch (const std::out_of_range&) {{
      return "???";
   }}
}}

bool toEnum(const char * str, {ENUM_NAME_PREFIX}{ENUM_NAME}& res)
{{
   for(const std::pair<{ENUM_NAME_PREFIX}{ENUM_NAME}, const enumname_t>& p : _trTable_{ENUM_NAME}) {{
      if (strcmp(p.second.first, str) == 0) {{
         res = p.first;
         return true;
      }}
   }}
   return false;
}}

'''
    },
    'python': {
        'FILE_HEADER': '''#
# IMPORTANT: This file is automatically generated. Any changes made here will be overwritten.
#

        ''',
        'FILE_FOOTER': '\n',
        'ENUM_HEADER': '''
# ----------------------------------------------------------------------
# {ENUM_NAME}

class Enum_{ENUM_NAME}(object):
    strtable = {{
''',
        'ENUM_VALUE': '''        {3}: ('{0}', '{2}'),''',
        'ENUM_FOOTER': '''
    }}
    @classmethod
    def to_string(kls, val, use_comment = True):
        idx = 1 if use_comment else 0
        return Enum_{ENUM_NAME}.strtable[val][idx]
    @classmethod
    def to_enum(kls, strval):
        for k, v in Enum_{ENUM_NAME}.strtable.items():
            if strval in v:
                return Enum_{ENUM_NAME}(k)
        raise KeyError(strval)
    @classmethod
    def to_int(kls, strval):
        return kls.to_enum(strval).value

    def __init__(self, v):
        if not v in Enum_{ENUM_NAME}.strtable:
            raise KeyError(v)
        self.value = v
    def __str__(self):
        return self.to_string(self.value)
''',
    }
}

LANGUAGE = {
    'c++': {'templates': TEMPLATES['c++'],
            'extension': '_enum.cpp'},
    'python': {'templates': TEMPLATES['python'],
               'extension': '_enum.py'},
}


def usage() :
   print '''usage: genEnumToString {<srcFile> | <srcDir>} [<dstDir>] [basePrjDir]

Generate the enumToString implementation file(s) given a source file or directory.
The generated files are written in dstDir. 
If basePrjDir is specified, the source path relative to basePrjDir is used in
the #include directive. 
'''

def parseComment(l) :
   m = re.match('(.*)//.*"(.+)".*', l)
   if m :
      p = None
      m1 = re.match('.*\\#\s*(\w+).*', m.group(2))
      if m1 :
         p = m1.group(1).strip()
      if verbose:
         print "debug: comment: %s" % m.group(1).strip()
      return (m.group(1).strip(), m.group(2).strip(), p)
   return (l.strip(), None, None)

def processOneFile(inpFileName, outPath, base_dir, lang = LANGUAGE['c++']):
   enumList = []
   finp = open(inpFileName, "r")
   fout = None
      
   while True :
      flProcess = 0
      enumName = ''
      enumNamePrefix = ''
      for line in finp:
         (enum_symbol, enum_string, prefix) = parseComment(line)
         if verbose:
            print "debug: line '%s', '%s'" % (enum_symbol, enum_string)
         if enum_symbol:
            # split the string into words to avoid getting confused by similar
            # enum names
            m = re.match('\s*enum\s*(\w+).*', enum_symbol)
            if m:
               enumName = m.group(1)
               if prefix :
                  enumNamePrefix = prefix + '::'
               enum_int = -1  # if unspecified, first enum value is 0
               if verbose:
                  print "debug: found enum '%s%s'" % (enumNamePrefix, enumName)
               flProcess = 1
            elif flProcess == 1 and enum_symbol.count("}") > 0 :
               flProcess = 0
               break
            elif flProcess == 1:
               m = re.match("(\w+)\s*(=\s([-x\d]+))?,?\s*", enum_symbol)
               if m:
                  enum_val = m.group(1)
                  if m.group(3):
                     # base 0 allows inline hex, 0x10AB, to be converted
                     enum_int = int(m.group(3), 0)
                  else:
                     enum_int = enum_int + 1
                  if not enum_string:
                     enum_string = enum_val
                  # TODO: we don't need repeated elements, also we need better names
                  # enum_val should be the value, enum_id should be the identifier  
                  enum_description = [enumNamePrefix+enum_val, enum_val, enum_string, enum_int]
                  if verbose:
                     print "debug: enum value %s -> '%s'" % (enum_val, enum_string)
                  enumList.append(enum_description)
               else :
                  if verbose:
                     print "debug: unmatched line: ", enum_symbol
      if not enumName :
         break

      if enumNamePrefix != 'IGNORE::' :
         if fout is None :
            outFileName = os.path.splitext(inpFileName)[0] + lang['extension']
            if outPath:
               outFileName = os.path.join(outPath, os.path.basename(outFileName))
            fout = open(outFileName, "w")
            if (base_dir) :
               include_header = os.path.relpath(inpFileName, base_dir)
            else :   
               include_header = os.path.basename(inpFileName)
               
            s = lang['templates']['FILE_HEADER'].format(INCLUDE_HDR = include_header)
            fout.write(s)
         
         if verbose:
            print "debug: generating enum:", enumName
         s = lang['templates']['ENUM_HEADER'].format(ENUM_NAME = enumName, ENUM_NAME_PREFIX=enumNamePrefix)
         fout.write(s)

         fout.write('\n'.join([lang['templates']['ENUM_VALUE'].format(*e) for e in enumList]))   
         
         s = lang['templates']['ENUM_FOOTER'].format(ENUM_NAME = enumName, ENUM_NAME_PREFIX=enumNamePrefix)
         fout.write(s)
      enumList = []

   finp.close()
   if (fout is not None) :
      s = lang['templates']['FILE_FOOTER']
      fout.write(s)
      fout.close()


def main(src, dst, base_dir, langType):
   if dst and not os.path.exists(dst):
      os.makedirs(dst)
   if os.path.isdir(src):
      for dirpath, subdirs, files in os.walk(src):
         for currentFile in [f for f in files if f.endswith(".h") ]:
            processOneFile(os.path.join(dirpath, currentFile), dst, base_dir,
                           lang = LANGUAGE[langType])
   else:
      processOneFile(src, dst, base_dir, lang = LANGUAGE[langType])
      
if __name__=='__main__':
   # Main procedure
   if len(sys.argv) < 2 or len(sys.argv) > 5:
      usage()
      sys.exit(1)
   from argparse import ArgumentParser
   parser = ArgumentParser()
   parser.add_argument('src', action='store')
   parser.add_argument('dst', nargs='?', action='store', default='.')
   parser.add_argument('base_dir', nargs='?', action='store', default='.')
   parser.add_argument('--lang', dest='lang', action='store', default='c++')   
   opts = parser.parse_args(sys.argv[1:])
   main(opts.src, opts.dst, opts.base_dir, opts.lang)
   
