/*
 *  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.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.apache.ivy.Ivy;
import org.apache.ivy.core.cache.ResolutionCacheManager;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
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.settings.IvySettings;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.filter.Filter;
import org.apache.ivy.util.filter.FilterHelper;
import org.apache.tools.ant.BuildException;

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

/**
 * Base class for tasks needing to be performed after a resolve.
 */
public abstract class IvyPostResolveTask extends IvyTask {
    private String conf;

    private boolean haltOnFailure = true;

    private boolean transitive = true;

    private boolean inline = false;

    private String organisation;

    private String branch = null;

    private String module;

    private String revision = "latest.integration";

    private String resolveId;

    private String type;

    private File file;

    private Filter<Artifact> artifactFilter = null;

    private boolean useOrigin = false;

    private Boolean keep = null;

    private boolean refresh = false;

    private String resolveMode = null;

    private String log = ResolveOptions.LOG_DEFAULT;

    private boolean changing = false;

    private IvyResolve resolve = new IvyResolve();

    public boolean isUseOrigin() {
        return useOrigin;
    }

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

    public String getLog() {
        return log;
    }

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

    public IvyDependency createDependency() {
        return resolve.createDependency();
    }

    public IvyExclude createExclude() {
        return resolve.createExclude();
    }

    public IvyConflict createConflict() {
        return resolve.createConflict();
    }

    protected void prepareAndCheck() {
        Ivy ivy = getIvyInstance();
        IvySettings settings = ivy.getSettings();

        boolean orgAndModSetManually = organisation != null && module != null;

        organisation = getProperty(organisation, settings, "ivy.organisation");
        module = getProperty(module, settings, "ivy.module");

        if (file == null) {
            String fileName = getProperty(settings, "ivy.resolved.file", resolveId);
            if (fileName != null) {
                file = getProject().resolveFile(fileName);
            }
        }

        if (isInline()) {
            if (conf == null) {
                conf = "*";
            }
            if (organisation == null) {
                throw new BuildException(
                        "no organisation provided for ivy cache task in inline mode: "
                                + "It can either be set explicitly via the attribute 'organisation' "
                                + "or via 'ivy.organisation' property");
            }
            if (module == null) {
                throw new BuildException(
                        "no module name provided for ivy cache task in inline mode: "
                                + "It can either be set explicitly via the attribute 'module' "
                                + "or via 'ivy.module' property");
            }
            String[] toResolve = getConfsToResolve(getOrganisation(), getModule() + "-caller",
                conf, true);
            // When we make an inline resolution, we can not resolve private confs.
            for (int i = 0; i < toResolve.length; i++) {
                if ("*".equals(toResolve[i])) {
                    toResolve[i] = "*(public)";
                }
            }
            if (toResolve.length > 0) {
                Message.verbose(String.format("using inline mode to resolve %s %s %s (%s)",
                        getOrganisation(), getModule(), getRevision(), joinArray(toResolve, ", ")));
                IvyResolve resolve = setupResolve(isHaltonfailure(), isUseOrigin());
                resolve.setOrganisation(getOrganisation());
                resolve.setModule(getModule());
                resolve.setBranch(getBranch());
                resolve.setRevision(getRevision());
                resolve.setInline(true);
                resolve.setChanging(isChanging());
                resolve.setConf(conf);
                resolve.setResolveId(resolveId);
                resolve.setTransitive(isTransitive());
                resolve.execute();
            } else {
                Message.verbose(String.format("inline resolve already done for %s %s %s (%s)",
                        getOrganisation(), getModule(), getRevision(), conf));
            }
            if ("*".equals(conf)) {
                conf = joinArray(
                    getResolvedConfigurations(getOrganisation(), getModule() + "-caller", true),
                    ", ");
            }
        } else {
            Message.debug("using standard ensure resolved");

            // if the organization and module has been manually specified, we'll reuse the resolved
            // data from another build (there is no way to know which configurations were resolved
            // there (TODO: maybe we can check which reports exist and extract the configurations
            // from these report names?)
            if (!orgAndModSetManually) {
                ensureResolved(settings);
            }

            conf = getProperty(conf, settings, "ivy.resolved.configurations");
            if ("*".equals(conf)) {
                conf = getProperty(settings, "ivy.resolved.configurations");
                if (conf == null) {
                    throw new BuildException("bad conf provided for ivy cache task: "
                            + "'*' can only be used with a prior call to <resolve/>");
                }
            }
        }
        organisation = getProperty(organisation, settings, "ivy.organisation");
        module = getProperty(module, settings, "ivy.module");
        if (organisation == null) {
            throw new BuildException("no organisation provided for ivy cache task: "
                    + "It can either be set explicitly via the attribute 'organisation' "
                    + "or via 'ivy.organisation' property or a prior call to <resolve/>");
        }
        if (module == null) {
            throw new BuildException("no module name provided for ivy cache task: "
                    + "It can either be set explicitly via the attribute 'module' "
                    + "or via 'ivy.module' property or a prior call to <resolve/>");
        }
        if (conf == null) {
            throw new BuildException("no conf provided for ivy cache task: "
                    + "It can either be set explicitly via the attribute 'conf' or "
                    + "via 'ivy.resolved.configurations' property or a prior call to <resolve/>");
        }

        artifactFilter = FilterHelper.getArtifactTypeFilter(type);
    }

    protected void ensureResolved(IvySettings settings) {
        String requestedConfigs = getProperty(getConf(), settings, "ivy.resolved.configurations");

        String[] confs = (getResolveId() == null)
                ? getConfsToResolve(getOrganisation(), getModule(), requestedConfigs, false)
                : getConfsToResolve(getResolveId(), requestedConfigs);

        if (confs.length > 0) {
            IvyResolve resolve = setupResolve(isHaltonfailure(), isUseOrigin());
            resolve.setFile(getFile());
            resolve.setTransitive(isTransitive());
            resolve.setConf(joinArray(confs, ", "));
            resolve.setResolveId(getResolveId());
            resolve.execute();
        }
    }

    protected String[] getConfsToResolve(String org, String module, String conf, boolean strict) {
        ModuleDescriptor reference = getResolvedDescriptor(org, module, strict);
        String[] rconfs = getResolvedConfigurations(org, module, strict);
        return getConfsToResolve(reference, conf, rconfs);
    }

    protected String[] getConfsToResolve(String resolveId, String conf) {
        ModuleDescriptor reference = getResolvedDescriptor(resolveId, false);
        if (reference == null) {
            // assume the module has been resolved outside this build, resolve the required
            // configurations again
            // TODO: find a way to discover which confs were resolved by that previous resolve
            if (conf == null) {
                return new String[] {"*"};
            }
            return splitToArray(conf);
        }
        String[] rconfs = getProject().getReference("ivy.resolved.configurations.ref." + resolveId);
        return getConfsToResolve(reference, conf, rconfs);
    }

    private String[] getConfsToResolve(ModuleDescriptor reference, String conf, String[] rconfs) {
        Message.debug("calculating configurations to resolve");

        if (reference == null) {
            Message.debug("module not yet resolved, all confs still need to be resolved");
            if (conf == null) {
                return new String[] {"*"};
            }
            return splitToArray(conf);
        }

        if (conf == null) {
            Message.debug("module already resolved, no configuration to resolve");
            return new String[0];
        }

        String[] confs;
        if ("*".equals(conf)) {
            confs = reference.getConfigurationsNames();
        } else {
            confs = splitToArray(conf);
        }

        Set<String> rconfsSet = new HashSet<>();

        // for each resolved configuration, check if the report still exists
        ResolutionCacheManager cache = getSettings().getResolutionCacheManager();
        for (String resolvedConf : rconfs) {
            String resolveId = getResolveId();
            if (resolveId == null) {
                resolveId = ResolveOptions.getDefaultResolveId(reference);
            }
            File report = cache.getConfigurationResolveReportInCache(resolveId, resolvedConf);
            // if the report does not exist any longer, we have to recreate it...
            if (report.exists()) {
                rconfsSet.add(resolvedConf);
            }
        }

        Set<String> confsSet = new HashSet<>(Arrays.asList(confs));
        Message.debug("resolved configurations:   " + rconfsSet);
        Message.debug("asked configurations:      " + confsSet);
        confsSet.removeAll(rconfsSet);
        Message.debug("to resolve configurations: " + confsSet);
        return confsSet.toArray(new String[confsSet.size()]);
    }

    protected IvyResolve setupResolve(boolean haltOnFailure, boolean useOrigin) {
        Message.verbose("no resolved descriptor found: launching default resolve");
        resolve.setTaskName(getTaskName());
        resolve.setProject(getProject());
        resolve.setHaltonfailure(haltOnFailure);
        resolve.setUseOrigin(useOrigin);
        resolve.setValidate(doValidate(getSettings()));
        resolve.setKeep(isKeep());
        resolve.setRefresh(isRefresh());
        resolve.setLog(getLog());
        resolve.setSettingsRef(getSettingsRef());
        resolve.setResolveMode(getResolveMode());
        return resolve;
    }

    protected ModuleRevisionId getResolvedMrid() {
        return new ModuleRevisionId(getResolvedModuleId(),
                (getRevision() == null) ? Ivy.getWorkingRevision() : getRevision());
    }

    protected ModuleId getResolvedModuleId() {
        return isInline() ? new ModuleId(getOrganisation(), getModule() + "-caller")
                : new ModuleId(getOrganisation(), getModule());
    }

    protected ResolveReport getResolvedReport() {
        return getResolvedReport(getOrganisation(), isInline() ? getModule() + "-caller"
                : getModule(), resolveId);
    }

    public String getType() {
        return type;
    }

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

    public String getConf() {
        return conf;
    }

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

    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 String getBranch() {
        return branch;
    }

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

    public boolean isHaltonfailure() {
        return haltOnFailure;
    }

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

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

    public String getRevision() {
        return revision;
    }

    public void setRevision(String rev) {
        revision = rev;
    }

    public Filter<Artifact> getArtifactFilter() {
        return artifactFilter;
    }

    public boolean isTransitive() {
        return transitive;
    }

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

    public boolean isInline() {
        return inline;
    }

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

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

    public String getResolveId() {
        return resolveId;
    }

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

    public File getFile() {
        return file;
    }

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

    public boolean isKeep() {
        return this.keep == null ? !isInline() : this.keep;
    }

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

    public boolean isChanging() {
        return this.changing;
    }

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

    public boolean isRefresh() {
        return refresh;
    }

    public String getResolveMode() {
        return resolveMode;
    }

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