/*
 * 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.maven.plugin.descriptor;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.apache.maven.api.xml.XmlNode;
import org.apache.maven.api.xml.XmlService;
import org.apache.maven.internal.xml.XmlPlexusConfiguration;
import org.apache.maven.plugin.descriptor.io.PluginDescriptorStaxReader;
import org.codehaus.plexus.component.repository.ComponentDependency;
import org.codehaus.plexus.component.repository.ComponentRequirement;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.configuration.PlexusConfigurationException;

/**
 * Build plugin descriptor object from {@code plugin.xml}.
 *
 * @author Jason van Zyl
 */
public class PluginDescriptorBuilder {

    public static final String PLUGIN_2_0_0 = "http://maven.apache.org/PLUGIN/2.0.0";
    private static final int BUFFER_SIZE = 8192;

    public interface StreamSupplier {
        InputStream open() throws IOException;
    }

    public interface ReaderSupplier {
        Reader open() throws IOException;
    }

    /**
     * @deprecated use {@link #build(ReaderSupplier)}
     */
    @Deprecated
    public PluginDescriptor build(Reader reader) throws PlexusConfigurationException {
        return build(reader, null);
    }

    /**
     * @deprecated use {@link #build(ReaderSupplier, String)}
     */
    @Deprecated
    public PluginDescriptor build(Reader reader, String source) throws PlexusConfigurationException {
        try {
            BufferedReader br = new BufferedReader(reader, BUFFER_SIZE);
            br.mark(BUFFER_SIZE);
            XMLStreamReader xsr = XmlService.newXMLInputFactory().createXMLStreamReader(br);
            xsr.nextTag();
            String nsUri = xsr.getNamespaceURI();
            br.reset();
            if (PLUGIN_2_0_0.equals(nsUri)) {
                xsr = XmlService.newXMLInputFactory().createXMLStreamReader(br);
                return new PluginDescriptor(new PluginDescriptorStaxReader().read(xsr, true));
            } else {
                // Call buildConfiguration() for backward compatibility with subclasses that override it
                PlexusConfiguration cfg = buildConfiguration(br);
                return build(source, cfg);
            }
        } catch (XMLStreamException | IOException e) {
            throw new PlexusConfigurationException(e.getMessage(), e);
        }
    }

    public PluginDescriptor build(ReaderSupplier readerSupplier) throws PlexusConfigurationException {
        return build(readerSupplier, null);
    }

    public PluginDescriptor build(ReaderSupplier readerSupplier, String source) throws PlexusConfigurationException {
        try (BufferedReader br = new BufferedReader(readerSupplier.open(), BUFFER_SIZE)) {
            br.mark(BUFFER_SIZE);
            XMLStreamReader xsr = XmlService.newXMLInputFactory().createXMLStreamReader(br);
            xsr.nextTag();
            String nsUri = xsr.getNamespaceURI();
            try (BufferedReader br2 = reset(readerSupplier, br)) {
                xsr = XmlService.newXMLInputFactory().createXMLStreamReader(br2);
                return build(source, nsUri, xsr);
            }
        } catch (XMLStreamException | IOException e) {
            throw new PlexusConfigurationException(e.getMessage(), e);
        }
    }

    /**
     * @deprecated use {@link #build(StreamSupplier, String)}
     */
    @Deprecated
    public PluginDescriptor build(InputStream input, String source) throws PlexusConfigurationException {
        try {
            BufferedInputStream bis = new BufferedInputStream(input, BUFFER_SIZE);
            bis.mark(BUFFER_SIZE);
            XMLStreamReader xsr = XmlService.newXMLInputFactory().createXMLStreamReader(bis);
            xsr.nextTag();
            String nsUri = xsr.getNamespaceURI();
            bis.reset();
            if (PLUGIN_2_0_0.equals(nsUri)) {
                xsr = XmlService.newXMLInputFactory().createXMLStreamReader(bis);
                return new PluginDescriptor(new PluginDescriptorStaxReader().read(xsr, true));
            } else {
                // Call buildConfiguration() for backward compatibility with subclasses that override it
                PlexusConfiguration cfg = buildConfiguration(bis);
                return build(source, cfg);
            }
        } catch (XMLStreamException | IOException e) {
            throw new PlexusConfigurationException(e.getMessage(), e);
        }
    }

    public PluginDescriptor build(StreamSupplier inputSupplier) throws PlexusConfigurationException {
        return build(inputSupplier, null);
    }

    public PluginDescriptor build(StreamSupplier inputSupplier, String source) throws PlexusConfigurationException {
        try (BufferedInputStream bis = new BufferedInputStream(inputSupplier.open(), BUFFER_SIZE)) {
            bis.mark(BUFFER_SIZE);
            XMLStreamReader xsr = XmlService.newXMLInputFactory().createXMLStreamReader(bis);
            xsr.nextTag();
            String nsUri = xsr.getNamespaceURI();
            try (BufferedInputStream bis2 = reset(inputSupplier, bis)) {
                xsr = XmlService.newXMLInputFactory().createXMLStreamReader(bis2);
                return build(source, nsUri, xsr);
            }
        } catch (XMLStreamException | IOException e) {
            throw new PlexusConfigurationException(e.getMessage(), e);
        }
    }

    private static BufferedInputStream reset(StreamSupplier inputSupplier, BufferedInputStream bis) throws IOException {
        try {
            bis.reset();
            return bis;
        } catch (IOException e) {
            return new BufferedInputStream(inputSupplier.open(), BUFFER_SIZE);
        }
    }

    private static BufferedReader reset(ReaderSupplier readerSupplier, BufferedReader br) throws IOException {
        try {
            br.reset();
            return br;
        } catch (IOException e) {
            return new BufferedReader(readerSupplier.open(), BUFFER_SIZE);
        }
    }

    private PluginDescriptor build(String source, String nsUri, XMLStreamReader xsr)
            throws XMLStreamException, PlexusConfigurationException {
        if (PLUGIN_2_0_0.equals(nsUri)) {
            org.apache.maven.api.plugin.descriptor.PluginDescriptor pd =
                    new PluginDescriptorStaxReader().read(xsr, true);
            return new PluginDescriptor(pd);
        } else {
            XmlNode node = XmlService.read(xsr);
            PlexusConfiguration cfg = XmlPlexusConfiguration.toPlexusConfiguration(node);
            return build(source, cfg);
        }
    }

    private PluginDescriptor build(String source, PlexusConfiguration c) throws PlexusConfigurationException {
        PluginDescriptor pluginDescriptor = new PluginDescriptor();

        pluginDescriptor.setSource(source);
        pluginDescriptor.setGroupId(extractGroupId(c));
        pluginDescriptor.setArtifactId(extractArtifactId(c));
        pluginDescriptor.setVersion(extractVersion(c));
        pluginDescriptor.setGoalPrefix(extractGoalPrefix(c));

        pluginDescriptor.setName(extractName(c));
        pluginDescriptor.setDescription(extractDescription(c));

        pluginDescriptor.setIsolatedRealm(extractIsolatedRealm(c));
        pluginDescriptor.setInheritedByDefault(extractInheritedByDefault(c));
        pluginDescriptor.setRequiredJavaVersion(extractRequiredJavaVersion(c).orElse(null));
        pluginDescriptor.setRequiredMavenVersion(extractRequiredMavenVersion(c).orElse(null));

        pluginDescriptor.addMojos(extractMojos(c, pluginDescriptor));

        pluginDescriptor.setDependencies(extractComponentDependencies(c));

        return pluginDescriptor;
    }

    private String extractGroupId(PlexusConfiguration c) {
        return c.getChild("groupId").getValue();
    }

    private String extractArtifactId(PlexusConfiguration c) {
        return c.getChild("artifactId").getValue();
    }

    private String extractVersion(PlexusConfiguration c) {
        return c.getChild("version").getValue();
    }

    private String extractGoalPrefix(PlexusConfiguration c) {
        return c.getChild("goalPrefix").getValue();
    }

    private String extractName(PlexusConfiguration c) {
        return c.getChild("name").getValue();
    }

    private String extractDescription(PlexusConfiguration c) {
        return c.getChild("description").getValue();
    }

    private List<MojoDescriptor> extractMojos(PlexusConfiguration c, PluginDescriptor pluginDescriptor)
            throws PlexusConfigurationException {
        List<MojoDescriptor> mojos = new ArrayList<>();

        PlexusConfiguration[] mojoConfigurations = c.getChild("mojos").getChildren("mojo");

        for (PlexusConfiguration component : mojoConfigurations) {
            mojos.add(buildComponentDescriptor(component, pluginDescriptor));
        }
        return mojos;
    }

    private boolean extractInheritedByDefault(PlexusConfiguration c) {
        String inheritedByDefault = c.getChild("inheritedByDefault").getValue();

        if (inheritedByDefault != null) {
            return Boolean.parseBoolean(inheritedByDefault);
        }
        return false;
    }

    private boolean extractIsolatedRealm(PlexusConfiguration c) {
        String isolatedRealm = c.getChild("isolatedRealm").getValue();

        if (isolatedRealm != null) {
            return Boolean.parseBoolean(isolatedRealm);
        }
        return false;
    }

    private Optional<String> extractRequiredJavaVersion(PlexusConfiguration c) {
        return Optional.ofNullable(c.getChild("requiredJavaVersion")).map(PlexusConfiguration::getValue);
    }

    private Optional<String> extractRequiredMavenVersion(PlexusConfiguration c) {
        return Optional.ofNullable(c.getChild("requiredMavenVersion")).map(PlexusConfiguration::getValue);
    }

    private List<ComponentDependency> extractComponentDependencies(PlexusConfiguration c) {

        PlexusConfiguration[] dependencyConfigurations =
                c.getChild("dependencies").getChildren("dependency");

        List<ComponentDependency> dependencies = new ArrayList<>();

        for (PlexusConfiguration d : dependencyConfigurations) {
            dependencies.add(extractComponentDependency(d));
        }
        return dependencies;
    }

    private ComponentDependency extractComponentDependency(PlexusConfiguration d) {
        ComponentDependency cd = new ComponentDependency();

        cd.setArtifactId(extractArtifactId(d));

        cd.setGroupId(extractGroupId(d));

        cd.setType(d.getChild("type").getValue());

        cd.setVersion(extractVersion(d));
        return cd;
    }

    @SuppressWarnings("checkstyle:methodlength")
    public MojoDescriptor buildComponentDescriptor(PlexusConfiguration c, PluginDescriptor pluginDescriptor)
            throws PlexusConfigurationException {
        MojoDescriptor mojo = new MojoDescriptor();
        mojo.setPluginDescriptor(pluginDescriptor);

        mojo.setGoal(c.getChild("goal").getValue());

        mojo.setImplementation(c.getChild("implementation").getValue());

        PlexusConfiguration langConfig = c.getChild("language");

        if (langConfig != null) {
            mojo.setLanguage(langConfig.getValue());
        }

        PlexusConfiguration configuratorConfig = c.getChild("configurator");

        if (configuratorConfig != null) {
            mojo.setComponentConfigurator(configuratorConfig.getValue());
        }

        PlexusConfiguration composerConfig = c.getChild("composer");

        if (composerConfig != null) {
            mojo.setComponentComposer(composerConfig.getValue());
        }

        String since = c.getChild("since").getValue();

        if (since != null) {
            mojo.setSince(since);
        }

        PlexusConfiguration deprecated = c.getChild("deprecated", false);

        if (deprecated != null) {
            mojo.setDeprecated(deprecated.getValue());
        }

        String phase = c.getChild("phase").getValue();

        if (phase != null) {
            mojo.setPhase(phase);
        }

        String executePhase = c.getChild("executePhase").getValue();

        if (executePhase != null) {
            mojo.setExecutePhase(executePhase);
        }

        String executeMojo = c.getChild("executeGoal").getValue();

        if (executeMojo != null) {
            mojo.setExecuteGoal(executeMojo);
        }

        String executeLifecycle = c.getChild("executeLifecycle").getValue();

        if (executeLifecycle != null) {
            mojo.setExecuteLifecycle(executeLifecycle);
        }

        mojo.setInstantiationStrategy(c.getChild("instantiationStrategy").getValue());

        mojo.setDescription(extractDescription(c));

        PlexusConfiguration dependencyResolution = c.getChild("requiresDependencyResolution", false);

        if (dependencyResolution != null) {
            mojo.setDependencyResolutionRequired(dependencyResolution.getValue());
        }

        PlexusConfiguration dependencyCollection = c.getChild("requiresDependencyCollection", false);

        if (dependencyCollection != null) {
            mojo.setDependencyCollectionRequired(dependencyCollection.getValue());
        }

        String directInvocationOnly = c.getChild("requiresDirectInvocation").getValue();

        if (directInvocationOnly != null) {
            mojo.setDirectInvocationOnly(Boolean.parseBoolean(directInvocationOnly));
        }

        String requiresProject = c.getChild("requiresProject").getValue();

        if (requiresProject != null) {
            mojo.setProjectRequired(Boolean.parseBoolean(requiresProject));
        }

        String requiresReports = c.getChild("requiresReports").getValue();

        if (requiresReports != null) {
            mojo.setRequiresReports(Boolean.parseBoolean(requiresReports));
        }

        String aggregator = c.getChild("aggregator").getValue();

        if (aggregator != null) {
            mojo.setAggregator(Boolean.parseBoolean(aggregator));
        }

        String requiresOnline = c.getChild("requiresOnline").getValue();

        if (requiresOnline != null) {
            mojo.setOnlineRequired(Boolean.parseBoolean(requiresOnline));
        }

        String inheritedByDefault = c.getChild("inheritedByDefault").getValue();

        if (inheritedByDefault != null) {
            mojo.setInheritedByDefault(Boolean.parseBoolean(inheritedByDefault));
        }

        String threadSafe = c.getChild("threadSafe").getValue();

        if (threadSafe != null) {
            mojo.setThreadSafe(Boolean.parseBoolean(threadSafe));
        }

        // ----------------------------------------------------------------------
        // Configuration
        // ----------------------------------------------------------------------

        PlexusConfiguration mojoConfig = c.getChild("configuration");
        mojo.setMojoConfiguration(mojoConfig);

        // ----------------------------------------------------------------------
        // Parameters
        // ----------------------------------------------------------------------

        PlexusConfiguration[] parameterConfigurations = c.getChild("parameters").getChildren("parameter");

        List<Parameter> parameters = new ArrayList<>();

        for (PlexusConfiguration d : parameterConfigurations) {
            Parameter parameter = new Parameter();

            parameter.setName(extractName(d));

            parameter.setAlias(d.getChild("alias").getValue());

            parameter.setType(d.getChild("type").getValue());

            String required = d.getChild("required").getValue();

            parameter.setRequired(Boolean.parseBoolean(required));

            PlexusConfiguration editableConfig = d.getChild("editable");

            // we need the null check for pre-build legacy plugins...
            if (editableConfig != null) {
                String editable = d.getChild("editable").getValue();

                parameter.setEditable(editable == null || Boolean.parseBoolean(editable));
            }

            parameter.setDescription(extractDescription(d));

            parameter.setDeprecated(d.getChild("deprecated").getValue());

            parameter.setImplementation(d.getChild("implementation").getValue());

            parameter.setSince(d.getChild("since").getValue());

            PlexusConfiguration paramConfig = mojoConfig.getChild(parameter.getName(), false);
            if (paramConfig != null) {
                parameter.setExpression(paramConfig.getValue(null));
                parameter.setDefaultValue(paramConfig.getAttribute("default-value"));
            }

            parameters.add(parameter);
        }

        mojo.setParameters(parameters);

        // TODO this should not need to be handed off...

        // ----------------------------------------------------------------------
        // Requirements
        // ----------------------------------------------------------------------

        PlexusConfiguration[] requirements = c.getChild("requirements").getChildren("requirement");

        for (PlexusConfiguration requirement : requirements) {
            ComponentRequirement cr = new ComponentRequirement();

            cr.setRole(requirement.getChild("role").getValue());

            cr.setRoleHint(requirement.getChild("role-hint").getValue());

            cr.setFieldName(requirement.getChild("field-name").getValue());

            mojo.addRequirement(cr);
        }

        return mojo;
    }

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------

    public PlexusConfiguration buildConfiguration(Reader configuration) throws PlexusConfigurationException {
        try {
            XMLStreamReader reader = XmlService.newXMLInputFactory().createXMLStreamReader(configuration);
            return XmlPlexusConfiguration.toPlexusConfiguration(XmlService.read(reader));
        } catch (XMLStreamException e) {
            throw new PlexusConfigurationException(e.getMessage(), e);
        }
    }

    public PlexusConfiguration buildConfiguration(InputStream configuration) throws PlexusConfigurationException {
        try {
            XMLStreamReader reader = XmlService.newXMLInputFactory().createXMLStreamReader(configuration);
            return XmlPlexusConfiguration.toPlexusConfiguration(XmlService.read(reader));
        } catch (XMLStreamException e) {
            throw new PlexusConfigurationException(e.getMessage(), e);
        }
    }
}
