/**
 * Copyright 2016 Red Hat, Inc, and individual contributors.
 *
 * 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 org.wildfly.swarm.maven.plugin;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Holds the testing Maven project setup and is able to prepare the {@code testing-project} directory to the desired state.
 * This means filling the blanks in {@code pom.xml}, ensuring that only one {@code web.xml} exists in the project,
 * adjusting {@code Main.java} and making sure that excluded {@code .java} files are deleted.
 *
 * @see Packaging
 * @see Dependencies
 * @see Autodetection
 * @see IncludedTechnology
 * @see AdditionalDependency
 * @see AdditionalFraction
 */
public final class TestingProject {
    public final Packaging packaging;

    public final Dependencies dependencies;

    public final Autodetection autodetection;

    public final Set<IncludedTechnology> includedTechnologies;

    public final AdditionalDependency additionalDependency;

    public final AdditionalFraction additionalFraction;

    public TestingProject(Packaging packaging, Dependencies dependencies, Autodetection autodetection,
                          IncludedTechnology[] includedTechnologies, AdditionalDependency additionalDependency,
                          AdditionalFraction additionalFraction) {
        this.packaging = packaging;
        this.dependencies = dependencies;
        this.autodetection = autodetection;
        Set<IncludedTechnology> techs = new HashSet<>(Arrays.asList(includedTechnologies));
        // to ensure that the test is well-formed: the intended set of included technologies must respect fraction dependencies
        for (IncludedTechnology includedTechnology : includedTechnologies) {
            if (!techs.containsAll(includedTechnology.dependsOn())) {
                throw new IllegalArgumentException("If you want to include " + includedTechnology
                        + ", you also have to include " + includedTechnology.dependsOn());
            }
        }
        if (packaging == Packaging.JAR_WITH_MAIN && !techs.contains(IncludedTechnology.SERVLET)) {
            throw new IllegalArgumentException("Project with JAR packaging needs SERVLET for the custom Main class");
        }
        this.includedTechnologies = Collections.unmodifiableSet(techs);
        this.additionalDependency = additionalDependency;
        this.additionalFraction = additionalFraction;
    }

    public void prepare(Path dir) throws IOException {
        String swarmVersion = System.getProperty("project.version"); // from Maven

        // pom.xml
        Path pomXml = dir.resolve("pom.xml");
        String pomXmlContent = new String(Files.readAllBytes(pomXml), StandardCharsets.UTF_8)
                .replace("<!--PLACEHOLDER:swarm-version-->", swarmVersion)
                .replace("<!--PLACEHOLDER:packaging-->", packaging.packagingType())
                .replace("<!--PLACEHOLDER:dependencies-->", dependenciesSnippet())
                .replace("<!--PLACEHOLDER:configuration-->", swarmPluginConfigurationSnippet());
        Files.write(pomXml, pomXmlContent.getBytes(StandardCharsets.UTF_8));

        // web.xml
        if (packaging == Packaging.WAR || packaging == Packaging.WAR_WITH_MAIN) {
            Files.delete(dir.resolve(Paths.get("src", "main", "resources", "web.xml")));
            Files.delete(dir.resolve(Paths.get("src", "main", "resources")));
        } else if (packaging == Packaging.JAR_WITH_MAIN) {
            Files.delete(dir.resolve(Paths.get("src", "main", "webapp", "WEB-INF", "web.xml")));
            Files.delete(dir.resolve(Paths.get("src", "main", "webapp", "WEB-INF")));
            Files.delete(dir.resolve(Paths.get("src", "main", "webapp")));
        } else {
            throw new IllegalStateException("Unknown packaging " + packaging);
        }

        Path javaFiles = dir.resolve(Paths.get("src", "main", "java", "org", "wildfly", "swarm", "test"));
        Path testJavaFiles = dir.resolve(Paths.get("src", "test", "java", "org", "wildfly", "swarm", "test"));

        // Main.java
        Path mainClass = javaFiles.resolve("Main.java");
        if (this.packaging.hasCustomMain()) {
            String mainClassContent = new String(Files.readAllBytes(mainClass), StandardCharsets.UTF_8);

            for (Packaging packaging : Packaging.values()) {
                // only retain one section /*BEGIN:custom main:<packaging>*/ ... /*END:custom main:<packaging>*/;
                // the one that corresponds to the project's packaging
                if (this.packaging == packaging) {
                    continue;
                }

                String beginning = Pattern.quote("/*BEGIN:custom main:" + packaging + "*/");
                String ending = Pattern.quote("/*END:custom main:" + packaging + "*/");

                mainClassContent = mainClassContent.replaceAll("(?s)" + beginning + ".*?" + ending, "");
            }

            Files.write(mainClass, mainClassContent.getBytes(StandardCharsets.UTF_8));
        } else {
            Files.delete(mainClass);
        }

        // remaining *.java
        if (!includedTechnologies.contains(IncludedTechnology.SERVLET)) {
            Files.delete(javaFiles.resolve("HelloServlet.java"));
            Files.delete(testJavaFiles.resolve("HelloServletIT.java"));
        }
        if (!includedTechnologies.contains(IncludedTechnology.JAX_RS)) {
            Files.delete(javaFiles.resolve("HelloJaxrsApplication.java"));
            Files.delete(javaFiles.resolve("HelloJaxrsResource.java"));
            Files.delete(testJavaFiles.resolve("HelloJaxrsIT.java"));
        }
        if (!includedTechnologies.contains(IncludedTechnology.EJB)) {
            Files.delete(javaFiles.resolve("HelloEjbBean.java"));
            Files.delete(javaFiles.resolve("HelloEjbServlet.java"));
            Files.delete(testJavaFiles.resolve("HelloEjbIT.java"));
        }
    }

    private String dependenciesSnippet() {
        StringBuilder result = new StringBuilder();

        if (packaging.hasCustomMain()) {
            result.append("<dependency>\n" +
                          "  <groupId>org.wildfly.swarm</groupId>\n" +
                          "  <artifactId>container</artifactId>\n" +
                          "</dependency>\n");
        }

        if (packaging == Packaging.JAR_WITH_MAIN && dependencies == Dependencies.JAVA_EE_APIS) {
            // for the Main class
            result.append("<dependency>\n" +
                          "  <groupId>org.wildfly.swarm</groupId>\n" +
                          "  <artifactId>undertow</artifactId>\n" +
                          "</dependency>\n");
        }

        for (IncludedTechnology technology : includedTechnologies) {
            result.append(technology.dependencySnippet(dependencies));
        }

        result.append(additionalDependency.dependencySnippet());

        return result.toString();
    }

    private String swarmPluginConfigurationSnippet() {
        String result = "<fractionDetectMode>" + autodetection.toSwarmPluginValue() + "</fractionDetectMode>\n";

        if (packaging.hasCustomMain()) {
            result += "<mainClass>org.wildfly.swarm.test.Main</mainClass>\n";
        }

        Optional<String> anotherFraction = additionalFraction.shouldBringFraction();
        if (anotherFraction.isPresent()) {
            result += "<fractions><fraction>" + anotherFraction.get() + "</fraction></fractions>\n";
        }

        return result;
    }

    public boolean hasExplicitFractionDependencies() {
        // with custom main, org.wildfly.swarm:container is always added, and that's an explicit fraction dependency
        return dependencies == Dependencies.FRACTIONS || packaging.hasCustomMain();
    }

    public boolean doesAutodetectionHappen() {
        switch (autodetection) {
            case FORCE:
                return true;
            case NEVER:
                return false;
            case WHEN_MISSING:
                return !hasExplicitFractionDependencies();
            default:
                throw new AssertionError();
        }
    }

    public boolean canRunTests() {
        if (dependencies == Dependencies.JAVA_EE_APIS) {
            return doesAutodetectionHappen();
        }

        return true;
    }

    public Set<String> fractionsThatShouldBePresent() {
        Set<String> expectedFractions = includedTechnologies
                .stream()
                .map(IncludedTechnology::fraction)
                .collect(Collectors.toCollection(HashSet::new));
        if (doesAutodetectionHappen()) {
            additionalDependency.shouldBringFraction().ifPresent(expectedFractions::add);
        }

        if (dependencies == Dependencies.JAVA_EE_APIS && !doesAutodetectionHappen()) {
            expectedFractions = new HashSet<>();
            if (packaging == Packaging.JAR_WITH_MAIN) {
                // always included for the custom main class
                expectedFractions.add(IncludedTechnology.SERVLET.fraction());
            }
        }

        additionalFraction.shouldBringFraction().ifPresent(expectedFractions::add);

        return expectedFractions;
    }

    public Set<String> fractionsThatShouldBeMissing() {
        Set<String> allFractions = Stream.of(IncludedTechnology.values())
                .map(IncludedTechnology::fraction)
                .collect(Collectors.toCollection(HashSet::new));
        allFractions.addAll(AdditionalDependency.allPossibleFractions());
        allFractions.addAll(AdditionalFraction.allPossibleFractions());

        allFractions.removeAll(fractionsThatShouldBePresent());
        return allFractions;
    }

    @Override
    public String toString() {
        return "packaging: " + packaging + ", dependencies: " + dependencies + ", autodetection: " + autodetection
                + ", technologies: " + includedTechnologies + ", additional dependency: " + additionalDependency
                + ", additional fraction: " + additionalFraction;
    }

    // ---

    public String serialize() {
        String technologies = includedTechnologies.stream()
                .map(IncludedTechnology::toString)
                .collect(Collectors.joining(","));
        return packaging + ":" + dependencies + ":" + autodetection + ":" + technologies + ":" + additionalDependency
                + ":" + additionalFraction;
    }

    public static TestingProject deserialize(String string) {
        String[] parts = string.split(":");
        Packaging packaging = Packaging.valueOf(parts[0]);
        Dependencies dependencies = Dependencies.valueOf(parts[1]);
        Autodetection autodetection = Autodetection.valueOf(parts[2]);
        IncludedTechnology[] includedTechnologies = Arrays.stream(parts[3].split(","))
                .map(IncludedTechnology::valueOf)
                .toArray(IncludedTechnology[]::new);
        AdditionalDependency additionalDependency = AdditionalDependency.valueOf(parts[4]);
        AdditionalFraction additionalFraction = AdditionalFraction.valueOf(parts[5]);
        return new TestingProject(packaging, dependencies, autodetection, includedTechnologies, additionalDependency,
                                  additionalFraction);
    }
}
