package nxm.sys.intr;

import static nxm.sys.lib.Build.BUILD_CLASS_FILES;
import static nxm.sys.lib.Build.BUILD_HOST_FILES;
import static nxm.sys.lib.Build.BUILD_NATIVE_FILES;

//import java.util.Arrays;

import static nxm.sys.lib.Build.BUILD_TYPE_LIST;
import static nxm.sys.lib.Build.CPP_SOURCE;
import static nxm.sys.lib.Build.C_SOURCE;
import static nxm.sys.lib.Build.FOR_SOURCE;

import java.io.File;
import java.util.ArrayList;

import nxm.sys.lib.Args;
import nxm.sys.lib.Foreign;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;

/** Compiles NeXtMidas Java, C++, C and Fortran files for an option tree.
    <br>
    This command primarily finds the files to compile and then passes them to
    {@link nxm.sys.lib.Build} for compilation.

    @author  Matthew Harrell / J. Czechowski IV / Neon Ngo
    @since NeXtMidas 2.5.0
*/
public class Build extends Intrinsic {

  /** The name of the "area" macros are located in. <br><br>Value: {@value } */
  public static final String MACRO_AREA=nxm.sys.lib.Build.MACRO_AREA;

  /** The name of the "area" intrinsics are located in. <br><br>Value: {@value} */
  public static final String INTR_AREA=nxm.sys.lib.Build.INTR_AREA;

  /** The name of the "area" primitives are located in. <br><br>Value: {@value} */
  public static final String PRIM_AREA=nxm.sys.lib.Build.PRIM_AREA;

  /** The name of the "area" X-Midas host primitives are located in. <br><br>Value: {@value} */
  public static final String HOST_AREA=nxm.sys.lib.Build.HOST_AREA;

  /** The name of the "area" where tests are located in. <br><br>Value: {@value} */
  public static final String TEST_AREA=nxm.sys.lib.Build.TEST_AREA;

  /** The name of the "area" where C/C++ libraries are located in. <br><br>Value: {@value} */
  public static final String LIBC_AREA=nxm.sys.lib.Build.LIBC_AREA;

  /** The name of the "area" where Fortran libraries are located in. <br><br>Value: {@value} */
  public static final String LIBF_AREA=nxm.sys.lib.Build.LIBF_AREA;

  /** The name of the "area" where math libraries are located in. <br><br>Value: {@value} */
  public static final String LIBM_AREA=nxm.sys.lib.Build.LIBM_AREA;

  private static final String[] ALL_SOURCE_EXTENSIONS = nxm.sys.lib.Build.ALL_SOURCE.split("[|]"); // All extensions

  private boolean noNative;  // No native files should be built.
  private boolean noHost;    // No host files should be built.
  private boolean debug;     // Enable debug info.
  private boolean generate;  // Indicates if math libs should be generated.
  private boolean keepFiles; // Intermediate files should be kept.
  private boolean warn;      // display warning messages (e.g. when no files are found to build). Since 3.3.0.

  private nxm.sys.lib.Table bldFlags; // Any build flags.
  /** type of files to build (e.g. Java,Native,Host). See BUILD_TYPE_LIST. */
  private int typesToBuild;

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

  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public int process() {
    String optPath = "," + (String)M.results.get("OPT.PATH") + ",";
    String name    = MA.getCS("NAME");
    String area    = MA.getCS("AREA");
    String opt     = MA.getU("OPT","SYS");
    if (name.equals("*")) name = "ALL"; // Since 2.3.0
    if (area.equals("*")) area = "ALL"; // Since 2.3.0

    boolean hostDef =  area.equalsIgnoreCase(HOST_AREA) || (optPath.indexOf(",XBC,") >=0 );

    noNative  = !getArgs().getState("/DONATIVE",  true);
    noHost    = !getArgs().getState("/DOHOST",    hostDef);
    generate  = getArgs().getState("/DOGENERATE", true);
    bldFlags  = getArgs().getTable("/BLDFLAGS",   null);
    debug     = MA.getState("/DEBUG");
    keepFiles = MA.getState("/KEEP");

    int defBuildTypes = BUILD_CLASS_FILES | (noNative? 0 : BUILD_NATIVE_FILES) | (noHost? 0 : BUILD_HOST_FILES);
    typesToBuild = MA.getOptionMask("/BLDTYPES", BUILD_TYPE_LIST, defBuildTypes);  // since 3.3.1: takes precedence over /DONATIVE & /DOHOST
    boolean defWarnNoFiles = (typesToBuild & BUILD_CLASS_FILES) != 0; // default to true if building Java files, otherwise false
    warn = MA.getState("/WARN", defWarnNoFiles); // since 3.3.0: to warn when no files found to compile

    int filesCompiled = findAndBuild(opt, area, name);

    // exit shell to force reload of new class files
    if (filesCompiled > 0) {
      if (!MA.getState("/NOEXIT",(M.macro!=null))
          && M.terminal.isExtCmdSupported() && !Shell.isOneShot()) {
        Foreign.runExternal(M,""); // Automatically restarts the shell
      }
      else if (M.terminal.isExtCmdSupported() && Shell.isOneShot()); // Do Nothing
      else M.warning("You must exit the shell to reload newly compiled files");
    }

    return FINISH;
  }

  /** Invoked to find all the files in the specified area and send them to the build processor.
      <br>
      To process all files within a given option tree use the following:
      <pre>
        findAndBuild(<i>opt</i>, "ALL", "ALL")
      </pre>
      To process all files within a given area of an option tree use the following:
      <pre>
        findAndBuild(<i>opt</i>, "ALL", <i>area</i>)
      </pre>
      The <code>area</code> may be left blank (<code>""</code>) in the case of a
      command that is already in the command dictionary (NeXtMidas will use the
      command dictionary entry to determine which area it is in).

      @param opt  The option to search.
      @param area The area within the option to search (e.g. <code>prim</code>).
      @param name The name of the file to process.

      @return The number of files that are compiled or <code>-1</code> on error.
   */
  public int findAndBuild (String opt, String area, String name) {
    int numCompiled = 0;

    // If a command is given but an area is not,
    // attempt to determine the area based on it's dictionary entry.
    if ((area.length() == 0) && (!name.equalsIgnoreCase("ALL"))) {
      String command = name;

      if (name.indexOf('.') > 0) command = name.substring(0, name.indexOf('.'));

      Args targs = Shell.parseCommand(this.getContext(), command );

      switch (targs.support) {
        case 'M':  area = MACRO_AREA; name = targs.name; break;
        case 'I':  area = INTR_AREA;  name = targs.name; break;
        case 'P':  area = PRIM_AREA;  name = targs.name; break;
        case 'H':  area = HOST_AREA;  name = targs.name; break;
        case 'O':                                        break; // External OS command
        case 'U':                                        break; // Internal OS command
        default:                      name = targs.name; break;
      }
      opt = targs.option;
    }

    // Convert the file name as appropriate for the area the file is in,
    // if it's a macro, intrinsic or primitive
    if (!name.equalsIgnoreCase("ALL")) {
      if (area.equals(MACRO_AREA) || area.equals(PRIM_AREA) || area.equals(HOST_AREA)) {
        name = name.toLowerCase();
      }
      else if (area.equals(INTR_AREA)) {
        name = name.substring(0,1).toUpperCase() + name.substring(1).toLowerCase();
      }
    }

    // find and build the specified files
    if (name.endsWith(".mm")) {
      nxm.sys.lib.Build.compileMacro(opt, name);
      numCompiled = 1;
    }
    else {
      nxm.sys.lib.Build build = nxm.sys.lib.Build.newInstance(M,verbose,debug,keepFiles,bldFlags);
      if (area.equalsIgnoreCase("ALL")) {
        // since 2.9.2: Break out building of ALL areas to two groups (to help on
        //              platforms like Windows with a max limit to javac) (BUG 2377)
        // since 3.1.2: Use properties from build.props to allow option trees to
        //              customize code areas to be included automatically with a
        //              "make code <optname>" or "build all all <optname>" (BUG 2588)
        String libs_area_list  = nxm.sys.lib.Build.getPropertyStatic(opt, "build.codeareas.libs");
        String other_area_list = nxm.sys.lib.Build.getPropertyStatic(opt, "build.codeareas.other");
        numCompiled  = build.compile(opt, libs_area_list, typesToBuild);  // build these areas first
        numCompiled += build.compile(opt, other_area_list, typesToBuild); // build these areas next
      }
      else if (name.equalsIgnoreCase("ALL")) {
        numCompiled = build.compile(opt, area, typesToBuild);
      }
      else if (nxm.sys.lib.Build.ALL_SOURCE_FILTER.accept(null,name)) {
        File file = new File(M.io.name(area, name, "", opt));
        numCompiled = build.compile(file, opt);
      }
      else {
        String path = M.io.name(area, "", "", opt);
        // NTN: moved this outside of for loop (so we are not constantly creating
        //      a new instance of the Build library)
        for (int i = 0; i < ALL_SOURCE_EXTENSIONS.length; i++) {
          String   extension = ALL_SOURCE_EXTENSIONS[i].substring(1); // change ".java" to "java"
          String[] fileNames = M.io.listFiles(path, name, extension);
          if (fileNames != null && fileNames.length > 0) { // only need to compile if files are found
            ArrayList<File> filesList = new ArrayList<>(fileNames.length);
            for (int jj = 0; jj < fileNames.length; jj++) {
              String filename = fileNames[jj];
              boolean isC   = C_SOURCE.equals(ALL_SOURCE_EXTENSIONS[i]);
              boolean isCpp = CPP_SOURCE.equals(ALL_SOURCE_EXTENSIONS[i]);
              if (isC || isCpp) {
                int indexPeriod = filename.lastIndexOf('.');
                String basefilename = (indexPeriod>0)? filename.substring(0,indexPeriod) : filename;
                // NTN: TODO: this and nxm.sys.lib.Build.compile (String opt, String areaList, int types)
                //            have duplicate precedence logic (Fortran, C++, then C) for host primitives
                File forFile = new File(path, basefilename + FOR_SOURCE);
                if (forFile.exists())  continue;  // Fortran primitive takes precedence over C or C++
                if (isC) {
                  File cppFile = new File(path, basefilename + CPP_SOURCE);
                  if (cppFile.exists()) continue; // C++ primitive takes precedence over C
                }
              }
              filesList.add(new File(path, filename)); // add to list of files to compile
            }
            File[] files = filesList.toArray(new File[0]);
            numCompiled += build.compile(files, opt, area);
  // if (debug) System.out.println("Build: ext = "+ extension +" numCompiled = "+numCompiled +" fileNames[] = "+Arrays.toString(fileNames));
          }
        }
      }
      updateLastBuildResultsTable(build, opt);  // update table that keeps track of build status
    }

    if (numCompiled > 0) {
      String message = "Build "+numCompiled+" files for opt="+opt;
      if (!area.equalsIgnoreCase("ALL")) { message += " area=" + area; }
      if (!name.equalsIgnoreCase("ALL")) { message += " name=" + name; }
      M.info (message);
    }
    else if (numCompiled == 0) {
      String fileTypes = Parser.mask2s(BUILD_TYPE_LIST, typesToBuild);
      String msg = StringUtil.padRight("** BUILD: No "+fileTypes+" files found.",51) + "**";
      if (warn) {
        M.warning("*****************************************************");
        M.warning(msg);
        M.warning("*****************************************************");
      } else if (verbose) {
        M.info(   "*****************************************************");
        M.info(msg);
        M.info(   "*****************************************************");
      }
    }
    else {
      M.warning(  "*****************************************************");
      M.warning(  "** BUILD FAILED: See error messages listed above.  **");
      M.warning(  "*****************************************************");
    }

    // since 3.3.1: the /COUNT=resname switch is for INTERNAL USE ONLY and may change between releases
    String resname = MA.getU("/COUNT");
    if (resname.length() > 0) {
      MR.put(resname, numCompiled); // since 3.3.1 for BUG 2705
    }
    // end since 3.3.1: /COUNT=resname for INTERNAL USE ONLY

    return numCompiled;
  }

  /** LASTBUILDRESULTS is a table with an build status entry for each option tree built
   *  Add this option tree's build status and, if applicable, build errors.
   *
   *  @param build nxm.sys.lib.Build build library instance
   *  @param opt   option tree being built
   */
  private void updateLastBuildResultsTable (nxm.sys.lib.Build build, String opt) {
    if (M != null) {
      String optTableString = "{STATUS="+M.getBuildStatus()+",ERRORS="+M.getBuildErrors()+"}";
      String fullBuildTable = "{"+opt+"="+optTableString+"}";
      nxm.sys.lib.Table resTable = Shell.getCurrent().M.results;
      String buildResName = MA.getS("/BUILDRESULTS","LASTBUILDRESULTS");
      if (resTable.containsKey(buildResName)) {
        nxm.sys.lib.Table envBuildTbl = (nxm.sys.lib.Table)resTable.get(buildResName);
        if (!envBuildTbl.isEmpty()) {
          nxm.sys.lib.Table thisOpt = new nxm.sys.lib.Table(optTableString);
          envBuildTbl.put(opt, thisOpt,nxm.sys.lib.Table.FORCE);
        }
        else {
          nxm.sys.lib.Table fullTable = new nxm.sys.lib.Table(fullBuildTable);
          resTable.put(buildResName, fullTable,nxm.sys.lib.Table.FORCE);
        }
      }
      else {
        nxm.sys.lib.Table fullTable = new nxm.sys.lib.Table(fullBuildTable);
        resTable.put(buildResName, fullTable,nxm.sys.lib.Table.FORCE);
      }
    }
  }
}
