/**
   * checkProgram invokes the Haskell compiler on a given file and reports the output.
   *
   * @param pathToProgramFile Specifies the file or folder containing that should be compiled.
   *     (accepts .lhs and .hs files)
   * @param compilerName The compiler to be used (usually ghc).
   * @param compilerFlags Additional flags to be passed to the compiler.
   * @throws FileNotFoundException Is thrown when the file in pathToProgramFile cannot be opened
   * @throws BadCompilerSpecifiedException Is thrown when the given compiler cannot be called
   * @return A {@link CompilerOutput} that contains all compiler messages and flags on how the
   *     compile run went.
   * @throws BadFlagException When ghc doesn't recognize a flag, this exception is thrown.
   */
  @Override
  public CompilerOutput checkProgram(
      Path pathToProgramFile, String compilerName, List<String> compilerFlags)
      throws FileNotFoundException, BadCompilerSpecifiedException, BadFlagException {

    Process compilerProcess = null;

    try {
      // create compiler invocation.
      List<String> compilerInvocation =
          createCompilerInvocation(pathToProgramFile, compilerName, compilerFlags);

      ProcessBuilder compilerProcessBuilder = new ProcessBuilder(compilerInvocation);

      // make sure the compiler stays in its directory.
      compilerProcessBuilder.directory(pathToProgramFile.getParent().toFile());

      compilerProcess = compilerProcessBuilder.start();
      // this will never happen because createCompilerInvocation never
      // throws this Exception. Throw declaration needs to be in method
      // declaration because of the implemented Interface although we
      // never use it in the HaskellCompileChecker
    } catch (CompilerOutputFolderExistsException e) {
      LOGGER.severe(
          "A problem while compiling, which never should happen, occured" + e.getMessage());
    } catch (BadCompilerSpecifiedException e) {
      throw new BadCompilerSpecifiedException(e.getMessage());
    } catch (IOException e) {

      // If we cannot call the compiler we return a CompilerOutput
      // initialized with false, false, indicating
      // that the compiler wasn't invoked properly and that there was no
      // clean Compile.
      CompilerOutput compilerInvokeError = new CompilerOutput();
      compilerInvokeError.setClean(false);
      compilerInvokeError.setCompilerInvoked(false);
      return compilerInvokeError;
    }

    // Now we read compiler output. If everything is ok ghc reports
    // nothing in the errorStream.
    InputStream compilerOutputStream = compilerProcess.getErrorStream();
    InputStreamReader compilerStreamReader = new InputStreamReader(compilerOutputStream);
    BufferedReader compilerOutputBuffer = new BufferedReader(compilerStreamReader);
    String line;

    CompilerOutput compilerOutput = new CompilerOutput();
    compilerOutput.setCompilerInvoked(true);

    List<String> compilerOutputLines = new LinkedList<>();

    try {
      while ((line = compilerOutputBuffer.readLine()) != null) {
        compilerOutputLines.add(line);
      }
      // Errors are separated via an empty line (""). But after the
      // the last error the OutputBuffer has nothing more to write.
      // In order to recognize the last error we insert an empty String
      // at the end of the list.
      // Only needs to be done when there are errors.
      if (compilerOutputLines.size() != 0) {
        line = "";
        compilerOutputLines.add(line);
      }

      compilerOutputStream.close();
      compilerStreamReader.close();
      compilerOutputBuffer.close();
      compilerProcess.destroy();

    } catch (IOException e) {

      // Reading might go wrong here if ghc should unexpectedly die
      LOGGER.severe("Error while reading from compiler stream.");
      compilerOutput.setClean(false);
      compilerOutput.setCompileStreamBroken(true);
      return compilerOutput;
    }

    // ghc -c generates a .o(object) and a .hi(haskell interface) file.
    // But we don't need those files so they can be deleted.
    // The generated files have the same name like our input file so we
    // can just exchange the file endings in order to get the
    // correct file paths for deletion
    if (Files.isDirectory(pathToProgramFile, LinkOption.NOFOLLOW_LINKS)) {

      // we use a file walker in order to find all files in the folder
      // and its subfolders
      RegexDirectoryWalker dirWalker = new RegexDirectoryWalker(".+\\.([Ll])?[Hh][Ss]");
      try {
        Files.walkFileTree(pathToProgramFile, dirWalker);
      } catch (IOException e) {
        LOGGER.severe(
            "Could not walk submission "
                + pathToProgramFile.toString()
                + " while building copiler invocation: "
                + e.getMessage());
      }

      for (Path candidatePath : dirWalker.getFoundFiles()) {
        File candidateFile = candidatePath.toFile();
        if (!candidateFile.isDirectory()) {
          String extension = FilenameUtils.getExtension(candidateFile.toString());
          if (extension.matches("[Ll]?[Hh][Ss]")) {
            File ghcGeneratedObject =
                new File(FilenameUtils.removeExtension(candidateFile.toString()) + ".o");
            File ghcGeneratedInterface =
                new File(FilenameUtils.removeExtension(candidateFile.toString()) + ".hi");
            ghcGeneratedObject.delete();
            ghcGeneratedInterface.delete();
          }
        }
      }
    } else {
      String extension = FilenameUtils.getExtension(pathToProgramFile.toString());
      if (extension.matches("[Ll]?[Hh][Ss]")) {
        File ghcGeneratedObject =
            new File(FilenameUtils.removeExtension(pathToProgramFile.toString()) + ".o");
        File ghcGeneratedInterface =
            new File(FilenameUtils.removeExtension(pathToProgramFile.toString()) + ".hi");
        ghcGeneratedObject.delete();
        ghcGeneratedInterface.delete();
      }
    }

    // if there are no errors there is no Output to handle
    if (compilerOutputLines.size() != 0) {
      compilerOutput = splitCompilerOutput(compilerOutputLines, compilerOutput);
    } else {
      compilerOutput.setClean(true);
    }
    return compilerOutput;
  }
  /**
   * This Method generates the command required to start the compiler. It generates a list of
   * strings that can be passed to a process builder.
   *
   * @param pathToProgramFile Where to look for the main file that will be compiled.
   * @param compilerName Which compiler to call
   * @param compilerFlags User supplied flags to be passed
   * @return List of string with the command for the process builder.
   * @throws BadCompilerSpecifiedException When no compiler is given.
   * @throws FileNotFoundException When the file to be compiled does not exist
   * @throws CompilerOutputFolderExistsException Due to slightly uncompatible
   *     CompileCheckerInterface this exception is in the declaration but it is never thrown.
   *     JavaCompileChecker uses this exception.
   */
  private List<String> createCompilerInvocation(
      Path pathToProgramFile, String compilerName, List<String> compilerFlags)
      throws BadCompilerSpecifiedException, FileNotFoundException,
          CompilerOutputFolderExistsException {

    List<String> compilerInvocation = new LinkedList<>();
    // We need a compiler name. Without it we cannot compile anything and
    // abort.
    if (("".equals(compilerName)) || (compilerName == null)) {
      throw new BadCompilerSpecifiedException("No compiler specified.");
    } else {
      compilerInvocation.add(compilerName);
    }

    // If compiler flags are passed, append them after the compiler name.
    // If we didn't get any we append nothing.
    if ((compilerFlags != null) && (!(compilerFlags.isEmpty()))) {
      compilerInvocation.addAll(compilerFlags);
    }

    // now we tell ghc to stop after compilation because we just want to
    // see if there are syntax errors in the code
    compilerInvocation.add("-c");

    // Check for the existence of the program file we are trying to
    // compile.
    if ((pathToProgramFile == null) || (pathToProgramFile.compareTo(Paths.get("")) == 0)) {
      throw new FileNotFoundException("No file to compile specified");
    } else {
      if (Files.isDirectory(pathToProgramFile, LinkOption.NOFOLLOW_LINKS)) {
        // we are supposed to compile a folder. Hence we'll scan for
        // lhs files and pass them to the compiler.
        RegexDirectoryWalker dirWalker = new RegexDirectoryWalker(".+\\.([Ll])?[Hh][Ss]");
        try {
          Files.walkFileTree(pathToProgramFile, dirWalker);
        } catch (IOException e) {
          LOGGER.severe(
              "Could not walk submission "
                  + pathToProgramFile.toString()
                  + " while building compiler invocation: "
                  + e.getMessage());
        }
        for (Path matchedFile : dirWalker.getFoundFiles()) {
          compilerInvocation.add(matchedFile.toFile().getAbsolutePath());
        }

      } else if (Files.exists(pathToProgramFile, LinkOption.NOFOLLOW_LINKS)) {
        // if the file exists, just pass the file name, since the
        // compiler will
        // be confined to the directory the file is in a few lines
        // down.
        compilerInvocation.add(pathToProgramFile.toString());
      } else {
        throw new FileNotFoundException(
            "Program file that should be compiled does not exist."
                + "Filename : \""
                + pathToProgramFile.toString()
                + "\"");
      }
    }
    return compilerInvocation;
  }