/*
 * 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.project.interpolation;

import javax.inject.Inject;
import javax.xml.stream.XMLStreamException;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.maven.model.Model;
import org.apache.maven.model.v4.MavenStaxReader;
import org.apache.maven.model.v4.MavenStaxWriter;
import org.apache.maven.project.DefaultProjectBuilderConfiguration;
import org.apache.maven.project.ProjectBuilderConfiguration;
import org.apache.maven.project.path.PathTranslator;
import org.codehaus.plexus.interpolation.AbstractValueSource;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.MapBasedValueSource;
import org.codehaus.plexus.interpolation.ObjectBasedValueSource;
import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
import org.codehaus.plexus.interpolation.PrefixedValueSourceWrapper;
import org.codehaus.plexus.interpolation.RecursionInterceptor;
import org.codehaus.plexus.interpolation.ValueSource;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;

/**
 * Use a regular expression search to find and resolve expressions within the POM.
 *
 * TODO Consolidate this logic with the PluginParameterExpressionEvaluator, minus deprecations/bans.
 */
@Deprecated
public abstract class AbstractStringBasedModelInterpolator extends AbstractLogEnabled
        implements ModelInterpolator, Initializable {

    private static final List<String> PROJECT_PREFIXES = Arrays.asList("pom.", "project.");

    private static final List<String> TRANSLATED_PATH_EXPRESSIONS;

    static {
        List<String> translatedPrefixes = new ArrayList<>();

        // MNG-1927, MNG-2124, MNG-3355:
        // If the build section is present and the project directory is non-null, we should make
        // sure interpolation of the directories below uses translated paths.
        // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the
        // code below...
        translatedPrefixes.add("build.directory");
        translatedPrefixes.add("build.outputDirectory");
        translatedPrefixes.add("build.testOutputDirectory");
        translatedPrefixes.add("build.sourceDirectory");
        translatedPrefixes.add("build.testSourceDirectory");
        translatedPrefixes.add("build.scriptSourceDirectory");
        translatedPrefixes.add("reporting.outputDirectory");

        TRANSLATED_PATH_EXPRESSIONS = translatedPrefixes;
    }

    @Inject
    private PathTranslator pathTranslator;

    private Interpolator interpolator;

    private RecursionInterceptor recursionInterceptor;

    // for testing.
    protected AbstractStringBasedModelInterpolator(PathTranslator pathTranslator) {
        this.pathTranslator = pathTranslator;
    }

    protected AbstractStringBasedModelInterpolator() {}

    @Override
    public Model interpolate(Model model, Map<String, ?> context) throws ModelInterpolationException {
        return interpolate(model, context, true);
    }

    /**
     * Serialize the inbound Model instance to a StringWriter, perform the regex replacement to resolve
     * POM expressions, then re-parse into the resolved Model instance.
     * <p>
     * <b>NOTE:</b> This will result in a different instance of Model being returned!!!
     *
     * @param model The inbound Model instance, to serialize and reference for expression resolution
     * @param context The other context map to be used during resolution
     *
     * @return The resolved instance of the inbound Model. This is a different instance!
     *
     * @deprecated Use {@link ModelInterpolator#interpolate(Model, File, ProjectBuilderConfiguration, boolean)} instead.
     */
    @Deprecated
    @Override
    public Model interpolate(Model model, Map<String, ?> context, boolean strict) throws ModelInterpolationException {
        Properties props = new Properties();
        props.putAll(context);

        return interpolate(model, null, new DefaultProjectBuilderConfiguration().setExecutionProperties(props), true);
    }

    @Override
    public Model interpolate(Model model, File projectDir, ProjectBuilderConfiguration config, boolean debugEnabled)
            throws ModelInterpolationException {
        StringWriter sWriter = new StringWriter(1024);

        MavenStaxWriter writer = new MavenStaxWriter();
        try {
            writer.write(sWriter, model.getDelegate());
        } catch (IOException | XMLStreamException e) {
            throw new ModelInterpolationException("Cannot serialize project model for interpolation.", e);
        }

        String serializedModel = sWriter.toString();
        serializedModel = interpolate(serializedModel, model, projectDir, config, debugEnabled);

        StringReader sReader = new StringReader(serializedModel);

        MavenStaxReader modelReader = new MavenStaxReader();
        try {
            model = new Model(modelReader.read(sReader));
        } catch (XMLStreamException e) {
            throw new ModelInterpolationException(
                    "Cannot read project model from interpolating filter of serialized version.", e);
        }

        return model;
    }

    /**
     * Interpolates all expressions in the src parameter.
     * <p>
     * The algorithm used for each expression is:
     * <ul>
     *   <li>If it starts with either "pom." or "project.", the expression is evaluated against the model.</li>
     *   <li>If the value is null, get the value from the context.</li>
     *   <li>If the value is null, but the context contains the expression, don't replace the expression string
     *       with the value, and continue to find other expressions.</li>
     *   <li>If the value is null, get it from the model properties.</li>
     * </ul>
     */
    @Override
    public String interpolate(
            String src, Model model, final File projectDir, ProjectBuilderConfiguration config, boolean debug)
            throws ModelInterpolationException {
        try {
            List<ValueSource> valueSources = createValueSources(model, projectDir, config);
            List<InterpolationPostProcessor> postProcessors = createPostProcessors(model, projectDir, config);

            return interpolateInternal(src, valueSources, postProcessors, debug);
        } finally {
            interpolator.clearAnswers();
        }
    }

    protected List<ValueSource> createValueSources(
            final Model model, final File projectDir, final ProjectBuilderConfiguration config) {
        String timestampFormat = DEFAULT_BUILD_TIMESTAMP_FORMAT;

        Properties modelProperties = model.getProperties();
        if (modelProperties != null) {
            timestampFormat = modelProperties.getProperty(BUILD_TIMESTAMP_FORMAT_PROPERTY, timestampFormat);
        }

        ValueSource modelValueSource1 = new PrefixedObjectValueSource(PROJECT_PREFIXES, model, false);
        ValueSource modelValueSource2 = new ObjectBasedValueSource(model);

        ValueSource basedirValueSource = new PrefixedValueSourceWrapper(
                new AbstractValueSource(false) {

                    @Override
                    public Object getValue(String expression) {
                        if (projectDir != null && "basedir".equals(expression)) {
                            return projectDir.getAbsolutePath();
                        }
                        return null;
                    }
                },
                PROJECT_PREFIXES,
                true);
        ValueSource baseUriValueSource = new PrefixedValueSourceWrapper(
                new AbstractValueSource(false) {

                    @Override
                    public Object getValue(String expression) {
                        if (projectDir != null && "baseUri".equals(expression)) {
                            return projectDir.getAbsoluteFile().toPath().toUri().toASCIIString();
                        }
                        return null;
                    }
                },
                PROJECT_PREFIXES,
                false);

        List<ValueSource> valueSources = new ArrayList<>(9);

        // NOTE: Order counts here!
        valueSources.add(basedirValueSource);
        valueSources.add(baseUriValueSource);
        valueSources.add(new BuildTimestampValueSource(config.getBuildStartTime(), timestampFormat));
        valueSources.add(modelValueSource1);
        valueSources.add(new MapBasedValueSource(config.getUserProperties()));
        valueSources.add(new MapBasedValueSource(modelProperties));
        valueSources.add(new MapBasedValueSource(config.getExecutionProperties()));
        valueSources.add(new AbstractValueSource(false) {

            @Override
            public Object getValue(String expression) {
                return config.getExecutionProperties().getProperty("env." + expression);
            }
        });
        valueSources.add(modelValueSource2);

        return valueSources;
    }

    protected List<InterpolationPostProcessor> createPostProcessors(
            final Model model, final File projectDir, final ProjectBuilderConfiguration config) {
        return Collections.singletonList(new PathTranslatingPostProcessor(
                PROJECT_PREFIXES, TRANSLATED_PATH_EXPRESSIONS, projectDir, pathTranslator));
    }

    @SuppressWarnings("unchecked")
    protected String interpolateInternal(
            String src, List<ValueSource> valueSources, List<InterpolationPostProcessor> postProcessors, boolean debug)
            throws ModelInterpolationException {
        if (!src.contains("${")) {
            return src;
        }

        Logger logger = getLogger();

        String result = src;
        synchronized (this) {
            for (ValueSource vs : valueSources) {
                interpolator.addValueSource(vs);
            }

            for (InterpolationPostProcessor postProcessor : postProcessors) {
                interpolator.addPostProcessor(postProcessor);
            }

            try {
                try {
                    result = interpolator.interpolate(result, recursionInterceptor);
                } catch (InterpolationException e) {
                    throw new ModelInterpolationException(e.getMessage(), e);
                }

                if (debug) {
                    List<Object> feedback = interpolator.getFeedback();
                    if (feedback != null && !feedback.isEmpty()) {
                        logger.debug("Maven encountered the following problems during initial POM interpolation:");

                        Object last = null;
                        for (Object next : feedback) {
                            if (next instanceof Throwable throwable) {
                                if (last == null) {
                                    logger.debug("", throwable);
                                } else {
                                    logger.debug(String.valueOf(last), throwable);
                                }
                            } else {
                                if (last != null) {
                                    logger.debug(String.valueOf(last));
                                }

                                last = next;
                            }
                        }

                        if (last != null) {
                            logger.debug(String.valueOf(last));
                        }
                    }
                }

                interpolator.clearFeedback();
            } finally {
                for (ValueSource vs : valueSources) {
                    interpolator.removeValuesSource(vs);
                }

                for (InterpolationPostProcessor postProcessor : postProcessors) {
                    interpolator.removePostProcessor(postProcessor);
                }
            }
        }

        return result;
    }

    protected RecursionInterceptor getRecursionInterceptor() {
        return recursionInterceptor;
    }

    protected void setRecursionInterceptor(RecursionInterceptor recursionInterceptor) {
        this.recursionInterceptor = recursionInterceptor;
    }

    protected abstract Interpolator createInterpolator();

    @Override
    public void initialize() throws InitializationException {
        interpolator = createInterpolator();
        recursionInterceptor = new PrefixAwareRecursionInterceptor(PROJECT_PREFIXES);
    }

    protected final Interpolator getInterpolator() {
        return interpolator;
    }
}
