/*******************************************************************************
 * Copyright (c) 2005, 2009 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
 *    James Blackburn (Broadcom Corp.)
 *******************************************************************************/
package org.eclipse.cdt.utils.envvar;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

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.settings.model.ICStorageElement;
import org.eclipse.cdt.internal.core.settings.model.xml.XmlStorageElement;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
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 class implements the common functionality that allows
 * storing and loading environment variable settings from eclipse properties
 * 
 * @since 3.0
 */
public abstract class StorableEnvironmentLoader {

	/**
	 * this interface represents the preference node and the preference name
	 * that are used for holding the environment data 
	 * @noextend This interface is not intended to be extended by clients.
	 * @noimplement This interface is not intended to be implemented by clients.
	 */
	public interface ISerializeInfo{

		/**
		 * {@link IEclipsePreferences} root node in the Preference store
		 * @return the Preferences Node into which environment should be (de) serialized
		 */
		Preferences getNode();

		/**
		 * Name in the preference store
		 * @return the key in the preference node to use for loading preferences 
		 */
		String getPrefName();
	}

	/**
	 * Creates the StorableEnvironment clone for a new configuration, say,
	 * based on an existing configuration
	 *
	 * @param context the configuration / workspace context the configuration is to be cloned for
	 * @param base the base environment to copy
	 * @return a StorableEnvironment clone of the configuration's environment
	 * @since 5.2
	 */
	public StorableEnvironment cloneEnvironmentWithContext(Object context, StorableEnvironment base, boolean isReadOnly) {
		PrefsStorableEnvironment env = new PrefsStorableEnvironment(base, getSerializeInfo(context), isReadOnly);			
		return env;
	}

	/**
	 * this method should return the ISerializeInfo representing the information
	 * of where the variable should be stored and loaded
	 * If the given context is not supported this method should return null
	 */
	protected abstract ISerializeInfo getSerializeInfo(Object context);

	/**
	 * Loads the environment from the context's {@link ISerializeInfo}.
	 * 
	 * NB the environment in the {@link ISerializeInfo} need not be available
	 * yet. The {@link ISerializeInfo} may be held by the {@link StorableEnvironment} 
	 * to pick up any external changes in the environment.
	 * 
	 * @param context
	 * @param readOnly
	 * @return StorableEnvironment
	 */
	protected StorableEnvironment loadEnvironment(Object context, boolean readOnly) {
		ISerializeInfo serializeInfo = getSerializeInfo(context);
		if(serializeInfo == null)
			return null;

		return new PrefsStorableEnvironment(serializeInfo, readOnly);
	}
	
	/*
	 * stores the given environment 
	 */
	protected void storeEnvironment(StorableEnvironment env, Object context, boolean force, boolean flush) throws CoreException{
		if(!env.isDirty() && !force)
			return;
		
		ISerializeInfo serializeInfo = getSerializeInfo(context);
		if(serializeInfo == null)
			return;

		if (env instanceof PrefsStorableEnvironment) {
			((PrefsStorableEnvironment)env).serialize();
		} else {
			// Backwards compatibility
			ByteArrayOutputStream stream = storeEnvironmentToStream(env);
			if(stream == null)
				return;
			storeOutputStream(stream,serializeInfo.getNode(), serializeInfo.getPrefName(), flush);
			
			env.setDirty(false);
		}
	}
	
	/**
	 * @param env String representing the encoded environment
	 * @return ICStorageElement tree from the passed in InputStream
	 *                          or null on failure
	 */
	static ICStorageElement environmentStorageFromString(String env) {
		if (env == null)
			return null;
		try{
			DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			InputSource inputSource = new InputSource(new ByteArrayInputStream(env.getBytes()));
			Document document = parser.parse(inputSource);
			Element el = document.getDocumentElement();
			XmlStorageElement rootElement = new XmlStorageElement(el);

			if(!StorableEnvironment.ENVIRONMENT_ELEMENT_NAME.equals(rootElement.getName()))
				return null;

			return rootElement;
		}
		catch(ParserConfigurationException e){
			CCorePlugin.log(e);
		} catch(SAXException e) {
			CCorePlugin.log(e);			
		} catch(IOException e) {
			CCorePlugin.log(e);
		}
		return null;
	}

	private ByteArrayOutputStream storeEnvironmentToStream(StorableEnvironment env) throws CoreException{
		try{
			DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
			DocumentBuilder builder= factory.newDocumentBuilder();		
			Document document= builder.newDocument();
			
			Element el = document.createElement(StorableEnvironment.ENVIRONMENT_ELEMENT_NAME);
			document.appendChild(el);
			XmlStorageElement rootElement = new XmlStorageElement(el);
			env.serialize(rootElement);
			
			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 new CoreException(new Status(IStatus.ERROR, 
					CCorePlugin.PLUGIN_ID,
					-1,
					e.getMessage(),
					e));
		}
		catch(TransformerConfigurationException e){
			throw new CoreException(new Status(IStatus.ERROR, 
					CCorePlugin.PLUGIN_ID,
					-1,
					e.getMessage(),
					e));
		}
		catch(TransformerException e){
			throw new CoreException(new Status(IStatus.ERROR, 
					CCorePlugin.PLUGIN_ID,
					-1,
					e.getMessage(),
					e));
		}
	}

	/**
	 * Preferences can be encoded as a single long ICStorageElement String
	 * @return String value stored in the node or null if no such value exists.
	 */
	static String loadPreferenceNode(ISerializeInfo serializeInfo) {
		if (serializeInfo == null)
			return null;
		return loadPreferenceNode(serializeInfo.getNode(), serializeInfo.getPrefName());
	}

	/**
	 * Returns the value stored in a Preferences node
	 * @param node Preferences node
	 * @param key
	 * @return String value stored in the node or null if no such value exists.
	 */
	static String loadPreferenceNode(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;
		
		return value;
	}
	
	private void storeOutputStream(ByteArrayOutputStream stream, Preferences node, String key, boolean flush) throws CoreException{
		if(stream == null || node == null || key == null)
			throw new CoreException(new Status(IStatus.ERROR, 
					CCorePlugin.PLUGIN_ID,
					-1,
					//TODO:ManagedMakeMessages.getResourceString(
					"StorableEnvironmentLoader.storeOutputStream.wrong.arguments" //$NON-NLS-1$
					//)
					, 
					null));
		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);
		
		if(flush){
			try{
				node.flush();
			}
			catch(BackingStoreException e){
				throw new CoreException(new Status(IStatus.ERROR, 
						CCorePlugin.PLUGIN_ID,
						-1,
						e.getMessage(),
						e));
			}
		}
	}
}
