/*******************************************************************************
 * Copyright (c) 2000, 2006 IBM 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.jdt.internal.ui.preferences.formatter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
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.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.jdt.core.JavaCore;

import org.eclipse.jdt.ui.JavaUI;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.JavaUIException;
import org.eclipse.jdt.internal.ui.JavaUIStatus;
import org.eclipse.jdt.internal.ui.preferences.PreferencesAccess;
import org.eclipse.jdt.internal.ui.preferences.formatter.ProfileManager.CustomProfile;
import org.eclipse.jdt.internal.ui.preferences.formatter.ProfileManager.Profile;

import org.osgi.service.prefs.BackingStoreException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;



public class ProfileStore {
	
	/**
	 * A SAX event handler to parse the xml format for profiles. 
	 */
	private final static class ProfileDefaultHandler extends DefaultHandler {
		
		private List fProfiles;
		private int fVersion;
		
		private String fName;
		private Map fSettings;


		public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {

			if (qName.equals(XML_NODE_SETTING)) {

				final String key= attributes.getValue(XML_ATTRIBUTE_ID);
				final String value= attributes.getValue(XML_ATTRIBUTE_VALUE);
				fSettings.put(key, value);

			} else if (qName.equals(XML_NODE_PROFILE)) {

				fName= attributes.getValue(XML_ATTRIBUTE_NAME);
				fSettings= new HashMap(200);

			}
			else if (qName.equals(XML_NODE_ROOT)) {

				fProfiles= new ArrayList();
				try {
					fVersion= Integer.parseInt(attributes.getValue(XML_ATTRIBUTE_VERSION));
				} catch (NumberFormatException ex) {
					throw new SAXException(ex);
				}

			}
		}
		
		public void endElement(String uri, String localName, String qName) {
			if (qName.equals(XML_NODE_PROFILE)) {
				fProfiles.add(new CustomProfile(fName, fSettings, fVersion));
				fName= null;
				fSettings= null;
			}
		}
		
		public List getProfiles() {
			return fProfiles;
		}
		
	}

	/**
	 * Preference key where all profiles are stored
	 */
	private static final String PREF_FORMATTER_PROFILES= "org.eclipse.jdt.ui.formatterprofiles"; //$NON-NLS-1$
	
	/**
	 * Preference key where all profiles are stored
	 */
	private static final String PREF_FORMATTER_PROFILES_VERSION= "org.eclipse.jdt.ui.formatterprofiles.version"; //$NON-NLS-1$
	
	
	
	/**
	 * Identifiers for the XML file.
	 */
	private final static String XML_NODE_ROOT= "profiles"; //$NON-NLS-1$
	private final static String XML_NODE_PROFILE= "profile"; //$NON-NLS-1$
	private final static String XML_NODE_SETTING= "setting"; //$NON-NLS-1$
	
	private final static String XML_ATTRIBUTE_VERSION= "version"; //$NON-NLS-1$
	private final static String XML_ATTRIBUTE_ID= "id"; //$NON-NLS-1$
	private final static String XML_ATTRIBUTE_NAME= "name"; //$NON-NLS-1$
	private final static String XML_ATTRIBUTE_VALUE= "value"; //$NON-NLS-1$
		
	private ProfileStore() {
	}
	
	/**
	 * @return Returns the collection of profiles currently stored in the preference store or
	 * <code>null</code> if the loading failed. The elements are of type {@link CustomProfile}
	 * and are all updated to the latest version.
	 * @throws CoreException
	 */
	public static List readProfiles(IScopeContext scope) throws CoreException {
		List res= readProfilesFromPreferences(scope);
		if (res == null) {
			return readOldForCompatibility(scope);
		}
		return res;
	}
	
	public static void writeProfiles(Collection profiles, IScopeContext instanceScope) throws CoreException {
		ByteArrayOutputStream stream= new ByteArrayOutputStream(2000);
		try {
			writeProfilesToStream(profiles, stream);
			String val;
			try {
				val= stream.toString("UTF-8"); //$NON-NLS-1$
			} catch (UnsupportedEncodingException e) {
				val= stream.toString(); 
			}
			IEclipsePreferences uiPreferences = instanceScope.getNode(JavaUI.ID_PLUGIN);
			uiPreferences.put(PREF_FORMATTER_PROFILES, val);
			uiPreferences.putInt(PREF_FORMATTER_PROFILES_VERSION, ProfileVersioner.CURRENT_VERSION);
		} finally {
			try { stream.close(); } catch (IOException e) { /* ignore */ }
		}
	}
	
	public static List readProfilesFromPreferences(IScopeContext scope) throws CoreException {
		String string= scope.getNode(JavaUI.ID_PLUGIN).get(PREF_FORMATTER_PROFILES, null);
		if (string != null && string.length() > 0) {
			byte[] bytes;
			try {
				bytes= string.getBytes("UTF-8"); //$NON-NLS-1$
			} catch (UnsupportedEncodingException e) {
				bytes= string.getBytes();
			}
			InputStream is= new ByteArrayInputStream(bytes);
			try {
				List res= readProfilesFromStream(new InputSource(is));
				if (res != null) {
					for (int i= 0; i < res.size(); i++) {
						ProfileVersioner.updateAndComplete((CustomProfile) res.get(i));
					}
				}
				return res;
			} finally {
				try { is.close(); } catch (IOException e) { /* ignore */ }
			}
		}
		return null;
	}	

	/**
	 * Read the available profiles from the internal XML file and return them
	 * as collection.
	 * @return returns a list of <code>CustomProfile</code> or <code>null</code>
	 */
	private static List readOldForCompatibility(IScopeContext instanceScope) {
		
		// in 3.0 M9 and less the profiles were stored in a file in the plugin's meta data
		final String STORE_FILE= "code_formatter_profiles.xml"; //$NON-NLS-1$

		File file= JavaPlugin.getDefault().getStateLocation().append(STORE_FILE).toFile();
		if (!file.exists())
			return null;
		
		try {
			// note that it's wrong to use a file reader when XML declares UTF-8: Kept for compatibility
			final FileReader reader= new FileReader(file);
			try {
				List res= readProfilesFromStream(new InputSource(reader));
				if (res != null) {
					for (int i= 0; i < res.size(); i++) {
						ProfileVersioner.updateAndComplete((CustomProfile) res.get(i));
					}
					writeProfiles(res, instanceScope);
				}
				file.delete(); // remove after successful write
				return res;
			} finally {
				reader.close();
			}
		} catch (CoreException e) {
			JavaPlugin.log(e); // log but ignore
		} catch (IOException e) {
			JavaPlugin.log(e); // log but ignore
		}
		return null;
	}
	
	
	/**
	 * Read the available profiles from the internal XML file and return them
	 * as collection or <code>null</code> if the file is not a profile file.
	 * @param file The file to read from
	 * @return returns a list of <code>CustomProfile</code> or <code>null</code>
	 * @throws CoreException
	 */
	public static List readProfilesFromFile(File file) throws CoreException {
		try {
			final FileInputStream reader= new FileInputStream(file); 
			try {
				return readProfilesFromStream(new InputSource(reader));
			} finally {
				try { reader.close(); } catch (IOException e) { /* ignore */ }
			}
		} catch (IOException e) {
			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
		}
	}
	
	/**
	 * Load profiles from a XML stream and add them to a map or <code>null</code> if the source is not a profile store.
	 * @param inputSource The input stream
	 * @return returns a list of <code>CustomProfile</code> or <code>null</code>
	 * @throws CoreException
	 */
	private static List readProfilesFromStream(InputSource inputSource) throws CoreException {
		
		final ProfileDefaultHandler handler= new ProfileDefaultHandler();
		try {
		    final SAXParserFactory factory= SAXParserFactory.newInstance();
			final SAXParser parser= factory.newSAXParser();
			parser.parse(inputSource, handler);
		} catch (SAXException e) {
			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
		} catch (IOException e) {
			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
		} catch (ParserConfigurationException e) {
			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
		}
		return handler.getProfiles();
	}
	
	/**
	 * Write the available profiles to the internal XML file.
	 * @param profiles List of <code>CustomProfile</code>
	 * @param file File to write
	 * @throws CoreException
	 */
	public static void writeProfilesToFile(Collection profiles, File file) throws CoreException {
		final OutputStream writer;
		try {
			writer= new FileOutputStream(file);
			try {
				writeProfilesToStream(profiles, writer);
			} finally {
				try { writer.close(); } catch (IOException e) { /* ignore */ }
			}
		} catch (IOException e) {
			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_serializing_xml_message);  
		}
	}
	
	/**
	 * Save profiles to an XML stream
	 * @param profiles List of <code>CustomProfile</code>
	 * @param stream Stream to write
	 * @throws CoreException
	 */
	private static void writeProfilesToStream(Collection profiles, OutputStream stream) throws CoreException {

		try {
			final DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
			final DocumentBuilder builder= factory.newDocumentBuilder();		
			final Document document= builder.newDocument();
			
			final Element rootElement = document.createElement(XML_NODE_ROOT);
			rootElement.setAttribute(XML_ATTRIBUTE_VERSION, Integer.toString(ProfileVersioner.CURRENT_VERSION));

			document.appendChild(rootElement);
			
			for(final Iterator iter= profiles.iterator(); iter.hasNext();) {
				final Profile profile= (Profile)iter.next();
				if (profile.isProfileToSave()) {
					final Element profileElement= createProfileElement(profile, document);
					rootElement.appendChild(profileElement);
				}
			}

			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$
			transformer.transform(new DOMSource(document), new StreamResult(stream));
		} catch (TransformerException e) {
			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_serializing_xml_message);  
		} catch (ParserConfigurationException e) {
			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_serializing_xml_message);  
		}
	}

	
	/*
	 * Create a new profile element in the specified document. The profile is not added
	 * to the document by this method. 
	 */
	private static Element createProfileElement(Profile profile, Document document) {
		final Element element= document.createElement(XML_NODE_PROFILE);
		element.setAttribute(XML_ATTRIBUTE_NAME, profile.getName());
		element.setAttribute(XML_ATTRIBUTE_VERSION, Integer.toString(profile.getVersion()));
		
		final Iterator keyIter= ProfileManager.getKeys().iterator();
		
		while (keyIter.hasNext()) {
			final String key= (String)keyIter.next();
			final String value= (String)profile.getSettings().get(key);
			if (value != null) {
				final Element setting= document.createElement(XML_NODE_SETTING);
				setting.setAttribute(XML_ATTRIBUTE_ID, key);
				setting.setAttribute(XML_ATTRIBUTE_VALUE, value);
				element.appendChild(setting);
			} else {
				JavaPlugin.logErrorMessage("ProfileStore: Profile does not contain value for key " + key); //$NON-NLS-1$
			}
		}
		return element;
	}
	
	public static void checkCurrentOptionsVersion() {
		PreferencesAccess access= PreferencesAccess.getOriginalPreferences();
		
		IScopeContext instanceScope= access.getInstanceScope();
		IEclipsePreferences uiPreferences= instanceScope.getNode(JavaUI.ID_PLUGIN);
		int version= uiPreferences.getInt(PREF_FORMATTER_PROFILES_VERSION, 0);
		if (version >= ProfileVersioner.CURRENT_VERSION) {
			return; // is up to date
		}
		try {
			List profiles= ProfileStore.readProfiles(instanceScope);
			if (profiles == null) {
				profiles= Collections.EMPTY_LIST;
			}
			ProfileManager manager= new ProfileManager(profiles, instanceScope, access);
			if (manager.getSelected() instanceof CustomProfile) {
				manager.commitChanges(instanceScope); // updates JavaCore options
			}
			uiPreferences.putInt(PREF_FORMATTER_PROFILES_VERSION, ProfileVersioner.CURRENT_VERSION);
			savePreferences(instanceScope);
						
			IProject[] projects= ResourcesPlugin.getWorkspace().getRoot().getProjects();
			for (int i= 0; i < projects.length; i++) {
				IScopeContext scope= access.getProjectScope(projects[i]);
				if (ProfileManager.hasProjectSpecificSettings(scope)) {
					manager= new ProfileManager(profiles, scope, access);
					manager.commitChanges(scope); // updates JavaCore project options
					savePreferences(scope);
				}
			}
		} catch (CoreException e) {
			JavaPlugin.log(e);
		} catch (BackingStoreException e) {
			JavaPlugin.log(e);
		}
	}
	
	private static void savePreferences(final IScopeContext context) throws BackingStoreException {
		try {
			context.getNode(JavaUI.ID_PLUGIN).flush();
		} finally {
			context.getNode(JavaCore.PLUGIN_ID).flush();
		}
	}
	
		
	/*
	 * Creates a UI exception for logging purposes
	 */
	private static JavaUIException createException(Throwable t, String message) {
		return new JavaUIException(JavaUIStatus.createError(IStatus.ERROR, message, t));
	}
}
