package org.apache.maven.project.injection;

/*
 * 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.
 */

import org.apache.maven.model.Build;
import org.apache.maven.model.BuildBase;
import org.apache.maven.model.ConfigurationContainer;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginContainer;
import org.apache.maven.model.PluginExecution;
import org.apache.maven.model.PluginManagement;
import org.apache.maven.model.Profile;
import org.apache.maven.model.ReportPlugin;
import org.apache.maven.model.ReportSet;
import org.apache.maven.model.Reporting;
import org.apache.maven.project.ModelUtils;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.Xpp3Dom;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Inject profile data into a Model, using the profile as the dominant data source, and
 * persisting results of the injection in the Model.
 *
 * This will look similar to the ModelUtils/DefaultModelInheritanceAssembler code, but
 * they are distinct. In model inheritance, the child provides data dominance AND persists
 * the results of the merge...sort of a 'merge-out' system.
 *
 * In this system, the profile is dominant, but the model receives the merge result...sort
 * of a 'merge-in' system. The two pieces of code look like they could be combined with a
 * set of flags to determine which direction to merge 'to', but there are enough differences
 * in the code to justify the extra code involved with separating them, in order to simplify
 * the logic.
 */
public class DefaultProfileInjector
    implements ProfileInjector
{

    public void inject( Profile profile, Model model )
    {

        model.setDependencies( injectDependencies( profile.getDependencies(), model.getDependencies() ) );

        injectModules( profile, model );

        model.setRepositories( ModelUtils.mergeRepositoryLists( profile.getRepositories(), model.getRepositories() ) );
        model.setPluginRepositories( ModelUtils.mergeRepositoryLists( profile.getPluginRepositories(), model
            .getPluginRepositories() ) );

        injectReporting( profile, model );

        injectDependencyManagement( profile, model );

        injectDistributionManagement( profile, model );

        injectBuild( profile, model );

        Properties props = new Properties();
        props.putAll( model.getProperties() );
        props.putAll( profile.getProperties() );

        model.setProperties( props );
    }

    private void injectBuild( Profile profile, Model model )
    {
        BuildBase profileBuild = profile.getBuild();
        Build modelBuild = model.getBuild();

        // if the parent build is null, obviously we cannot inherit from it...
        if ( profileBuild != null )
        {
            if ( modelBuild == null )
            {
                modelBuild = new Build();
                model.setBuild( modelBuild );
            }

            if ( profileBuild.getDirectory() != null )
            {
                modelBuild.setDirectory( profileBuild.getDirectory() );
            }

            if ( profileBuild.getDefaultGoal() != null )
            {
                modelBuild.setDefaultGoal( profileBuild.getDefaultGoal() );
            }

            if ( profileBuild.getFinalName() != null )
            {
                modelBuild.setFinalName( profileBuild.getFinalName() );
            }

            ModelUtils.mergeFilterLists( modelBuild.getFilters(), profileBuild.getFilters() );
            ModelUtils.mergeResourceLists( modelBuild.getResources(), profileBuild.getResources() );
            ModelUtils.mergeResourceLists( modelBuild.getTestResources(), profileBuild.getTestResources() );

            injectPlugins( profileBuild, modelBuild );

            // Plugin management :: aggregate
            PluginManagement profilePM = profileBuild.getPluginManagement();
            PluginManagement modelPM = modelBuild.getPluginManagement();

            if ( modelPM == null )
            {
                modelBuild.setPluginManagement( profilePM );
            }
            else
            {
                injectPlugins( profilePM, modelPM );
            }
        }
    }

    /**
     * This should be the resulting ordering of plugins after injection:
     *
     * Given:
     *
     *   model: X -> A -> B -> D -> E
     *   profile: Y -> A -> C -> D -> F
     *
     * Result:
     *
     *   X -> Y -> A -> B -> C -> D -> E -> F
     */
    protected void injectPlugins( PluginContainer profileContainer, PluginContainer modelContainer )
    {
        if ( ( profileContainer == null ) || ( modelContainer == null ) )
        {
            // nothing to do...
            return;
        }

        List modelPlugins = modelContainer.getPlugins();

        if ( modelPlugins == null )
        {
            modelContainer.setPlugins( profileContainer.getPlugins() );
        }
        else if ( profileContainer.getPlugins() != null )
        {
            List mergedPlugins = new ArrayList();

            Map profilePlugins = profileContainer.getPluginsAsMap();

            for ( Iterator it = modelPlugins.iterator(); it.hasNext(); )
            {
                Plugin modelPlugin = (Plugin) it.next();

                Plugin profilePlugin = (Plugin) profilePlugins.get( modelPlugin.getKey() );

                if ( ( profilePlugin != null ) && !mergedPlugins.contains( profilePlugin ) )
                {
                    Plugin mergedPlugin = modelPlugin;

                    injectPluginDefinition( profilePlugin, modelPlugin );

                    mergedPlugins.add( mergedPlugin );
                }
            }

            List results = ModelUtils.orderAfterMerge( mergedPlugins, modelPlugins, profileContainer.getPlugins() );

            modelContainer.setPlugins( results );

            modelContainer.flushPluginMap();
        }
    }

    private void injectPluginDefinition( Plugin profilePlugin, Plugin modelPlugin )
    {
        if ( ( profilePlugin == null ) || ( modelPlugin == null ) )
        {
            // nothing to do.
            return;
        }

        if ( profilePlugin.isExtensions() )
        {
            modelPlugin.setExtensions( true );
        }

        if ( profilePlugin.getVersion() != null )
        {
            modelPlugin.setVersion( profilePlugin.getVersion() );
        }

        modelPlugin.setDependencies( injectDependencies( profilePlugin.getDependencies(), modelPlugin.getDependencies() ) );

        // merge the lists of goals that are not attached to an <execution/>
        injectConfigurationContainer( profilePlugin, modelPlugin );

        // from here to the end of the method is dealing with merging of the <executions/> section.
        List modelExecutions = modelPlugin.getExecutions();

        if ( ( modelExecutions == null ) || modelExecutions.isEmpty() )
        {
            modelPlugin.setExecutions( profilePlugin.getExecutions() );
        }
        else
        {
            Map executions = new LinkedHashMap();

            Map profileExecutions = profilePlugin.getExecutionsAsMap();

            for ( Iterator it = modelExecutions.iterator(); it.hasNext(); )
            {
                PluginExecution modelExecution = (PluginExecution) it.next();

                PluginExecution profileExecution = (PluginExecution) profileExecutions.get( modelExecution.getId() );

                if ( profileExecution != null )
                {
                    injectConfigurationContainer( profileExecution, modelExecution );

                    if ( profileExecution.getPhase() != null )
                    {
                        modelExecution.setPhase( profileExecution.getPhase() );
                    }

                    List profileGoals = profileExecution.getGoals();
                    List modelGoals = modelExecution.getGoals();

                    List goals = new ArrayList();

                    if ( ( modelGoals != null ) && !modelGoals.isEmpty() )
                    {
                        goals.addAll( modelGoals );
                    }

                    if ( profileGoals != null )
                    {
                        for ( Iterator goalIterator = profileGoals.iterator(); goalIterator.hasNext(); )
                        {
                            String goal = (String) goalIterator.next();

                            if ( !goals.contains( goal ) )
                            {
                                goals.add( goal );
                            }
                        }
                    }

                    modelExecution.setGoals( goals );
                }

                executions.put( modelExecution.getId(), modelExecution );
            }

            for ( Iterator it = profileExecutions.entrySet().iterator(); it.hasNext(); )
            {
                Map.Entry entry = (Map.Entry) it.next();

                String id = (String) entry.getKey();

                if ( !executions.containsKey( id ) )
                {
                    executions.put( id, entry.getValue() );
                }
            }

            modelPlugin.setExecutions( new ArrayList( executions.values() ) );

            modelPlugin.flushExecutionMap();
        }

    }

    /**
     * Merge two DOMs. Copy the dominant DOM so the original one is left unchanged.
     * <p>
     * Use this method instead of a direct call to {@link Xpp3Dom#mergeXpp3Dom(Xpp3Dom, Xpp3Dom)}.
     * Profiles are dominant, thus they are merge targets, but they may be merged in several times
     * (e.g. if they are inherited). So with the second merge, you don't get the profile's original
     * DOM, but an already merged one.
     * 
     * @param dominant Dominant DOM
     * @param recessive Recessive DOM
     * @return Merged DOM
     */
    private Xpp3Dom merge( Xpp3Dom dominant, Xpp3Dom recessive )
    {
        Xpp3Dom dominantCopy = ( dominant == null ) ? null : new Xpp3Dom( dominant );
        return Xpp3Dom.mergeXpp3Dom( dominantCopy, recessive );
    }

    private void injectConfigurationContainer( ConfigurationContainer profileContainer,
                                               ConfigurationContainer modelContainer )
    {
        Xpp3Dom configuration = (Xpp3Dom) profileContainer.getConfiguration();
        Xpp3Dom parentConfiguration = (Xpp3Dom) modelContainer.getConfiguration();

        configuration = merge( configuration, parentConfiguration );

        modelContainer.setConfiguration( configuration );
    }

    /**
     * Append modules specified in the profile to the end of the list supplied by the model, if
     * they don't already exist.
     */
    private void injectModules( Profile profile, Model model )
    {
        List modules = new ArrayList();

        List modelModules = model.getModules();

        if ( ( modelModules != null ) && !modelModules.isEmpty() )
        {
            modules.addAll( modelModules );
        }

        List profileModules = profile.getModules();

        if ( profileModules != null )
        {
            for ( Iterator it = profileModules.iterator(); it.hasNext(); )
            {
                String module = (String) it.next();

                if ( !modules.contains( module ) )
                {
                    modules.add( module );
                }
            }
        }

        model.setModules( modules );
    }

    private void injectDistributionManagement( Profile profile, Model model )
    {
        DistributionManagement pDistMgmt = profile.getDistributionManagement();
        DistributionManagement mDistMgmt = model.getDistributionManagement();

        if ( mDistMgmt == null )
        {
            model.setDistributionManagement( pDistMgmt );
        }
        else if ( pDistMgmt != null )
        {
            if ( pDistMgmt.getRepository() != null )
            {
                mDistMgmt.setRepository( pDistMgmt.getRepository() );
            }

            if ( pDistMgmt.getSnapshotRepository() != null )
            {
                mDistMgmt.setSnapshotRepository( pDistMgmt.getSnapshotRepository() );
            }

            if ( StringUtils.isNotEmpty( pDistMgmt.getDownloadUrl() ) )
            {
                mDistMgmt.setDownloadUrl( pDistMgmt.getDownloadUrl() );
            }

            if ( pDistMgmt.getRelocation() != null )
            {
                mDistMgmt.setRelocation( pDistMgmt.getRelocation() );
            }

            if ( pDistMgmt.getSite() != null )
            {
                mDistMgmt.setSite( pDistMgmt.getSite() );
            }

            // NOTE: We SHOULD NOT be inheriting status, since this is an assessment of the POM quality.
        }
    }

    private void injectDependencyManagement( Profile profile, Model model )
    {
        DependencyManagement modelDepMgmt = model.getDependencyManagement();

        DependencyManagement profileDepMgmt = profile.getDependencyManagement();

        if ( profileDepMgmt != null )
        {
            if ( modelDepMgmt == null )
            {
                model.setDependencyManagement( profileDepMgmt );
            }
            else
            {
                Map depsMap = new LinkedHashMap();

                List deps = modelDepMgmt.getDependencies();

                if ( deps != null )
                {
                    for ( Iterator it = deps.iterator(); it.hasNext(); )
                    {
                        Dependency dependency = (Dependency) it.next();
                        depsMap.put( dependency.getManagementKey(), dependency );
                    }
                }

                deps = profileDepMgmt.getDependencies();

                if ( deps != null )
                {
                    for ( Iterator it = deps.iterator(); it.hasNext(); )
                    {
                        Dependency dependency = (Dependency) it.next();
                        depsMap.put( dependency.getManagementKey(), dependency );
                    }
                }

                modelDepMgmt.setDependencies( new ArrayList( depsMap.values() ) );
            }
        }
    }

    private void injectReporting( Profile profile, Model model )
    {
        // Reports :: aggregate
        Reporting profileReporting = profile.getReporting();
        Reporting modelReporting = model.getReporting();

        if ( profileReporting != null )
        {
            if ( modelReporting == null )
            {
                model.setReporting( profileReporting );
            }
            else
            {
                if ( StringUtils.isEmpty( modelReporting.getOutputDirectory() ) )
                {
                    modelReporting.setOutputDirectory( profileReporting.getOutputDirectory() );
                }

                Map mergedReportPlugins = new LinkedHashMap();

                Map profileReportersByKey = profileReporting.getReportPluginsAsMap();

                List modelReportPlugins = modelReporting.getPlugins();

                if ( modelReportPlugins != null )
                {
                    for ( Iterator it = modelReportPlugins.iterator(); it.hasNext(); )
                    {
                        ReportPlugin modelReportPlugin = (ReportPlugin) it.next();

                        String inherited = modelReportPlugin.getInherited();

                        if ( StringUtils.isEmpty( inherited ) || Boolean.valueOf( inherited ).booleanValue() )
                        {
                            ReportPlugin profileReportPlugin = (ReportPlugin) profileReportersByKey
                                .get( modelReportPlugin.getKey() );

                            ReportPlugin mergedReportPlugin = modelReportPlugin;

                            if ( profileReportPlugin != null )
                            {
                                mergedReportPlugin = profileReportPlugin;

                                mergeReportPlugins( profileReportPlugin, modelReportPlugin );
                            }
                            else if ( StringUtils.isEmpty( inherited ) )
                            {
                                mergedReportPlugin.unsetInheritanceApplied();
                            }

                            mergedReportPlugins.put( mergedReportPlugin.getKey(), mergedReportPlugin );
                        }
                    }
                }

                for ( Iterator it = profileReportersByKey.entrySet().iterator(); it.hasNext(); )
                {
                    Map.Entry entry = (Map.Entry) it.next();

                    String key = (String) entry.getKey();

                    if ( !mergedReportPlugins.containsKey( key ) )
                    {
                        mergedReportPlugins.put( key, entry.getValue() );
                    }
                }

                modelReporting.setPlugins( new ArrayList( mergedReportPlugins.values() ) );

                modelReporting.flushReportPluginMap();
            }
        }
    }

    private void mergeReportPlugins( ReportPlugin dominant, ReportPlugin recessive )
    {
        if ( StringUtils.isEmpty( recessive.getVersion() ) )
        {
            recessive.setVersion( dominant.getVersion() );
        }

        Xpp3Dom dominantConfig = (Xpp3Dom) dominant.getConfiguration();
        Xpp3Dom recessiveConfig = (Xpp3Dom) recessive.getConfiguration();

        recessive.setConfiguration( merge( dominantConfig, recessiveConfig ) );

        Map mergedReportSets = new LinkedHashMap();

        Map dominantReportSetsById = dominant.getReportSetsAsMap();

        for ( Iterator it = recessive.getReportSets().iterator(); it.hasNext(); )
        {
            ReportSet recessiveReportSet = (ReportSet) it.next();

            ReportSet dominantReportSet = (ReportSet) dominantReportSetsById.get( recessiveReportSet.getId() );

            ReportSet merged = recessiveReportSet;

            if ( dominantReportSet != null )
            {
                merged = recessiveReportSet;

                Xpp3Dom dominantRSConfig = (Xpp3Dom) dominantReportSet.getConfiguration();
                Xpp3Dom mergedRSConfig = (Xpp3Dom) merged.getConfiguration();

                merged.setConfiguration( merge( dominantRSConfig, mergedRSConfig ) );

                List mergedReports = merged.getReports();

                if ( mergedReports == null )
                {
                    mergedReports = new ArrayList();

                    merged.setReports( mergedReports );
                }

                List dominantRSReports = dominantReportSet.getReports();

                if ( dominantRSReports != null )
                {
                    for ( Iterator reportIterator = dominantRSReports.iterator(); reportIterator.hasNext(); )
                    {
                        String report = (String) reportIterator.next();

                        if ( !mergedReports.contains( report ) )
                        {
                            mergedReports.add( report );
                        }
                    }
                }

                mergedReportSets.put( merged.getId(), merged );
            }
        }

        for ( Iterator rsIterator = dominantReportSetsById.entrySet().iterator(); rsIterator.hasNext(); )
        {
            Map.Entry entry = (Map.Entry) rsIterator.next();

            String key = (String) entry.getKey();

            if ( !mergedReportSets.containsKey( key ) )
            {
                mergedReportSets.put( key, entry.getValue() );
            }
        }

        recessive.setReportSets( new ArrayList( mergedReportSets.values() ) );

        recessive.flushReportSetMap();
    }

    private List injectDependencies( List profileDeps, List modelDeps )
    {
        Map depsMap = new LinkedHashMap();

        if ( modelDeps != null )
        {
            for ( Iterator it = modelDeps.iterator(); it.hasNext(); )
            {
                Dependency dependency = (Dependency) it.next();
                depsMap.put( dependency.getManagementKey(), dependency );
            }
        }

        if ( profileDeps != null )
        {
            for ( Iterator it = profileDeps.iterator(); it.hasNext(); )
            {
                Dependency dependency = (Dependency) it.next();
                depsMap.put( dependency.getManagementKey(), dependency );
            }
        }

        return new ArrayList( depsMap.values() );
    }

}
