/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2018, Gluon Software
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.javafxports.jfxmobile.plugin;

import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.file.FileCollection;
import org.gradle.util.VersionNumber;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class RetrolambdaExec {

    private static final int COMMANDLINE_LENGTH_LIMIT = 3496;

    private FileCollection retrolambdaClasspath;
    private File inputDir;
    private File outputDir;
    private FileCollection includedFiles;
    private List<String> jvmArgs;
    private int bytecodeVersion;
    private boolean defaultMethods;

    private final Project project;

    public RetrolambdaExec(Project project) {
        this.project = project;
    }

    public FileCollection getRetrolambdaClasspath() {
        return retrolambdaClasspath;
    }

    public void setRetrolambdaClasspath(FileCollection retrolambdaClasspath) {
        this.retrolambdaClasspath = retrolambdaClasspath;
    }

    public File getInputDir() {
        return inputDir;
    }

    public void setInputDir(File inputDir) {
        this.inputDir = inputDir;
    }

    public File getOutputDir() {
        return outputDir;
    }

    public void setOutputDir(File outputDir) {
        this.outputDir = outputDir;
    }

    public FileCollection getIncludedFiles() {
        return includedFiles;
    }

    public void setIncludedFiles(FileCollection includedFiles) {
        this.includedFiles = includedFiles;
    }

    public List<String> getJvmArgs() {
        return jvmArgs;
    }

    public void setJvmArgs(List<String> jvmArgs) {
        this.jvmArgs = jvmArgs;
    }

    public int getBytecodeVersion() {
        return bytecodeVersion;
    }

    public void setBytecodeVersion(int bytecodeVersion) {
        this.bytecodeVersion = bytecodeVersion;
    }

    public boolean isDefaultMethods() {
        return defaultMethods;
    }

    public void setDefaultMethods(boolean defaultMethods) {
        this.defaultMethods = defaultMethods;
    }

    public Project getProject() {
        return project;
    }

    public void exec() {
        project.javaexec(exec -> {
            Configuration retrolambdaConfig = project.getConfigurations().getByName("retrolambdaConfig");

            String path = retrolambdaClasspath.getAsPath();

            exec.setClasspath(project.files(retrolambdaConfig));
            exec.setMain("net.orfjackal.retrolambda.Main");
            exec.setJvmArgs(Arrays.asList(
                    "-Dretrolambda.inputDir=" + inputDir,
                    "-Dretrolambda.outputDir=" + outputDir,
                    "-Dretrolambda.classpath=" + path,
                    "-Dretrolambda.bytecodeVersion=" + bytecodeVersion
            ));

            VersionNumber retrolambdaVersion = retrolambdaVersion(retrolambdaConfig);
            boolean requiresJavaAgent = !requireVersion(retrolambdaVersion, "1.6.0", false);
            if (requiresJavaAgent) {
                exec.getJvmArgs().add("-javaagent:" + exec.getClasspath().getAsPath());
            }

            boolean supportIncludeFiles = requireVersion(retrolambdaVersion, "2.1.0", false);
            if (supportIncludeFiles && classpathLengthGreaterThanLimit(path)) {
                try {
                    File classpathFile = File.createTempFile("inc-", ".path");
                    try (BufferedWriter writer = Files.newBufferedWriter(classpathFile.toPath(), StandardCharsets.UTF_8)) {
                        for (File item : this.retrolambdaClasspath) {
                            writer.write(item.toString() + "\n");
                        }
                    }
                    classpathFile.deleteOnExit();
                    exec.getJvmArgs().add("-Dretrolambda.classpathFile=" + classpathFile.getAbsolutePath());
                } catch (IOException e) {

                }
            } else {
                exec.getJvmArgs().add("-Dretrolambda.classpath=" + path);
            }

            if (includedFiles != null) {
                if (supportIncludeFiles && changeFileLengthGreaterThanLimit(includedFiles)) {
                    try {
                        File includedFile = File.createTempFile("inc-", ".list");
                        try (BufferedWriter writer = Files.newBufferedWriter(includedFile.toPath(), StandardCharsets.UTF_8)) {
                            for (File file : includedFiles) {
                                writer.write(file.toString() + "\n");
                            }
                        }
                        includedFile.deleteOnExit();
                        exec.getJvmArgs().add("-Dretrolambda.includedFilesFile=" + includedFile.getAbsolutePath());
                    } catch (IOException e) {

                    }
                } else {
                    String includedArg = includedFiles.getFiles().stream().map(f -> f.toString())
                            .collect(Collectors.joining(File.pathSeparator));
                    exec.getJvmArgs().add(includedArg);
                    project.getLogger().quiet(includedArg);
                }
            }

            if (defaultMethods) {
                exec.getJvmArgs().add("-Dretrolambda.defaultMethods=true");
            }

            for (String arg : jvmArgs) {
                exec.getJvmArgs().add(arg);
            }
        });
    }

    private static boolean classpathLengthGreaterThanLimit(String path) {
        return path.length() > COMMANDLINE_LENGTH_LIMIT;
    }

    private static boolean changeFileLengthGreaterThanLimit(FileCollection includedFiles) {
        int total = 0;
        for (File file : includedFiles) {
            total += file.toString().length();
            if (total > COMMANDLINE_LENGTH_LIMIT) {
                return true;
            }
        }
        return false;
    }

    private static VersionNumber retrolambdaVersion(Configuration retrolambdaConfig) {
        retrolambdaConfig.resolve();
        Dependency retrolambdaDep = retrolambdaConfig.getDependencies().iterator().next();
        if (retrolambdaDep.getVersion() == null) {
            // Don't know version
            return null;
        }
        return VersionNumber.parse(retrolambdaDep.getVersion());
    }

    private static boolean requireVersion(VersionNumber retrolambdaVersion, String version, boolean fallback) {
        if (retrolambdaVersion == null) {
            // Don't know version, assume fallback
            return fallback;
        }
        VersionNumber targetVersionNumber = VersionNumber.parse(version);
        return retrolambdaVersion.compareTo(targetVersionNumber) >= 0;
    }
}