/*******************************************************************************
 * Copyright (c) 2005, 2008 Intel Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Intel Corporation - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.core.cdtvariables;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
import org.eclipse.cdt.core.cdtvariables.ICdtVariable;
import org.eclipse.cdt.core.settings.model.ICStorageElement;
import org.eclipse.cdt.core.settings.model.util.CDataUtil;
import org.eclipse.cdt.core.settings.model.util.XmlStorageElement;
import org.eclipse.cdt.internal.core.settings.model.CConfigurationSpecSettings;
import org.eclipse.cdt.internal.core.settings.model.ExceptionFactory;
import org.eclipse.cdt.internal.core.settings.model.IInternalCCfgInfo;
import org.eclipse.cdt.utils.cdtvariables.CdtVariableResolver;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * This supplier is used to obtain the user-defined macros
 * 
 * @since 3.0
 */
public class UserDefinedVariableSupplier extends CoreMacroSupplierBase {
//	public static final String MACROS_ELEMENT_NAME = "macros"; //$NON-NLS-1$
	public static final String NODENAME = "macros";  //$NON-NLS-1$
	public static final String PREFNAME_WORKSPACE = "workspace";  //$NON-NLS-1$
	static final String OLD_VARIABLE_PREFIX = "pathEntryVariable."; //$NON-NLS-1$


	private static UserDefinedVariableSupplier fInstance;
	
	private StorableCdtVariables fWorkspaceMacros;
	private Set<ICdtVariableChangeListener> fListeners;
	
	private StorableCdtVariables getStorableMacros(int contextType, Object contextData){
		StorableCdtVariables macros = null;
		switch(contextType){
		case ICoreVariableContextInfo.CONTEXT_CONFIGURATION:
			if(contextData instanceof IInternalCCfgInfo){
				try {
					CConfigurationSpecSettings settings = ((IInternalCCfgInfo)contextData).getSpecSettings();
					macros = settings.getMacros();
				} catch (CoreException e) {
				}
			}
			break;
		case ICoreVariableContextInfo.CONTEXT_WORKSPACE:
			if(contextData == null || contextData instanceof IWorkspace){
				if(fWorkspaceMacros == null)
					fWorkspaceMacros = loadWorkspaceMacros();
				macros = fWorkspaceMacros;
			}
		}

		return macros;
	}
	
	private UserDefinedVariableSupplier(){
		fListeners = Collections.synchronizedSet(new HashSet<ICdtVariableChangeListener>());
	}

	public static UserDefinedVariableSupplier getInstance(){
		if(fInstance == null)
			fInstance = getInstanceSynch();
		return fInstance;
	}

	private static UserDefinedVariableSupplier getInstanceSynch(){
		if(fInstance == null)
			fInstance = new UserDefinedVariableSupplier();
		return fInstance;
	}


	/* (non-Javadoc)
	 * @see org.eclipse.cdt.managedbuilder.macros.IBuildMacroSupplier#getMacro(java.lang.String, int, java.lang.Object)
	 */
	@Override
	public ICdtVariable getMacro(String macroName, int contextType, Object contextData) {
		if(macroName == null || "".equals(macroName))  //$NON-NLS-1$
			return null;
		
		StorableCdtVariables macros = getStorableMacros(contextType,contextData);
		if(macros != null)
			return macros.getMacro(macroName);
		return null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.managedbuilder.macros.IBuildMacroSupplier#getMacros(int, java.lang.Object)
	 */
	@Override
	public ICdtVariable[] getMacros(int contextType, Object contextData) {
		StorableCdtVariables macros = getStorableMacros(contextType,contextData);
		if(macros != null)
			return macros.getMacros();
		return null;
	}
	
	public ICdtVariable createMacro(String macroName, 
					int type, 
					String value, 
					int contextType,
					Object contextData){
		if(macroName == null || "".equals(macroName))  //$NON-NLS-1$
			return null;
		StorableCdtVariables macros = getStorableMacros(contextType, contextData);
		if(macros == null)
			return null;
		
		ICdtVariable oldVar = macros.getMacro(macroName);
		
		ICdtVariable macro = macros.createMacro(macroName,type,value);
		if(macros.isChanged()){
			macros.setChanged(false);
		}
		
		if(macro != null){
			VariableChangeEvent event = createVariableChangeEvent(macro, oldVar);
			if(event != null){
//				updateProjectInfo(contextType, contextData);
				notifyListeners(event);
			}
		}

		return macro;

	}

	public ICdtVariable createMacro(String macroName, 
				int type, 
				String value[], 
				int contextType,
				Object contextData){
		if(macroName == null || "".equals(macroName))  //$NON-NLS-1$
			return null;
		StorableCdtVariables macros = getStorableMacros(contextType, contextData);
		if(macros == null)
			return null;
		
		ICdtVariable oldVar = macros.getMacro(macroName);

		ICdtVariable macro = macros.createMacro(macroName,type,value);
		if(macros.isChanged()){
			macros.setChanged(false);
		}
		
		if(macro != null){
			VariableChangeEvent event = createVariableChangeEvent(macro, oldVar);
			if(event != null){
//				updateProjectInfo(contextType, contextData);
				notifyListeners(event);
			}
		}

		return macro;

	}

	public ICdtVariable createMacro(ICdtVariable copy, int contextType, Object contextData){
		if(copy == null)
			return null;
		String macroName = copy.getName();
		if(macroName == null || "".equals(macroName))  //$NON-NLS-1$
			return null;
		StorableCdtVariables macros = getStorableMacros(contextType, contextData);
		if(macros == null)
			return null;
		
		ICdtVariable oldVar = macros.getMacro(macroName);
		
		ICdtVariable macro = macros.createMacro(copy);
		if(macros.isChanged()){
			macros.setChanged(false);
		}
		
		if(macro != null){
			VariableChangeEvent event = createVariableChangeEvent(macro, oldVar);
			if(event != null){
//				updateProjectInfo(contextType, contextData);
				notifyListeners(event);
			}
		}
		return macro;
	}

	public ICdtVariable deleteMacro(String name, int contextType, Object contextData){
		StorableCdtVariables macros = getStorableMacros(contextType,contextData);
		if(macros == null)
			return null;
		ICdtVariable macro = macros.deleteMacro(name);
		if(macro != null){
			
			VariableChangeEvent event = createVariableChangeEvent(null, macro);
			if(event != null){
//				updateProjectInfo(contextType, contextData);
				notifyListeners(event);
			}

		}
		
		return macro;
	}
	
	public void deleteAll(int contextType, Object contextData){
		StorableCdtVariables macros = getStorableMacros(contextType, contextData);
		if(macros == null)
			return;

		ICdtVariable[] oldVars = macros.getMacros();
		
		if(macros.deleteAll()){
//			setRebuildStateForContext(contextType, contextData);
		}
		
		VariableChangeEvent event = createVariableChangeEvent(null, oldVars);
		if(event != null){
//			updateProjectInfo(contextType, contextData);
			notifyListeners(event);
		}

	}
	
	public void setMacros(ICdtVariable m[], int contextType, Object contextData){
		StorableCdtVariables macros = getStorableMacros(contextType, contextData);
		if(macros == null)
			return;

		ICdtVariable[] oldVars = macros.getMacros();
		
		macros.setMacros(m);
		if(macros.isChanged()){
			macros.setChanged(false);
			
			VariableChangeEvent event = createVariableChangeEvent(m, oldVars);
			if(event != null){
//				updateProjectInfo(contextType, contextData);
				notifyListeners(event);
			}

		}
	}
	
	static class VarKey {
		private ICdtVariable fVar;
		private boolean fNameOnly;
		
		VarKey(ICdtVariable var, boolean nameOnly){
			fVar = var;
			fNameOnly = nameOnly;
		}
		
		public ICdtVariable getVariable(){
			return fVar; 
		}

		@Override
		public boolean equals(Object obj) {
			if(obj == this)
				return true;
			
			if(!(obj instanceof VarKey))
				return false;
			
			VarKey other = (VarKey)obj;
			
			ICdtVariable otherVar = other.fVar;
			
			if(fVar == otherVar)
				return true;
			
			if(!CDataUtil.objectsEqual(fVar.getName(), otherVar.getName()))
				return false;

			if(fNameOnly)
				return true;
			
			if(fVar.getValueType() != otherVar.getValueType())
				return false;

			if(CdtVariableResolver.isStringListVariable(fVar.getValueType())){
				try {
					if(!Arrays.equals(fVar.getStringListValue(), otherVar.getStringListValue()))
						return false;
				} catch (CdtVariableException e) {
					CCorePlugin.log(e);
				}
			} else {
				try {
					if(!CDataUtil.objectsEqual(fVar.getStringValue(), otherVar.getStringValue()))
						return false;
				} catch (CdtVariableException e) {
					CCorePlugin.log(e);
				}
			}
				
			return true;
		}

		@Override
		public int hashCode() {
			int code = 51;
			
			String name = fVar.getName();
			if(name != null)
				code += name.hashCode();

			if(fNameOnly)
				return code;
			
			code += fVar.getValueType();
			if(CdtVariableResolver.isStringListVariable(fVar.getValueType())){
				try {
					String[] value = fVar.getStringListValue();
					if(value != null){
						for (String element : value) {
							code += element.hashCode();
						}
					}
				} catch (CdtVariableException e) {
					CCorePlugin.log(e);
				}
			} else {
				try {
					String value =fVar.getStringValue();
					if(value != null){
						code += value.hashCode();
					}
				} catch (CdtVariableException e) {
					CCorePlugin.log(e);
				}
			}
			
			return code;
		}
		
	}

	static VariableChangeEvent createVariableChangeEvent(ICdtVariable newVar, ICdtVariable oldVar){
		ICdtVariable newVars[] = newVar != null ? new ICdtVariable[]{newVar} : null;
		ICdtVariable oldVars[] = oldVar != null ? new ICdtVariable[]{oldVar} : null;
		
		return createVariableChangeEvent(newVars, oldVars);
	}

	static ICdtVariable[] varsFromKeySet(Set<VarKey> set){
		ICdtVariable vars[] = new ICdtVariable[set.size()];
		int i = 0;
		for(VarKey key : set) {
			vars[i++] = key.getVariable();
		}
		return vars;
	}
	
	@SuppressWarnings("unchecked")
	static VariableChangeEvent createVariableChangeEvent(ICdtVariable[] newVars, ICdtVariable[] oldVars){
		ICdtVariable[] addedVars = null, removedVars = null, changedVars = null;
		
		if(oldVars == null || oldVars.length == 0){
			if(newVars != null && newVars.length != 0)
				addedVars = newVars.clone() ;
		} else if(newVars == null || newVars.length == 0){
			removedVars = oldVars.clone();
		} else {
			HashSet<VarKey> newSet = new HashSet<VarKey>(newVars.length);
			HashSet<VarKey> oldSet = new HashSet<VarKey>(oldVars.length);
			
			for (ICdtVariable newVar : newVars) {
				newSet.add(new VarKey(newVar, true));
			}
	
			for (ICdtVariable oldVar : oldVars) {
				oldSet.add(new VarKey(oldVar, true));
			}
	
			HashSet<VarKey> newSetCopy = (HashSet<VarKey>)newSet.clone();
	
			newSet.removeAll(oldSet);
			oldSet.removeAll(newSetCopy);
			
			if(newSet.size() != 0){
				addedVars = varsFromKeySet(newSet);
			}
			
			if(oldSet.size() != 0){
				removedVars = varsFromKeySet(oldSet);
			}
			
			newSetCopy.removeAll(newSet);
			
			HashSet modifiedSet = new HashSet(newSetCopy.size());
			for (Object element : newSetCopy) {
				VarKey key = (VarKey)element;
				modifiedSet.add(new VarKey(key.getVariable(), false));
			}
			
			for (ICdtVariable oldVar : oldVars) {
				modifiedSet.remove(new VarKey(oldVar, false));
			}
			
			if(modifiedSet.size() != 0)
				changedVars = varsFromKeySet(modifiedSet); 
		}
		
		if(addedVars != null || removedVars != null || changedVars != null)
			return new VariableChangeEvent(addedVars, removedVars, changedVars);
		return null;
	}

	/*
	 * 
	 * methods used for loadding/storing workspace macros from properties 
	 * 
	 */
	public void storeWorkspaceVariables(boolean force){
		try{
			if(fWorkspaceMacros != null)
				storeWorkspaceMacros(fWorkspaceMacros,force);
		}catch(CoreException e){
		}
	}
	
	public StorableCdtVariables getWorkspaceVariablesCopy(){
		StorableCdtVariables vars = getStorableMacros(ICoreVariableContextInfo.CONTEXT_WORKSPACE, null);
		return new StorableCdtVariables(vars, false);
	}
	
	public boolean setWorkspaceVariables(StorableCdtVariables vars) throws CoreException{
		StorableCdtVariables old = getStorableMacros(ICoreVariableContextInfo.CONTEXT_WORKSPACE, null);
		ICdtVariable[] oldVars = null;
		if(old != null)
			oldVars = old.getMacros();
		
		ICdtVariable[] newVars = vars.getMacros();
		
		fWorkspaceMacros = new StorableCdtVariables(vars, false);
		
		VariableChangeEvent event = createVariableChangeEvent(newVars, oldVars);
		if(event != null){
//			updateProjectInfo(ICoreVariableContextInfo.CONTEXT_WORKSPACE, null);
			notifyListeners(event);
		}

		storeWorkspaceVariables(true);
		return event != null;
	}
	
	private Preferences getWorkspaceNode(){
		Preferences prefNode = new InstanceScope().getNode(CCorePlugin.PLUGIN_ID);
		if(prefNode == null)
			return null;
		
		return prefNode.node(NODENAME);
	}

	
	/*
	 * loads the stored workspace macros
	 */
	protected StorableCdtVariables loadWorkspaceMacros(){
		StorableCdtVariables macros = loadNewStileWorkspaceMacros();
		
		//now load PathEntry Variables from preferences
		loadPathEntryVariables(macros);
		
		if(macros.isDirty()){
			try {
				storeWorkspaceMacros(macros, true);
			} catch (CoreException e) {
				CCorePlugin.log(e);
			}
		}
		return macros;
	}
	
	protected void loadPathEntryVariables(StorableCdtVariables vars){
		org.eclipse.core.runtime.Preferences prefs = CCorePlugin.getDefault().getPluginPreferences();
		String[] names = prefs.propertyNames();
		for (String name : names) {
			if (name.startsWith(OLD_VARIABLE_PREFIX)) {
				String value = prefs.getString(name);
				prefs.setToDefault(name);
				if(value.length() != 0){
					name = name.substring(OLD_VARIABLE_PREFIX.length());
					vars.createMacro(name, ICdtVariable.VALUE_PATH_ANY, value);
				}
			}
		}
	}
	
	protected StorableCdtVariables loadNewStileWorkspaceMacros(){
		InputStream stream = loadInputStream(getWorkspaceNode(),PREFNAME_WORKSPACE);
		if(stream == null)
			return new StorableCdtVariables(false);
		return loadMacrosFromStream(stream, false);
	}
	
	
	/*
	 * stores the given macros 
	 */
	protected void storeWorkspaceMacros(StorableCdtVariables macros, boolean force) throws CoreException{
		if(!macros.isDirty() && !force)
			return;
		
		ByteArrayOutputStream stream = storeMacrosToStream(macros);
		if(stream == null)
			return;
		storeOutputStream(stream,getWorkspaceNode(),PREFNAME_WORKSPACE);
	}
	
	private StorableCdtVariables loadMacrosFromStream(InputStream stream, boolean readOnly){
		try{
			DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			InputSource inputSource = new InputSource(stream);
			Document document = parser.parse(inputSource);
			Element rootElement = document.getDocumentElement();
			
			if(!StorableCdtVariables.MACROS_ELEMENT_NAME.equals(rootElement.getNodeName()))
				return null;
			
			return new StorableCdtVariables(new XmlStorageElement(rootElement), readOnly);
		}
		catch(ParserConfigurationException e){
			
		}
		catch(SAXException e){
			
		}
		catch(IOException e){
			
		}
		
		return null;
	}
	
	private ByteArrayOutputStream storeMacrosToStream(StorableCdtVariables macros) throws CoreException{
		try{
			DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
			DocumentBuilder builder= factory.newDocumentBuilder();		
			Document document= builder.newDocument();
			
			Element rootElement = document.createElement(StorableCdtVariables.MACROS_ELEMENT_NAME);
			document.appendChild(rootElement);
			ICStorageElement storageElement = new XmlStorageElement(rootElement);
			macros.serialize(storageElement);
			
			Transformer transformer=TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
			transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
			DOMSource source = new DOMSource(document);
			
			ByteArrayOutputStream stream = new ByteArrayOutputStream();
			StreamResult result = new StreamResult(stream);
	
			transformer.transform(source, result);
			return stream;
		}
		catch(ParserConfigurationException e){
			throw ExceptionFactory.createCoreException(e.getMessage(),
					e);
		}
		catch(TransformerConfigurationException e){
			throw ExceptionFactory.createCoreException(e.getMessage(),
					e);
		}
		catch(TransformerException e){
			throw ExceptionFactory.createCoreException(e.getMessage(),
					e);
		}
	}
	
	private InputStream loadInputStream(Preferences node, String key){
		if(node == null || key == null)
			return null;
		
		String value = node.get(key,null);
		if(value == null || value.length() == 0)
			return null;
		
		byte[] bytes;
		try {
			bytes = value.getBytes("UTF-8"); //$NON-NLS-1$
		} catch (UnsupportedEncodingException e) {
			bytes = value.getBytes();
		}
		
		return new ByteArrayInputStream(bytes);
	}
	
	private void storeOutputStream(ByteArrayOutputStream stream, Preferences node, String key) throws CoreException{
		if(stream == null || node == null || key == null)
			throw ExceptionFactory.createCoreException(new IllegalArgumentException());
		byte[] bytes= stream.toByteArray();
		
		String val = null;
		try {
			val= new String(bytes, "UTF-8"); //$NON-NLS-1$
		} catch (UnsupportedEncodingException e) {
			val= new String(bytes);
		}
		
		node.put(key,val);
		
		try{
			node.flush();
		}
		catch(BackingStoreException e){
			throw ExceptionFactory.createCoreException(e.getMessage(),
					e);
		}
	}
	
//	protected void updateProjectInfo(int type, Object context){
//	}
	
	public void addListener(ICdtVariableChangeListener listener){
		fListeners.add(listener);
	}
	
	public void removeListener(ICdtVariableChangeListener listener){
		fListeners.remove(listener);
	}
	
	private void notifyListeners(VariableChangeEvent event){
		ICdtVariableChangeListener[] listeners = fListeners.toArray(new ICdtVariableChangeListener[fListeners.size()]);
		for (ICdtVariableChangeListener listener : listeners) {
			listener.variablesChanged(event);
		}
	}
	
	public boolean containsVariable(int context, Object data, ICdtVariable var){
		ICdtVariable varContained = getMacro(var.getName(), context, data);
		if(varContained == null)
			return false;
		
		if(new VarKey(varContained, false).equals(new VarKey(var, false)))
			return true;
		
		return false;
	}
	
}
