/*
 * JBoss, Home of Professional Open Source
 * Copyright 2012, Red Hat Middleware LLC, and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of 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.jboss.shrinkwrap.resolver.impl.maven.archive.packaging;

import java.io.File;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import com.google.inject.Guice;
import org.codehaus.plexus.compiler.CompilerConfiguration;
import org.codehaus.plexus.compiler.CompilerException;
import org.codehaus.plexus.compiler.CompilerMessage;
import org.codehaus.plexus.compiler.CompilerResult;
import org.codehaus.plexus.compiler.javac.JavacCompiler;
import org.eclipse.sisu.space.SpaceModule;
import org.eclipse.sisu.space.URLClassSpace;
import org.eclipse.sisu.wire.WireModule;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.resolver.api.maven.MavenResolvedArtifact;
import org.jboss.shrinkwrap.resolver.api.maven.MavenWorkingSession;
import org.jboss.shrinkwrap.resolver.api.maven.ScopeType;
import org.jboss.shrinkwrap.resolver.api.maven.archive.importer.MavenImporterException;
import org.jboss.shrinkwrap.resolver.api.maven.strategy.AcceptScopesStrategy;
import org.jboss.shrinkwrap.resolver.api.maven.strategy.MavenResolutionStrategy;
import org.jboss.shrinkwrap.resolver.impl.maven.archive.plugins.CompilerPluginConfiguration;
import org.jboss.shrinkwrap.resolver.impl.maven.task.AddScopedDependenciesTask;
import org.jboss.shrinkwrap.resolver.impl.maven.util.Validate;
import org.jboss.shrinkwrap.resolver.spi.maven.archive.packaging.PackagingProcessor;
import static org.eclipse.sisu.space.SpaceModule.LOCAL_INDEX;

/**
 * Packaging processor which is able to compile Java sources
 *
 * @author <a href="mailto:kpiwko@redhat.com">Karel Piwko</a>
 *
 * @param <ARCHIVETYPE> Type of the archive produced
 */
public abstract class AbstractCompilingProcessor<ARCHIVETYPE extends Archive<ARCHIVETYPE>> implements
    PackagingProcessor<ARCHIVETYPE> {
    private static final Logger log = Logger.getLogger(AbstractCompilingProcessor.class.getName());

    // this pattern is used to check whether archive name was autogenerated
    private static final Pattern UUID4_PATTERN = Pattern.compile("[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}\\.[a-z]+");
    private JavacCompiler compiler;
    private boolean skipCompilation;

    protected MavenWorkingSession session;

    protected PackagingProcessor<ARCHIVETYPE> configure(MavenWorkingSession session) {
        this.session = session;
        this.skipCompilation = Boolean.getBoolean("org.jboss.shrinkwrap.resolver.maven.skipCompilation") || session.skipCompilation();
        if (skipCompilation) {
            log.fine("Compilation was skipped due to system property org.jboss.shrinkwrap.resolver.maven.importer.skipCompilation being set to true");
        } else {
            compiler = Guice.createInjector(new WireModule(
                    new SpaceModule(new URLClassSpace(AbstractCompilingProcessor.class.getClassLoader()),
                            LOCAL_INDEX, true))).getInstance(JavacCompiler.class);
        }
        return this;
    }

    protected AbstractCompilingProcessor<ARCHIVETYPE> compile(File inputDirectory, File outputDirectory, ScopeType... scopes) {
        Validate.notNullAndNoNullValues(scopes, "Cannot compile sources, there were no scopes defined");
        Validate.notNull(inputDirectory, "Directory with sources to be compiled must not be null");
        Validate.notNull(outputDirectory, "Target directory for compiled sources must not be null");

        if (compiler == null) {
            if (!skipCompilation) {
                log.warning("No compiler found, skipping compilation");
            }
            return this;
        }

        CompilerConfiguration configuration = getCompilerConfiguration();

        if (log.isLoggable(Level.FINE)) {
            log.log(Level.FINE, "Compiling sources from {0} directory into {1}",
                new Object[] { inputDirectory, outputDirectory });
        }

        // in order to compile sources, we need to resolve dependencies first,
        // so we have a classpath available
        new AddScopedDependenciesTask(ScopeType.values()).execute(session);
        final MavenResolutionStrategy scopeStrategy = new AcceptScopesStrategy(scopes);
        final Collection<MavenResolvedArtifact> artifactResults = session.resolveDependencies(scopeStrategy);

        for (MavenResolvedArtifact artifact : artifactResults) {
            String classpathEntry = artifact.asFile().getAbsolutePath();
            configuration.addClasspathEntry(classpathEntry);
            if (log.isLoggable(Level.FINER)) {
                log.log(Level.FINER, "Adding {0} to compilation classpath", classpathEntry);
            }
        }

        configuration.addSourceLocation(inputDirectory.getPath());
        configuration.setOutputLocation(outputDirectory.getPath());
        try {
            CompilerResult result = compiler.performCompile(configuration);
            if (!result.isSuccess()) {
                // try to fork compilation process if it wasn't set to fork already
                if (!configuration.isFork()) {
                    log.log(Level.WARNING,
                        "MavenImporter was not able to identify javac compiler, probably JAVA_HOME points to JRE instead of JDK. MavenImporter will try to fork and use javac from $PATH");
                    configuration.setFork(true);
                    CompilerResult result2 = compiler.performCompile(configuration);
                    if (!result2.isSuccess()) {
                        log.log(Level.WARNING,
                            "Unable to compile project with neither forked nor embedded compiler. Returning original exception message");
                        // throw original exception
                        throw constructCompilationException(result, inputDirectory);
                    }
                }
                else {
                    throw constructCompilationException(result, inputDirectory);
                }
            }

        } catch (CompilerException e) {
            log.log(Level.SEVERE, "Compilation failed with {0}", e.getMessage());
            throw new MavenImporterException("Unable to compile source at " + inputDirectory.getPath() + " due to: ", e);
        }

        return this;
    }

    private CompilerConfiguration getCompilerConfiguration() {
        CompilerPluginConfiguration pluginConfiguration = new CompilerPluginConfiguration(session.getParsedPomFile());
        return pluginConfiguration.asCompilerConfiguration();
    }

    protected static boolean hasGeneratedName(Archive<?> archive) {
        String archiveName = archive.getName();
        return archiveName != null && UUID4_PATTERN.matcher(archiveName.toLowerCase()).matches();
    }

    private static MavenImporterException constructCompilationException(CompilerResult result, File sourceDirectory) {
        StringBuilder sb = new StringBuilder("Unable to compile sources at ");
        sb.append(sourceDirectory.getPath());
        sb.append(" due to following reason(s): ");

        String delimiter = "";
        for (CompilerMessage m : result.getCompilerMessages()) {
            sb.append(delimiter).append(m.toString());
            delimiter = ", ";
        }

        log.log(Level.SEVERE, sb.toString());

        return new MavenImporterException(sb.toString());
    }

}
