/*
 * Copyright 2012 dorkbox, llc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dorkbox.build;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import com.esotericsoftware.wildcard.Paths;
import com.esotericsoftware.yamlbeans.YamlConfig;
import com.esotericsoftware.yamlbeans.YamlException;
import com.esotericsoftware.yamlbeans.YamlWriter;
import com.esotericsoftware.yamlbeans.scalar.ScalarSerializer;

import dorkbox.BuildVersion;
import dorkbox.Builder;
import dorkbox.build.util.BuildLog;
import dorkbox.build.util.CrossCompileClass;
import dorkbox.build.util.DependencyWalker;
import dorkbox.build.util.FileNotFoundRuntimeException;
import dorkbox.build.util.classloader.ByteClassloader;
import dorkbox.build.util.classloader.JavaMemFileManager;
import dorkbox.license.License;
import dorkbox.license.LicenseType;
import dorkbox.util.FileUtil;
import dorkbox.util.OS;

@SuppressWarnings({"AccessStaticViaInstance", "Convert2Diamond"})
public
class ProjectJava extends Project<ProjectJava> {

    protected ArrayList<String> extraArgs;

    protected Paths sourcePaths = new Paths();
    public Paths classPaths = new Paths();

    private transient ByteClassloader bytesClassloader = null;

    protected transient Jarable jarable = null;

    private boolean suppressSunWarnings = false;
    private final List<CrossCompileClass> crossCompileClasses = new ArrayList<CrossCompileClass>(4);

    Integer targetJavaVersion = null;
    private boolean isUploaded = false;

    public static
    ProjectJava create(String projectName) {
        ProjectJava project = new ProjectJava(projectName);
        Project.create(project);
        return project;
    }

    private
    ProjectJava(String projectName) {
        super(projectName);
    }

    /**
     * Add paths to the list of sources that are available when compiling the code
     */
    public
    ProjectJava sourcePath(Paths sourcePaths) {
        if (sourcePaths == null) {
            throw new NullPointerException("Source paths cannot be null!");
        }
        this.sourcePaths.add(sourcePaths);

        // ALWAYS add the source paths to be checksumed!
        hash.add(sourcePaths);

        return this;
    }

    /**
     * Add paths to the list of sources that are available when compiling the code
     */
    public
    ProjectJava sourcePath(String srcDir) {
        if (srcDir.endsWith("src")) {
            String parent = new File(srcDir).getAbsoluteFile().getParent();
            hash.add(new Paths(parent));
        }

        return sourcePath(new Paths(srcDir, "./"));
    }

    /**
     * Add paths to the list of sources that are available when compiling the code
     */
    public
    ProjectJava sourcePath(String dir, String... patterns) {
        return sourcePath(new Paths(dir, patterns));
    }

    /**
     * Add a class to the list of sources that are available when compiling the code
     */
    public
    ProjectJava sourcePath(final Class<?> sourceClass) {
        return sourcePath(Builder.getJavaFile(sourceClass));
    }

    public
    ProjectJava classPath(ProjectJar project) {
        if (project == null) {
            throw new NullPointerException("Project cannot be null!");
        }

        Paths paths = new Paths();
        paths.addFile(project.outputFile.get().getAbsolutePath());
        this.classPaths.add(paths);

        return this;
    }

    public
    ProjectJava classPath(Paths classPaths) {
        if (classPaths == null) {
            throw new NullPointerException("Class paths cannot be null!");
        }

        this.classPaths.add(classPaths);

        return this;
    }

    public
    ProjectJava classPath(String dir, String... patterns) {
        if (new File(dir).isFile() && (patterns == null || patterns.length == 0)) {
            Paths paths = new Paths();
            paths.addFile(dir);
            return classPath(paths);
        }
        return classPath(new Paths(dir, patterns));
    }


    public
    ProjectJava addArg(String arg) {
        if (this.extraArgs == null) {
            this.extraArgs = new ArrayList<String>();
        }
        this.extraArgs.add(arg);

        return this;
    }

    public
    Jarable jar() {
        if (this.jarable == null) {
            this.jarable = new Jarable(this);
        }
        return this.jarable;
    }

    /**
     * Suppress sun warnings during the compile stage. ONLY enable this is you know what you are doing in your project!
     */
    public
    ProjectJava suppressSunWarnings() {
        this.suppressSunWarnings = true;
        return this;
    }

    /**
     * Specifies the version to compile to?
     * <p/>
     * IE: compile for 1.6 on JDK 1.8. When compiling for a different version, you have that version's 1.6 rt.jar
     *
     * @return true if this project was built, false otherwise
     */
    @Override
    public
    boolean build(final int targetJavaVersion) throws IOException {
        // save off the target version we build
        this.targetJavaVersion = targetJavaVersion;

        // check dependencies for this project
        resolveDependencies();

        // we want to make sure that we build IF one of our dependencies needs to build too
        for (Project<?> project : fullDependencyList) {
            if (!shouldBuild && !(project instanceof ProjectJar)) {
                // if one of our dependencies has to build, so do we (don't keep checking if we have to build)
                // also, we DO NOT check jar versions/etc here (that happens later)

                // if true, this means that the files ARE the same and they have not changed
                final boolean b = project.hash.verifyChecksums();
                shouldBuild |= !b;
            }
        }

        // has our dependencies or their versions changed at all?
        final ArrayList<String> depWithVersionInfo = new ArrayList<String>();
        for (Project<?> project : fullDependencyList) {
            // version can be null, which is OK for our tests here
            depWithVersionInfo.add(project.name + ":" + project.version);
        }
        final String origDepsWithVersion = Builder.settings.get(this.name + ":deps", String.class);
        shouldBuild |= !depWithVersionInfo.toString().equals(origDepsWithVersion);


        shouldBuild |= !verifyChecksums();

        // exit early if we already built this project, unless we force a rebuild
        if (!forceRebuild && buildList.contains(this.name)) {
            if (!this.isBuildingDependencies) {
                BuildLog.title("Building")
                        .println(this.name + " already built this run");
            }
            return true;
        }
        else {
            buildList.add(this.name);
        }

        BuildLog.start();


        String version = "";
        if (this.version != null) {
            this.version.verify();
            version = this.version.toString();
        }

        if (OS.javaVersion > targetJavaVersion) {
            BuildLog.title("Cross-Compile")
                    .println(this.name + " " + version + "  [Java v1." + targetJavaVersion + "]");
        }
        else {
            BuildLog.title("Compiling").println(this.name  + " " + version);
        }

        if (!fullDependencyList.isEmpty()) {
            String[] array2 = new String[fullDependencyList.size() + 1];
            array2[0] = "Depends";
            int i = 1;
            for (Project<?> project : fullDependencyList) {
                array2[i] = project.name;

                if (project.version != null) {
                    array2[i] += " " + project.version;
                }

                if (project instanceof ProjectJar) {
                    array2[i] += " (jar)";
                }

                i++;
            }
            BuildLog.println().println((Object[]) array2).println();
        }

        if (shouldBuild) {
            // We have to make sure that TEMPORARY projects are built - even if that temp project DOES NOT need to build b/c of source code
            // changes.
            for (Project<?> project : fullDependencyList) {
                // dep can be a jar as well (don't have to build a jar)
                if (!(project instanceof ProjectJar)) {
                    if (!buildList.contains(project.name)) {
                        // let the build itself determine if it needs to run,
                        boolean prev = project.isBuildingDependencies;
                        project.isBuildingDependencies = true;

                        boolean prevTemp = project.overrideTemporary;
                        project.overrideTemporary = true; // setting this to true will force a temp project to build

                        project.build(targetJavaVersion);

                        project.isBuildingDependencies = prev;

                        project.overrideTemporary = prevTemp;
                    }
                }
            }

            // barf if we don't have source files!
            if (this.sourcePaths.getFiles().isEmpty()) {
                throw new IOException("No source files specified for project: " + this.name);
            }

            // make sure ALL dependencies are on the classpath.
            for (Project<?> project : fullDependencyList) {
                // dep can be a jar as well
                final File file = project.outputFile.get();
                if (!file.canRead()) {
                    throw new IOException("Dependency for project: '" + this.name + "' does not exist. '" + file.getAbsolutePath() + "'");
                }

                // if we are compiling our build instructions (and projects), this won't exist. This is OK,
                // because we run from memory instead (in the classloader)
                this.classPaths.addFile(file.getAbsolutePath());
            }

            // add source class file dependencies
            if (!this.sourceDependencies.isEmpty()) {
                Map<File, String> relativeLocations = new HashMap<File, String>();

                Set<String> dependencies = new HashSet<String>();


                if (OS.javaVersion > targetJavaVersion) {
                    BuildLog.title("Cross-Compile")
                            .println("Class dependencies  [Java v1." + targetJavaVersion + "]");
                }
                else {
                    BuildLog.title("Compiling").println("Class dependencies");
                }

                List<File> files = sourceDependencies.getFiles();
                Collections.sort(files);

                for (File sourceFile : files) {
                    String relativeNameNoExtension = DependencyWalker.collect(sourceFile, dependencies);
                    relativeLocations.put(sourceFile, relativeNameNoExtension);

                    BuildLog.println(relativeNameNoExtension + ".java");
                }

                // have to compile these classes!
                BuildLog.disable();

                ProjectJava tempProject = ProjectJava.create("ClassFileDependencies:" + this.name)
                                                     .temporary()
                                                     .options(buildOptions)
                                                     .sourcePath(sourceDependencies);

                FileUtil.delete(tempProject.stagingDir);
                FileUtil.mkdir(tempProject.stagingDir);
                tempProject.shouldBuild = true; // always build temp projects

                try {
                    tempProject.build(this.targetJavaVersion);
                } catch (RuntimeException e) {
                    e.printStackTrace();
                }

                File buildLocation = new File(this.stagingDir.getParent(), "classFileDeps");
                FileUtil.delete(buildLocation);
                FileUtil.mkdir(buildLocation);

                // now have to save out the source files (that are now converted to .class files)
                // There can be inner-classes, so the ALL children of the parent dir must be added.
                files = FileUtil.parseDir(tempProject.stagingDir);
                int root = tempProject.stagingDir.getAbsolutePath().length() + 1; // include slash
                for (File file : files) {
                    String relativeName = file.getAbsolutePath().substring(root);
                    FileUtil.copyFile(file, new File(buildLocation, relativeName));
                }

                FileUtil.delete(tempProject.stagingDir);
                BuildLog.enable();

                // now have to add this dir to our project
                this.classPaths.addFile(buildLocation.getAbsolutePath());
            }


            // have to build cross-compiled files first.
            File crossCompatBuiltFile = null;
            if (!crossCompileClasses.isEmpty()) {
                BuildLog.println();

                crossCompatBuiltFile = new File(this.stagingDir.getParent(), "crossCompileBuilt");
                FileUtil.delete(crossCompatBuiltFile);
                FileUtil.mkdir(crossCompatBuiltFile);

                Map<File, String> relativeLocations = new HashMap<File, String>();
                for (CrossCompileClass crossCompileClass : crossCompileClasses) {
                    Paths sourceFiles = crossCompileClass.sourceFiles;

                    if (OS.javaVersion > targetJavaVersion && targetJavaVersion < crossCompileClass.targetJavaVersion) {
                        Set<String> dependencies = new HashSet<String>();

                        for (File sourceFile : sourceFiles.getFiles()) {
                            BuildLog.title("Cross-Compile").println(sourceFile.getName() + "  [Java v1." +
                                                                    crossCompileClass.targetJavaVersion + "]");

                            String relativeNameNoExtension = DependencyWalker.collect(sourceFile, dependencies);
                            relativeLocations.put(sourceFile, relativeNameNoExtension);
                        }

                        BuildLog.disable();

                        Paths tempSource = new Paths();
                        for (String dependency : dependencies) {
                            tempSource.addFile(dependency);
                        }


                        ProjectJava tempProject = ProjectJava.create("CrossCompileClasses")
                                                             .temporary()
                                                             .options(buildOptions)
                                                             .sourcePath(tempSource)
                                                             .sourcePath(sourceFiles);

                        FileUtil.delete(tempProject.stagingDir);
                        FileUtil.mkdir(tempProject.stagingDir);
                        tempProject.shouldBuild = true; // always build temp projects

                        try {
                            tempProject.build(crossCompileClass.targetJavaVersion);
                        } catch (RuntimeException e) {
                            e.printStackTrace();
                        }

                        // now have to save out the source files (that are now converted to .class files)
                        for (File sourceFile : sourceFiles.getFiles()) {
                            String s = relativeLocations.get(sourceFile) + ".class";
                            File file = new File(tempProject.stagingDir, s);
                            FileUtil.copyFile(file, new File(crossCompatBuiltFile, s));
                        }

                        FileUtil.delete(tempProject.stagingDir);
                        BuildLog.enable();
                    }
                }

                // now have to add this dir to our project
                this.classPaths.addFile(crossCompatBuiltFile.getAbsolutePath());

                Paths crossIncludeFiles = new Paths();
                for (String relativeName : relativeLocations.values()) {
                    crossIncludeFiles.add(crossCompatBuiltFile.getAbsolutePath(), relativeName + ".class");

                }
                // now that this file is compiled, we add it to our "extra files" to be bundled up when we make the jar
                extraFiles(crossIncludeFiles);


                // have to remove all the relative java files (since we don't want to normally build them)
                // the EASIEST way is to make a copy
                List<File> files = sourcePaths.getFiles();
                Paths copy = new Paths();
                for (File file : files) {
                    boolean canAdd = true;

                    for (File crossFile : relativeLocations.keySet()) {
                        if (crossFile.equals(file)) {
                            canAdd = false;
                            break;
                        }
                    }

                    if (canAdd) {
                        copy.add(file.getParent(), file.getName());
                    }
                }
                sourcePaths = copy;
            }
            // done with extra-file cross-compile
            BuildLog.println();



            if (this.bytesClassloader == null && this.jarable == null) {
                FileUtil.delete(this.stagingDir);
            }


            runCompile(targetJavaVersion);
            BuildLog.println("Compile success");

            if (!temporary && this.version != null) {
                // only save the version info + files if we are NOT temporary
                // update the version BEFORE creating the jar!
                this.version.save();
            }

            if (this.jarable != null) {
                this.jarable.buildJar();
            }

            // calculate the hash of all the files in the source path
            saveChecksums();

            // save our dependencies and their version info
            Builder.settings.save(this.name + ":deps", depWithVersionInfo.toString());

            if (crossCompatBuiltFile != null) {
                FileUtil.delete(crossCompatBuiltFile);
            }

            BuildLog.println();
            BuildLog.title("Staging").println(this.stagingDir);
        }
        else {
            skippedBuild = true;
            BuildLog.println().println("Skipped (nothing changed)");
        }

        BuildLog.finish();

        return shouldBuild;
    }

    /**
     * Compiles into class files.
     */
    private synchronized
    void runCompile(final int targetJavaVersion) throws IOException {
        // if you get messages, such as
        // warning: [path] bad path element "/x/y/z/lib/fubar-all.jar": no such file or directory
        //   That is because that file exists in a MANIFEST.MF somewhere on the classpath! Find the jar that has that, and rip out
        //   the offending manifest.mf file.
        // see: http://stackoverflow.com/questions/1344202/bad-path-warning-where-is-it-coming-from

        if (this.sourcePaths.isEmpty()) {
            throw new IOException("No source files found.");
        }

        ArrayList<String> args = new ArrayList<String>();
        if (this.buildOptions.compiler.enableCompilerTrace) {
            // TODO: Interesting to note, that when COMPILING this with verbose, we can get a list (from the compiler) of EVERY CLASS NEEDED
            //         to run our application! This would be useful in "trimming" the necessary files needed by the JVM.
            args.add("-verbose");
        }

        if (this.buildOptions.compiler.debugEnabled) {
            BuildLog.println("Adding debug info");

            args.add("-g"); // Generate all debugging information, including local variables. By default, only line number and source file information is generated.
        }
        else {
            args.add("-g:none");
        }

        if (this.bytesClassloader == null) {
            // we only want to use an output directory if we have output!
            FileUtil.delete(this.stagingDir);
            FileUtil.mkdir(this.stagingDir);

            args.add("-d");
            args.add(this.stagingDir.getAbsolutePath());
        }

        args.add("-encoding");
        args.add("UTF-8");

        if (OS.javaVersion > targetJavaVersion) {
            // if our runtime env. is NOT equal to our target env.
            args.add("-source");
            args.add("1." + targetJavaVersion);

            args.add("-target");
            args.add("1." + targetJavaVersion);

            args.add("-bootclasspath");
            String location = this.buildOptions.compiler.crossCompileLibrary.getCrossCompileLibraryLocation(targetJavaVersion);
            File file = FileUtil.normalize(location);

            if (!file.canRead()) {
                throw new FileNotFoundRuntimeException("Unable to read cross compile jar: " + location);
            }

            args.add(file.getAbsolutePath());
        }

        // suppress sun proprietary warnings
        if (this.suppressSunWarnings) {
            args.add("-XDignore.symbol.file");
        }

        if (this.extraArgs != null) {
            boolean extraArgsHaveXlint = false;
            for (String arg : this.extraArgs) {
                if (arg.startsWith("-Xlint")) {
                    extraArgsHaveXlint = true;
                    break;
                }
            }

            if (!extraArgsHaveXlint) {
                args.add("-Xlint:all");
            }

            // add any extra arguments
            args.addAll(this.extraArgs);
        }


        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            throw new RuntimeException("No compiler available. Ensure you are running from a JDK, and not a JRE.");
        }

        if (this.classPaths != null && !this.classPaths.isEmpty()) {
            args.add("-classpath");
            // System.err.println("CP " + this.classPaths.toString(File.pathSeparator));
            String cp = this.classPaths.toString(File.pathSeparator);
            if (OS.javaVersion == 7) {
                // we have to add javaFX to the classpath (they are not included on the classpath by default), otherwise we
                // can't compile javaFX binaries. This was fixed in Java 1.8.
                cp += File.pathSeparator + System.getProperty("java.home") + File.separator + "lib" + File.separator + "jfxrt.jar";
            }
            args.add(cp);
        }

        // now compile the code
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
        JavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

        PrintStream error = System.err;
        PrintStream out = System.out;
        final StringBuilder errorsDuringCompile = new StringBuilder();
        try {
            Iterable<? extends JavaFileObject> javaFileObjectsFromFiles;
            if (this.bytesClassloader == null) {
                javaFileObjectsFromFiles = ((StandardJavaFileManager) fileManager).getJavaFileObjectsFromFiles(this.sourcePaths.getFiles());
            }
            else {
                fileManager = new JavaMemFileManager((StandardJavaFileManager) fileManager, this.bytesClassloader);
                ((JavaMemFileManager) fileManager).setSource(this.sourcePaths);
                javaFileObjectsFromFiles = ((JavaMemFileManager) fileManager).getSourceFiles();
            }

            // redirect OUT/ERR
            PrintStream nullErrorStream = new PrintStream(new OutputStream() {
                @Override
                public
                void write(int b) throws IOException {
                    errorsDuringCompile.append((char)b);
                }
            });

            System.setErr(nullErrorStream);
            System.setOut(nullErrorStream);

            compiler.getTask(null, fileManager, diagnostics, args, null, javaFileObjectsFromFiles).call();
        } finally {
            try {
                fileManager.close();
            } catch (Exception e) {
                e.printStackTrace();
            }

            System.setErr(error);
            System.setOut(out);
        }

        if (errorsDuringCompile.length() > 0) {
            int length = errorsDuringCompile.length() - 1;
            if (errorsDuringCompile.charAt(length) == '\n') {
                errorsDuringCompile.deleteCharAt(length);
                length = errorsDuringCompile.length() - 1;

                if (errorsDuringCompile.charAt(length) == '\r') {
                    errorsDuringCompile.deleteCharAt(length);
                }
            }
            RuntimeException runtimeException = new RuntimeException("Compilation error: " + errorsDuringCompile.toString());
            runtimeException.setStackTrace(new StackTraceElement[0]);
            throw runtimeException;
        }

        boolean hasError = false;
        for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
            if (diagnostic.getKind() == javax.tools.Diagnostic.Kind.ERROR) {
                hasError = true;
                break;
            }
        }
        if (hasError) {
            final String temp = FileUtil.tempDirectory("temp");
            final File tempFile = new File(temp);
            String tempDir = tempFile.getParent();
            if (!tempFile.delete()) {
                BuildLog.println("Error deleting temp directory!!", tempDir);
            }

            BuildLog.enable();
            for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
                final String message = diagnostic.getMessage(null);
                final Diagnostic.Kind kind = diagnostic.getKind();

                if (this.suppressSunWarnings &&
                    (kind == Diagnostic.Kind.WARNING || kind == Diagnostic.Kind.MANDATORY_WARNING) &&
                    message.startsWith("sun.")) {
                    // skip all sun.XYZ warnings
                    continue;
                }

                BuildLog.start();
                BuildLog.title(kind.toString()).println(message);

                final JavaFileObject source = diagnostic.getSource();
                String className = null;

                if (source != null) {
                    final String name = source.getName();

                    // source.getName() : /tmp/Builder2506000973028434501.tmp/dorkbox/util/FileUtil.java
                    // we want:  dorkbox.util.FileUtil

                    // source.getName() can ALSO be the full path to the class file.

                    if (name.startsWith(tempDir)) {
                        final int lastIndexOf = name.lastIndexOf(".tmp");

                        if (lastIndexOf > 0) {
                            String croppedName = name.substring(lastIndexOf + 5);

                            className = croppedName.replace(File.separatorChar, '.').substring(0, croppedName.lastIndexOf('.'));
                            className += ":" + diagnostic.getLineNumber();
                        }
                    }

                    if (className == null) {
                        BuildLog.title("Location").println(getLineInfo(diagnostic));
                        className = name;
                    }
                }


                if (className != null) {
                    BuildLog.println(className);
                }
                else {
                    BuildLog.title("Location").println(getLineInfo(diagnostic));
                    BuildLog.println("Unknown location");
                }
                BuildLog.finish();
            }
            BuildLog.disable();

            RuntimeException runtimeException = new RuntimeException("Compilation errors");
            runtimeException.setStackTrace(new StackTraceElement[0]);
            throw runtimeException;
        }

        System.gc();
        try {
            Thread.sleep(100);
        } catch (InterruptedException ignored) {
        }
    }

    private static
    String getLineInfo(final Diagnostic<? extends JavaFileObject> diagnostic) {
        if (diagnostic.getLineNumber() > 0) {
            return  "Line: " + Long.toString(diagnostic.getLineNumber()) + ":" + Long.toString(diagnostic.getColumnNumber());
        } else {
            return "Unknown line";
        }
    }

    @Override
    public
    String getExtension() {
        return Project.JAR_EXTENSION;
    }


    /**
     * Specifies different source files to be cross compiled to a different version of java. This is useful when building
     * libraries that use runtime/etc features that have changed over time
     * </p>
     * The way this operates, is that is builds this file + any dependency (recursively auto-detected) to the target java version.
     * </p>
     * Next, it then removes ALL other files except this ones specified, and then it includes these files as a classpath to the compiler
     * for the primary build.
     * </p>
     * IMPORTANT!! - this can only auto-detect dependencies that are under the same root path - meaning NO jars, external classes, etc.
     *
     * @param sourceFiles The source files MUST be the FULL NAME (but not the path!) of the java file
     */
    public
    ProjectJava crossBuild(final int targetJavaVersion, final Paths sourceFiles) {
        crossCompileClasses.add(new CrossCompileClass(targetJavaVersion, sourceFiles));
        return this;
    }

    public
    interface OnJarEntryAction {
        boolean canHandle(String fileName);

        int onEntry(String fileName, ByteArrayInputStream inputStream, OutputStream output) throws Exception;
    }

    /**
     * Take all of the parameters of this project, and convert it to a text file.
     */
    @Override
    public
    void save(final String location) {
        try {
            YamlWriter writer = new YamlWriter(new FileWriter(location));
            YamlConfig config = writer.getConfig();

            config.writeConfig.setWriteRootTags(false);

            config.setPropertyElementType(ProjectJava.class, "licenses", License.class);
            config.setPrivateFields(true);

            config.readConfig.setConstructorParameters(License.class,
                                                       new Class[] {String.class, LicenseType.class},
                                                       new String[] {"licenseName", "licenseType"});
            config.readConfig.setConstructorParameters(ProjectJava.class, new Class[] {String.class}, new String[] {"projectName"});

            config.setScalarSerializer(Paths.class, new ScalarSerializer<Paths>() {
                @Override
                public
                Paths read(String value) throws YamlException {
                    String[] split = value.split(File.pathSeparator);
                    Paths paths = new Paths();
                    for (String s : split) {
                        paths.addFile(s);
                    }
                    return paths;
                }

                @Override
                public
                String write(Paths paths) throws YamlException {
                    return paths.toString(File.pathSeparator);
                }
            });

            writer.write(this);
            writer.close();
        } catch (IOException e) {
            throw new RuntimeException("Unable to save file.", e);
        }
    }

    /**
     * The specified byte loading classloader to save the compiled class bytes into,
     */
    public
    ProjectJava compilerClassloader(ByteClassloader bytesClassloader) {
        this.bytesClassloader = bytesClassloader;
        return this;
    }

    @Override
    public
    ProjectJava version(BuildVersion version) {
        super.version(version);
        return this;
    }

    @Override
    public
    void uploadToMaven() throws IOException {
        // only if we save the build. Test builds don't save, and we shouldn't upload them to maven
        final boolean export = buildOptions.compiler.saveBuild;
        exportToMaven = export;

        if (!skippedBuild) {
            for (Project project : fullDependencyList) {
                project.exportToMaven = export;

                // dep can be a jar as well (don't upload dependency jars)
                if (project instanceof ProjectJava) {
                    project.uploadToMaven();
                }
            }

            if (this.mavenExporter != null) {
                this.mavenExporter.setProject(this);

                if (!isUploaded) {
                    // mark that we have been uploaded already, so we can only do it once per project
                    isUploaded = true;

                    this.mavenExporter.export();
                }
            }
        }
    }

    /**
     * @return true if the checksums for path match the saved checksums.  If there is a JAR file, it also checks to see if it is built &
     * matches the saved checksums.  If it's a temp project (and specifies a jar) the jarChecksum is ignored (so only checksums based on source code changes)
     */
    boolean verifyChecksums() throws IOException {
        // if temporary + we override the status, we ALWAYS build it
        if (this.temporary && this.overrideTemporary) {
            return false;
        }

        boolean sourceHashesSame = hash.verifyChecksums();
        if (!sourceHashesSame) {
            return false;
        }

        // if we have no jar file (and the sources are the same) OR we are temporary + don't override, it will have a jar, but won't save it
        if (this.jarable == null || (this.temporary && !this.overrideTemporary)) {
            return true;
        }

        // when we verify checksums, we verify the ORIGINAL (if there is version info) -- and when we SAVE checksums, we save the NEW version
        final File originalOutputFile = this.outputFile.getOriginal();

        if (originalOutputFile.canRead()) {
            String jarChecksum = hash.generateChecksum(originalOutputFile);
            String checkContents = Builder.settings.get(this.name + ":" + originalOutputFile.getAbsolutePath(), String.class);

            boolean outputFileGood = jarChecksum != null && jarChecksum.equals(checkContents);

            if (outputFileGood) {
                if (!this.jarable.includeSourceAsSeparate) {
                    return true;
                }
                else {
                    final File originalOutputFileSource = this.outputFile.getSourceOriginal();

                    // now check the src.zip file (if there was one).
                    jarChecksum = hash.generateChecksum(originalOutputFileSource);
                    checkContents = Builder.settings.get(this.name + ":" + originalOutputFileSource.getAbsolutePath(), String.class);

                    return jarChecksum != null && jarChecksum.equals(checkContents);
                }
            }
        }
        else {
            // output file was removed
            BuildLog.println("Output file was removed.");
            return false;
        }

        return true;
    }



    /**
     * Saves the checksums for a given path - PER PROJECT (otherwise updating a jar in one place, and saving it's checksum, will verify
     * it everywhere else)
     */
    void saveChecksums() throws IOException {
        // by default, we save the build. When building a 'test' build, we opt to NOT save the build hashes, so that a 'normal' build
        // will then compile.
        if (!buildOptions.compiler.saveBuild) {
            return;
        }

        hash.saveChecksums();

        // when we verify checksums, we verify the ORIGINAL (if there is version info) -- and when we SAVE checksums, we save the NEW version
        final File currentOutputFile = this.outputFile.get();

        // hash/save the jar file (if there was one)
        if (currentOutputFile.exists()) {
            String fileChecksum = hash.generateChecksum(currentOutputFile);
            Builder.settings.save(this.name + ":" + currentOutputFile.getAbsolutePath(), fileChecksum);

            if (this.jarable != null && this.jarable.includeSourceAsSeparate) {
                final File currentOutputFileSource = this.outputFile.getSource();

                // now check the src.zip file (if there was one).
                fileChecksum = hash.generateChecksum(currentOutputFileSource);

                Builder.settings.save(this.name + ":" + currentOutputFileSource.getAbsolutePath(), fileChecksum);
            }
        }
    }
}
