/*
 *  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
 *
 *      https://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.ivy.ant;

import java.io.File;
import java.net.MalformedURLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.apache.ivy.Ivy;
import org.apache.ivy.core.LogOptions;
import org.apache.ivy.core.module.descriptor.DefaultExcludeRule;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ExtendsDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.report.ResolveReport;
import org.apache.ivy.core.resolve.ResolveOptions;
import org.apache.ivy.core.resolve.ResolveProcessException;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.filter.FilterHelper;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;

import static org.apache.ivy.util.StringUtils.splitToArray;

/**
 * This task allow to call the Ivy dependency resolution from ant.
 */
public class IvyResolve extends IvyTask {
    private File file = null;

    private String conf = null;

    private String organisation = null;

    private String module = null;

    private String branch = null;

    private String revision = null;

    private String pubdate = null;

    private boolean inline = false;

    private boolean haltOnFailure = true;

    private boolean showProgress = true;

    private boolean useCacheOnly = false;

    private String type = null;

    private boolean transitive = true;

    private boolean refresh = false;

    private boolean changing = false;

    private Boolean keep = null;

    private String failureProperty = null;

    private boolean useOrigin = false;

    private String resolveMode = null;

    private String resolveId = null;

    private String log = ResolveOptions.LOG_DEFAULT;

    private boolean checkIfChanged = true; // for backward compatibility

    private List<IvyDependency> dependencies = new ArrayList<>();

    private List<IvyExclude> excludes = new ArrayList<>();

    private List<IvyConflict> conflicts = new ArrayList<>();

    public boolean isUseOrigin() {
        return useOrigin;
    }

    public void setUseOrigin(boolean useOrigin) {
        this.useOrigin = useOrigin;
    }

    public String getDate() {
        return pubdate;
    }

    public void setDate(String pubdate) {
        this.pubdate = pubdate;
    }

    public String getRevision() {
        return revision;
    }

    public void setRevision(String revision) {
        this.revision = revision;
    }

    public String getBranch() {
        return branch;
    }

    public void setBranch(String branch) {
        this.branch = branch;
    }

    public void setCache(File cache) {
        cacheAttributeNotSupported();
    }

    public String getConf() {
        return conf;
    }

    public void setConf(String conf) {
        this.conf = conf;
    }

    public File getFile() {
        return file;
    }

    public void setFile(File file) {
        this.file = file;
    }

    public boolean isHaltonfailure() {
        return haltOnFailure;
    }

    public void setHaltonfailure(boolean haltOnFailure) {
        this.haltOnFailure = haltOnFailure;
    }

    public void setShowprogress(boolean show) {
        this.showProgress = show;
    }

    public boolean isUseCacheOnly() {
        return useCacheOnly;
    }

    public void setUseCacheOnly(boolean useCacheOnly) {
        this.useCacheOnly = useCacheOnly;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public boolean isRefresh() {
        return refresh;
    }

    public void setRefresh(boolean refresh) {
        this.refresh = refresh;
    }

    public String getLog() {
        return log;
    }

    public void setLog(String log) {
        this.log = log;
    }

    /**
     * @param failureProperty String
     * @deprecated Use {@link #setFailureProperty(String)} instead
     */
    @Deprecated
    public void setFailurePropery(String failureProperty) {
        log("The 'failurepropery' attribute is deprecated. "
                + "Please use the 'failureproperty' attribute instead", Project.MSG_WARN);
        setFailureProperty(failureProperty);
    }

    public void setFailureProperty(String failureProperty) {
        this.failureProperty = failureProperty;
    }

    public String getFailureProperty() {
        return failureProperty;
    }

    public IvyDependency createDependency() {
        IvyDependency dep = new IvyDependency();
        dependencies.add(dep);
        return dep;
    }

    public IvyExclude createExclude() {
        IvyExclude ex = new IvyExclude();
        excludes.add(ex);
        return ex;
    }

    public IvyConflict createConflict() {
        IvyConflict c = new IvyConflict();
        conflicts.add(c);
        return c;
    }

    @Override
    protected void prepareTask() {
        super.prepareTask();
        Message.setShowProgress(showProgress);
    }

    @Override
    public void doExecute() throws BuildException {
        Ivy ivy = getIvyInstance();
        IvySettings settings = ivy.getSettings();
        try {
            conf = getProperty(conf, settings, "ivy.configurations");
            type = getProperty(type, settings, "ivy.resolve.default.type.filter");
            String[] confs = splitToArray(conf);

            boolean childs = !dependencies.isEmpty() || !excludes.isEmpty() || !conflicts.isEmpty();

            ResolveReport report;
            if (childs) {
                if (isInline()) {
                    throw new BuildException("the inline mode is incompatible with child elements");
                }
                if (organisation != null) {
                    throw new BuildException("'organisation' is not allowed with child elements");
                }
                if (module != null) {
                    throw new BuildException("'module' is not allowed with child elements");
                }
                if (file != null) {
                    throw new BuildException("'file' not allowed with child elements");
                }
                if (!getAllowedLogOptions().contains(log)) {
                    throw new BuildException("invalid option for 'log': " + log
                            + ". Available options are " + getAllowedLogOptions());
                }

                ModuleRevisionId mrid = ModuleRevisionId.newInstance("", "",
                    Ivy.getWorkingRevision());
                DefaultModuleDescriptor md = DefaultModuleDescriptor.newBasicInstance(mrid, null);

                for (IvyDependency dep : dependencies) {
                    DependencyDescriptor dd = dep.asDependencyDescriptor(md, "default", settings);
                    md.addDependency(dd);
                }

                for (IvyExclude exclude : excludes) {
                    DefaultExcludeRule rule = exclude.asRule(settings);
                    rule.addConfiguration("default");
                    md.addExcludeRule(rule);
                }

                for (IvyConflict conflict : conflicts) {
                    conflict.addConflict(md, settings);
                }

                report = ivy
                        .resolve(md, getResolveOptions(ivy, new String[] {"default"}, settings));
            } else if (isInline()) {
                if (organisation == null) {
                    throw new BuildException("'organisation' is required when using inline mode");
                }
                if (module == null) {
                    throw new BuildException("'module' is required when using inline mode");
                }
                if (file != null) {
                    throw new BuildException("'file' not allowed when using inline mode");
                }
                if (!getAllowedLogOptions().contains(log)) {
                    throw new BuildException("invalid option for 'log': " + log
                            + ". Available options are " + getAllowedLogOptions());
                }
                for (int i = 0; i < confs.length; i++) {
                    if ("*".equals(confs[i])) {
                        confs[i] = "*(public)";
                    }
                }
                if (revision == null) {
                    revision = "latest.integration";
                }
                report = ivy.resolve(
                    ModuleRevisionId.newInstance(organisation, module, branch, revision),
                    getResolveOptions(ivy, confs, settings), changing);

            } else {
                if (organisation != null) {
                    throw new BuildException(
                            "'organisation' not allowed when not using 'org' attribute");
                }
                if (module != null) {
                    throw new BuildException("'module' not allowed when not using 'org' attribute");
                }
                if (file == null) {
                    file = getProject().resolveFile(getProperty(settings, "ivy.dep.file"));
                }
                report = ivy.resolve(file.toURI().toURL(), getResolveOptions(ivy, confs, settings));
            }
            if (report.hasError()) {
                if (failureProperty != null) {
                    getProject().setProperty(failureProperty, "true");
                }
                if (isHaltonfailure()) {
                    throw new BuildException("resolve failed - see output for details");
                }
            }
            setResolved(report, resolveId, isKeep());
            confs = report.getConfigurations();

            if (isKeep()) {
                ModuleDescriptor md = report.getModuleDescriptor();
                // put resolved infos in ant properties and ivy variables
                // putting them in ivy variables is important to be able to change from one resolve
                // call to the other
                String mdOrg = md.getModuleRevisionId().getOrganisation();
                String mdName = md.getModuleRevisionId().getName();
                String mdRev = md.getResolvedModuleRevisionId().getRevision();
                getProject().setProperty("ivy.organisation", mdOrg);
                settings.setVariable("ivy.organisation", mdOrg);
                getProject().setProperty("ivy.module", mdName);
                settings.setVariable("ivy.module", mdName);
                getProject().setProperty("ivy.revision", mdRev);
                settings.setVariable("ivy.revision", mdRev);
                List<ExtendsDescriptor> parents = Arrays.asList(md.getInheritedDescriptors());
                for (ExtendsDescriptor parent : parents) {
                    int i = parents.indexOf(parent);
                    String parentOrg = parent.getResolvedParentRevisionId().getOrganisation();
                    String parentModule = parent.getResolvedParentRevisionId().getName();
                    String parentRevision = parent.getResolvedParentRevisionId().getRevision();
                    String parentBranch = parent.getResolvedParentRevisionId().getBranch();
                    getProject().setProperty("ivy.parent[" + i + "].organisation", parentOrg);
                    settings.setVariable("ivy.parent[" + i + "].organisation", parentOrg);
                    getProject().setProperty("ivy.parent[" + i + "].module", parentModule);
                    settings.setVariable("ivy.parent[" + i + "].module", parentModule);
                    getProject().setProperty("ivy.parent[" + i + "].revision", parentRevision);
                    settings.setVariable("ivy.parent[" + i + "].revision", parentRevision);
                    if (parentBranch != null) {
                        getProject().setProperty("ivy.parent[" + i + "].branch", parentBranch);
                        settings.setVariable("ivy.parent[" + i + "].branch", parentBranch);
                    }
                }
                getProject().setProperty("ivy.parents.count",
                    String.valueOf(md.getInheritedDescriptors().length));
                settings.setVariable("ivy.parents.count",
                    String.valueOf(md.getInheritedDescriptors().length));

                Boolean hasChanged = null;
                if (getCheckIfChanged()) {
                    hasChanged = report.hasChanged();
                    getProject().setProperty("ivy.deps.changed", hasChanged.toString());
                    settings.setVariable("ivy.deps.changed", hasChanged.toString());
                }
                getProject().setProperty("ivy.resolved.configurations", mergeConfs(confs));
                settings.setVariable("ivy.resolved.configurations", mergeConfs(confs));
                if (file != null) {
                    getProject().setProperty("ivy.resolved.file", file.getAbsolutePath());
                    settings.setVariable("ivy.resolved.file", file.getAbsolutePath());
                }
                if (resolveId != null) {
                    getProject().setProperty("ivy.organisation." + resolveId, mdOrg);
                    settings.setVariable("ivy.organisation." + resolveId, mdOrg);
                    getProject().setProperty("ivy.module." + resolveId, mdName);
                    settings.setVariable("ivy.module." + resolveId, mdName);
                    getProject().setProperty("ivy.revision." + resolveId, mdRev);
                    settings.setVariable("ivy.revision." + resolveId, mdRev);
                    if (getCheckIfChanged()) {
                        // hasChanged has already been set earlier
                        getProject().setProperty("ivy.deps.changed." + resolveId,
                            hasChanged.toString());
                        settings.setVariable("ivy.deps.changed." + resolveId, hasChanged.toString());
                    }
                    getProject().setProperty("ivy.resolved.configurations." + resolveId,
                        mergeConfs(confs));
                    settings.setVariable("ivy.resolved.configurations." + resolveId,
                        mergeConfs(confs));
                    if (file != null) {
                        getProject().setProperty("ivy.resolved.file." + resolveId,
                            file.getAbsolutePath());
                        settings.setVariable("ivy.resolved.file." + resolveId,
                            file.getAbsolutePath());
                    }
                }
            }
        } catch (MalformedURLException e) {
            throw new BuildException("unable to convert given ivy file to url: " + file + ": " + e,
                    e);
        } catch (ParseException e) {
            log(e.getMessage(), Project.MSG_ERR);
            throw new BuildException("syntax errors in ivy file: " + e, e);
        } catch (ResolveProcessException e) {
            throw new BuildException("impossible to resolve dependencies:\n\t" + e.getMessage(), e);
        } catch (Exception e) {
            throw new BuildException("impossible to resolve dependencies:\n\t" + e, e);
        }
    }

    protected Collection<String> getAllowedLogOptions() {
        return Arrays.asList(LogOptions.LOG_DEFAULT, LogOptions.LOG_DOWNLOAD_ONLY,
            LogOptions.LOG_QUIET);
    }

    private ResolveOptions getResolveOptions(Ivy ivy, String[] confs, IvySettings settings) {
        if (useOrigin) {
            settings.useDeprecatedUseOrigin();
        }
        return ((ResolveOptions) new ResolveOptions().setLog(log)).setConfs(confs)
                .setValidate(doValidate(settings))
                .setArtifactFilter(FilterHelper.getArtifactTypeFilter(type)).setRevision(revision)
                .setDate(getPubDate(pubdate, null)).setUseCacheOnly(useCacheOnly)
                .setRefresh(refresh).setTransitive(transitive).setResolveMode(resolveMode)
                .setResolveId(resolveId).setCheckIfChanged(checkIfChanged);
    }

    public String getModule() {
        return module;
    }

    public void setModule(String module) {
        this.module = module;
    }

    public String getOrganisation() {
        return organisation;
    }

    public void setOrganisation(String organisation) {
        this.organisation = organisation;
    }

    public boolean isTransitive() {
        return transitive;
    }

    public void setTransitive(boolean transitive) {
        this.transitive = transitive;
    }

    public boolean isChanging() {
        return changing;
    }

    public void setChanging(boolean changing) {
        this.changing = changing;
    }

    public boolean isKeep() {
        return (keep == null) ? organisation == null : keep;
    }

    public void setKeep(boolean keep) {
        this.keep = keep;
    }

    public boolean isInline() {
        return inline;
    }

    public void setInline(boolean inline) {
        this.inline = inline;
    }

    public String getResolveId() {
        return resolveId;
    }

    public void setResolveId(String resolveId) {
        this.resolveId = resolveId;
    }

    public String getResolveMode() {
        return resolveMode;
    }

    public void setResolveMode(String resolveMode) {
        this.resolveMode = resolveMode;
    }

    public boolean getCheckIfChanged() {
        return checkIfChanged;
    }

    public void setCheckIfChanged(boolean checkIfChanged) {
        this.checkIfChanged = checkIfChanged;
    }
}
