/*
 *  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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.ivy.Ivy;
import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.IvyPatternHelper;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.DependencyArtifactDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptorMediator;
import org.apache.ivy.core.module.descriptor.ExcludeRule;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.descriptor.OverrideDependencyDescriptorMediator;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.module.id.ModuleRules;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.core.settings.IvyVariableContainer;
import org.apache.ivy.plugins.matcher.MapMatcher;
import org.apache.ivy.plugins.parser.m2.PomWriterOptions.ConfigurationScopeMapping;
import org.apache.ivy.plugins.parser.m2.PomWriterOptions.ExtraDependency;
import org.apache.ivy.util.ConfigurationUtils;

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

public final class PomModuleDescriptorWriter {

    private static final String SKIP_LINE = "SKIP_LINE";

    @SuppressWarnings("serial")
    private static final ConfigurationScopeMapping DEFAULT_MAPPING = new ConfigurationScopeMapping(
            new LinkedHashMap<String, String>() {
                {
                    put("compile", "compile");
                    put("runtime", "runtime");
                    put("provided", "provided");
                    put("test", "test");
                    put("system", "system");
                }
            });

    private PomModuleDescriptorWriter() {
    }

    public static void write(ModuleDescriptor md, File output, PomWriterOptions options)
            throws IOException {
        LineNumberReader in;
        if (options.getTemplate() == null) {
            in = new LineNumberReader(new InputStreamReader(PomModuleDescriptorWriter.class.getResourceAsStream("pom.template")));
        } else {
            in = new LineNumberReader(new InputStreamReader(new FileInputStream(options.getTemplate())));
        }

        if (output.getParentFile() != null) {
            output.getParentFile().mkdirs();
        }

        try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), StandardCharsets.UTF_8))) {
            IvySettings settings = IvyContext.getContext().getSettings();
            IvyVariableContainer variables = new IvyVariableContainerWrapper(settings.getVariableContainer());

            variables.setVariable("ivy.pom.license", SKIP_LINE, true);
            variables.setVariable("ivy.pom.header", SKIP_LINE, true);
            variables.setVariable("ivy.pom.groupId", SKIP_LINE, true);
            variables.setVariable("ivy.pom.artifactId", SKIP_LINE, true);
            variables.setVariable("ivy.pom.version", SKIP_LINE, true);
            variables.setVariable("ivy.pom.packaging", SKIP_LINE, true);
            variables.setVariable("ivy.pom.name", SKIP_LINE, true);
            variables.setVariable("ivy.pom.description", SKIP_LINE, true);
            variables.setVariable("ivy.pom.url", SKIP_LINE, true);

            if (options.getLicenseHeader() != null) {
                variables.setVariable("ivy.pom.license", options.getLicenseHeader(), true);
            }
            if (options.isPrintIvyInfo()) {
                String header
                    = "<!--\n" // TODO: replace \n with lineSeparator()
                    + "   Apache Maven 2 POM generated by Apache Ivy\n"
                    + "   " + Ivy.getIvyHomeURL() + "\n"
                    + "   Apache Ivy version: " + Ivy.getIvyVersion() + " " + Ivy.getIvyDate() + "\n"
                    + "-->";
                variables.setVariable("ivy.pom.header", header, true);
            }

            setModuleVariables(md, variables, options);

            boolean dependencyManagement = false;
            boolean dependenciesPrinted = false;
            boolean overridesPrinted = false;

            int lastIndent = 0;
            int indent = 0;
            String line;
            while ((line = in.readLine()) != null) {
                line = IvyPatternHelper.substituteVariables(line, variables);
                if (line.contains(SKIP_LINE)) {
                    // skip this line
                    continue;
                }

                if (!line.trim().isEmpty()) {
                    lastIndent = indent;
                    indent = line.indexOf('<');

                    if (line.contains("<dependencyManagement>")) {
                        dependencyManagement = true;
                    }

                    if (line.contains("</dependencyManagement>")) {
                        dependencyManagement = false;
                    }

                    if (line.contains("</dependencies>")) {
                        if (!overridesPrinted && dependencyManagement) {
                            printOverrides(md, out, indent, false);
                            overridesPrinted = true;
                        }
                        if (!dependenciesPrinted && !dependencyManagement) {
                            printDependencies(md, out, options, indent, false);
                            dependenciesPrinted = true;
                        }
                    }

                    if (line.contains("</project>")) {
                        if (!overridesPrinted) {
                            printOverrides(md, out, lastIndent, true);
                            overridesPrinted = true;
                        }
                        if (!dependenciesPrinted) {
                            printDependencies(md, out, options, lastIndent, true);
                            dependenciesPrinted = true;
                        }
                    }
                }
                out.println(line);
            }
        } finally {
            in.close();
        }
    }

    private static void setModuleVariables(ModuleDescriptor md, IvyVariableContainer variables,
            PomWriterOptions options) {
        ModuleRevisionId mrid = md.getModuleRevisionId();
        variables.setVariable("ivy.pom.groupId", mrid.getOrganisation(), true);

        String artifactId = options.getArtifactName();
        if (artifactId == null) {
            artifactId = mrid.getName();
        }

        String packaging = options.getArtifactPackaging();
        if (packaging == null) {
            // find artifact to determine the packaging
            Artifact artifact = findArtifact(md, artifactId);
            if (artifact == null) {
                // no suitable artifact found, default to 'pom'
                packaging = "pom";
            } else {
                packaging = artifact.getType();
            }
        }

        variables.setVariable("ivy.pom.artifactId", artifactId, true);
        variables.setVariable("ivy.pom.packaging", packaging, true);
        if (mrid.getRevision() != null) {
            variables.setVariable("ivy.pom.version", mrid.getRevision(), true);
        }
        if (options.getDescription() != null) {
            variables.setVariable("ivy.pom.description", options.getDescription(), true);
        } else if (!isNullOrEmpty(md.getDescription())) {
            variables.setVariable("ivy.pom.description", md.getDescription(), true);
        }
        if (md.getHomePage() != null) {
            variables.setVariable("ivy.pom.url", md.getHomePage(), true);
        }
    }

    /**
     * Returns the first artifact with the correct name and without a classifier.
     */
    private static Artifact findArtifact(ModuleDescriptor md, String artifactName) {
        for (Artifact artifact : md.getAllArtifacts()) {
            if (artifact.getName().equals(artifactName)
                    && artifact.getAttribute("classifier") == null) {
                return artifact;
            }
        }

        return null;
    }

    private static void indent(PrintWriter out, int indent) {
        for (int i = 0; i < indent; i++) {
            out.print(' ');
        }
    }

    private static void printDependencies(ModuleDescriptor md, PrintWriter out,
            PomWriterOptions options, int indent, boolean printDependencies) {
        List<ExtraDependency> extraDeps = options.getExtraDependencies();
        DependencyDescriptor[] dds = getDependencies(md, options);

        if (!extraDeps.isEmpty() || (dds.length > 0)) {
            if (printDependencies) {
                indent(out, indent);
                out.println("<dependencies>");
            }

            // print the extra dependencies first
            for (ExtraDependency dep : extraDeps) {
                String groupId = dep.getGroup();
                if (groupId == null) {
                    groupId = md.getModuleRevisionId().getOrganisation();
                }
                String version = dep.getVersion();
                if (version == null) {
                    version = md.getModuleRevisionId().getRevision();
                }
                printDependency(out, indent, groupId, dep.getArtifact(), version, dep.getType(),
                    dep.getClassifier(), dep.getScope(), dep.isOptional(), true, null);
            }

            // now print the dependencies listed in the ModuleDescriptor
            ConfigurationScopeMapping mapping = options.getMapping();
            if (mapping == null) {
                mapping = DEFAULT_MAPPING;
            }

            for (DependencyDescriptor dd : dds) {
                ModuleRevisionId mrid = dd.getDependencyRevisionId();
                ExcludeRule[] excludes = null;
                if (dd.canExclude()) {
                    excludes = dd.getAllExcludeRules();
                }
                DependencyArtifactDescriptor[] dads = dd.getAllDependencyArtifacts();
                if (dads.length > 0) {
                    for (DependencyArtifactDescriptor dad : dads) {
                        String type = dad.getType();
                        String classifier = dad.getExtraAttribute("classifier");
                        String scope = mapping.getScope(dd.getModuleConfigurations());
                        boolean optional = mapping.isOptional(dd.getModuleConfigurations());
                        printDependency(out, indent, mrid.getOrganisation(), mrid.getName(),
                            mrid.getRevision(), type, classifier, scope, optional,
                            dd.isTransitive(), excludes);
                    }
                } else {
                    String scope = mapping.getScope(dd.getModuleConfigurations());
                    boolean optional = mapping.isOptional(dd.getModuleConfigurations());
                    final String classifier = dd.getExtraAttribute("classifier");
                    printDependency(out, indent, mrid.getOrganisation(), mrid.getName(),
                        mrid.getRevision(), null, classifier, scope, optional, dd.isTransitive(),
                        excludes);
                }
            }

            if (printDependencies) {
                indent(out, indent);
                out.println("</dependencies>");
            }
        }
    }

    private static void printDependency(PrintWriter out, int indent, String groupId,
            String artifactId, String version, String type, String classifier, String scope,
            boolean isOptional, boolean isTransitive, ExcludeRule[] excludes) {
        indent(out, indent * 2);
        out.println("<dependency>");
        indent(out, indent * 3);
        out.println("<groupId>" + groupId + "</groupId>");
        indent(out, indent * 3);
        out.println("<artifactId>" + artifactId + "</artifactId>");
        indent(out, indent * 3);
        out.println("<version>" + version + "</version>");
        if (type != null && !"jar".equals(type)) {
            indent(out, indent * 3);
            out.println("<type>" + type + "</type>");
        }
        if (classifier != null) {
            indent(out, indent * 3);
            out.println("<classifier>" + classifier + "</classifier>");
        }
        if (scope != null) {
            indent(out, indent * 3);
            out.println("<scope>" + scope + "</scope>");
        }
        if (isOptional) {
            indent(out, indent * 3);
            out.println("<optional>true</optional>");
        }
        if (!isTransitive) {
            indent(out, indent * 3);
            out.println("<exclusions>");
            indent(out, indent * 4);
            out.println("<exclusion>");
            indent(out, indent * 5);
            out.println("<groupId>*</groupId>");
            indent(out, indent * 5);
            out.println("<artifactId>*</artifactId>");
            indent(out, indent * 4);
            out.println("</exclusion>");
            indent(out, indent * 3);
            out.println("</exclusions>");
        } else if (excludes != null) {
            printExclusions(excludes, out, indent);
        }
        indent(out, indent * 2);
        out.println("</dependency>");
    }

    private static void printExclusions(ExcludeRule[] exclusions, PrintWriter out, int indent) {
        indent(out, indent * 3);
        out.println("<exclusions>");

        for (ExcludeRule exclusion : exclusions) {
            indent(out, indent * 4);
            out.println("<exclusion>");
            indent(out, indent * 5);
            out.println(
                "<groupId>" + exclusion.getId().getModuleId().getOrganisation() + "</groupId>");
            indent(out, indent * 5);
            out.println(
                "<artifactId>" + exclusion.getId().getModuleId().getName() + "</artifactId>");
            indent(out, indent * 4);
            out.println("</exclusion>");
        }

        indent(out, indent * 3);
        out.println("</exclusions>");
    }

    private static void printOverrides(ModuleDescriptor md, PrintWriter out, int indent, boolean container) {
        ModuleRules<DependencyDescriptorMediator> mr = md.getAllDependencyDescriptorMediators();
        Map<MapMatcher, DependencyDescriptorMediator> m = new LinkedHashMap<>(mr.getAllRules());

        m.values().removeIf(value -> !(value instanceof OverrideDependencyDescriptorMediator));
        m.keySet().removeIf(key -> {
            String artifactId = key.getAttributes().get("module");
            return artifactId == null || artifactId.equals("*");
        });
        m.keySet().removeIf(key -> {
            String groupId = key.getAttributes().get("organisation");
            return groupId == null || groupId.equals("*");
        });

        if (m.isEmpty()) {
            return;
        }

        if (container) {
            indent(out, indent);
            out.println("<dependencyManagement>");
            indent(out, indent * 2);
            out.println("<dependencies>");
        } else {
            indent /= 2; // <dependencies> is second-level child of <project>
        }

        for (Map.Entry<MapMatcher, DependencyDescriptorMediator> entry : m.entrySet()) {
            String artifactId = entry.getKey().getAttributes().get("module");
            String groupId = entry.getKey().getAttributes().get("organisation");
            String version = ((OverrideDependencyDescriptorMediator) entry.getValue()).getVersion();

            indent(out, indent * 3);
            out.println("<dependency>");
            indent(out, indent * 4);
            out.println("<groupId>" + groupId + "</groupId>");
            indent(out, indent * 4);
            out.println("<artifactId>" + artifactId + "</artifactId>");
            indent(out, indent * 4);
            out.println("<version>" + version + "</version>");
            indent(out, indent * 3);
            out.println("</dependency>");
        }

        if (container) {
            indent(out, indent * 2);
            out.println("</dependencies>");
            indent(out, indent);
            out.println("</dependencyManagement>");
        }
    }

    private static DependencyDescriptor[] getDependencies(ModuleDescriptor md, PomWriterOptions options) {
        String[] confs = ConfigurationUtils.replaceWildcards(options.getConfs(), md);

        List<DependencyDescriptor> result = new ArrayList<>();
        for (DependencyDescriptor dd : md.getDependencies()) {
            String[] depConfs = dd.getDependencyConfigurations(confs);
            if (depConfs != null && depConfs.length > 0) {
                result.add(dd);
            }
        }

        return result.toArray(new DependencyDescriptor[result.size()]);
    }

    /**
     * Wraps an {@link IvyVariableContainer} delegating most method calls to the wrapped instance,
     * except for a set of variables which are only stored locally in the wrapper, and not
     * propagated to the wrapped instance.
     */
    private static final class IvyVariableContainerWrapper implements IvyVariableContainer {

        private final IvyVariableContainer variables;

        private Map<String, String> localVariables = new HashMap<>();

        private IvyVariableContainerWrapper(IvyVariableContainer variables) {
            this.variables = variables;
        }

        public void setVariable(String varName, String value, boolean overwrite) {
            localVariables.put(varName, value);
        }

        public void setEnvironmentPrefix(String prefix) {
            variables.setEnvironmentPrefix(prefix);
        }

        public String getVariable(String name) {
            String result = variables.getVariable(name);
            if (result == null) {
                result = localVariables.get(name);
            }
            return result;
        }

        public Object clone() {
            throw new UnsupportedOperationException();
        }
    }

}
