#!/usr/bin/env python

import os
import sys

hFileTemplate = """/**
 * This file is auto-generated! Do NOT modify!
 **/

#ifndef %s
#define %s

%s

#endif // %s
"""

jniFileTemplate = """/**
 * This file is auto-generated! Do NOT modify!
 **/

#include <stdint.h>
#include "native/%s.h"

#ifdef __cplusplus
extern "C" {
#endif

#include "jni.h"

// Helper functions ////////////////////////////////////////////////////////////////////////////////
%s* Get%sAtIndex(JNIEnv* env, jobject buffer, int index) {
  jclass base_class = (*env)->FindClass(env, "android/filterfw/core/NativeBuffer");
  jfieldID ptr_field = (*env)->GetFieldID(env, base_class, "mDataPointer", "J");
  uintptr_t data_ptr = (*env)->GetLongField(env, buffer, ptr_field);
  %s* array = (%s*)data_ptr;
  (*env)->DeleteLocalRef(env, base_class);
  return &array[index];
}

// Declarations ////////////////////////////////////////////////////////////////////////////////////
JNIEXPORT jint JNICALL
Java_%s_getElementSize(JNIEnv* env, jobject thiz);

%s

#ifdef __cplusplus
}
#endif

// Implementation //////////////////////////////////////////////////////////////////////////////////
jint Java_%s_getElementSize(JNIEnv* env, jobject thiz) {
  return sizeof(%s);
}

%s
"""

javaFileTemplate = """/**
 * This file is auto-generated! Do NOT modify!
 **/

package %s;

import android.filterfw.core.NativeBuffer;

%s
"""


def ToJavaName(cname, start_upper_at = 1):
  lower = cname.split("_")
  upper = [c.title() for c in lower]
  return "".join(lower[:start_upper_at] + upper[start_upper_at:])

def ToJNIPackage(package, jclassname):
  return "%s_%s" % (package.replace(".", "_"), jclassname)

def ToMacroDefName(cname, pname):
  return "%s_%s" % (pname.replace(".", "_").upper(), cname.upper())

class ParseError:
  def __init__(self, lineno, message):
    self.lineno = lineno
    self.message = message

  def __str__(self):
    return "On line %d: %s" % (self.lineno, self.message)

class FieldType_BasePOD:
  def __init__(self, name, structname, jclassname, package, ctype, jtype, defval):
    self.name = name
    self.structname = structname
    self.jclassname = jclassname
    self.package = package
    self.ctype = ctype
    self.jtype = jtype
    self.defval = defval

  def cString(self):
    return "  %s %s;" % (self.ctype, self.name)

  def javaGetter(self):
    return "    public native %s get%s(int index);" % (self.ctype, ToJavaName(self.name, 0))

  def javaSetter(self):
    return "    public void set%s(int index, %s value) {\n"\
           "        assertWritable();\n"\
           "        nativeSet%s(index, value);\n"\
           "    }" % (ToJavaName(self.name, 0), self.ctype, ToJavaName(self.name, 0))

  def javaNativeSetter(self):
    return "    private native boolean nativeSet%s(int index, %s value);"\
           % (ToJavaName(self.name, 0), self.ctype)

  def jniGetterDefString(self):
    return "JNIEXPORT %s JNICALL\n" \
           "Java_%s_get%s(JNIEnv* env, jobject thiz, jint index);" \
           % (self.jtype, ToJNIPackage(self.package, self.jclassname), ToJavaName(self.name, 0))

  def jniGetterImplString(self):
    return \
      "%s Java_%s_get%s(JNIEnv* env, jobject thiz, jint index) {\n"\
      "  %s* instance = Get%sAtIndex(env, thiz, index);\n"\
      "  return instance ? instance->%s : %s;\n"\
      "}\n" % (self.jtype, ToJNIPackage(self.package, self.jclassname), ToJavaName(self.name, 0),\
               self.structname, self.structname, self.name, self.defval)

  def jniSetterDefString(self):
    return "JNIEXPORT jboolean JNICALL\n" \
           "Java_%s_nativeSet%s(JNIEnv* env, jobject thiz, jint index, %s value);" \
           % (ToJNIPackage(self.package, self.jclassname), ToJavaName(self.name, 0), self.jtype)

  def jniSetterImplString(self):
    return \
      "jboolean Java_%s_nativeSet%s(JNIEnv* env, jobject thiz, jint index, %s value) {\n"\
      "  %s* instance = Get%sAtIndex(env, thiz, index);\n"\
      "  if (instance) {\n"\
      "    instance->%s = value;\n"\
      "    return JNI_TRUE;\n"\
      "  }\n"\
      "  return JNI_FALSE;\n"\
      "}\n" % (ToJNIPackage(self.package, self.jclassname), ToJavaName(self.name, 0),\
               self.jtype, self.structname, self.structname, self.name)

class FieldType_Float(FieldType_BasePOD):
  def __init__(self, name, structname, jclassname, package):
    FieldType_BasePOD.__init__(self, name, structname, jclassname, package, "float", "jfloat", "0.0")

class FieldType_Int(FieldType_BasePOD):
  def __init__(self, name, structname, jclassname, package):
    FieldType_BasePOD.__init__(self, name, structname, jclassname, package, "int", "jint", "0")

class StructSpec:

  def parseTextFile(self, filepath):
    # Init
    self.name = None
    self.package = None
    self.fields = []
    self.structname = None
    self.jclassname = None
    self.libname = None

    # Open the file
    txtfile = open(filepath)

    # Parse it line by line
    lineno = 0
    for line in txtfile:
      # Split line into components
      linecomps = line.split()
      if len(linecomps) == 0:
        continue

      # Execute command
      cmd = linecomps[0]
      if cmd == "@name":
        self.commandArgAssert(linecomps, 1, lineno)
        self.name = linecomps[1]
        if not self.structname:
          self.structname = self.name
        if not self.jclassname:
          self.jclassname = self.name
      elif cmd == "@package":
        self.commandArgAssert(linecomps, 1, lineno)
        self.package = linecomps[1]
      elif cmd == "@libname":
        self.commandArgAssert(linecomps, 1, lineno)
        self.libname = linecomps[1]
      elif cmd == "@structname":
        self.commandArgAssert(linecomps, 1, lineno)
        self.structname = linecomps[1]
      elif cmd == "@javaclassname":
        self.commandArgAssert(linecomps, 1, lineno)
        self.jclassname = linecomps[1]
      elif cmd == "@field":
        self.commandArgAssert(linecomps, 2, lineno)
        typestr = linecomps[1]
        if typestr == "int":
          fieldtype = FieldType_Int(linecomps[2], self.structname, self.jclassname, self.package)
        elif typestr == "float":
          fieldtype = FieldType_Float(linecomps[2], self.structname, self.jclassname, self.package)
        else:
          raise ParseError(lineno, "Unknown field type '%s'!" % typestr)
        self.fields.append(fieldtype)
      else:
        raise ParseError(lineno, "Unknown command: '%s'!" % cmd)

      lineno = lineno + 1

    # Make sure we have all required info
    if not self.name:
      raise ParseError(lineno, "Required field '@name' missing!")
    elif not self.package:
      raise ParseError(lineno, "Required field '@package' missing!")
    elif not self.libname:
      raise ParseError(lineno, "Required field '@libname' missing!")

    # Normalize values
    if self.libname[:3] == "lib":
      self.libname = self.libname[3:]

  def commandArgAssert(self, linecomps, expectedcount, lineno):
    foundcount = len(linecomps) - 1
    if foundcount < expectedcount:
      raise ParseError(lineno, "Not enough arguments specifed for command '%s'! Expected %d, " \
                       "but got only %d!" % (linecomps[0], expectedcount, foundcount))
    elif foundcount > expectedcount + 1:
      raise ParseError(lineno, "Too many arguments specifed for command '%s'! Expected %d, " \
                       "but got %d!" % (linecomps[0], expectedcount, foundcount))


  def cStructString(self):
    cfields = [f.cString() for f in self.fields]
    return "typedef struct Struct%s {\n%s\n} %s;\n" % (self.structname,\
                                                       "\n".join(cfields),\
                                                       self.structname)

  def javaClassString(self):
    jgetters = [f.javaGetter() for f in self.fields]
    jsetters = [f.javaSetter() for f in self.fields]
    jnativesetters = [f.javaNativeSetter() for f in self.fields]
    return "public class %s extends NativeBuffer {\n\n"\
           "    public %s() {\n"\
           "        super();\n"\
           "    }\n"\
           "\n"\
           "    public %s(int count) {\n"\
           "        super(count);\n"\
           "    }\n"\
           "\n"\
           "    public native int getElementSize();\n"\
           "\n"\
           "%s\n\n"\
           "%s\n\n"\
           "%s\n\n"\
           "    static {\n"\
           "        System.loadLibrary(\"%s\");\n"\
           "    }\n"\
           "\n"\
           "};\n" % (self.jclassname,\
                     self.jclassname,\
                     self.jclassname,\
                     "\n\n".join(jgetters),\
                     "\n\n".join(jsetters),\
                     "\n\n".join(jnativesetters),\
                     self.libname)

  def jniDeclString(self):
    jnigetters = [f.jniGetterDefString() for f in self.fields]
    jnisetters = [f.jniSetterDefString() for f in self.fields]
    return "\n\n".join(jnigetters + jnisetters)

  def jniImplString(self):
    jnigetters = [f.jniGetterImplString() for f in self.fields]
    jnisetters = [f.jniSetterImplString() for f in self.fields]
    return "\n\n".join(jnigetters + jnisetters)

  def hFileString(self):
    defname = ToMacroDefName(self.structname, self.package)
    return hFileTemplate % (defname, defname, self.cStructString(), defname)

  def javaFileString(self):
    return javaFileTemplate % (self.package, self.javaClassString())

  def jniFileString(self):
    return jniFileTemplate % (self.structname.lower(),\
                              self.structname,\
                              self.structname,\
                              self.structname,\
                              self.structname,\
                              ToJNIPackage(self.package, self.jclassname),\
                              self.jniDeclString(),\
                              ToJNIPackage(self.package, self.jclassname),\
                              self.structname,
                              self.jniImplString())

def main(argv):
  if len(argv) != 2:
    print("Usage: %s <file.struct>" % argv[0])
    return -1

  filepath = argv[1]

  structspec = StructSpec()
  structspec.parseTextFile(filepath)

  hfilename = "%s.h" % structspec.structname.lower()
  javafilename = "%s.java" % structspec.jclassname
  jnifilename = "jni_%s.c" % structspec.structname.lower()

  rootdir = os.path.dirname(filepath)
  hfilepath = "%s/../native/%s" % (rootdir, hfilename)
  javafilepath = "%s/../java/%s" % (rootdir, javafilename)
  jnifilepath = "%s/../jni/%s" % (rootdir, jnifilename)

  hfile = open(hfilepath, 'w')
  hfile.write(structspec.hFileString())
  hfile.close()

  javafile = open(javafilepath, 'w')
  javafile.write(structspec.javaFileString())
  javafile.close()

  jnifile = open(jnifilepath, 'w')
  jnifile.write(structspec.jniFileString())
  jnifile.close()


if __name__ == "__main__":
  sys.exit(main(sys.argv))

