/*
 * Renjin : JVM-based interpreter for the R language for the statistical analysis
 * Copyright Â© 2010-2019 BeDataDriven Groep B.V. and contributors
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, a copy is available at
 * https://www.gnu.org/licenses/gpl-2.0.txt
 */
package org.renjin.gcc;

import org.renjin.gcc.gimple.GimpleCompilationUnit;
import org.renjin.repackaged.guava.base.Charsets;
import org.renjin.repackaged.guava.hash.Hashing;
import org.renjin.repackaged.guava.io.Files;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Tests GCC against random C programs generated by Csmith
 */
public class CsmithDriver {

  private static class TestCase {
    private final String id;
    private String source;
    private String checksum;

    public TestCase(String source, String checksum) {
      this.id = Hashing.sha256().hashString(source, Charsets.UTF_8).toString().substring(0, 8);
      this.source = source;
      this.checksum = checksum;
    }
  }

  private final Gcc gcc;
  private File rootDir;
  private File workingDir;
  private final File resourceDir;
  private final File sourceDir;
  private final File outputDir;

  private int nextTestId = 1;

  private List<TestCase> testCases = new ArrayList<>();


  public CsmithDriver() throws IOException {
    rootDir = new File("/home/alex/dev/renjin/tools/gcc-bridge/compiler");
    resourceDir = new File(rootDir, "src/test/resources/org/renjin/gcc/csmith");
    resourceDir.mkdirs();

    sourceDir = new File(rootDir, "src/test/java/org/renjin/gcc/csmith");
    outputDir = new File(rootDir, "target/test-classes");

    workingDir = Files.createTempDir();
    gcc = new Gcc(workingDir);
    gcc.addIncludeDirectory(new File("/usr/local/include/csmith-2.3.0"));
    gcc.extractPlugin();
    gcc.setLink(true);
    gcc.setDebug(true);
  }

  public void run() throws Exception {
    System.out.println("Working directory: " + workingDir);
    for (int i = 0; i < 3; i++) {
      nextTest();
    }

    writeJunit();

  }

  private void writeJunit() throws IOException {

    File javaSource = new File(sourceDir, "CsmithTest.java");
    try(PrintWriter pw = new PrintWriter(javaSource)) {

      pw.println("package org.renjin.gcc.csmith");
      pw.println();
      pw.println("import org.junit.Test;");
      pw.println();
      pw.println("public class CsmithTest extends AbstractCsmithTest {");
      pw.println();

      int testIndex = 0;
      for (TestCase testCase : testCases) {

        File sourceFile = new File(resourceDir, testCase.id + ".c");
        Files.write(testCase.source, sourceFile, Charsets.UTF_8);

        pw.println();
        pw.println("  @Test");
        pw.println("  public void test" + (testIndex++) + "() throw Exception {");
        pw.println("    test(\"" + testCase.id + "\", \"" + testCase.checksum + "\");");
        pw.println("  }");
      }
      pw.println();
      pw.println("}");
    }
  }

  private void nextTest() throws Exception {
    int testId = nextTestId++;
    File sourceFile = generateSource(testId);

    gcc.compileToGimple(sourceFile);

    String checkSum = executeWithGcc();

    TestCase testCase = new TestCase(Files.toString(sourceFile, Charsets.UTF_8), checkSum);

    testCases.add(testCase);
  }


  /**
   * Invokes CSmith to generate a random C program.
   */
  private File generateSource(int testId) throws IOException, InterruptedException {
    File sourceFile = new File(workingDir, "test" + testId + ".c");
    int exitCode = new ProcessBuilder("csmith")
        .directory(workingDir)
        .redirectOutput(sourceFile)
        .start()
        .waitFor();

    if(exitCode != 0) {
      throw new RuntimeException("Exit code: " + exitCode);
    }

    return sourceFile;
  }

  /**
   * Executes the random program with GCC to obtain the expected checksum.
   */
  private String executeWithGcc() throws IOException, InterruptedException {

    File binary = new File(workingDir, "a.out");
    File expectedOutput = new File(workingDir, "expected.txt");


    int exitCode = new ProcessBuilder(binary.getAbsolutePath())
        .directory(workingDir)
        .redirectOutput(expectedOutput)
        .start()
        .waitFor();

    if(exitCode != 0) {
      throw new RuntimeException("Exit code: " + exitCode);
    }

    String output = Files.toString(expectedOutput, Charsets.UTF_8);
    if(!output.startsWith("checksum = ")) {
      throw new UnsupportedOperationException(output);
    }

    int startChecksum = "checksum = ".length();
    int endChecksum = output.indexOf('\n');

    return output.substring(startChecksum, endChecksum);

  }

  private void compileAndRun(int testId, File sourceFile) throws Exception {
    GimpleCompilationUnit unit = gcc.compileToGimple(sourceFile);
    GimpleCompiler compiler = new GimpleCompiler();
    compiler.setClassName("Test" + testId);
    compiler.setOutputDirectory(workingDir);
    compiler.setVerbose(true);
    compiler.compile(Collections.singletonList(unit));

    Class<?> testClass = Class.forName("Test" + testId);
    Method mainMethod = findMainMethod(testClass);

    mainMethod.invoke(null);
  }

  private Method findMainMethod(Class<?> compiledClass) {
    for (Method method : compiledClass.getMethods()) {
      if(method.getName().equals("main")) {
        return method;
      }
    }
    throw new IllegalStateException("No main method found");
  }



  public static void main(String[] args) throws Exception {

    CsmithDriver driver = new CsmithDriver();
    driver.run();

  }
}
