/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.apache.tools.ant.taskdefs.optional.extension;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Manifest;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.optional.extension.resolvers.AntResolver;
import org.apache.tools.ant.taskdefs.optional.extension.resolvers.LocationResolver;
import org.apache.tools.ant.taskdefs.optional.extension.resolvers.URLResolver;

/**
 * Tries to locate a JAR to satisfy an extension and place
 * location of JAR into property.
 *
 * @ant.task name="jarlib-resolve"
 */
public class JarLibResolveTask extends Task {
    /**
     * The name of the property in which the location of
     * library is stored.
     */
    private String propertyName;

    /**
     * The extension that is required.
     */
    private Extension requiredExtension;

    /**
     * The set of resolvers to use to attempt to locate library.
     */
    private final List<ExtensionResolver> resolvers = new ArrayList<>();

    /**
     * Flag to indicate that you should check that
     * the libraries resolved actually contain
     * extension and if they don't then raise
     * an exception.
     */
    private boolean checkExtension = true;

    /**
     * Flag indicating whether or not you should
     * throw a BuildException if you cannot resolve
     * library.
     */
    private boolean failOnError = true;

    /**
     * The name of the property in which the location of
     * library is stored.
     *
     * @param property The name of the property in which the location of
     *                 library is stored.
     */
    public void setProperty(final String property) {
        this.propertyName = property;
    }

    /**
     * Check nested libraries for extensions
     *
     * @param checkExtension if true, libraries returned by nested
     * resolvers should be checked to see if they supply extension.
     */
    public void setCheckExtension(final boolean checkExtension) {
        this.checkExtension = checkExtension;
    }

    /**
     * Set whether to fail if error.
     *
     * @param failOnError if true, failure to locate library should fail build.
     */
    public void setFailOnError(final boolean failOnError) {
        this.failOnError = failOnError;
    }

    /**
     * Adds location resolver to look for a library in a location
     * relative to project directory.
     *
     * @param loc the resolver location to search.
     */
    public void addConfiguredLocation(final LocationResolver loc) {
        resolvers.add(loc);
    }

    /**
     * Adds a URL resolver to download a library from a URL
     * to a local file.
     *
     * @param url the URL resolver from which to download the library
     */
    public void addConfiguredUrl(final URLResolver url) {
        resolvers.add(url);
    }

    /**
     * Adds Ant resolver to run an Ant build file to generate a library.
     *
     * @param ant the AntResolver to generate the library.
     */
    public void addConfiguredAnt(final AntResolver ant) {
        resolvers.add(ant);
    }

    /**
     * Set the Extension looking for.
     *
     * @param extension Set the Extension looking for.
     */
    public void addConfiguredExtension(final ExtensionAdapter extension) {
        if (null != requiredExtension) {
            throw new BuildException(
                "Can not specify extension to resolve multiple times.");
        }
        requiredExtension = extension.toExtension();
    }

    /**
     * Execute the task.
     *
     * @throws BuildException if the task fails.
     */
    @Override
    public void execute() throws BuildException {
        validate();

        getProject().log("Resolving extension: " + requiredExtension, Project.MSG_VERBOSE);

        String candidate = getProject().getProperty(propertyName);

        if (null != candidate) {
            final String message = "Property Already set to: " + candidate;
            if (failOnError) {
                throw new BuildException(message);
            }
            getProject().log(message, Project.MSG_ERR);
            return;
        }

        for (ExtensionResolver resolver : resolvers) {
            getProject().log("Searching for extension using Resolver:" + resolver,
                    Project.MSG_VERBOSE);
            try {
                final File file =
                    resolver.resolve(requiredExtension, getProject());
                try {
                    checkExtension(file);
                    return;
                } catch (final BuildException be) {
                    getProject().log("File " + file + " returned by "
                        + "resolver failed to satisfy extension due to: "
                        + be.getMessage(), Project.MSG_WARN);
                }
            } catch (final BuildException be) {
                getProject()
                    .log(
                        "Failed to resolve extension to file "
                            + "using resolver " + resolver + " due to: " + be,
                        Project.MSG_WARN);
            }
        }
        missingExtension();
    }

    /**
     * Utility method that will throw a {@link BuildException}
     * if {@link #failOnError} is true else it just displays
     * a warning.
     */
    private void missingExtension() {
        final String message = "Unable to resolve extension to a file";
        if (failOnError) {
            throw new BuildException(message);
        }
        getProject().log(message, Project.MSG_ERR);
    }

    /**
     * Check if specified file satisfies extension.
     * If it does then set the relevant property
     * else throw a BuildException.
     *
     * @param file the candidate library
     * @throws BuildException if library does not satisfy extension
     */
    private void checkExtension(final File file) {
        if (!file.exists()) {
            throw new BuildException("File %s does not exist", file);
        }
        if (!file.isFile()) {
            throw new BuildException("File %s is not a file", file);
        }
        if (!checkExtension) {
            getProject().log("Setting property to " + file
                    + " without verifying library satisfies extension", Project.MSG_VERBOSE);
            setLibraryProperty(file);
        } else {
            getProject().log("Checking file " + file + " to see if it satisfies extension",
                    Project.MSG_VERBOSE);
            final Manifest manifest = ExtensionUtil.getManifest(file);
            for (final Extension extension : Extension.getAvailable(manifest)) {
                if (extension.isCompatibleWith(requiredExtension)) {
                    setLibraryProperty(file);
                    return;
                }
            }
            final String message = "File " + file + " skipped as it "
                + "does not satisfy extension";
            getProject().log(message, Project.MSG_VERBOSE);
            throw new BuildException(message);
        }
    }

    /**
     * Utility method to set the appropriate property
     * to indicate that specified file satisfies library
     * requirements.
     *
     * @param file the library
     */
    private void setLibraryProperty(final File file) {
        getProject().setNewProperty(propertyName, file.getAbsolutePath());
    }

    /**
     * Validate the tasks parameters.
     *
     * @throws BuildException if invalid parameters found
     */
    private void validate() throws BuildException {
        if (null == propertyName) {
            throw new BuildException("Property attribute must be specified.");
        }
        if (null == requiredExtension) {
            throw new BuildException("Extension element must be specified.");
        }
    }
}
