package org.codehaus.mojo.flatten.cifriendly;

/*
 * 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 java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.maven.model.Model;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelProblem.Severity;
import org.apache.maven.model.building.ModelProblem.Version;
import org.apache.maven.model.building.ModelProblemCollector;
import org.apache.maven.model.building.ModelProblemCollectorRequest;
import org.apache.maven.model.interpolation.MavenBuildTimestamp;
import org.apache.maven.model.interpolation.ModelInterpolator;
import org.apache.maven.model.path.PathTranslator;
import org.apache.maven.model.path.UrlNormalizer;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
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.interpolation.util.ValueSourceUtils;

/*
 * Based on StringSearchModelInterpolator in maven-model-builder
 */
@Component(role = CiInterpolator.class)
public class CiModelInterpolator implements CiInterpolator, ModelInterpolator {
	
    public CiModelInterpolator()
    {
        interpolator = createInterpolator();
        recursionInterceptor = new PrefixAwareRecursionInterceptor( PROJECT_PREFIXES );
    }

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

	private static final Collection<String> TRANSLATED_PATH_EXPRESSIONS;

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

		// 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 Interpolator interpolator;
	private RecursionInterceptor recursionInterceptor;

    @Requirement
    private PathTranslator pathTranslator;

    @Requirement
    private UrlNormalizer urlNormalizer;

	private static final Map<Class<?>, InterpolateObjectAction.CacheItem> CACHED_ENTRIES = new ConcurrentHashMap<Class<?>, InterpolateObjectAction.CacheItem>(
			80, 0.75f, 2);
	// Empirical data from 3.x, actual =40

	public Model interpolateModel(Model model, File projectDir, ModelBuildingRequest config,
			ModelProblemCollector problems) {
		interpolateObject(model, model, projectDir, config, problems);

		return model;
	}

	protected void interpolateObject(Object obj, Model model, File projectDir, ModelBuildingRequest config,
			ModelProblemCollector problems) {
		try {
			List<? extends ValueSource> valueSources = createValueSources(model, projectDir, config, problems);
			List<? extends InterpolationPostProcessor> postProcessors = createPostProcessors(model, projectDir, config);

			InterpolateObjectAction action = new InterpolateObjectAction(obj, valueSources, postProcessors, this,
					problems);

			AccessController.doPrivileged(action);
		} finally {
			getInterpolator().clearAnswers();
		}
	}

	protected String interpolateInternal(String src, List<? extends ValueSource> valueSources,
			List<? extends InterpolationPostProcessor> postProcessors, ModelProblemCollector problems) {
		if (src != null && !src.contains("${revision}") && !src.contains("${sha1}") && !src.contains("${changelist}")) {
			return src;
		}

		String result = src;
		synchronized (this) {

			for (ValueSource vs : valueSources) {
				getInterpolator().addValueSource(vs);
			}

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

			try {
				try {
					result = getInterpolator().interpolate(result, getRecursionInterceptor());
				} catch (InterpolationException e) {
					problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE)
							.setMessage(e.getMessage()).setException(e));
				}

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

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

		return result;
	}

	protected Interpolator createInterpolator() {
		CiInterpolatorImpl interpolator = new CiInterpolatorImpl();
		interpolator.setCacheAnswers(true);

		return interpolator;
	}

	private static final class InterpolateObjectAction implements PrivilegedAction<Object> {

		private final LinkedList<Object> interpolationTargets;

		private final CiModelInterpolator modelInterpolator;

		private final List<? extends ValueSource> valueSources;

		private final List<? extends InterpolationPostProcessor> postProcessors;

		private final ModelProblemCollector problems;

		public InterpolateObjectAction(Object target, List<? extends ValueSource> valueSources,
				List<? extends InterpolationPostProcessor> postProcessors, CiModelInterpolator modelInterpolator,
				ModelProblemCollector problems) {
			this.valueSources = valueSources;
			this.postProcessors = postProcessors;

			this.interpolationTargets = new LinkedList<Object>();
			interpolationTargets.add(target);

			this.modelInterpolator = modelInterpolator;

			this.problems = problems;
		}

		public Object run() {
			while (!interpolationTargets.isEmpty()) {
				Object obj = interpolationTargets.removeFirst();

				traverseObjectWithParents(obj.getClass(), obj);
			}

			return null;
		}

		private String interpolate(String value) {
			return modelInterpolator.interpolateInternal(value, valueSources, postProcessors, problems);
		}

		private void traverseObjectWithParents(Class<?> cls, Object target) {
			if (cls == null) {
				return;
			}

			CacheItem cacheEntry = getCacheEntry(cls);
			if (cacheEntry.isArray()) {
				evaluateArray(target, this);
			} else if (cacheEntry.isQualifiedForInterpolation) {
				cacheEntry.interpolate(target, this);

				traverseObjectWithParents(cls.getSuperclass(), target);
			}
		}

		private CacheItem getCacheEntry(Class<?> cls) {
			CacheItem cacheItem = CACHED_ENTRIES.get(cls);
			if (cacheItem == null) {
				cacheItem = new CacheItem(cls);
				CACHED_ENTRIES.put(cls, cacheItem);
			}
			return cacheItem;
		}

		private static void evaluateArray(Object target, InterpolateObjectAction ctx) {
			int len = Array.getLength(target);
			for (int i = 0; i < len; i++) {
				Object value = Array.get(target, i);
				if (value != null) {
					if (String.class == value.getClass()) {
						String interpolated = ctx.interpolate((String) value);

						if (!interpolated.equals(value)) {
							Array.set(target, i, interpolated);
						}
					} else {
						ctx.interpolationTargets.add(value);
					}
				}
			}
		}

		private static class CacheItem {
			private final boolean isArray;

			private final boolean isQualifiedForInterpolation;

			private final CacheField[] fields;

			private boolean isQualifiedForInterpolation(Class<?> cls) {
				return !cls.getName().startsWith("java");
			}

			private boolean isQualifiedForInterpolation(Field field, Class<?> fieldType) {
				if (Map.class.equals(fieldType) && "locations".equals(field.getName())) {
					return false;
				}

				// noinspection SimplifiableIfStatement
				if (fieldType.isPrimitive()) {
					return false;
				}

				return !"parent".equals(field.getName());
			}

			CacheItem(Class clazz) {
				this.isQualifiedForInterpolation = isQualifiedForInterpolation(clazz);
				this.isArray = clazz.isArray();
				List<CacheField> fields = new ArrayList<CacheField>();
				for (Field currentField : clazz.getDeclaredFields()) {
					Class<?> type = currentField.getType();
					if (isQualifiedForInterpolation(currentField, type)) {
						if (String.class == type) {
							if (!Modifier.isFinal(currentField.getModifiers())) {
								fields.add(new StringField(currentField));
							}
						} else if (List.class.isAssignableFrom(type)) {
							fields.add(new ListField(currentField));
						} else if (Collection.class.isAssignableFrom(type)) {
							throw new RuntimeException("We dont interpolate into collections, use a list instead");
						} else if (Map.class.isAssignableFrom(type)) {
							fields.add(new MapField(currentField));
						} else {
							fields.add(new ObjectField(currentField));
						}
					}

				}
				this.fields = fields.toArray(new CacheField[fields.size()]);

			}

			public void interpolate(Object target, InterpolateObjectAction interpolateObjectAction) {
				for (CacheField field : fields) {
					field.interpolate(target, interpolateObjectAction);
				}
			}

			public boolean isArray() {
				return isArray;
			}
		}

		abstract static class CacheField {
			protected final Field field;

			CacheField(Field field) {
				this.field = field;
			}

			void interpolate(Object target, InterpolateObjectAction interpolateObjectAction) {
				synchronized (field) {
					boolean isAccessible = field.isAccessible();
					field.setAccessible(true);
					try {
						doInterpolate(target, interpolateObjectAction);
					} catch (IllegalArgumentException e) {
						interpolateObjectAction.problems
								.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE)
										.setMessage("Failed to interpolate field3: " + field + " on class: "
												+ field.getType().getName())
										.setException(e)); // todo: Not entirely
															// the same message
					} catch (IllegalAccessException e) {
						interpolateObjectAction.problems
								.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE)
										.setMessage("Failed to interpolate field4: " + field + " on class: "
												+ field.getType().getName())
										.setException(e));
					} finally {
						field.setAccessible(isAccessible);
					}
				}

			}

			abstract void doInterpolate(Object target, InterpolateObjectAction ctx) throws IllegalAccessException;
		}

		static final class StringField extends CacheField {
			StringField(Field field) {
				super(field);
			}

			@Override
			void doInterpolate(Object target, InterpolateObjectAction ctx) throws IllegalAccessException {
				String value = (String) field.get(target);
				if (value == null) {
					return;
				}

				String interpolated = ctx.interpolate(value);

				if (!interpolated.equals(value)) {
					field.set(target, interpolated);
				}
			}
		}

		static final class ListField extends CacheField {
			ListField(Field field) {
				super(field);
			}

			@Override
			void doInterpolate(Object target, InterpolateObjectAction ctx) throws IllegalAccessException {
				@SuppressWarnings("unchecked")
				List<Object> c = (List<Object>) field.get(target);
				if (c == null) {
					return;
				}

				int size = c.size();
				Object value;
				for (int i = 0; i < size; i++) {

					value = c.get(i);

					if (value != null) {
						if (String.class == value.getClass()) {
							String interpolated = ctx.interpolate((String) value);

							if (!interpolated.equals(value)) {
								try {
									c.set(i, interpolated);
								} catch (UnsupportedOperationException e) {
									return;
								}
							}
						} else {
							if (value.getClass().isArray()) {
								evaluateArray(value, ctx);
							} else {
								ctx.interpolationTargets.add(value);
							}
						}
					}
				}
			}
		}

		static final class MapField extends CacheField {
			MapField(Field field) {
				super(field);
			}

			@Override
			void doInterpolate(Object target, InterpolateObjectAction ctx) throws IllegalAccessException {
				@SuppressWarnings("unchecked")
				Map<Object, Object> m = (Map<Object, Object>) field.get(target);
				if (m == null || m.isEmpty()) {
					return;
				}

				for (Map.Entry<Object, Object> entry : m.entrySet()) {
					Object value = entry.getValue();

					if (value == null) {
						continue;
					}

					if (String.class == value.getClass()) {
						String interpolated = ctx.interpolate((String) value);

						if (!interpolated.equals(value)) {
							try {
								entry.setValue(interpolated);
							} catch (UnsupportedOperationException ignore) {
								// nop
							}
						}
					} else if (value.getClass().isArray()) {
						evaluateArray(value, ctx);
					} else {
						ctx.interpolationTargets.add(value);
					}
				}
			}
		}

		static final class ObjectField extends CacheField {
			private final boolean isArray;

			ObjectField(Field field) {
				super(field);
				this.isArray = field.getType().isArray();
			}

			@Override
			void doInterpolate(Object target, InterpolateObjectAction ctx) throws IllegalAccessException {
				Object value = field.get(target);
				if (value != null) {
					if (isArray) {
						evaluateArray(value, ctx);
					} else {
						ctx.interpolationTargets.add(value);
					}
				}
			}
		}

	}

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

		ValueSource modelValueSource1 = new PrefixedObjectValueSource(PROJECT_PREFIXES, model, false);
		if (config.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
			modelValueSource1 = new ProblemDetectingValueSource(modelValueSource1, "pom.", "project.", problems);
		}

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

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

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

			ValueSource baseUriValueSource = new PrefixedValueSourceWrapper(new AbstractValueSource(false) {
				public Object getValue(String expression) {
					if ("baseUri".equals(expression)) {
						return projectDir.getAbsoluteFile().toURI().toString();
					}
					return null;
				}
			}, PROJECT_PREFIXES, false);
			valueSources.add(baseUriValueSource);
			valueSources.add(new BuildTimestampValueSource(config.getBuildStartTime(), modelProperties));
		}

		valueSources.add(modelValueSource1);

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

		valueSources.add(new MapBasedValueSource(modelProperties));

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

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

		valueSources.add(modelValueSource2);

		return valueSources;
	}

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

	protected RecursionInterceptor getRecursionInterceptor() {
		return recursionInterceptor;
	}

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

	protected final Interpolator getInterpolator() {
		return interpolator;
	}

	class BuildTimestampValueSource extends AbstractValueSource {
		private final MavenBuildTimestamp mavenBuildTimestamp;

		public BuildTimestampValueSource(Date startTime, Properties properties) {
			super(false);
			this.mavenBuildTimestamp = new MavenBuildTimestamp(startTime, properties);
		}

		public Object getValue(String expression) {
			if ("build.timestamp".equals(expression) || "maven.build.timestamp".equals(expression)) {
				return mavenBuildTimestamp.formattedTimestamp();
			}
			return null;
		}
	}

	class ProblemDetectingValueSource implements ValueSource {

		private final ValueSource valueSource;

		private final String bannedPrefix;

		private final String newPrefix;

		private final ModelProblemCollector problems;

		public ProblemDetectingValueSource(ValueSource valueSource, String bannedPrefix, String newPrefix,
				ModelProblemCollector problems) {
			this.valueSource = valueSource;
			this.bannedPrefix = bannedPrefix;
			this.newPrefix = newPrefix;
			this.problems = problems;
		}

		public Object getValue(String expression) {
			Object value = valueSource.getValue(expression);

			if (value != null && expression.startsWith(bannedPrefix)) {
				String msg = "The expression ${" + expression + "} is deprecated.";
				if (newPrefix != null && newPrefix.length() > 0) {
					msg += " Please use ${" + newPrefix + expression.substring(bannedPrefix.length()) + "} instead.";
				}
				problems.add(new ModelProblemCollectorRequest(Severity.WARNING, Version.V20).setMessage(msg));
			}

			return value;
		}

		@SuppressWarnings("unchecked")
		public List getFeedback() {
			return valueSource.getFeedback();
		}

		public void clearFeedback() {
			valueSource.clearFeedback();
		}

	}

	class PathTranslatingPostProcessor implements InterpolationPostProcessor {

		private final Collection<String> unprefixedPathKeys;
		private final File projectDir;
		private final PathTranslator pathTranslator;
		private final List<String> expressionPrefixes;

		public PathTranslatingPostProcessor(List<String> expressionPrefixes, Collection<String> unprefixedPathKeys,
				File projectDir, PathTranslator pathTranslator) {
			this.expressionPrefixes = expressionPrefixes;
			this.unprefixedPathKeys = unprefixedPathKeys;
			this.projectDir = projectDir;
			this.pathTranslator = pathTranslator;
		}

		public Object execute(String expression, Object value) {
			if (value != null) {
				expression = ValueSourceUtils.trimPrefix(expression, expressionPrefixes, true);

				if (unprefixedPathKeys.contains(expression)) {
					return pathTranslator.alignToBaseDirectory(String.valueOf(value), projectDir);
				}
			}

			return null;
		}

	}

	class UrlNormalizingPostProcessor implements InterpolationPostProcessor {


		private UrlNormalizer normalizer;

		public UrlNormalizingPostProcessor(UrlNormalizer normalizer) {
			this.normalizer = normalizer;
		}

		public Object execute(String expression, Object value) {
			Set<String> expressions = new HashSet<String>();
			expressions.add("project.url");
			expressions.add("project.scm.url");
			expressions.add("project.scm.connection");
			expressions.add("project.scm.developerConnection");
			expressions.add("project.distributionManagement.site.url");

			if (value != null && expressions.contains(expression)) {
				return normalizer.normalize(value.toString());
			}

			return null;
		}

	}

}