/*
 *  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.plugins.parser.m2;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.cache.ArtifactOrigin;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultArtifact;
import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.License;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.resolve.ResolveData;
import org.apache.ivy.core.resolve.ResolveEngine;
import org.apache.ivy.core.resolve.ResolveOptions;
import org.apache.ivy.core.resolve.ResolvedModuleRevision;
import org.apache.ivy.plugins.circular.CircularDependencyException;
import org.apache.ivy.plugins.parser.ModuleDescriptorParser;
import org.apache.ivy.plugins.parser.ParserSettings;
import org.apache.ivy.plugins.parser.m2.PomModuleDescriptorBuilder.PomDependencyDescriptor;
import org.apache.ivy.plugins.parser.m2.PomReader.PomDependencyData;
import org.apache.ivy.plugins.parser.m2.PomReader.PomDependencyMgtElement;
import org.apache.ivy.plugins.parser.m2.PomReader.PomPluginElement;
import org.apache.ivy.plugins.parser.m2.PomReader.PomProfileElement;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorWriter;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.plugins.resolver.DependencyResolver;
import org.apache.ivy.util.Message;
import org.xml.sax.SAXException;

import static org.apache.ivy.core.module.descriptor.Configuration.Visibility.PUBLIC;
import static org.apache.ivy.plugins.namespace.NameSpaceHelper.toSystem;
import static org.apache.ivy.plugins.parser.m2.PomModuleDescriptorBuilder.MAVEN2_CONFIGURATIONS;
import static org.apache.ivy.plugins.parser.m2.PomModuleDescriptorBuilder.extractPomProperties;
import static org.apache.ivy.plugins.parser.m2.PomModuleDescriptorBuilder.getDependencyManagements;
import static org.apache.ivy.plugins.parser.m2.PomModuleDescriptorBuilder.getPlugins;

/**
 * A parser for Maven 2 POM.
 * <p>
 * The configurations used in the generated module descriptor mimics the behavior defined by Maven 2
 * scopes, as documented <a href=
 * "https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html">here</a>.
 * The PomModuleDescriptorParser use a PomDomReader to read the pom, and the
 * PomModuleDescriptorBuilder to write the ivy module descriptor using the info read by the
 * PomDomReader.
 * </p>
 */
public final class PomModuleDescriptorParser implements ModuleDescriptorParser {

    private static final PomModuleDescriptorParser INSTANCE = new PomModuleDescriptorParser();

    private static final String PARENT_MAP_KEY = PomModuleDescriptorParser.class.getName() + ".parentMap";

    public static PomModuleDescriptorParser getInstance() {
        return INSTANCE;
    }

    private PomModuleDescriptorParser() {
    }

    public void toIvyFile(InputStream is, Resource res, File destFile, ModuleDescriptor md)
            throws ParseException, IOException {
        try {
            XmlModuleDescriptorWriter.write(md, destFile);
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }

    public boolean accept(Resource res) {
        return res.getName().endsWith(".pom") || res.getName().endsWith("pom.xml")
                || res.getName().endsWith("project.xml");
    }

    public String toString() {
        return "pom parser";
    }

    public Artifact getMetadataArtifact(ModuleRevisionId mrid, Resource res) {
        return DefaultArtifact.newPomArtifact(mrid, new Date(res.getLastModified()));
    }

    public String getType() {
        return "pom";
    }

    public ModuleDescriptor parseDescriptor(ParserSettings ivySettings, URL descriptorURL,
            boolean validate) throws ParseException, IOException {
        URLResource resource = new URLResource(descriptorURL);
        return parseDescriptor(ivySettings, descriptorURL, resource, validate);
    }

    public ModuleDescriptor parseDescriptor(ParserSettings ivySettings, URL descriptorURL,
            Resource res, boolean validate) throws ParseException, IOException {

        PomModuleDescriptorBuilder mdBuilder = new PomModuleDescriptorBuilder(this, res,
                ivySettings);

        try {
            final IvyContext ivyContext = IvyContext.pushNewCopyContext();
            Set<ModuleRevisionId> parents = ivyContext.get(PARENT_MAP_KEY);
            if (parents == null) {
                parents = new LinkedHashSet<>();
                ivyContext.set(PARENT_MAP_KEY, parents);
            }

            PomReader domReader = new PomReader(descriptorURL, res);
            domReader.setProperty("parent.version", domReader.getParentVersion());
            domReader.setProperty("parent.groupId", domReader.getParentGroupId());
            domReader.setProperty("project.parent.version", domReader.getParentVersion());
            domReader.setProperty("project.parent.groupId", domReader.getParentGroupId());

            Message.debug("parent.groupId: " + domReader.getParentGroupId());
            Message.debug("parent.artifactId: " + domReader.getParentArtifactId());
            Message.debug("parent.version: " + domReader.getParentVersion());

            for (final Map.Entry<String, String> prop : domReader.getPomProperties().entrySet()) {
                domReader.setProperty(prop.getKey(), prop.getValue());
                mdBuilder.addProperty(prop.getKey(), prop.getValue());
            }
            final List<PomProfileElement> activeProfiles = new ArrayList<>();
            // add profile specific properties
            for (final PomProfileElement profile : domReader.getProfiles()) {
                if (!profile.isActive()) {
                    continue;
                }
                // keep track of this active profile for later use
                activeProfiles.add(profile);

                final Map<String, String> profileProps = profile.getProfileProperties();
                if (profileProps.isEmpty()) {
                    continue;
                }
                for (final Map.Entry<String, String> entry : profileProps.entrySet()) {
                    domReader.setProperty(entry.getKey(), entry.getValue());
                    mdBuilder.addProperty(entry.getKey(), entry.getValue());
                }
            }

            ModuleDescriptor parentDescr = null;
            if (domReader.hasParent()) {
                // Is there any other parent properties?

                ModuleRevisionId parentModRevID = ModuleRevisionId.newInstance(
                    domReader.getParentGroupId(), domReader.getParentArtifactId(),
                    domReader.getParentVersion());

                // check for cycles
                if (parents.contains(parentModRevID)) {
                    throw new CircularDependencyException(parents);
                } else {
                    parents.add(parentModRevID);
                }

                final ResolvedModuleRevision parentModule = parseOtherPom(ivySettings, parentModRevID, true);
                if (parentModule == null) {
                    throw new IOException("Impossible to load parent for " + res.getName()
                            + ". Parent=" + parentModRevID);
                }
                parentDescr = parentModule.getDescriptor();
                if (parentDescr != null) {
                    for (Map.Entry<String, String> prop
                            : extractPomProperties(parentDescr.getExtraInfos()).entrySet()) {
                        domReader.setProperty(prop.getKey(), prop.getValue());
                    }
                }
            }

            String groupId = domReader.getGroupId();
            String artifactId = domReader.getArtifactId();
            String version = domReader.getVersion();
            mdBuilder.setModuleRevId(groupId, artifactId, version);

            mdBuilder.setHomePage(domReader.getHomePage());
            mdBuilder.setDescription(domReader.getDescription());
            // if this module doesn't have an explicit license, use the parent's license (if any)
            final License[] licenses = domReader.getLicenses();
            if (licenses != null && licenses.length > 0) {
                mdBuilder.setLicenses(licenses);
            } else if (parentDescr != null) {
                mdBuilder.setLicenses(parentDescr.getLicenses());
            }

            ModuleRevisionId relocation = domReader.getRelocation();

            if (relocation != null) {
                if (groupId != null && artifactId != null && artifactId.equals(relocation.getName())
                        && groupId.equals(relocation.getOrganisation())) {
                    Message.error("Relocation to an other version number not supported in ivy : "
                            + mdBuilder.getModuleDescriptor().getModuleRevisionId()
                            + " relocated to " + relocation
                            + ". Please update your dependency to directly use the right version.");
                    Message.warn("Resolution will only pick dependencies of the relocated element."
                            + "  Artifact and other metadata will be ignored.");
                    ResolvedModuleRevision relocatedModule = parseOtherPom(ivySettings, relocation, false);
                    if (relocatedModule == null) {
                        throw new ParseException(
                                "impossible to load module " + relocation + " to which "
                                        + mdBuilder.getModuleDescriptor().getModuleRevisionId()
                                        + " has been relocated",
                                0);
                    }
                    for (DependencyDescriptor dd : relocatedModule.getDescriptor()
                            .getDependencies()) {
                        mdBuilder.addDependency(dd);
                    }
                } else {
                    Message.info(
                        mdBuilder.getModuleDescriptor().getModuleRevisionId() + " is relocated to "
                                + relocation + ". Please update your dependencies.");
                    Message.verbose("Relocated module will be considered as a dependency");
                    DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(
                            mdBuilder.getModuleDescriptor(), relocation, true, false, true);
                    /* Map all public dependencies */
                    for (Configuration m2Conf : MAVEN2_CONFIGURATIONS) {
                        if (PUBLIC.equals(m2Conf.getVisibility())) {
                            dd.addDependencyConfiguration(m2Conf.getName(), m2Conf.getName());
                        }
                    }
                    mdBuilder.addDependency(dd);
                }
            } else {
                domReader.setProperty("project.groupId", groupId);
                domReader.setProperty("pom.groupId", groupId);
                domReader.setProperty("groupId", groupId);
                domReader.setProperty("project.artifactId", artifactId);
                domReader.setProperty("pom.artifactId", artifactId);
                domReader.setProperty("artifactId", artifactId);
                domReader.setProperty("project.version", version);
                domReader.setProperty("pom.version", version);
                domReader.setProperty("version", version);

                if (parentDescr != null) {
                    mdBuilder.addExtraInfos(parentDescr.getExtraInfos());

                    // add dependency management info from parent
                    for (PomDependencyMgt dep : getDependencyManagements(parentDescr)) {
                        if (dep instanceof PomDependencyMgtElement) {
                            dep = domReader.new PomDependencyMgtElement(
                                    (PomDependencyMgtElement) dep);
                        }
                        mdBuilder.addDependencyMgt(dep);
                    }

                    // add plugins from parent
                    for (PomDependencyMgt pomDependencyMgt : getPlugins(parentDescr)) {
                        mdBuilder.addPlugin(pomDependencyMgt);
                    }
                }

                for (PomDependencyMgt dep : domReader.getDependencyMgt()) {
                    addTo(mdBuilder, dep, ivySettings);
                }
                for (PomDependencyData dep : domReader.getDependencies()) {
                    mdBuilder.addDependency(res, dep);
                }

                for (PomPluginElement plugin : domReader.getPlugins()) {
                    mdBuilder.addPlugin(plugin);
                }

                // consult active profiles:
                for (final PomProfileElement activeProfile : activeProfiles) {
                    for (PomDependencyMgt dep : activeProfile.getDependencyMgt()) {
                        addTo(mdBuilder, dep, ivySettings);
                    }
                    for (PomDependencyData dep : activeProfile.getDependencies()) {
                        mdBuilder.addDependency(res, dep);
                    }
                    for (PomPluginElement plugin : activeProfile.getPlugins()) {
                        mdBuilder.addPlugin(plugin);
                    }
                }

                if (parentDescr != null) {
                    for (DependencyDescriptor descriptor : parentDescr.getDependencies()) {
                        if (descriptor instanceof PomDependencyDescriptor) {
                            PomDependencyData parentDep = ((PomDependencyDescriptor) descriptor)
                                    .getPomDependencyData();
                            PomDependencyData dep = domReader.new PomDependencyData(parentDep);
                            mdBuilder.addDependency(res, dep);
                        } else {
                            mdBuilder.addDependency(descriptor);
                        }
                    }
                }

                mdBuilder.addMainArtifact(artifactId, domReader.getPackaging());

                addSourcesAndJavadocArtifactsIfPresent(mdBuilder, ivySettings);
            }
        } catch (SAXException e) {
            throw newParserException(e);
        } finally {
            IvyContext.popContext();
        }

        return mdBuilder.getModuleDescriptor();
    }

    private void addTo(PomModuleDescriptorBuilder mdBuilder, PomDependencyMgt dep,
            ParserSettings ivySettings) throws ParseException, IOException {
        if ("import".equals(dep.getScope())) {
            // In Maven, "import" scope semantics are equivalent to getting (only) the
            // dependency management section of the imported module, into the current
            // module, so that those "managed dependency versions" are usable/applicable
            // in the current module's dependencies
            ModuleRevisionId importModRevID = ModuleRevisionId.newInstance(dep.getGroupId(),
                    dep.getArtifactId(), dep.getVersion());
            ResolvedModuleRevision importModule = parseOtherPom(ivySettings, importModRevID, false);
            if (importModule == null) {
                throw new IOException("Impossible to import module for "
                        + mdBuilder.getModuleDescriptor().getResource().getName() + ". Import="
                        + importModRevID);
            }
            ModuleDescriptor importDescr = importModule.getDescriptor();

            // add dependency management info from imported module
            for (PomDependencyMgt importedDepMgt : getDependencyManagements(importDescr)) {
                mdBuilder.addDependencyMgt(new DefaultPomDependencyMgt(importedDepMgt.getGroupId(),
                        importedDepMgt.getArtifactId(), importedDepMgt.getType(), importedDepMgt.getClassifier(),
                        importedDepMgt.getVersion(), importedDepMgt.getScope(), importedDepMgt.getExcludedModules()));
            }
        } else {
            mdBuilder.addDependencyMgt(dep);
        }

    }

    private void addSourcesAndJavadocArtifactsIfPresent(PomModuleDescriptorBuilder mdBuilder,
            ParserSettings ivySettings) {
        if (mdBuilder.getMainArtifact() == null) {
            // no main artifact in pom, we don't need to search for meta artifacts
            return;
        }

        boolean sourcesLookup = !"false"
                .equals(ivySettings.getVariable("ivy.maven.lookup.sources"));
        boolean javadocLookup = !"false"
                .equals(ivySettings.getVariable("ivy.maven.lookup.javadoc"));
        if (!sourcesLookup && !javadocLookup) {
            Message.debug("Sources and javadocs lookup disabled");
            return;
        }

        ModuleDescriptor md = mdBuilder.getModuleDescriptor();
        ModuleRevisionId mrid = md.getModuleRevisionId();
        DependencyResolver resolver = ivySettings.getResolver(mrid);

        if (resolver == null) {
            Message.debug(
                "no resolver found for " + mrid + ": no source or javadoc artifact lookup");
        } else {
            ArtifactOrigin mainArtifact = resolver.locate(mdBuilder.getMainArtifact());

            if (!ArtifactOrigin.isUnknown(mainArtifact)) {
                String mainArtifactLocation = mainArtifact.getLocation();

                if (sourcesLookup) {
                    ArtifactOrigin sourceArtifact = resolver.locate(mdBuilder.getSourceArtifact());
                    if (!ArtifactOrigin.isUnknown(sourceArtifact)
                            && !sourceArtifact.getLocation().equals(mainArtifactLocation)) {
                        Message.debug("source artifact found for " + mrid);
                        mdBuilder.addSourceArtifact();
                    } else {
                        // it seems that sometimes the 'src' classifier is used instead of 'sources'
                        // Cfr. IVY-1138
                        ArtifactOrigin srcArtifact = resolver.locate(mdBuilder.getSrcArtifact());
                        if (!ArtifactOrigin.isUnknown(srcArtifact)
                                && !srcArtifact.getLocation().equals(mainArtifactLocation)) {
                            Message.debug("source artifact found for " + mrid);
                            mdBuilder.addSrcArtifact();
                        } else {
                            Message.debug("no source artifact found for " + mrid);
                        }
                    }
                } else {
                    Message.debug("sources lookup disabled");
                }

                if (javadocLookup) {
                    ArtifactOrigin javadocArtifact = resolver
                            .locate(mdBuilder.getJavadocArtifact());
                    if (!ArtifactOrigin.isUnknown(javadocArtifact)
                            && !javadocArtifact.getLocation().equals(mainArtifactLocation)) {
                        Message.debug("javadoc artifact found for " + mrid);
                        mdBuilder.addJavadocArtifact();
                    } else {
                        Message.debug("no javadoc artifact found for " + mrid);
                    }
                } else {
                    Message.debug("javadocs lookup disabled");
                }
            }
        }
    }

    private ResolvedModuleRevision parseOtherPom(final ParserSettings ivySettings,
            final ModuleRevisionId parentModRevID, final boolean isParentPom) throws ParseException {

        Set<ModuleRevisionId> previousParents = null;
        if (!isParentPom) {
            // IVY-1588: we "reset" the parent tracking, since the parent tracking should only be
            // non-null when we are parsing a parent pom.
            previousParents = IvyContext.getContext().get(PARENT_MAP_KEY);
            if (previousParents != null) {
                IvyContext.getContext().set(PARENT_MAP_KEY, null);
            }
        }
        try {
            DependencyDescriptor dd = new DefaultDependencyDescriptor(parentModRevID, true);
            ResolveData data = IvyContext.getContext().getResolveData();
            if (data == null) {
                ResolveEngine engine = IvyContext.getContext().getIvy().getResolveEngine();
                ResolveOptions options = new ResolveOptions();
                options.setDownload(false);
                data = new ResolveData(engine, options);
            }

            DependencyResolver resolver = ivySettings.getResolver(parentModRevID);
            if (resolver == null) {
                // TODO: Throw exception here?
                return null;
            }
            dd = toSystem(dd, ivySettings.getContextNamespace());
            return resolver.getDependency(dd, data);
        } finally {
            if (!isParentPom) {
                // switch back to the previous state of the parent tracking
                IvyContext.getContext().set(PARENT_MAP_KEY, previousParents);
            }
        }
    }

    private ParseException newParserException(Exception e) {
        Message.error(e.getMessage());
        ParseException pe = new ParseException(e.getMessage(), 0);
        pe.initCause(e);
        return pe;
    }

}
