package nxm.xbc.prim;

import java.util.ArrayList;

import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;

/**
 * Command to generate C and C++ wrappers for XBC to support MIDAS APIs.
 *
 * <pre>
 * Parameters:
 * &lt;IN&gt;   file containing the fllib template (e.g. nxm.xbc.inc.midaslib.fllib)
 * &lt;OUT&gt;  output filename for stubbed APIs that are NOT implemented in XBC       (e.g. nxm.xbc.lib.midaslib.c)
 * &lt;OUTCPPHEADER&gt; output filename of header file for C++ prototypes/wrapper of of APIs in .fllib (e.g. nxm.xbc.inc.midaslib.h) (optional since not finish yet)
 * &lt;TAB&gt;  Table configuration file containing the APIs that supported (aka implemented) in XBC (e.g. nxm.xbc.cfg.supported_calls.tbl)
 * </pre>
 *
 * @author  J. Czechowski IV / Neon Ngo
 */
public class genwrappers extends Primitive {

  private static final String[] TYPE_MAP = { "*",   "void",
                                             "A1",  "char",  // single char
                                             "A",   "std::string&",  // string
                                             "B",   "int_1", // byte
                                             "L4",  "int_4", // JC4: special case in XM 3.8.X (?)
                                             "I2",  "int_2",
                                             "I4",  "int_4",
                                             "I8",  "int_8",
                                             "U1",  "int_u1",
                                             "U2",  "int_u2",
                                             "U4",  "int_u4",
                                             "U8",  "int_u8",
                                             "R4",  "real_4",
                                             "R8",  "real_8",
                                             "C8",  "complex_8",
                                             "C16", "complex_16",
                                           };

  private static final String[] STRUCT_MAP = { "VRB", "HEADER", "MQ_HEADER", "M_PTR",
                                               "T6SUBR_STRUCT", "TRANSMATRIX", "PKDSTRUCT", "PKS",
                                               "COLORTYPE", "MX", "PIXSTRUCT", "MAPSTRUCT",
                                               "XFACILITIES", "SCROLLBAR", "XFANNOTATION",
                                               "XWIDGET", "STKSTRUCT" };


  private static final String[] HEADER_NOT_IMPLEMENTED = {
          "/******************************************************************/",
          "/*  Auto generated wrappers for functions not implemented in XBC. */",
          "/******************************************************************/",
          "",
          "#include \"cdefs.h\"",
          "#include \"native.h\"",
          "#include \"headers.h\"",
          "#include \"context.h\"",
          "#include \"commandable.h\"",
          "#include \"midas.h\"",
          "#include \"fcinterface.h\"",
          "",
          "/** Prints out a warning message about an unsupported function. */",
          "static void warnNotImplemented(const char* func) {",
          "  fprintf(stderr,\"WARN: %s is not supported in XBC.\",func);",
          "}",
          "" };

  private static final String[] CPP_HEADER_INCLUDES = {
          "// -*- C++ -*-",
          "#include <string>",
          "",
          "#include \"cdefs.h\"",
          "#include \"fcinterface.h\"",
          "#include \"mtypes.h\"",
          "#include \"complex_n.h\"",
          "" };

  /** default constructor */
  public genwrappers () {}

  public int open () {
    TextFile inFile  = MA.getTextFile("IN", Args.REQUIRED);   // input (source) .fllib filename
    TextFile outFile = MA.getTextFile("OUT", Args.REQUIRED);  // output filename for stub implementation of APIs not supported
    TextFile outCppHeaderFile = MA.getTextFile("OUTCPPHEADER"); // C++ Header output filename (optional for now)
    Table supported = getSupported(MA.getTable("TAB"));       // contains list of XBC supported/not supported X-Midas APIs
    FileName sourceFileName = inFile.getName();
    String[] fllib;          // to hold contents of input in memory
    boolean writeCppHeader;  // true if we should write C++ header output file, otherwise false
    String cppdefine = null; // #define to use for C++ header

    // open and read input file
    inFile.open(BaseFile.FORCEABORT);
    fllib = inFile.readAllLines();
    inFile.close();

    // open output file (for stub impl of unsupported X-Midas APIs)
    outFile.open(BaseFile.OUTPUT|BaseFile.FORCEABORT);

    // open output file for C++ header (wrappers)
    writeCppHeader = outCppHeaderFile.open(BaseFile.OUTPUT|BaseFile.OPTIONAL);

    // write header for stub implementation of APIs not supported
    writeCNotImplementedStub(outFile, sourceFileName);

    // write header/top part for C++ header file
    if (writeCppHeader) cppdefine = writeCppHeaderTop(outCppHeaderFile, sourceFileName);

    // loop through input file and process it for the output file(s)
    for (int ii=0; ii < fllib.length; ii++) {
      String line = fllib[ii].trim();

      if ((line.length() == 0) || line.startsWith("!")) { // comment lines
          if (writeCppHeader && line.startsWith("!//")) { // include C++ comments starting with "!//"
            String commentLine = line.substring(1);       // do not include leading '!'
            outCppHeaderFile.writeln(commentLine);        // ONLY for C++ header file
          }
      } else if (line.startsWith("include")) {            // include header files
        String[] includeFiles = line.split("\\s+");
        for (int jj=1; jj<includeFiles.length; jj++) {    // using jj=1 to skip "include"
          String includeLine= "#include \""+includeFiles[jj]+".h\"";
          outFile.writeln(includeLine);
          if (writeCppHeader) outCppHeaderFile.writeln(includeLine);
        }
        outFile.writeln("");                              // add extra blank line after include(s)
        if (writeCppHeader) outCppHeaderFile.writeln(""); // add extra blank line after include(s)
      } else {
        try {
          //System.out.println("line "+ii+": "+line);
          String[] tokens     = line.split("\\s+");
          String   funcType   = tokens[0];
          String   funcName   = null;
          String   funcReturn = null;
          String[] params     = null;

          if (funcType.endsWith("func")) {
            funcReturn = convertParam(tokens[1]);
            funcName   = tokens[2];
            params     = convertParams(tokens,3);
            if (funcReturn.equals("std::string&")) {
              String[] p2 = new String[params.length+2];
              System.arraycopy(params,0,p2,2,params.length);
              params = p2;
              funcReturn  = "void";
              params[0]   = "char* ret";
              params[1]   = "int_4 lret";
            }
          } else {
            funcReturn = "void";
            funcName   = tokens[1];
            params     = convertParams(tokens,2);
          }

          String funcPrototype = funcReturn+" "+funcName+"_ ("+StringUtil.join(params)+")"; // Fortran/C API prototype

          // write function prototype into header file
          if (writeCppHeader) {
            // mangle funcName to C++ by replacing first '$' with '_'
            String cppFuncPrototype = funcReturn+" "+mangle_name_to_cxx(funcName)+" ("+StringUtil.join(params)+")";
            outCppHeaderFile.writeln("// "+line); // original line from .fllib source file
            if (funcType.startsWith("CXX")) {
              // method is already implemented in C++ (write C++ m_ method prototype)
              outCppHeaderFile.writeln("extern " + cppFuncPrototype+";");
            }
            else {
              outCppHeaderFile.writeln("extern \"C\" "+funcPrototype+";");
              // TODO: write C++ m_ method wrapper code to C/Fortran API
              outCppHeaderFile.writeln("// TODO: write C++ m_ method wrapper code to Fortran/C API");
            }
          }

          if (!isSupported(supported,funcName)) {
            outFile.writeln("/* " + line + " */"); // include original line from .fllib source file (since 3.1.2)
            outFile.writeln(funcPrototype+" {");
            outFile.writeln("  warnNotImplemented(\""+funcName.toUpperCase()+"\");");
            if (!funcReturn.contains("void")) {
              if (funcReturn.contains("complex_8") || funcReturn.contains("complex_16")) {
                outFile.writeln("  "+funcReturn+" retval = {0,0};");
                outFile.writeln("  return retval;");
              } else { outFile.writeln("  return 0;"); }
            }
            outFile.writeln("}");
            outFile.writeln("");  // write blank line for spacing
          }

          if (writeCppHeader) outCppHeaderFile.writeln(""); // insert blank line before next item
        } catch (WrapException e) {
          M.warning("line "+ii+": "+e.getMessage());
          M.warning("    "+line);
        }
      }
    } // end for (int ii=0; ii < fllib.length; ii++) {

    outFile.close();
    if (writeCppHeader) {
      outCppHeaderFile.writeln(""); // write blank line for spacing
      outCppHeaderFile.writeln("#endif // "+cppdefine);
      outCppHeaderFile.writeln(""); // write trailing blank line
      outCppHeaderFile.close();
    }

    return FINISH;
  }

  /** Write note that file is autogenerated to specified TextFile.
      @param textfile output file to write to
      @param sourceFN filename where this file is derived from (e.g. midaslib.fllib)
      @since NeXtMidas 2.7.2
   */
  private void writeAutoGeneratedNote (TextFile textfile, FileName sourceFN) {
    // add auto generated note
    textfile.writeln("/* DO NOT EDIT THIS FILE - it is machine generated */");
    textfile.writeln("/* DERIVED from: "+sourceFN+" */");
    textfile.writeln("/* AUTOGENERATED by $NMROOT/nxm/xbc/prim/genwrappers.java */");
    textfile.writeln("/* version $Id: genwrappers.java,v 1.14 2014/04/10 15:54:09 ntn Exp $ */");
    textfile.writeln("");
  }

  /** Write top portion of C source file for XBC functions that are NOT implemented.
      @param textfile C output file
      @param sourceFN filename where this file is derived from (e.g. midaslib.fllib)
      @since NeXtMidas 2.7.2
   */
  private void writeCNotImplementedStub (TextFile textfile, FileName sourceFN) {
    writeAutoGeneratedNote(textfile, sourceFN);

    // write header for stub implementation of APIs not supported
    for (String str : HEADER_NOT_IMPLEMENTED) {
      textfile.writeln(str);
    }
  }

  /** Write top portion of C++ Header file which includes:
      #ifndef, #define, autogenerated info, and other common #includes.
      @param sourceFN filename where this file is derived from (e.g. midaslib.fllib)
      @param textfile c++ header file
      @return String used in #ifndef and #define
      @since NeXtMidas 2.7.2
   */
  private String writeCppHeaderTop (TextFile textfile, FileName sourceFN) {
    FileName fn = textfile.getName();
    String cppdefine = "XM_"+ fn.getRoot().toUpperCase() + "_" + fn.getExt().toUpperCase() + "_";

    writeAutoGeneratedNote(textfile, sourceFN);

    // add #ifndef and #define to C++ header file
    textfile.writeln("#ifndef "+cppdefine);
    textfile.writeln("#define "+cppdefine);

    // add necessary #includes
    for (String includeHeader : CPP_HEADER_INCLUDES) {
      textfile.writeln(includeHeader);
    }
    return cppdefine;
  }

  /** Find if a command is supported. */
  private boolean isSupported (Table supported, String func) {
    func = func.toUpperCase();
    return supported.getState(func,false);
  }

  /** Create the supported command table. */
  private Table getSupported (Table tab) {
    Table    supported = new Table(Table.HT);
    String[] subTables = tab.getKeys();

    for (String subTable : subTables) {
      if (subTable.startsWith("_") && subTable.endsWith("_")) {
        // skip
      } else {
        Table    tbl  = tab.getTable(subTable);
        String[] keys = tbl.getKeys();
        for (String key : keys) {
          Table   sup     = tbl.getTable(key);
          String  support = sup.getS("SUPPORT","None").toUpperCase();
          boolean ok      = support.equals("FULL") || support.equals("PARTIAL");
          supported.put(key, ok);
        }
      }
    }
    return supported;
  }

  /** Converts a set of params starting at a given index. */
  private String[] convertParams (String[] tokens, int start) {
    ArrayList<String> params  = new ArrayList<>(tokens.length);
    ArrayList<String> strings = new ArrayList<>();

    for (int i = 1; start < tokens.length; i++,start++) {
      String type = convertParam(tokens[start]);

      if (type.contains("std::string&")) {
        type = type.replace("std::string&", "char"); // star char for "char*" will be added below
        strings.add("fstring_len_t lp"+i);
      }
      params.add((type+"* p"+i));
    }
    params.addAll(strings);

    return params.toArray(StringUtil.EMPTY_STRING_ARRAY);
  }

  /** Converts a single param. */
  private String convertParam (String id) {
    final String orig = id;
    String type = null;

    // CONVERT SPECIAL CASES TO NORMAL
    if (id.indexOf('[') > 0) {
      id = id.substring(id.indexOf('[')+1,id.indexOf(']'));
    }

    if (id.indexOf("d-") > 0) { // for 1d-XX specifiers
      id = id.substring(id.indexOf("d-")+2);
    }

    boolean inputOnly = id.startsWith("<"); // NOTE: MUST be AFTER above special cases. since 3.1.2
    if (inputOnly || id.startsWith(">")) {
      id = id.substring(1);
    }
    inputOnly |= orig.startsWith("<");
    id = id.toUpperCase();

    // CHECK STANDARD MAPPINGS
    for (int i = 0; (type == null) && (i < TYPE_MAP.length); i+=2) {
      if (TYPE_MAP[i].equals(id)) {
        type = TYPE_MAP[i+1];
      }
    }

    // CHECK STRUCTURES
    for (int i = 0; (type == null) && (i < STRUCT_MAP.length); i++) {
      if (STRUCT_MAP[i].equals(id)) {
        type = "void";//STRUCT_MAP[i];
      }
    }

    // NON-STANDARD MAPPINGS
    if (type == null) {
      if (id.startsWith("A")) { type = "std::string&"; }
    }

    // CHECK FOR ERRORS
    if (type == null) {
      throw new WrapException("Unknown type id [ "+id + " ] orig id = [ "+orig+" ] inputOnly = "+inputOnly);
    }

    return (inputOnly? "const " : "") + type;
  }

  /** mangle Fortran function name to C++ by replacing first '$' with '_'
      @param funcName Fortran X-Midas function name (cannot be null)
      @return C++ version of the function name
      @since NeXtMidas 3.1.2
   */
  public static String mangle_name_to_cxx (String funcName) {
    int iDollar = funcName.indexOf('$');
    if (iDollar < 0) return funcName;
    return funcName.substring(0,iDollar) + '_' + funcName.substring(iDollar+1);
  }

  /** Internal exception used for special handling. */
  private static final class WrapException extends RuntimeException {
    /** NOTE: this should be changed if the structure/fields of this class changes. */
    private static final long serialVersionUID = -3864129338255538063L;

    public WrapException (String msg) { super(msg); }
  }
}
