/*******************************************************************************
 * Copyright (c) 2007, 2016 Intel Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 * Intel Corporation - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.utils.cdtvariables;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.eclipse.cdt.core.cdtvariables.CdtVariable;
import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
import org.eclipse.cdt.core.cdtvariables.ICdtVariable;
import org.eclipse.cdt.core.cdtvariables.ICdtVariableStatus;

public class SupplierBasedCdtVariableSubstitutor implements IVariableSubstitutor {
	//	private static final Object UNDEFINED_MACRO_VALUE = new Object();
	private static final String EMPTY_STRING = ""; //$NON-NLS-1$
	private IVariableContextInfo fContextInfo;
	private String fInexistentMacroValue;
	private String fListDelimiter;
	private String fIncorrectlyReferencedMacroValue;
	private Map<?, ?> fDelimiterMap;

	protected class ResolvedMacro extends CdtVariable {
		private boolean fIsDefined;
		private boolean fIsList;

		public ResolvedMacro(String name) {
			super(name, VALUE_TEXT, (String) null);
			fIsDefined = false;
		}

		public ResolvedMacro(String name, String value, boolean isDefined) {
			super(name, VALUE_TEXT, value);
			fIsDefined = isDefined;
			fIsList = false;
		}

		public ResolvedMacro(String name, String value) {
			super(name, VALUE_TEXT, value);
			fIsDefined = true;
			fIsList = false;
		}

		public ResolvedMacro(String name, String value[]) {
			super(name, VALUE_TEXT_LIST, value);
			fIsDefined = true;
			fIsList = true;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.cdt.managedbuilder.macros.IBuildMacro#getStringValue()
		 */
		@Override
		public String getStringValue() throws CdtVariableException {
			//			if(!fIsDefined)
			//				throw new BuildMacroException(BuildMacroException.TYPE_MACROS_UNDEFINED,fName);
			if (fIsList && fStringValue == null)
				fStringValue = stringListToString(fStringListValue);
			return fStringValue;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.cdt.managedbuilder.macros.IBuildMacro#getStringListValue()
		 */
		@Override
		public String[] getStringListValue() throws CdtVariableException {
			//			if(!fIsDefined)
			//				throw new BuildMacroException(BuildMacroException.TYPE_MACROS_UNDEFINED,fName);
			if (!fIsList && fStringListValue == null) {
				if (fStringValue != null && fStringValue.length() != 0)
					fStringListValue = new String[] { fStringValue };
				else
					fStringListValue = new String[0];
			}
			return fStringListValue;
		}

		protected String getDelimiter() {
			if (fDelimiterMap != null) {
				Object delimiter = fDelimiterMap.get(fName);
				if (delimiter instanceof String)
					return (String) delimiter;
			}
			return fListDelimiter;
		}

		protected String stringListToString(String values[]) throws CdtVariableException {
			String result = null;
			String delimiter;
			if (values != null) {
				if (values.length == 0)
					result = EMPTY_STRING;
				else if (values.length == 1)
					result = values[0];
				else if ((delimiter = getDelimiter()) != null) {
					StringBuilder buffer = new StringBuilder();
					for (int i = 0; i < values.length; i++) {
						buffer.append(values[i]);
						if (i < values.length - 1)
							buffer.append(delimiter);
					}
					result = buffer.toString();
				} else {
					ICdtVariableStatus eStatus = new SupplierBasedCdtVariableStatus(
							ICdtVariableStatus.TYPE_MACRO_NOT_STRING, null, null, fName, fContextInfo);
					throw new CdtVariableException(eStatus);
				}
			}
			return result;
		}

		public boolean isList() {
			return fIsList;
		}

		public boolean isDefined() {
			return fIsDefined;
		}
	}

	/*
	 * describes the macro and the context where the macro was found
	 */
	protected class MacroDescriptor {
		private String fName;
		private IVariableContextInfo fInfo;
		private ICdtVariable fMacro;
		private boolean fInitialized;
		private int fSupplierNum;
		//		private int fEnvSupplierNum;

		public MacroDescriptor(String name, IVariableContextInfo info) {
			fName = name;
			fInfo = info;
		}

		public MacroDescriptor(String name, IVariableContextInfo info, int supplierNum) {
			fName = name;
			fInfo = info;
			fSupplierNum = supplierNum;
		}

		public MacroDescriptor(ICdtVariable macro, IVariableContextInfo info, int supplierNum) {
			fName = macro.getName();
			fInfo = info;
			fMacro = macro;
			fSupplierNum = supplierNum;
			fInitialized = true;
		}

		public MacroDescriptor getNext() {
			return new MacroDescriptor(fName, getInfo(), getSupplierNum() + 1);
		}

		public int getSupplierNum() {
			init();
			return fSupplierNum;
		}

		private void init() {
			if (fInitialized)
				return;
			fInitialized = true;
			for (; fInfo != null; fInfo = fInfo.getNext()) {
				ICdtVariableSupplier suppliers[] = fInfo.getSuppliers();
				if (suppliers != null) {
					for (; fSupplierNum < suppliers.length; fSupplierNum++) {
						if ((fMacro = suppliers[fSupplierNum].getVariable(fName, fInfo)) != null) {
							return;
						}
					}
				}
				fSupplierNum = 0;
			}
		}

		protected IVariableContextInfo getInfo() {
			init();
			return fInfo;
		}

		public ICdtVariable getMacro() {
			init();
			return fMacro;
		}

	}

	private Map<String, ResolvedMacro> fResolvedMacros = new HashMap<>();
	private HashSet<String> fMacrosUnderResolution = new HashSet<>();
	private Stack<MacroDescriptor> fMacroDescriptors = new Stack<>();

	public SupplierBasedCdtVariableSubstitutor(IVariableContextInfo contextInfo, String inexistentMacroValue,
			String listDelimiter) {
		this(contextInfo, inexistentMacroValue, listDelimiter, null, inexistentMacroValue);
	}

	public SupplierBasedCdtVariableSubstitutor(IVariableContextInfo contextInfo, String inexistentMacroValue,
			String listDelimiter, Map<?, ?> delimiterMap, String incorrectlyReferencedMacroValue) {
		fContextInfo = contextInfo;
		fInexistentMacroValue = inexistentMacroValue;
		fListDelimiter = listDelimiter;
		fDelimiterMap = delimiterMap;
		fIncorrectlyReferencedMacroValue = incorrectlyReferencedMacroValue;
	}

	protected String resolveToString(MacroDescriptor des) throws CdtVariableException {
		String result = null;
		ResolvedMacro value = getResolvedMacro(des);
		result = value.getStringValue();

		return result;
	}

	protected String[] resolveToStringList(MacroDescriptor des) throws CdtVariableException {
		String result[] = null;
		ResolvedMacro value = getResolvedMacro(des);
		result = value.getStringListValue();

		return result;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.managedbuilder.internal.macros.IMacroSubstitutor#resolveToString(java.lang.String)
	 */
	@Override
	public String resolveToString(String macroName) throws CdtVariableException {
		return resolveToString(new MacroDescriptor(macroName, fContextInfo));
	}

	public void setMacroContextInfo(IVariableContextInfo info) throws CdtVariableException {
		if (checkEqual(fContextInfo, info))
			return;

		reset();
		fContextInfo = info;
	}

	protected ResolvedMacro getResolvedMacro(MacroDescriptor des) throws CdtVariableException {
		ResolvedMacro value = checkResolvingMacro(des);

		if (value == null) {
			try {
				value = resolveMacro(des);
			} finally {
				if (value != null)
					addResolvedMacro(des, value);
				else {
					value = new ResolvedMacro(des.fName, fInexistentMacroValue, false);
					addResolvedMacro(des, value);
				}
			}
		}

		return value;
	}

	protected ResolvedMacro resolveMacro(MacroDescriptor des) throws CdtVariableException {
		return des.fMacro != null ? resolveMacro(des.fMacro) : resolveMacro(des.fName);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.managedbuilder.internal.macros.IMacroSubstitutor#resolveToStringList(java.lang.String)
	 */
	@Override
	public String[] resolveToStringList(String macroName) throws CdtVariableException {
		return resolveToStringList(new MacroDescriptor(macroName, fContextInfo));
	}

	protected ResolvedMacro resolveMacro(String macroName) throws CdtVariableException {
		ICdtVariable variable = SupplierBasedCdtVariableManager.getVariable(macroName, fContextInfo, true);
		if (variable == null)
			return null;

		return resolveMacro(variable);
	}

	protected ResolvedMacro resolveParentMacro(MacroDescriptor macroDes) throws CdtVariableException {
		MacroDescriptor des = macroDes.getNext();

		ResolvedMacro macro = null;

		if (des != null) {
			try {
				fMacroDescriptors.push(des);
				macro = resolveMacro(des.getMacro());
			} finally {
				fMacroDescriptors.pop();
			}
		}
		return macro;
	}

	protected ResolvedMacro resolveMacro(ICdtVariable macro) throws CdtVariableException {
		if (macro == null)
			return null;

		String macroName = macro.getName();
		IVariableSubstitutor substitutor = this;

		ResolvedMacro resolvedMacro = null;

		if (CdtVariableResolver.isStringListVariable(macro.getValueType())) {
			String result[] = null;
			String unresolvedValues[] = macro.getStringListValue();
			if (unresolvedValues == null || unresolvedValues.length == 0)
				resolvedMacro = new ResolvedMacro(macroName, EMPTY_STRING);
			else {
				String resolvedValues[][] = new String[unresolvedValues.length][];

				for (int i = 0; i < unresolvedValues.length; i++) {
					try {
						resolvedValues[i] = CdtVariableResolver.resolveToStringList(unresolvedValues[i], substitutor);
					} catch (CdtVariableException e) {
						ICdtVariableStatus statuses[] = e.getVariableStatuses();
						if (statuses != null && statuses.length == 1) {
							ICdtVariableStatus status = statuses[0];
							if (status.getVariableName() == null) {
								ICdtVariableStatus eStatus = new SupplierBasedCdtVariableStatus(status.getCode(),
										macro.getName(), status.getExpression(), status.getReferencedMacroName(),
										fContextInfo);
								e = new CdtVariableException(eStatus);
							}
						}
						throw e;
					}
				}

				if (resolvedValues.length == 1)
					result = resolvedValues[0];
				else {
					List<String> list = new ArrayList<>();
					for (String[] resolvedValue : resolvedValues)
						list.addAll(Arrays.asList(resolvedValue));

					result = list.toArray(new String[list.size()]);
				}
				resolvedMacro = new ResolvedMacro(macroName, result);
			}
		} else {
			try {
				resolvedMacro = new ResolvedMacro(macroName,
						CdtVariableResolver.resolveToString(macro.getStringValue(), substitutor));
			} catch (CdtVariableException e) {
				ICdtVariableStatus statuses[] = e.getVariableStatuses();
				if (statuses != null && statuses.length == 1) {
					ICdtVariableStatus status = statuses[0];
					if (status.getVariableName() == null) {
						ICdtVariableStatus eStatus = new SupplierBasedCdtVariableStatus(status.getCode(),
								macro.getName(), status.getExpression(), status.getReferencedMacroName(), fContextInfo);
						e = new CdtVariableException(eStatus);
					}
				}
				throw e;
			}
		}
		return resolvedMacro;
	}

	private ResolvedMacro checkResolvingMacro(MacroDescriptor des) throws CdtVariableException {
		String name = des.fName;
		ResolvedMacro value = fResolvedMacros.get(name);
		if (value == null) {
			if (fMacrosUnderResolution.add(name)) {
				fMacroDescriptors.push(des);
			} else {
				// the macro of the specified name is referenced from the other macros that
				// are referenced by the given macro
				// e.g. ${macro1}="...${macro2}...", ${macro2}="...${macro1}..."
				// in this case when resolving the ${macro1} value, the ${macro2} resolution will be requested,
				// that in turn will call the ${macro1} resolution again.
				// In this case if the fIncorrectlyReferencedMacroValue is null, the BuildMacroException will be thrown
				// with the IBuildMacroStatus.TYPE_MACRO_REFERENCE_INCORRECT status
				// The special case is when the , macro references itself, e.g.
				// ${macro1} = "...${macro1}..."
				// In the above example the ${macro1} reference will be expanded to the value of the ${macro1} macro of the
				// parent context or to an empty string if there is no such macro defined in the parent contexts
				MacroDescriptor last = fMacroDescriptors.lastElement();
				if (last != null && last.fName.equals(name)) {
					value = resolveParentMacro(last);
					if (value == null)
						value = new ResolvedMacro(name, EMPTY_STRING, false);
				} else if (fIncorrectlyReferencedMacroValue != null) {
					value = new ResolvedMacro(name, fIncorrectlyReferencedMacroValue, false);
				} else {
					ICdtVariableStatus status = new SupplierBasedCdtVariableStatus(
							ICdtVariableStatus.TYPE_MACRO_REFERENCE_INCORRECT, (String) null, null, name, fContextInfo);
					throw new CdtVariableException(status);
				}
			}
		}

		return value;
	}

	protected void addResolvedMacro(MacroDescriptor des, ResolvedMacro value) {
		String name = des.fName;
		fMacrosUnderResolution.remove(name);
		fResolvedMacros.put(name, value);
		fMacroDescriptors.pop();
	}

	protected ResolvedMacro removeResolvedMacro(String name) {
		return fResolvedMacros.remove(name);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.managedbuilder.internal.macros.IMacroSubstitutor#getMacroContextInfo()
	 */
	public IVariableContextInfo getMacroContextInfo() {
		return fContextInfo;
	}

	public void reset() throws CdtVariableException {
		if (fMacrosUnderResolution.size() != 0)
			throw new CdtVariableException(ICdtVariableStatus.TYPE_ERROR, (String) null, null, null);

		fResolvedMacros.clear();
	}

	public Map<?, ?> getDelimiterMap() {
		return fDelimiterMap;
	}

	public void setDelimiterMap(Map<?, ?> delimiterMap) throws CdtVariableException {
		if (checkEqual(fDelimiterMap, delimiterMap))
			return;
		reset();
		fDelimiterMap = delimiterMap;
	}

	public String getIncorrectlyReferencedMacroValue() {
		return fIncorrectlyReferencedMacroValue;
	}

	protected boolean checkEqual(Object o1, Object o2) {
		if (o1 != null) {
			if (o1.equals(o2))
				return true;
		} else if (o2 == null)
			return true;
		return false;
	}

	public void setIncorrectlyReferencedMacroValue(String incorrectlyReferencedMacroValue) throws CdtVariableException {
		if (checkEqual(fIncorrectlyReferencedMacroValue, incorrectlyReferencedMacroValue))
			return;
		reset();
		fIncorrectlyReferencedMacroValue = incorrectlyReferencedMacroValue;
	}

	public String getInexistentMacroValue() {
		return fInexistentMacroValue;
	}

	public void setInexistentMacroValue(String inexistentMacroValue) throws CdtVariableException {
		if (checkEqual(fInexistentMacroValue, inexistentMacroValue))
			return;
		reset();
		fInexistentMacroValue = inexistentMacroValue;
	}

	public String getListDelimiter() {
		return fListDelimiter;
	}

	public void setListDelimiter(String listDelimiter) throws CdtVariableException {
		if (checkEqual(fListDelimiter, listDelimiter))
			return;
		reset();
		fListDelimiter = listDelimiter;
	}
}
