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

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.ivy.core.IvyPatternHelper;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
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.ExtendsDescriptor;
import org.apache.ivy.core.module.descriptor.ExtraInfoHolder;
import org.apache.ivy.core.module.descriptor.IncludeRule;
import org.apache.ivy.core.module.descriptor.License;
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.plugins.matcher.MapMatcher;
import org.apache.ivy.util.DateUtil;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.StringUtils;
import org.apache.ivy.util.XMLHelper;
import org.apache.ivy.util.extendable.ExtendableItem;

/**
 *
 */
public final class XmlModuleDescriptorWriter {

    private XmlModuleDescriptorWriter() {
        // Utility class
    }

    public static void write(ModuleDescriptor md, File output) throws IOException {
        write(md, null, output);
    }

    public static void write(ModuleDescriptor md, String licenseHeader, File output)
            throws IOException {
        if (output.getParentFile() != null) {
            output.getParentFile().mkdirs();
        }
        PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output),
                "UTF-8"));
        try {
            out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            if (licenseHeader != null) {
                out.print(licenseHeader);
            }
            StringBuffer xmlNamespace = new StringBuffer();
            Map namespaces = md.getExtraAttributesNamespaces();
            for (Iterator iter = namespaces.entrySet().iterator(); iter.hasNext();) {
                Entry ns = (Entry) iter.next();
                xmlNamespace.append(" xmlns:").append(ns.getKey()).append("=\"")
                        .append(ns.getValue()).append("\"");
            }

            String version = "2.0";
            if (md.getInheritedDescriptors().length > 0) {
                version = "2.2";
            }

            out.println("<ivy-module version=\"" + version + "\"" + xmlNamespace + ">");
            printInfoTag(md, out);
            printConfigurations(md, out);
            printPublications(md, out);
            printDependencies(md, out);
            out.println("</ivy-module>");
        } finally {
            out.close();
        }
    }

    private static void printDependencies(ModuleDescriptor md, PrintWriter out) {
        DependencyDescriptor[] dds = md.getDependencies();
        if (dds.length > 0) {
            out.println("\t<dependencies>");
            for (int i = 0; i < dds.length; i++) {
                DependencyDescriptor dep = dds[i];
                out.print("\t\t");
                printDependency(md, dep, out);
            }
            printAllExcludes(md, out);
            printAllMediators(md, out);
            out.println("\t</dependencies>");
        }
    }

    protected static void printDependency(ModuleDescriptor md, DependencyDescriptor dep,
            PrintWriter out) {
        out.print("<dependency");
        out.print(" org=\"" + XMLHelper.escape(dep.getDependencyRevisionId().getOrganisation())
                + "\"");
        out.print(" name=\"" + XMLHelper.escape(dep.getDependencyRevisionId().getName()) + "\"");
        if (dep.getDependencyRevisionId().getBranch() != null) {
            out.print(" branch=\"" + XMLHelper.escape(dep.getDependencyRevisionId().getBranch())
                    + "\"");
        }
        out.print(" rev=\"" + XMLHelper.escape(dep.getDependencyRevisionId().getRevision()) + "\"");
        if (!dep.getDynamicConstraintDependencyRevisionId().equals(dep.getDependencyRevisionId())) {
            if (dep.getDynamicConstraintDependencyRevisionId().getBranch() != null) {
                out.print(" branchConstraint=\""
                        + XMLHelper.escape(dep.getDynamicConstraintDependencyRevisionId()
                                .getBranch()) + "\"");
            }
            out.print(" revConstraint=\""
                    + XMLHelper
                            .escape(dep.getDynamicConstraintDependencyRevisionId().getRevision())
                    + "\"");
        }
        if (dep.isForce()) {
            out.print(" force=\"" + dep.isForce() + "\"");
        }
        if (dep.isChanging()) {
            out.print(" changing=\"" + dep.isChanging() + "\"");
        }
        if (!dep.isTransitive()) {
            out.print(" transitive=\"" + dep.isTransitive() + "\"");
        }
        out.print(" conf=\"");
        String[] modConfs = dep.getModuleConfigurations();
        for (int j = 0; j < modConfs.length; j++) {
            String[] depConfs = dep.getDependencyConfigurations(modConfs[j]);
            out.print(XMLHelper.escape(modConfs[j]) + "->");
            for (int k = 0; k < depConfs.length; k++) {
                out.print(XMLHelper.escape(depConfs[k]));
                if (k + 1 < depConfs.length) {
                    out.print(",");
                }
            }
            if (j + 1 < modConfs.length) {
                out.print(";");
            }
        }
        out.print("\"");

        printExtraAttributes(dep, out, " ");

        DependencyArtifactDescriptor[] depArtifacts = dep.getAllDependencyArtifacts();
        if (depArtifacts.length > 0) {
            out.println(">");
        }
        printDependencyArtefacts(md, out, depArtifacts);

        IncludeRule[] includes = dep.getAllIncludeRules();
        if (includes.length > 0 && depArtifacts.length == 0) {
            out.println(">");
        }
        printDependencyIncludeRules(md, out, includes);

        ExcludeRule[] excludes = dep.getAllExcludeRules();
        if (excludes.length > 0 && includes.length == 0 && depArtifacts.length == 0) {
            out.println(">");
        }
        printDependencyExcludeRules(md, out, excludes);
        if (includes.length + excludes.length + depArtifacts.length == 0) {
            out.println("/>");
        } else {
            out.println("\t\t</dependency>");
        }
    }

    private static void printAllMediators(ModuleDescriptor md, PrintWriter out) {
        Map/* <MapMatcher, DependencyDescriptorMediator> */mediators = md
                .getAllDependencyDescriptorMediators().getAllRules();

        for (Iterator iterator = mediators.entrySet().iterator(); iterator.hasNext();) {
            Map.Entry mediatorRule = (Map.Entry) iterator.next();
            MapMatcher matcher = (MapMatcher) mediatorRule.getKey();
            DependencyDescriptorMediator mediator = (DependencyDescriptorMediator) mediatorRule
                    .getValue();

            if (mediator instanceof OverrideDependencyDescriptorMediator) {
                OverrideDependencyDescriptorMediator oddm = (OverrideDependencyDescriptorMediator) mediator;

                out.print("\t\t<override");
                out.print(" org=\""
                        + XMLHelper.escape((String) matcher.getAttributes().get(
                            IvyPatternHelper.ORGANISATION_KEY)) + "\"");
                out.print(" module=\""
                        + XMLHelper.escape((String) matcher.getAttributes().get(
                            IvyPatternHelper.MODULE_KEY)) + "\"");
                out.print(" matcher=\"" + XMLHelper.escape(matcher.getPatternMatcher().getName())
                        + "\"");
                if (oddm.getBranch() != null) {
                    out.print(" branch=\"" + XMLHelper.escape(oddm.getBranch()) + "\"");
                }
                if (oddm.getVersion() != null) {
                    out.print(" rev=\"" + XMLHelper.escape(oddm.getVersion()) + "\"");
                }
                out.println("/>");
            } else {
                Message.verbose("ignoring unhandled DependencyDescriptorMediator: "
                        + mediator.getClass());
            }
        }
    }

    private static void printAllExcludes(ModuleDescriptor md, PrintWriter out) {
        ExcludeRule[] excludes = md.getAllExcludeRules();
        if (excludes.length > 0) {
            for (int j = 0; j < excludes.length; j++) {
                out.print("\t\t<exclude");
                out.print(" org=\""
                        + XMLHelper.escape(excludes[j].getId().getModuleId().getOrganisation())
                        + "\"");
                out.print(" module=\""
                        + XMLHelper.escape(excludes[j].getId().getModuleId().getName()) + "\"");
                out.print(" artifact=\"" + XMLHelper.escape(excludes[j].getId().getName()) + "\"");
                out.print(" type=\"" + XMLHelper.escape(excludes[j].getId().getType()) + "\"");
                out.print(" ext=\"" + XMLHelper.escape(excludes[j].getId().getExt()) + "\"");
                String[] ruleConfs = excludes[j].getConfigurations();
                if (!Arrays.asList(ruleConfs).equals(Arrays.asList(md.getConfigurationsNames()))) {
                    out.print(" conf=\"");
                    for (int k = 0; k < ruleConfs.length; k++) {
                        out.print(XMLHelper.escape(ruleConfs[k]));
                        if (k + 1 < ruleConfs.length) {
                            out.print(",");
                        }
                    }
                    out.print("\"");
                }
                out.print(" matcher=\"" + XMLHelper.escape(excludes[j].getMatcher().getName())
                        + "\"");
                out.println("/>");
            }
        }
    }

    private static void printDependencyExcludeRules(ModuleDescriptor md, PrintWriter out,
            ExcludeRule[] excludes) {
        if (excludes.length > 0) {
            for (int j = 0; j < excludes.length; j++) {
                out.print("\t\t\t<exclude");
                out.print(" org=\""
                        + XMLHelper.escape(excludes[j].getId().getModuleId().getOrganisation())
                        + "\"");
                out.print(" module=\""
                        + XMLHelper.escape(excludes[j].getId().getModuleId().getName()) + "\"");
                out.print(" name=\"" + XMLHelper.escape(excludes[j].getId().getName()) + "\"");
                out.print(" type=\"" + XMLHelper.escape(excludes[j].getId().getType()) + "\"");
                out.print(" ext=\"" + XMLHelper.escape(excludes[j].getId().getExt()) + "\"");
                String[] ruleConfs = excludes[j].getConfigurations();
                if (!Arrays.asList(ruleConfs).equals(Arrays.asList(md.getConfigurationsNames()))) {
                    out.print(" conf=\"");
                    for (int k = 0; k < ruleConfs.length; k++) {
                        out.print(XMLHelper.escape(ruleConfs[k]));
                        if (k + 1 < ruleConfs.length) {
                            out.print(",");
                        }
                    }
                    out.print("\"");
                }
                out.print(" matcher=\"" + XMLHelper.escape(excludes[j].getMatcher().getName())
                        + "\"");
                out.println("/>");
            }
        }
    }

    private static void printDependencyIncludeRules(ModuleDescriptor md, PrintWriter out,
            IncludeRule[] includes) {
        if (includes.length > 0) {
            for (int j = 0; j < includes.length; j++) {
                out.print("\t\t\t<include");
                out.print(" name=\"" + XMLHelper.escape(includes[j].getId().getName()) + "\"");
                out.print(" type=\"" + XMLHelper.escape(includes[j].getId().getType()) + "\"");
                out.print(" ext=\"" + XMLHelper.escape(includes[j].getId().getExt()) + "\"");
                String[] ruleConfs = includes[j].getConfigurations();
                if (!Arrays.asList(ruleConfs).equals(Arrays.asList(md.getConfigurationsNames()))) {
                    out.print(" conf=\"");
                    for (int k = 0; k < ruleConfs.length; k++) {
                        out.print(XMLHelper.escape(ruleConfs[k]));
                        if (k + 1 < ruleConfs.length) {
                            out.print(",");
                        }
                    }
                    out.print("\"");
                }
                out.print(" matcher=\"" + XMLHelper.escape(includes[j].getMatcher().getName())
                        + "\"");
                out.println("/>");
            }
        }
    }

    private static void printDependencyArtefacts(ModuleDescriptor md, PrintWriter out,
            DependencyArtifactDescriptor[] depArtifacts) {
        if (depArtifacts.length > 0) {
            for (int j = 0; j < depArtifacts.length; j++) {
                out.print("\t\t\t<artifact");
                out.print(" name=\"" + XMLHelper.escape(depArtifacts[j].getName()) + "\"");
                out.print(" type=\"" + XMLHelper.escape(depArtifacts[j].getType()) + "\"");
                out.print(" ext=\"" + XMLHelper.escape(depArtifacts[j].getExt()) + "\"");
                String[] dadconfs = depArtifacts[j].getConfigurations();
                if (!Arrays.asList(dadconfs).equals(Arrays.asList(md.getConfigurationsNames()))) {
                    out.print(" conf=\"");
                    for (int k = 0; k < dadconfs.length; k++) {
                        out.print(XMLHelper.escape(dadconfs[k]));
                        if (k + 1 < dadconfs.length) {
                            out.print(",");
                        }
                    }
                    out.print("\"");
                }
                printExtraAttributes(depArtifacts[j], out, " ");
                out.println("/>");
            }
        }
    }

    /**
     * Writes the extra attributes of the given {@link ExtendableItem} to the given
     * <tt>PrintWriter</tt>.
     * 
     * @param item
     *            the {@link ExtendableItem}, cannot be <tt>null</tt>
     * @param out
     *            the writer to use
     * @param prefix
     *            the string to write before writing the attributes (if any)
     */
    private static void printExtraAttributes(ExtendableItem item, PrintWriter out, String prefix) {
        printExtraAttributes(item.getQualifiedExtraAttributes(), out, prefix);
    }

    /**
     * Writes the specified <tt>Map</tt> containing the extra attributes to the given
     * <tt>PrintWriter</tt>.
     * 
     * @param extra
     *            the extra attributes, can be <tt>null</tt>
     * @param out
     *            the writer to use
     * @param prefix
     *            the string to write before writing the attributes (if any)
     */
    private static void printExtraAttributes(Map extra, PrintWriter out, String prefix) {
        if (extra == null) {
            return;
        }

        String delim = prefix;
        for (Iterator iter = extra.entrySet().iterator(); iter.hasNext();) {
            Map.Entry entry = (Map.Entry) iter.next();
            out.print(delim + entry.getKey() + "=\""
                    + XMLHelper.escape(entry.getValue().toString()) + "\"");
            delim = " ";
        }
    }

    private static void printPublications(ModuleDescriptor md, PrintWriter out) {
        out.println("\t<publications>");
        Artifact[] artifacts = md.getAllArtifacts();
        for (int i = 0; i < artifacts.length; i++) {
            out.print("\t\t<artifact");
            out.print(" name=\"" + XMLHelper.escape(artifacts[i].getName()) + "\"");
            out.print(" type=\"" + XMLHelper.escape(artifacts[i].getType()) + "\"");
            out.print(" ext=\"" + XMLHelper.escape(artifacts[i].getExt()) + "\"");
            out.print(" conf=\"" + XMLHelper.escape(getConfs(md, artifacts[i])) + "\"");
            printExtraAttributes(artifacts[i], out, " ");
            out.println("/>");
        }
        out.println("\t</publications>");
    }

    private static void printConfigurations(ModuleDescriptor md, PrintWriter out) {
        Configuration[] confs = md.getConfigurations();
        if (confs.length > 0) {
            out.println("\t<configurations>");
            for (int i = 0; i < confs.length; i++) {
                Configuration conf = confs[i];
                out.print("\t\t");
                printConfiguration(conf, out);
            }
            out.println("\t</configurations>");
        }
    }

    protected static void printConfiguration(Configuration conf, PrintWriter out) {
        out.print("<conf");
        out.print(" name=\"" + XMLHelper.escape(conf.getName()) + "\"");
        out.print(" visibility=\"" + XMLHelper.escape(conf.getVisibility().toString()) + "\"");
        if (conf.getDescription() != null) {
            out.print(" description=\"" + XMLHelper.escape(conf.getDescription()) + "\"");
        }
        String[] exts = conf.getExtends();
        if (exts.length > 0) {
            out.print(" extends=\"");
            for (int j = 0; j < exts.length; j++) {
                out.print(XMLHelper.escape(exts[j]));
                if (j + 1 < exts.length) {
                    out.print(",");
                }
            }
            out.print("\"");
        }
        if (!conf.isTransitive()) {
            out.print(" transitive=\"false\"");
        }
        if (conf.getDeprecated() != null) {
            out.print(" deprecated=\"" + XMLHelper.escape(conf.getDeprecated()) + "\"");
        }
        printExtraAttributes(conf, out, " ");
        out.println("/>");
    }

    private static void printInfoTag(ModuleDescriptor md, PrintWriter out) {
        out.println("\t<info organisation=\""
                + XMLHelper.escape(md.getModuleRevisionId().getOrganisation()) + "\"");
        out.println("\t\tmodule=\"" + XMLHelper.escape(md.getModuleRevisionId().getName()) + "\"");
        String branch = md.getResolvedModuleRevisionId().getBranch();
        if (branch != null) {
            out.println("\t\tbranch=\"" + XMLHelper.escape(branch) + "\"");
        }
        String revision = md.getResolvedModuleRevisionId().getRevision();
        if (revision != null) {
            out.println("\t\trevision=\"" + XMLHelper.escape(revision) + "\"");
        }
        out.println("\t\tstatus=\"" + XMLHelper.escape(md.getStatus()) + "\"");
        out.println("\t\tpublication=\"" + DateUtil.format(md.getResolvedPublicationDate()) + "\"");
        if (md.isDefault()) {
            out.println("\t\tdefault=\"true\"");
        }
        if (md instanceof DefaultModuleDescriptor) {
            DefaultModuleDescriptor dmd = (DefaultModuleDescriptor) md;
            if (dmd.getNamespace() != null && !dmd.getNamespace().getName().equals("system")) {
                out.println("\t\tnamespace=\"" + XMLHelper.escape(dmd.getNamespace().getName())
                        + "\"");
            }
        }
        if (!md.getExtraAttributes().isEmpty()) {
            printExtraAttributes(md, out, "\t\t");
            out.println();
        }
        if (requireInnerInfoElement(md)) {
            out.println("\t>");
            ExtendsDescriptor[] parents = md.getInheritedDescriptors();
            for (int i = 0; i < parents.length; i++) {
                ExtendsDescriptor parent = parents[i];
                ModuleRevisionId mrid = parent.getParentRevisionId();
                out.print("\t\t<extends organisation=\"" + XMLHelper.escape(mrid.getOrganisation())
                        + "\"" + " module=\"" + XMLHelper.escape(mrid.getName()) + "\""
                        + " revision=\"" + XMLHelper.escape(mrid.getRevision()) + "\"");

                String location = parent.getLocation();
                if (location != null) {
                    out.print(" location=\"" + XMLHelper.escape(location) + "\"");
                }
                out.print(" extendType=\"" + StringUtils.join(parent.getExtendsTypes(), ",") + "\"");
                out.println("/>");
            }
            License[] licenses = md.getLicenses();
            for (int i = 0; i < licenses.length; i++) {
                License license = licenses[i];
                out.print("\t\t<license ");
                if (license.getName() != null) {
                    out.print("name=\"" + XMLHelper.escape(license.getName()) + "\" ");
                }
                if (license.getUrl() != null) {
                    out.print("url=\"" + XMLHelper.escape(license.getUrl()) + "\" ");
                }
                out.println("/>");
            }
            if (md.getHomePage() != null || md.getDescription() != null) {
                out.print("\t\t<description");
                if (md.getHomePage() != null) {
                    out.print(" homepage=\"" + XMLHelper.escape(md.getHomePage()) + "\"");
                }
                if (md.getDescription() != null && md.getDescription().trim().length() > 0) {
                    out.println(">");
                    out.println("\t\t" + XMLHelper.escape(md.getDescription()));
                    out.println("\t\t</description>");
                } else {
                    out.println(" />");
                }
            }
            for (ExtraInfoHolder extraInfo : md.getExtraInfos()) {
                printExtraInfoElement(out, extraInfo, 2);
            }
            out.println("\t</info>");
        } else {
            out.println("\t/>");
        }

    }

    private static void printExtraInfoElement(PrintWriter out, ExtraInfoHolder extraInfo, int indent) {
        for (int i = 1; i <= indent; i++) {
            out.print("\t");
        }
        out.print("<");
        out.print(extraInfo.getName());
        for (Entry<String, String> entry : extraInfo.getAttributes().entrySet()) {
            out.print(" ");
            out.print(entry.getKey());
            out.print("=");
            out.print("\"");
            out.print(entry.getValue());
            out.print("\"");
        }
        boolean requireClosingTag = false;
        if (extraInfo.getContent() != null && extraInfo.getContent().trim().length() > 0) {
            out.print(">");
            out.print(XMLHelper.escape(extraInfo.getContent()));
            requireClosingTag = true;
        }
        if (!extraInfo.getNestedExtraInfoHolder().isEmpty()) {
            out.println(">");
            for (ExtraInfoHolder nestedElement : extraInfo.getNestedExtraInfoHolder()) {
                printExtraInfoElement(out, nestedElement, indent + 1);
            }
            requireClosingTag = true;
            // prepare indentation for closing tag
            for (int i = 1; i <= indent; i++) {
                out.print("\t");
            }
        }
        if (requireClosingTag) {
            out.print("</");
            out.print(extraInfo.getName());
            out.println(">");
        } else {
            out.println("/>");
        }
    }

    private static boolean requireInnerInfoElement(ModuleDescriptor md) {
        return md.getExtraInfo().size() > 0 || md.getExtraInfos().size() > 0
                || md.getHomePage() != null
                || (md.getDescription() != null && md.getDescription().trim().length() > 0)
                || md.getLicenses().length > 0 || md.getInheritedDescriptors().length > 0;
    }

    private static String getConfs(ModuleDescriptor md, Artifact artifact) {
        StringBuffer ret = new StringBuffer();

        String[] confs = md.getConfigurationsNames();
        for (int i = 0; i < confs.length; i++) {
            if (Arrays.asList(md.getArtifacts(confs[i])).contains(artifact)) {
                ret.append(confs[i]).append(",");
            }
        }
        if (ret.length() > 0) {
            ret.setLength(ret.length() - 1);
        }
        return ret.toString();
    }
}
