package eu.dnetlib.contract.ctx;

import java.security.InvalidParameterException;

import org.apache.log4j.Logger;

import eu.dnetlib.contract.runner.ContractTestRunnerException;
import eu.dnetlib.contract.runner.InvokerDataResultEntry;


/**
 * GlobalContractContext placeholder class.
 * Replaces placeholder plug with it's reference from context.
 * Supports injecting text reference.
 * @author mhorst
 *
 */
public class GlobalContractContextPlaceholder {

	public static final String DEFAULT_PLACEHOLDER_PREFIX = "@{";

	public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";

	public static final String DEFAULT_BEFORE_PLACEHOLDER_PREFIX = "result-before-";

	public static final String DEFAULT_AFTER_PLACEHOLDER_PREFIX = "result-after-";

	public static final String DEFAULT_RESULT_PLACEHOLDER_PREFIX = "result-";
	
	public static final String DEFAULT_SINGLE_RESULT_PLACEHOLDER = "result";
	
	public static final int DEFAULT_START_INDEX = 0;
	
	public static final Logger log = Logger.getLogger(GlobalContractContextPlaceholder.class);
	
	/**
	 * Context object reference.
	 */
	private GlobalContractContext context;

	/**
	 * Placeholder prefix.
	 */
	private String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;

	/**
	 * Placeholder suffix.
	 */
	private String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX;

	/**
	 *  Ignoring unresolvable placeholders flag.
	 */
	private boolean ignoreUnresolvablePlaceholders = false;
	
	/**
	 * Start index for resolving 'after' and 'before' results.
	 * Can be set to 0 or 1, set to 0 by default.
	 */
	private int startIndex = DEFAULT_START_INDEX;

	/**
	 * Resolves placeholders in source {@link String}.
	 * @param source
	 * @return resolved object.
	 */
	public Object resolve(String source) throws ContractTestRunnerException {
		if (source == null) {
			return null;
		}
		if (isSinglePlaceholderOnly(source)) {
			source = source.trim();
			log.debug("processing single placeholder: " + source);
			return resolvePlaceholder(source.substring(placeholderPrefix.length(), 
					source.length()-placeholderSuffix.length()));
		} else {
			return parseStringValue(source);
		}
	}
	
	/**
	 * Resolves placeholders in source array of objects. 
	 * @param source
	 * @return resolved objects
	 * @throws ContractTestRunnerException
	 */
	public Object[] resolve(Object[] source) throws ContractTestRunnerException {
		if (source==null) {
			return null;
		}
		Object[] result = new Object[source.length];
		for (int i=0; i<source.length; i++) {
			if (source[i] instanceof String) {
				result[i] = resolve((String) source[i]);
			} else {
				result[i] = source[i];
			}
		}
		return result;
	}

	/**
	 * Checks wether the source parameter contains single placeholder only.
	 * @param source
	 * @return true if source parameter contains single placeholder only
	 */
	protected boolean isSinglePlaceholderOnly(String source) {
		if (source==null) {
			return false;
		}
		source = source.trim();
		if (source.startsWith(placeholderPrefix)) {
			if (source.endsWith(placeholderSuffix) && 
					source.indexOf(placeholderSuffix)==source.length()-placeholderSuffix.length()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Parses source string value and replaces all placeholder occurences with its 
	 * proper representation from context.
	 * @param source
	 * @return parsed source String with all placeholders replaced.
	 * @throws ContractTestRunnerException
	 */
	protected String parseStringValue(String source) throws ContractTestRunnerException {

		StringBuffer buf = new StringBuffer(source);
		String nullResultValue = "null";
		
		int startIndex = source.indexOf(this.placeholderPrefix);
		while (startIndex != -1) {
			int endIndex = buf.toString().indexOf(this.placeholderSuffix,
					startIndex + this.placeholderPrefix.length());
			if (endIndex != -1) {
				String placeholder = buf.substring(startIndex
						+ this.placeholderPrefix.length(), endIndex);
				Object propVal = resolvePlaceholder(placeholder);
//				resolvePlaceholder() may return null
				buf.replace(startIndex, endIndex + this.placeholderSuffix.length(), 
						propVal!=null?propVal.toString():nullResultValue);
				log.debug("Resolved placeholder '" + placeholder + "' to value: '" + 
						(propVal!=null?propVal.toString():"null") + "'");
				startIndex = buf.toString().indexOf(this.placeholderPrefix,
						startIndex + (propVal!=null?propVal.toString().length():nullResultValue.length()));
			} else {
				startIndex = -1;
			}
		}
		return buf.toString();
	}

	/**
	 * Resolves placeholder from context.
	 * @param placeholder
	 * @return resolved placeholder value
	 */
	protected Object resolvePlaceholder(String placeholder) throws ContractTestRunnerException {
		if (context==null) {
			String message = "Could not resolve placeholder '" + placeholder
				+ "', GlobalContractContext is null!";
			if (this.ignoreUnresolvablePlaceholders) {
				log.error(message);
				return this.placeholderPrefix + placeholder + this.placeholderSuffix;
			} else {
				throw new ContractTestRunnerException(message);
			}
		}
		if (placeholder.equals(DEFAULT_SINGLE_RESULT_PLACEHOLDER)) {
			if (context.getResultContext().getExecutionDataResultEntries()!=null) {
				if (context.getResultContext().getExecutionDataResultEntries().length==1) {
					return context.getResultContext().getExecutionDataResultEntries()[0].getResult();
				} else {
					throw new ContractTestRunnerException(
							"Could not resolve placeholder '" + placeholder
									+ "', expected single result, found: " + 
									context.getResultContext().getExecutionDataResultEntries().length);
				}
			} else {
				if (this.ignoreUnresolvablePlaceholders) {
					return this.placeholderPrefix + placeholder + this.placeholderSuffix;
				} else {
					throw new ContractTestRunnerException(
							"Could not resolve placeholder '" + placeholder
									+ "', tested method wasn't performed yet!");
				}
			}
		} else {
			try {
				if (placeholder.startsWith(DEFAULT_BEFORE_PLACEHOLDER_PREFIX)) {
					return resolveFromInvokerDataResultEntries(placeholder, DEFAULT_BEFORE_PLACEHOLDER_PREFIX,
							context.getResultContext().getBeforeDataResultEntries());
				} else if (placeholder.startsWith(DEFAULT_AFTER_PLACEHOLDER_PREFIX)) {
					return resolveFromInvokerDataResultEntries(placeholder, DEFAULT_AFTER_PLACEHOLDER_PREFIX,
							context.getResultContext().getAfterDataResultEntries());
				} else if (placeholder.startsWith(DEFAULT_RESULT_PLACEHOLDER_PREFIX)) {
//					needs to be defined as the last one because of the overlapping 'result-' prefix
					return resolveFromInvokerDataResultEntries(placeholder, DEFAULT_RESULT_PLACEHOLDER_PREFIX,
							context.getResultContext().getExecutionDataResultEntries());
				}
			} catch (NumberFormatException e) {
				log.warn("couldn't extract index value from placeholder: " + placeholder + 
						", trying to resolve from auxilary context...");
			}
			if (context.getAuxContext().containsKey(placeholder)) {
				return context.getAuxContext().get(placeholder);
			} else {
				if (this.ignoreUnresolvablePlaceholders) {
					return this.placeholderPrefix + placeholder + this.placeholderSuffix;
				} else {
					throw new ContractTestRunnerException(
						"Could not resolve placeholder '" + placeholder + "'");
				}
			}
		}
	}
	
	/**
	 * Resolves from {@link InvokerDataResultEntry} array.
	 * @param placeholder placeholder string
	 * @param placeholderRoot placeholder's unchangable root
	 * @param dataResultEntries
	 * @return resolved object
	 * @throws NumberFormatException thrown when index part of placeholder is invalid
	 * @throws ContractTestRunnerException thrown when couldn't resolve placeholder and 
	 * if ignoreUnresolvablePlaceholders is set to false.
	 */
	protected Object resolveFromInvokerDataResultEntries(String placeholder, String placeholderRoot, 
			InvokerDataResultEntry[] dataResultEntries) throws NumberFormatException, ContractTestRunnerException {
		String indexStr = placeholder.substring(placeholderRoot.length());
//		may throw NumberFormatException when not valid number
		int index = Integer.parseInt(indexStr);
		int convertedIndex = index-startIndex;
		if (dataResultEntries==null) {
			throw new ContractTestRunnerException(
					"Could not resolve placeholder '" + placeholder + "', null InvokerDataResultEntry array!");
		}
		if (convertedIndex >= 0 && 
				convertedIndex < dataResultEntries.length) {
			if (dataResultEntries[convertedIndex]!=null) {
				log.debug("returning object: " + dataResultEntries[convertedIndex].getResult() +
						" of type: " + (dataResultEntries[convertedIndex].getResult()!=null
								?dataResultEntries[convertedIndex].getResult().getClass().getName():"null") + 
						" for placeholder: " + placeholder + ", placeholder root: " + placeholderRoot);
				return dataResultEntries[convertedIndex].getResult();
			} else {
				if (this.ignoreUnresolvablePlaceholders) {
					return this.placeholderPrefix + placeholder + this.placeholderSuffix;
				} else {
					throw new ContractTestRunnerException(
						"Could not resolve placeholder '" + placeholder + "', no data result entry created yet!");
				}
			}
		} else {
			String message = "index value :" + index + 
				" is out of range: [" + startIndex + ',' + 
				(dataResultEntries.length-1+startIndex) + ']';
			if (this.ignoreUnresolvablePlaceholders) {
				log.error(message);
				return this.placeholderPrefix + placeholder + this.placeholderSuffix;
			} else {
				throw new ContractTestRunnerException(message);
			}
		}
	}
	
	/**
	 * Returns {@link GlobalContractContext}.
	 * @return GlobalContractContext
	 */
	public GlobalContractContext getContext() {
		return context;
	}

	/**
	 * Sets {@link GlobalContractContext}.
	 * @param context
	 */
	public void setContext(GlobalContractContext context) {
		this.context = context;
	}

	/**
	 * Returns placeholder prefix.
	 * @return placeholder prefix
	 */
	public String getPlaceholderPrefix() {
		return placeholderPrefix;
	}

	/**
	 * Sets placeholder prefix.
	 * @param placeholderPrefix
	 */
	public void setPlaceholderPrefix(String placeholderPrefix) {
		this.placeholderPrefix = placeholderPrefix;
	}

	/**
	 * Returns placeholder suffix.
	 * @return placeholder suffix
	 */
	public String getPlaceholderSuffix() {
		return placeholderSuffix;
	}

	/**
	 * Sets placeholder suffix.
	 * @param placeholderSuffix
	 */
	public void setPlaceholderSuffix(String placeholderSuffix) {
		this.placeholderSuffix = placeholderSuffix;
	}

	/**
	 * Returns ignoring unresolvable placeholders flag.
	 * @return ignoring unresolvable placeholders flag
	 */
	public boolean isIgnoreUnresolvablePlaceholders() {
		return ignoreUnresolvablePlaceholders;
	}

	/**
	 * Sets ignoring unresolvable placeholders flag.
	 * @param ignoreUnresolvablePlaceholders
	 */
	public void setIgnoreUnresolvablePlaceholders(
			boolean ignoreUnresolvablePlaceholders) {
		this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
	}

	/**
	 * Returns start index for resolving 'after' and 'before' results. 
	 * @return start index for resolving 'after' and 'before' results
	 */
	public int getStartIndex() {
		return startIndex;
	}

	/**
	 * Sets start index for resolving 'after' and 'before' results.
	 * Can be set to either 0 or 1.
	 * @param startIndex
	 */
	public void setStartIndex(int startIndex) {
		if (startIndex==0 || startIndex==1) {
			this.startIndex = startIndex;
		} else {
			throw new InvalidParameterException("start index value can be set either to 0 " +
					"or 1!");
		}
		
	}
}
