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

import javax.inject.Inject;

import java.io.File;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.apache.maven.api.model.Model;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelProblemCollector;
import org.apache.maven.model.path.PathTranslator;
import org.apache.maven.model.path.UrlNormalizer;
import org.apache.maven.model.root.RootLocator;
import org.codehaus.plexus.interpolation.AbstractValueSource;
import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
import org.codehaus.plexus.interpolation.MapBasedValueSource;
import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
import org.codehaus.plexus.interpolation.PrefixedValueSourceWrapper;
import org.codehaus.plexus.interpolation.RecursionInterceptor;
import org.codehaus.plexus.interpolation.ValueSource;

/**
 * Use a regular expression search to find and resolve expressions within the POM.
 *
 */
public abstract class AbstractStringBasedModelInterpolator implements ModelInterpolator {
    private static final String PREFIX_PROJECT = "project.";
    private static final String PREFIX_POM = "pom.";
    private static final List<String> PROJECT_PREFIXES_3_1 = Arrays.asList(PREFIX_POM, PREFIX_PROJECT);
    private static final List<String> PROJECT_PREFIXES_4_0 = Collections.singletonList(PREFIX_PROJECT);

    private static final Collection<String> TRANSLATED_PATH_EXPRESSIONS;

    static {
        Collection<String> translatedPrefixes = new HashSet<>();

        // 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;
    }

    private final PathTranslator pathTranslator;
    private final UrlNormalizer urlNormalizer;

    private final RootLocator rootLocator;

    @Inject
    public AbstractStringBasedModelInterpolator(
            PathTranslator pathTranslator, UrlNormalizer urlNormalizer, RootLocator rootLocator) {
        this.pathTranslator = pathTranslator;
        this.urlNormalizer = urlNormalizer;
        this.rootLocator = rootLocator;
    }

    @Override
    public org.apache.maven.model.Model interpolateModel(
            org.apache.maven.model.Model model,
            File projectDir,
            ModelBuildingRequest request,
            ModelProblemCollector problems) {
        return new org.apache.maven.model.Model(interpolateModel(model.getDelegate(), projectDir, request, problems));
    }

    protected List<String> getProjectPrefixes(ModelBuildingRequest config) {
        return config.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_4_0
                ? PROJECT_PREFIXES_4_0
                : PROJECT_PREFIXES_3_1;
    }

    protected List<ValueSource> createValueSources(
            final Model model,
            final File projectDir,
            final ModelBuildingRequest config,
            ModelProblemCollector problems) {
        Map<String, String> modelProperties = model.getProperties();

        ValueSource projectPrefixValueSource;
        ValueSource prefixlessObjectBasedValueSource;
        if (config.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_4_0) {
            projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_4_0, model, false);
            prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model);
        } else {
            projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_3_1, model, false);
            if (config.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
                projectPrefixValueSource =
                        new ProblemDetectingValueSource(projectPrefixValueSource, PREFIX_POM, PREFIX_PROJECT, problems);
            }

            prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model);
            if (config.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
                prefixlessObjectBasedValueSource =
                        new ProblemDetectingValueSource(prefixlessObjectBasedValueSource, "", PREFIX_PROJECT, problems);
            }
        }

        // NOTE: Order counts here!
        List<ValueSource> valueSources = new ArrayList<>(9);

        if (projectDir != null) {
            ValueSource basedirValueSource = new PrefixedValueSourceWrapper(
                    new AbstractValueSource(false) {
                        @Override
                        public Object getValue(String expression) {
                            if ("basedir".equals(expression)) {
                                return projectDir.getAbsoluteFile().toPath().toString();
                            } else if (expression.startsWith("basedir.")) {
                                Path basedir = projectDir.getAbsoluteFile().toPath();
                                return new ObjectBasedValueSource(basedir)
                                        .getValue(expression.substring("basedir.".length()));
                            }
                            return null;
                        }
                    },
                    getProjectPrefixes(config),
                    true);
            valueSources.add(basedirValueSource);

            ValueSource baseUriValueSource = new PrefixedValueSourceWrapper(
                    new AbstractValueSource(false) {
                        @Override
                        public Object getValue(String expression) {
                            if ("baseUri".equals(expression)) {
                                return projectDir
                                        .getAbsoluteFile()
                                        .toPath()
                                        .toUri()
                                        .toASCIIString();
                            } else if (expression.startsWith("baseUri.")) {
                                URI baseUri =
                                        projectDir.getAbsoluteFile().toPath().toUri();
                                return new ObjectBasedValueSource(baseUri)
                                        .getValue(expression.substring("baseUri.".length()));
                            }
                            return null;
                        }
                    },
                    getProjectPrefixes(config),
                    false);
            valueSources.add(baseUriValueSource);
            valueSources.add(new BuildTimestampValueSource(config.getBuildStartTime(), modelProperties));
        }

        valueSources.add(new PrefixedValueSourceWrapper(
                new AbstractValueSource(false) {
                    @Override
                    public Object getValue(String expression) {
                        if ("rootDirectory".equals(expression)) {
                            Path base = projectDir != null ? projectDir.toPath() : null;
                            Path root = rootLocator.findMandatoryRoot(base);
                            return root.toFile().getPath();
                        } else if (expression.startsWith("rootDirectory.")) {
                            Path base = projectDir != null ? projectDir.toPath() : null;
                            Path root = rootLocator.findMandatoryRoot(base);
                            return new ObjectBasedValueSource(root)
                                    .getValue(expression.substring("rootDirectory.".length()));
                        }
                        return null;
                    }
                },
                getProjectPrefixes(config)));

        valueSources.add(projectPrefixValueSource);

        valueSources.add(new MapBasedValueSource(config.getUserProperties()));

        valueSources.add(new MapBasedValueSource(modelProperties));

        valueSources.add(new MapBasedValueSource(config.getSystemProperties()));

        valueSources.add(new AbstractValueSource(false) {
            @Override
            public Object getValue(String expression) {
                return config.getSystemProperties().getProperty("env." + expression);
            }
        });

        valueSources.add(prefixlessObjectBasedValueSource);

        return valueSources;
    }

    protected List<? extends InterpolationPostProcessor> createPostProcessors(
            final Model model, final File projectDir, final ModelBuildingRequest config) {
        List<InterpolationPostProcessor> processors = new ArrayList<>(2);
        if (projectDir != null) {
            processors.add(new PathTranslatingPostProcessor(
                    getProjectPrefixes(config), TRANSLATED_PATH_EXPRESSIONS, projectDir, pathTranslator));
        }
        processors.add(new UrlNormalizingPostProcessor(urlNormalizer));
        return processors;
    }

    protected RecursionInterceptor createRecursionInterceptor(ModelBuildingRequest config) {
        return new PrefixAwareRecursionInterceptor(getProjectPrefixes(config));
    }
}
