/*

Copyright: All contributers to the Umple Project

This file is made available subject to the open source license found at:
http://umple.org/license

Command line tools: To compile Java, Php etc. generated by Umple

*/

namespace cruise.umple.compiler;

class CodeCompiler {

  depend java.util.*;
  depend java.util.regex.*;
  depend java.io.*;
  depend cruise.umple.compiler.*;
  depend cruise.umple.compiler.exceptions.*;
  depend cruise.umple.util.StringFormatter;
  depend javax.tools.ToolProvider;
  depend javax.tools.JavaCompiler;

  public static String console;
  public static boolean subsequentCompilation = false; // true if compile used more than once to quell messages

  public static boolean compile(UmpleModel model, String generator, boolean useExec, boolean beQuiet, String entryClass, String errorFileName, String... extraArgs) {
    boolean wasSuccessful = true;
    String extra = "";
    for(String arg:extraArgs)
    {
      extra+=arg;
    }
    for (UmpleElement currentElement : model.getUmpleElements())
    {
      if ("external".equals(currentElement.getModifier()))
      {
        continue;
      }
      if (entryClass.equals("-") || entryClass.equals(currentElement.getName())) {
        if(generator.equals("Java")) {
          wasSuccessful = wasSuccessful & compileJava(currentElement, model, useExec, beQuiet, errorFileName,  extra);
          subsequentCompilation = true;
        }
        else if (generator.equals("Php")) {
          wasSuccessful = wasSuccessful & compilePhp(currentElement, model, beQuiet, extra);
          subsequentCompilation = true;
        }
        else if (generator.equals("Python")) {
          wasSuccessful = wasSuccessful & compilePython(currentElement, model, beQuiet,errorFileName, extra);
          subsequentCompilation = true;
        }
        else {
          System.out.println("Compilation of generated code not supported yet for generator "+generator);
        }
      }
    }
    return wasSuccessful;
  }
 
  private static boolean compilePython(UmpleElement aClass, UmpleModel model, boolean beQuiet,String errorFileName, String args) {
    String path="";
    String base="";
    boolean isMain=false;
    boolean runtimeErr=false;
    for (GenerateTarget gt : model.getGenerates()) {
      if (gt.getLanguage().equals("Python")) {
        base = StringFormatter.addPathOrAbsolute( 
            model.getUmpleFile().getPath() +File.separator+ gt.getPath(), "");
        path = base + aClass.getPackageName().replace(".", File.separator);
      }
    }

    for(UmpleClass mainClass: CodeCompiler.getMainClasses(model)){
      if(aClass.getName().equals(mainClass.getName())){
        isMain=true;
      }
    }
    
    String filename = path + File.separator + aClass.getName() + ".py";
    int retval=0;

    try {
      if(base.contains(" ") && !(beQuiet && subsequentCompilation)) {
        System.err.println("Warning: Compiling generated Python from a directory or path containing a space character may produce an error message from the Python compiler.");
      }
      
      BufferedReader reader;
      //used python3 for linux, use python for windows
      String pythonVersion="python3";

      if(System.getProperty("os.name").toLowerCase().contains("windows"))
        pythonVersion="python";

      String cmd=pythonVersion+" -m py_compile " +filename;
      String cmd2 = pythonVersion + " " + filename;

      //ProcessBuilder processBuilder = new ProcessBuilder(pythonVersion, "-m", "compileall", filename);
      //Process p = processBuilder.start();
      Process p = Runtime.getRuntime().exec(cmd);
      reader=new BufferedReader(new InputStreamReader(p.getErrorStream())); 

      // All output of the above should go to standard output
      try {
        retval=p.waitFor();
      }
      catch (InterruptedException e) {}

      String line;
      if(reader.ready()) {
        line = reader.readLine();
      }
      else {
        // no syntax error, check runtime error
        if(isMain){          
          //check runtime error
          Process p2 = Runtime.getRuntime().exec(cmd2);
          reader=new BufferedReader(new InputStreamReader(p2.getErrorStream()));

          try {
            retval=p2.waitFor();
          }
          catch (InterruptedException e) {}

          if(reader.ready()) {
            line = reader.readLine();
            runtimeErr=true;
          }else{
            return (retval==0);
          }
        }else{
           return (retval==0);
        }    
      }

      StringBuilder builder = new StringBuilder();

      while (line!=null) {
        //System.err.println(translateLineToUmplePython(line, base+model.getUmpleFile().getPath(), filename, runtimeErr));
        //System.err.println(line);
        // DEBUG
        //builder = builder.append(translateLineToUmplePython(line, base+"model.ump", filename)+"\n");
        builder.append(line+"\n");
        if(reader.ready()) {
          line = reader.readLine();
        }
        else {
          line = null;
        }
      }

      String translatedOutput=translateLineToUmplePython(builder.toString(), base+"model.ump", filename, runtimeErr);
      System.err.println(translatedOutput);

      if(retval!=0 && errorFileName != null) {
        String error = translatedOutput;
          try
          {
            File newFile = new File(errorFileName);
            newFile.createNewFile();   
          }
          catch (IOException ex)  
          {
            System.err.println(ex);
          }

          // Write to the file
          try (PrintWriter fileout = new PrintWriter(errorFileName)) 
          { 
//DEBUG to test if the the compiler writes to the file
            fileout.println(error);
          }  
          catch (FileNotFoundException ex) 
          {
            System.err.println(ex);
          }
      }
      
    } catch (IOException e) {
      println(e.getMessage());
      retval = 999;
    }

    return (retval == 0);
  }  

  private static boolean compilePhp(UmpleElement aClass, UmpleModel model, boolean beQuiet, String args) {
    String path="";
    String base="";
    for (GenerateTarget gt : model.getGenerates()) {
      if (gt.getLanguage().equals("Php")) {
        base = StringFormatter.addPathOrAbsolute( 
            model.getUmpleFile().getPath() +File.separator+ gt.getPath(), "");
        path = base;
      }
    }
    String filename = path + File.separator + aClass.getName() + ".php";
    int retval=0;

    try {
      if(base.contains(" ") && !(beQuiet && subsequentCompilation)) {
        System.err.println("Warning: Compiling generated Php from a directory or path containing a space character may produce an error message from the PhP compiler.");
      }
      
      BufferedReader reader;
      
      Process p = Runtime.getRuntime().exec("php -l -e " +filename);
      reader=new BufferedReader(new InputStreamReader(p.getInputStream())); 

      // All output of the above should go to standard output
      try {
        retval=p.waitFor(); 
      }
      catch (InterruptedException e) {}

      String line;
      if(reader.ready()) {
        line = reader.readLine();
      }
      else {
        line = null;
      }

      while (line!=null) {
        if(!line.startsWith("No syntax errors detected in")) {
          System.err.println(line);
          retval = 1;
        }

        if(reader.ready()) {
          line = reader.readLine();
        }
        else {
          line = null;
        }
      }
    } catch (IOException e) {
      println(e.getMessage());
      retval = 999;
    }
    return (retval == 0);
  }
  
  // Compiles graphviz files to various outputs: .svg (default), using dot
  public static boolean genDotDiagram (UmpleModel model,
    GenerateTarget gt, boolean beQuiet, String graphicFormat) {

    String generator = gt.getLanguage();
    
    String fileroot = model.getUmpleFile().getFileName();
    if (fileroot.endsWith(".ump")) {
      fileroot = fileroot.substring(0, fileroot.length() - 4);
    }
    if(generator.equals("GvClassDiagram")) {
      fileroot+="cd";
    }
    else if(generator.equals("GvEntityRelationshipDiagram")) {
      fileroot+="erd";
    }
    else if(generator.equals("GvFeatureDiagram")) {
      fileroot+="GvFeatureDiagram";
    }
    else if(generator.equals("GvClassTraitDiagram")) {
      fileroot+="cdt";
    }
    // Note that GV state diagrams do not have a fileroot suffix

    String path="";
    String extraPath=gt.getPath();

    if(extraPath.startsWith("/")) {
      // Absolute
      path=extraPath;
    }
    else
    {
      path=model.getUmpleFile().getPath() + File.separator + extraPath;
    }
    
    String inputFullPath=path + File.separator + fileroot + ".gv";
    String outputFullPath=path + File.separator + fileroot + "."+ graphicFormat;

    int retval=0;

    try {
      if(path.contains(" ") && !beQuiet) {
        System.err.println("Warning: Compiling generated Graphviz files from a directory or path containing a space character may produce an error message from the PhP compiler.");
      }
      
      BufferedReader reader;
      
      Process p = Runtime.getRuntime().exec("dot -T"+graphicFormat+" "+inputFullPath+" -o "+outputFullPath);
      reader=new BufferedReader(new InputStreamReader(p.getInputStream())); 

      // All output of the above should go to standard output
      try {
        retval=p.waitFor(); 
      }
      catch (InterruptedException e) {}

      String line;
      if(reader.ready()) {
        line = reader.readLine();
      }
      else {
        line = null;
      }

      while (line!=null) {
        if(reader.ready()) {
          line = reader.readLine();
        }
        else {
          line = null;
        }
      }
    } catch (IOException e) {
      println(e.getMessage());
      if(e.getMessage().startsWith("Cannot run program")) {
        println("It is necessary to install the graphviz package that includes the dot program in order to convert .gv files directly to graphical output files");
      }
      retval = 999;
    }
    return (retval == 0);
  }

  
  private static boolean compileJava(UmpleElement aClass, UmpleModel model, boolean useExec, boolean beQuiet, String errorFileName, String args) {
    String path="";
    String base="";
    for (GenerateTarget gt : model.getGenerates()) {
      if (gt.getLanguage().equals("Java")) {
        base = StringFormatter.addPathOrAbsolute( 
            model.getUmpleFile().getPath() +File.separator+ gt.getPath(), "");
        path = base + aClass.getPackageName().replace(".", File.separator);
      }
    }
    String filename = path + File.separator + aClass.getName() + ".java";
    boolean successful = true;
    PipedOutputStream out = null;

    try {
      if(base.contains(" ") && !(beQuiet && subsequentCompilation)) {
        System.err.println("Warning: Compiling generated Java from a directory or path containing a space character may produce an error message from the Java compiler.");
      }
      
      BufferedReader reader;
      
      // Method 1: Use tool provider
      if(!useExec) {
        // Determine a safe size of the output stream to capture any error messages
        // The larger the file the more error messages are likely
        // There were crashes when the buffer was not large enough for the number of messages
        File theFile = new File(filename);
        int bufferSizeToUse = Math.max(16384,((int)theFile.length() )*3);

        PipedInputStream in = new PipedInputStream(bufferSizeToUse);
        out = new PipedOutputStream(in);
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        compiler.run(null, null, out, "-cp",base,filename);

        reader=new BufferedReader(new InputStreamReader(in)); 
      }
      // Method 2: Use exec to compile files
      // This older approach seemed to use more CPU resources but may be more reliable
      else {
        Process p = Runtime.getRuntime().exec("javac -cp " + base + " " +filename+args);
        reader=new BufferedReader(new InputStreamReader(p.getErrorStream())); 
      }
      String line;

      if(reader.ready()) {
        line = reader.readLine();
      }
      else {
        line = null;
      }

      StringBuilder builder = new StringBuilder();

      while (line!=null) {
        System.err.println(translateLineToUmple(line, model));
        builder = builder.append(translateLineToUmple(line, model)+"\n");
        if(reader.ready()) {
          line = reader.readLine();
        }
        else {
          line = null;
        }
        successful = false;
      }

      // Write to the error file if given
      if(!successful && errorFileName != null) {
        String error = builder.toString().trim();
          try
          {
            File newFile = new File(errorFileName);
            newFile.createNewFile();   
          }
          catch (IOException ex)  
          {
            System.err.println(ex);
          }

          // Write to the file
          try (PrintWriter fileout = new PrintWriter(errorFileName)) 
          {
            fileout.println(error);
          }  
          catch (FileNotFoundException ex) 
          {
            System.err.println(ex);
          }
      }

    } catch (IOException e) {
      println(e.getMessage());
    }
    finally {
      if (out != null) {
        // close the PipedOutputStream cleanly
        try {
          out.close();
        }
        catch (IOException e) {} 
      }
    }
    
    return successful;
  }

  private static String translateLineToUmplePython(String msg, String model, String filename, boolean runtime) {
      String[] lines = msg.split("\n");
      String line;
      boolean errored=false;
      StringBuilder builder = new StringBuilder();

      // find the main method line number in model.ump
      int modelMainLine=0;

      //Regex pattern for python main
      String regex = "public\\s+static\\s+void\\s+main\\s*\\(\\s*String\\s*\\[\\]\\s*argc\\s*\\)\\s*Python\\s*";
      //Regex pattern for normal main
      String regex2 = "public\\s+static\\s+void\\s+main\\s*\\(\\s*String\\s*\\[\\]\\s*argc\\s*\\)\\s*";

      try (BufferedReader br = new BufferedReader(new FileReader(model))) {
          String modelLine;
          int lineNumber=0;

          // Read lines from the file
          while ((modelLine = br.readLine()) != null) {
              lineNumber++;
              // Check if the line match the Pattern1
              Pattern pattern = Pattern.compile(regex);
              Matcher matcher = pattern.matcher(modelLine);
              if(matcher.find()){
                System.err.println("matched to python pattern");
                modelMainLine=lineNumber;
                break;
              }

              Pattern pattern2 = Pattern.compile(regex2);
              Matcher matcher2 = pattern2.matcher(modelLine);
              if(matcher2.find()){
                System.err.println("matched to normal pattern");
                modelMainLine=lineNumber;
              }
          }
      } catch (IOException e) {
          System.err.println("Error reading the file: " + e.getMessage());
      }

      // find main line in python file
      int pythonMainLine = 0;

      try (BufferedReader br2 = new BufferedReader(new FileReader(filename))) {
          String pythonLine;
          int lineNumber = 0;

          // Read lines from the file
          while ((pythonLine = br2.readLine()) != null) {
              lineNumber++;
              // Check if the line contains the string "@staticmethod", main is one line under it
              if (pythonLine.contains("@staticmethod")) {
                  pythonMainLine = lineNumber+1;
                  break;
              }
          }
      } catch (IOException e) {
          System.err.println("Error reading the file: " + e.getMessage());
      }

      System.err.println("modelMainLine: "+modelMainLine);
      System.err.println("pythonMainLine: "+pythonMainLine);
      int gap = modelMainLine - pythonMainLine;

      for (int i = 0; i < lines.length; i++) {
          line = lines[i];
          if (!line.contains("line")) {
              builder.append(line + "\n");
              // System.out.println("no line: " + line);
          } else {
              // convert the line for first error
              if(!errored){
                String[] words = line.split(" ");
                  for (int k = 0; k < words.length; k++) {
                      if (words[k].equals("line")) {
                          String num = words[k + 1];
                          System.out.println("num = " + num);
                          boolean convert = false;
                          int lineNum = 0;
                          while (!convert) {
                              try {
                                  lineNum = Integer.parseInt(num);
                                  convert = true;
                              } catch (NumberFormatException e) {
                                  num = num.substring(0, num.length() - 1);
                              }
                          }
                          System.err.println("num: "+num);
                          builder.append("model.ump:" + (Integer.parseInt(num) + gap) + ": error \nCompiler msg: "
                                  + line + "\n");
                          errored=true;
                          break;
                      }
                  }
              }else{
                builder.append(line+"\n");
              }
          }
      }
      return builder.toString();
  }

  // To do: Use model to determine generation path
  private static String translateLineToUmple(String line, UmpleModel model) {
    String modifiedLine = line;
    StackTraceElement ust;
    String javaFileName;
    int javaLineNumber;
    String lineParts[] = null;
    if(File.separator.equals("\\")) {
    	// Windows System
    	String tempLineParts[] = line.split(":", 4);
    	if(tempLineParts.length == 4) {
    		lineParts = new String[3];
    		lineParts[0] = tempLineParts[0] + ":" + tempLineParts[1];
    		lineParts[1] = tempLineParts[2];
    		lineParts[2] = tempLineParts[3];
    	} else {
    		lineParts = line.split(":",3);
    	}
    }
    
    else {
    	// Mac or Linux
    	lineParts = line.split(":",3);
    }
    
    // Case 1: No colons so nothing to translate
    if(lineParts.length < 3) {
      return line;
    }
    
    // case 2: no colon before .java, so nothing to translate
    if (!lineParts[0].endsWith(".java")) {
      return line;
    }
    
    // We have a line that needs translating
    String javaFileParts[] =
      lineParts[0].split("\\"+System.getProperty("file.separator"),-1);
    String javaFile = lineParts[0];//javaFileParts[javaFileParts.length-1];
    
    String declaringClass = javaFile.substring(0,javaFile.length()-5);
    for(int i=javaFileParts.length-2; i>=0 ; i--) {
      if(!javaFileParts[i].equals(".")) {
        declaringClass = javaFileParts[i]+"."+declaringClass;
      }
    }
    // System.err.println("**"+declaringClass+"**"+javaFile); //debug
    
    //Code Smell: javaToUmpleStackTrace needs to not be static
    ust = cruise.umple.util.ExceptionDumper.javaToUmpleStackTrace(
      new StackTraceElement(
        declaringClass,
        "",
        javaFile,
        Integer.parseInt(lineParts[1])),  "");
    
    // System.err.println("*"+lineParts[0]+"*"); // Debug
    
    if(ust==null) {
      return line;
    }
    return(ust.getFileName()+":"+ust.getLineNumber()+":"+lineParts[2]);
  }

  private static void println(String output)
  {
    console += output + System.lineSeparator();
    System.out.println(output);
  }

  public static String getSimpleFileName(String fileName)
  {

    int lastIndex = fileName.lastIndexOf("/");
    if (lastIndex == -1)
    {
      return fileName;
    }
    else
    {
      return fileName.substring(lastIndex+1, fileName.length());
    }
  }
  
  public static List<UmpleClass> getMainClasses(UmpleModel model)
  {
    List<UmpleClass> mainClasses = new ArrayList<UmpleClass>();
    for(UmpleClass uClass:model.getUmpleClasses())
    {
	if (!uClass.hasMethods()) {
    		continue;
	}
      for (Method aMethod : uClass.getMethods()) 
      {
        
        if(!aMethod.getExistsInLanguage("Java")) {
          continue;
        }
        String methodModifier = aMethod.getModifier().equals("") ? "public" : aMethod.getModifier();
        String methodName = aMethod.getName();
        String methodType = aMethod.getType();
        
	if(methodName.equals("main")&&methodType.equals("void")&&methodModifier.contains("public")&&methodModifier.contains("static"))
        {
          mainClasses.add(uClass);
	}

	} // end for loop for all methods in class

	} // end for loop for all classes in model



    return mainClasses;
  }

}
