/*******************************************************************************
 * Copyright (c) 2007, 2011 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.validation.internal;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.wst.validation.Friend;
import org.eclipse.wst.validation.MessageSeveritySetting;
import org.eclipse.wst.validation.Validator;
import org.eclipse.wst.validation.Validator.V2;
import org.eclipse.wst.validation.internal.model.FilterGroup;
import org.eclipse.wst.validation.internal.model.GlobalPreferences;
import org.eclipse.wst.validation.internal.model.GlobalPreferencesValues;
import org.eclipse.wst.validation.internal.plugin.ValidationPlugin;
import org.osgi.service.prefs.BackingStoreException;

/**
 * A class that knows how to manage the global persisted validation settings.
 * @author karasiuk
 */
public final class ValPrefManagerGlobal {
	
	/** 
	 * Version of the framework properties.
	 * <ul>
	 * <li>Version 2 - used the filter approach
	 * <li>Version 3 - switched to a difference based approach. (See Bugzilla 224179)
	 * </ul>
	 * 
	 */
	public final static int frameworkVersion = 3;
	
	private final Set<IValChangedListener> _listeners = new CopyOnWriteArraySet<IValChangedListener>();
	
	private final AtomicReference<List<Validator>> _validators = new AtomicReference<List<Validator>>();
	
	private ValPrefManagerGlobal(){}
	
	public static ValPrefManagerGlobal getDefault(){
		return Singleton.valPrefManagerGlobal;
	}
	
	public void addListener(IValChangedListener listener){
		_listeners.add(listener);
	}
	
	public void removeListener(IValChangedListener listener){
		_listeners.remove(listener);
	}
	
	private void updateListeners(boolean validationSettingChanged){
		for (IValChangedListener cl : _listeners)cl.validatorsForProjectChanged(null, validationSettingChanged); 
	}
			
	/**
	 * Update the validator filters from the preference store.
	 *  
	 * @param val
	 * 
	 * @return false if there are no preferences, that means that the user has never changed any
	 * of the default settings. Also answer false if there was some sort of error, which essentially
	 * means that the preferences aren't valid for whatever reason.   
	 * 
	 * @deprecated
	 */
//	public boolean loadPreferences(Validator[] val) {
//	
//		try {
//			IEclipsePreferences pref = ValidationFramework.getDefault().getPreferenceStore();
//			if (!pref.nodeExists(PrefConstants.filters))return false;
//		
//			Preferences filters = pref.node(PrefConstants.filters);
//			for (Validator v : val){
//				String id = v.getId();
//				if (filters.nodeExists(id)){
//					Preferences vp = filters.node(id);
//					loadPreferences(v, vp);
//				}
//			}			
//		}
//		catch (Exception e){
//			ValidationPlugin.getPlugin().handleException(e);
//			return false;
//		}
//		
//		return true;
//	}
	
	/**
	 * Answer the v2 validators that have been overridden by the global preferences.
	 */
	public List<Validator> getValidators() throws BackingStoreException {
		List<Validator> vals = _validators.get();
		while (vals == null){
			vals = loadValidators();
			if (!_validators.compareAndSet(null, vals))vals = _validators.get();
		}
		return vals;
	}
	
	/**
	 * Load the validators from the preference store.
	 * @return the validators that have been overridden by the global references.
	 */
	private List<Validator> loadValidators() throws BackingStoreException {
		LinkedList<Validator> list = new LinkedList<Validator>();
		PreferencesWrapper pref = PreferencesWrapper.getPreferences(null, null);
		if (pref.nodeExists(PrefConstants.vals)){
			PreferencesWrapper vals = pref.node(PrefConstants.vals);
			for (String id : vals.childrenNames()){
				Validator base = ExtensionValidators.instance().getMapV2().get(id);
				Validator v = loadValidator(id, vals, base);
				if (v != null){
					V2 v2 = v.asV2Validator();
					if (v2 != null)v2.setLevel(Validator.Level.Global);					
					list.add(v);
				}
			}
		}
		return list;
	}

	/**
	 * Answer a copy of the validator that has been updated with the given preferences.
	 * 
	 * @param id
	 *            Validator id.
	 * @param valsNode
	 *            The /vals node in the preference store.
	 * @param base
	 *            The base validator that is being customized. This can be null,
	 *            in which case null will be returned.
	 * 
	 * @return A new validator that is a copy of the extension point validator
	 *         with the updates from the preference store.
	 */
	static Validator loadValidator(String id, PreferencesWrapper valsNode, Validator base) {
		if (base == null)return null;
		
		PreferencesWrapper vp = valsNode.node(id);
		base = base.copy();
		V2 v2 = base.asV2Validator();

		String global = vp.get(PrefConstants.global, ""); //$NON-NLS-1$
		if (global.length() > 0){
		Global g = new Global(global);
			base.setBuildValidation(g.isBuild());
			base.setManualValidation(g.isManual());
			base.setDelegatingId(g.getDelegating());
		}
		
		if (v2 != null){
			String groups = vp.get(PrefConstants.groups, ""); //$NON-NLS-1$
			if (groups.length() > 0){
				List<FilterGroup> list = new LinkedList<FilterGroup>();
				Deserializer des = new Deserializer(groups);
				while(des.hasNext())list.add(FilterGroup.create(des));
				v2.setGroups(list);
			}
			
			String settings = vp.get(PrefConstants.msgs, ""); //$NON-NLS-1$
			if (settings.length() >0)
			{
				Map<String, MessageSeveritySetting.Severity> map = Msgs.deserialize(settings);
				Map<String, MessageSeveritySetting> msg = base.getMessageSettings();
			
				for (Map.Entry<String, MessageSeveritySetting.Severity> me : map.entrySet()){
					MessageSeveritySetting ms = msg.get(me.getKey());
					if (ms != null)ms.setCurrent(me.getValue());
				}	
			}
		}					
		return base;
	}

	/**
	 * The only valid way to get the global preferences is through the ValManager.
	 * 
	 * @see ValManager#getGlobalPreferences()
	 */
	public GlobalPreferences loadGlobalPreferences() {
		PreferencesWrapper pref = PreferencesWrapper.getPreferences(null, null);
		GlobalPreferencesValues gp = new GlobalPreferencesValues();
		gp.saveAutomatically = pref.getBoolean(PrefConstants.saveAuto, GlobalPreferences.DefaultAutoSave);
		gp.disableAllValidation = pref.getBoolean(PrefConstants.suspend, GlobalPreferences.DefaultSuspend);
		gp.confirmDialog = pref.getBoolean(PrefConstants.confirmDialog, GlobalPreferences.DefaultConfirm);
		gp.override = pref.getBoolean(PrefConstants.override, GlobalPreferences.DefaultOverride);
		gp.version = pref.getInt(PrefConstants.frameworkVersion, GlobalPreferences.DefaultFrameworkVersion);
		gp.stateTimeStamp = pref.getLong(PrefConstants.stateTS, 0);
		
		if (gp.version != frameworkVersion)migrate(gp.version, pref);
		return new GlobalPreferences(gp);
	}
	
	/**
	 * If necessary migrate the preferences.
	 * @param version The incoming version of the preferences.
	 * @param pref the root of the preference store
	 */
	static void migrate(int version, PreferencesWrapper pref) {
		try {
			boolean update = false;
			if (version == 2){
				if (pref.nodeExists(PrefConstants.filters)){
					pref.node(PrefConstants.filters).removeNode();
					update = true;
				}
				if (pref.nodeExists(PrefConstants.msgs)){
					pref.node(PrefConstants.msgs).removeNode();
					update = true;
				}
			}
			if (update){
				pref.putInt(PrefConstants.frameworkVersion, frameworkVersion);
				pref.flush();
			}
		}
		catch (BackingStoreException e){
			ValidationPlugin.getPlugin().handleException(e);
		}		
	}

	/**
	 * Load the preferences for a validator.
	 * 
	 * @param v the validator that is being built up
	 * @param p the node in the preference tree for the validator, 
	 * 	e.g. /instance/validator-framework-id/filters/validator-id
	 * 
	 * @deprecated
	 */
//	static void loadPreferences(Validator v, Preferences p) throws BackingStoreException {
//		v.setBuildValidation(p.getBoolean(PrefConstants.build, true));
//		v.setManualValidation(p.getBoolean(PrefConstants.manual, true));
//		v.setVersion(p.getInt(PrefConstants.version, 1));
//		v.setDelegatingId(p.get(PrefConstants.delegate, null));
//		
//		Validator.V2 v2 = v.asV2Validator();
//		if (v2 == null)return;
//		if (!p.nodeExists(PrefConstants.groups))return;
//		
//		Preferences groupNode = p.node(PrefConstants.groups);
//		for (String groupName : groupNode.childrenNames()){
//			Preferences group = groupNode.node(groupName);
//			String type = group.get(PrefConstants.type, null);
//			if (type == null)throw new IllegalStateException(ValMessages.ErrGroupNoType);
//			FilterGroup fg = FilterGroup.create(type);
//			if (fg == null)throw new IllegalStateException(NLS.bind(ValMessages.ErrGroupInvalidType, type));
//			v2.add(fg);
//			
//			if (group.nodeExists(PrefConstants.rules)){
//				Preferences ruleNode = group.node(PrefConstants.rules);
//				for (String ruleName : ruleNode.childrenNames()){
//					Preferences rule = ruleNode.node(ruleName);
//					FilterRule fr = FilterRule.create(rule.get(PrefConstants.ruleType, null));
//					if (fr != null){
//						fr.load(rule);
//						fg.add(fr);
//					}
//				}
//			}
//		}		
//	}
	
	/**
	 * Save the validator into the preference store.
	 * 
	 * @param validator
	 *            The validator being saved.
	 * 
	 * @param root
	 *            The top of the preference tree for validators, i.e.
	 *            /instance/validator-framework-id/vals for workspace validators
	 *            and /vals for project validators.
	 *            
	 * @param baseValidators
	 *            A map of the validators that are one level higher in the
	 *            storage hierarchy. So if we are updating the preference page
	 *            validators, then this map would be the extension point
	 *            validators. If we are updating a project's validators, then
	 *            this map would be the preference page validators.
	 */
	static void save(Validator validator, PreferencesWrapper root, Map<String, Validator> baseValidators) throws BackingStoreException {
		Validator.V2 v2 = validator.asV2Validator();
		if (v2 == null)return;
		
		final String id = validator.getId();
		boolean hasNode = root.nodeExists(id);
		
		if (validator.sameConfig(baseValidators.get(id))){
			if (hasNode){
				PreferencesWrapper vp = root.node(id);
				vp.removeNode();
			}
			return;
		}
		if (!validator.isChanged())return;
		PreferencesWrapper vp = root.node(id);
		if (validator.hasGlobalChanges()){
			Global g = new Global(validator.isManualValidation(), validator.isBuildValidation(), validator.getVersion(),
				validator.getDelegatingId());
			vp.put(PrefConstants.global, g.serialize());
			Friend.setMigrated(validator, false);
		}
		
		if (validator.getChangeCountMessages() > 0){
			Collection<MessageSeveritySetting> msgs = validator.getMessageSettings().values();
			if (msgs.size() > 0){
				vp.put(PrefConstants.msgs, Msgs.serialize(msgs));
			}
		}
		
		if (v2.getChangeCountGroups() > 0){
			FilterGroup[] groups = v2.getGroups();
			if (groups.length > 0){
				Serializer ser = new Serializer(500);
				for (FilterGroup group : groups)group.save(ser);
				vp.put(PrefConstants.groups, ser.toString());
			}
		}
	}
	/**
	 * Save the validator into the preference store.
	 * 
	 * @param validator
	 *            The validator being saved.
	 * 
	 * @param root
	 *            The top of the preference tree for validators, i.e.
	 *            /instance/validator-framework-id/vals for workspace validators
	 *            and /vals for project validators.
	 *            
	 * @param baseValidators
	 *            A map of the validators that are one level higher in the
	 *            storage hierarchy. So if we are updating the preference page
	 *            validators, then this map would be the extension point
	 *            validators. If we are updating a project's validators, then
	 *            this map would be the preference page validators.
	 */
	static void save(ValidatorMutable validator, PreferencesWrapper root, Map<String, Validator> baseValidators) throws BackingStoreException {
		if (!validator.isV2Validator())return;
		
		PreferencesWrapper vp = root.node(validator.getId());
		if (validator.sameConfig(baseValidators.get(validator.getId()))){
			vp.removeNode();
			return;
		}
		if (!validator.isChanged())return;
		if (validator.hasGlobalChanges()){
			Global g = new Global(validator.isManualValidation(), validator.isBuildValidation(), validator.getVersion(),
				validator.getDelegatingId());
			vp.put(PrefConstants.global, g.serialize());
//			Friend.setMigrated(validator, false);
		}
				
		if (validator.getChangeCountGroups() > 0){
			FilterGroup[] groups = validator.getGroups();
			if (groups.length > 0){
				Serializer ser = new Serializer(500);
				for (FilterGroup group : groups)group.save(ser);
				vp.put(PrefConstants.groups, ser.toString());
			}
		}
		if (validator.getChangeCountMessages() > 0){
			Collection<MessageSeveritySetting> msgs = validator.getMessageSettings().values();
			if (msgs.size() > 0){
				vp.put(PrefConstants.msgs, Msgs.serialize(msgs));
			}
		}
		
	}
	
	public void saveAsPrefs(Validator[] val) {
		try {
			PreferencesWrapper pref = PreferencesWrapper.getPreferences(null, null);
			PreferencesWrapper vals = pref.node(PrefConstants.vals);
			Map<String, Validator> base = ExtensionValidators.instance().getMapV2();
			for (Validator v : val)save(v, vals, base);
			pref.flush();
			_validators.set(null);
			updateListeners(true);
		}
		catch (BackingStoreException e){
			throw new RuntimeException(e);
		}
	}

	
	/**
	 * Save the global preferences and the validators.
	 */
	public synchronized void savePreferences(GlobalPreferences gp, Validator[] validators){
		try {
			PreferencesWrapper prefs = PreferencesWrapper.getPreferences(null, null);
			savePreferences(prefs, gp);
			PreferencesWrapper vals = prefs.node(PrefConstants.vals);

			Map<String, Validator> base = ExtensionValidators.instance().getMapV2();
			for (Validator v : validators)save(v, vals, base);
			prefs.flush();
			_validators.set(null);
			updateListeners(true);
		}
		catch (BackingStoreException e){
			ValidationPlugin.getPlugin().handleException(e);
		}
	}
	
	/**
	 * Save the global preferences and the validators.
	 */
	public synchronized void savePreferences(GlobalPreferences gp, ValidatorMutable[] validators, Boolean persist){
		try {
			PreferencesWrapper prefs = PreferencesWrapper.getPreferences(null, persist);
			savePreferences(prefs, gp);
			PreferencesWrapper vals = prefs.node(PrefConstants.vals);
			Map<String, Validator> base = ExtensionValidators.instance().getMapV2();
			for (ValidatorMutable v : validators)save(v, vals, base);

			prefs.flush();
			_validators.set(null);
			updateListeners(true);
		}
		catch (BackingStoreException e){
			ValidationPlugin.getPlugin().handleException(e);
		}
	}
		
	/**
	 * Save the V1 preferences, so that the old validators continue to work.
	 */
	public static void saveV1Preferences(ValidatorMutable[] validators, Boolean persistent){
		try {
			GlobalConfiguration gc = ConfigurationManager.getManager().getGlobalConfiguration();
			gc.setEnabledManualValidators(getEnabledManualValidators(validators));				
			gc.setEnabledBuildValidators(getEnabledBuildValidators(validators));

			gc.passivate();
			gc.store(persistent);
		}
		catch (InvocationTargetException e){
			ValidationPlugin.getPlugin().handleException(e);
		}			
	}

	/**
	 * Answer all the V1 validators that are manually enabled.
	 * @return
	 */
	private static ValidatorMetaData[] getEnabledManualValidators(ValidatorMutable[] validators) {
		List<ValidatorMetaData> list = new LinkedList<ValidatorMetaData>();
		for (ValidatorMutable v : validators){
			if (v.isManualValidation() && v.isV1Validator())list.add(v.getVmd());
		}
		ValidatorMetaData[] result = new ValidatorMetaData[list.size()];
		list.toArray(result);
		return result;
	}

	/**
	 * Answer all the V1 validators that are enabled for build.
	 * @return
	 */
	private static ValidatorMetaData[] getEnabledBuildValidators(ValidatorMutable[] validators) {
		List<ValidatorMetaData> list = new LinkedList<ValidatorMetaData>();
		for (ValidatorMutable v : validators){
			if (v.isBuildValidation() && v.isV1Validator())list.add(v.getVmd());
		}
		ValidatorMetaData[] result = new ValidatorMetaData[list.size()];
		list.toArray(result);
		return result;
	}

	
	/**
	 * Save the global preferences and the validators.
	 */
	public synchronized void savePreferences(){
		try {
			GlobalPreferences gp = ValManager.getDefault().getGlobalPreferences();
			PreferencesWrapper prefs = PreferencesWrapper.getPreferences(null, null);
			savePreferences(prefs, gp);
			prefs.flush();
			updateListeners(true);
		}
		catch (BackingStoreException e){
			ValidationPlugin.getPlugin().handleException(e);
		}
	}
	
	/**
	 * Save the global preferences and the validators.
	 */
	private void savePreferences(PreferencesWrapper prefs, GlobalPreferences gp){
		prefs.putBoolean(PrefConstants.saveAuto, gp.getSaveAutomatically());
		prefs.putBoolean(PrefConstants.suspend, gp.getDisableAllValidation());
		prefs.putLong(PrefConstants.stateTS, gp.getStateTimeStamp());
		prefs.putBoolean(PrefConstants.confirmDialog, gp.getConfirmDialog());
		prefs.putBoolean(PrefConstants.override, gp.getOverride());
		prefs.putInt(PrefConstants.frameworkVersion, ValPrefManagerGlobal.frameworkVersion);
	}

	/**
	 * Update any message preferences in the map.
	 * @param validator
	 * @param settings
	 */
	public void loadMessages(Validator validator, Map<String, MessageSeveritySetting> settings) {
		PreferencesWrapper pref = PreferencesWrapper.getPreferences(null, null);
		try {
			loadMessageSettings(validator, settings, pref);
		}
		catch (BackingStoreException e){
			ValidationPlugin.getPlugin().handleException(e);
		}
	}
		
	/**
	 * Load the message preferences for the validator into the map.
	 * 
	 * @param val
	 * @param settings
	 * @param root the root of the preference store
	 */
	static void loadMessageSettings(Validator val, Map<String, MessageSeveritySetting> settings, PreferencesWrapper root) 
		throws BackingStoreException {
		if (!root.nodeExists(PrefConstants.vals))return;
		
		PreferencesWrapper vals = root.node(PrefConstants.vals); 
		if (!vals.nodeExists(val.getId()))return;
		
		PreferencesWrapper valPrefs = vals.node(val.getId());
		String msgs = valPrefs.get(PrefConstants.msgs, ""); //$NON-NLS-1$
		if (msgs.length() == 0)return;
		
		Map<String, MessageSeveritySetting.Severity> map = Msgs.deserialize(msgs);
		
		for (Map.Entry<String, MessageSeveritySetting.Severity> me : map.entrySet()){
			MessageSeveritySetting ms = settings.get(me.getKey());
			if (ms != null)ms.setCurrent(me.getValue());
		}		
	}

	/**
	 * Save whether the validator is enabled or not. 
	 * @param validator
	 * @param prefs up to the filter part of the preference tree
	 */
//	private void saveShallowPreference(Validator validator, Preferences prefs) {
//		if (validator.asV2Validator() == null)return;
//		Preferences val = prefs.node(validator.getId());
//		val.putBoolean(PrefConstants.build, validator.isBuildValidation());
//		val.putBoolean(PrefConstants.manual, validator.isManualValidation());
//		val.putInt(PrefConstants.version, validator.getVersion());
//	}
	
//	/**
//	 * Load the customized message settings from the preference store.
//	 * @param messageSettings
//	 */
//	public void loadMessageSettings(Validator val, MessageCategory[] messageSettings) {
//		try {
//			loadMessageSettings(val, messageSettings, ValidationFramework.getDefault().getPreferenceStore());
//		}
//		catch (Exception e){
//			ValidationPlugin.getPlugin().handleException(e);
//		}
//	}
	
	private final static class Global {
		private final boolean 	_manual;
		private final boolean 	_build;
		private final int		_version;
		private final String	_delegating;
		
		public Global(String value){
			Deserializer d = new Deserializer(value);
			_manual = d.getBoolean();
			_build = d.getBoolean();
			_version = d.getInt();
			_delegating = d.hasNext() ? d.getString() : null;
		}
		
		public Global(boolean manual, boolean build, int version, String delegating){
			_manual = manual;
			_build = build;
			_version = version;
			_delegating = delegating;
		}
		
		public String serialize(){
			Serializer s = new Serializer(50);
			s.put(_manual);
			s.put(_build);
			s.put(_version);
			if (_delegating != null)s.put(_delegating);
			return s.toString();
		}

		public boolean isManual() {
			return _manual;
		}

		public boolean isBuild() {
			return _build;
		}

		public String getDelegating() {
			return _delegating;
		}
	}
	
	private final static class Msgs {
		public static String serialize(Collection<MessageSeveritySetting> messages){
			Serializer s = new Serializer(100);
			for (MessageSeveritySetting ms : messages){
				s.put(ms.getId());
				s.put(ms.getCurrent().ordinal());
			}
			return s.toString();	
		}
		
		/**
		 * Answer a map for all the messages.
		 * The key is the message id and the value is the current setting for that message
		 * @param v
		 * @return
		 */
		public static Map<String, MessageSeveritySetting.Severity> deserialize(String v){
			Map<String, MessageSeveritySetting.Severity> map = new HashMap<String, MessageSeveritySetting.Severity>(10);
			Deserializer d = new Deserializer(v);
			while(d.hasNext()){
				String id = d.getString();
				int sev = d.getInt();
				map.put(id, MessageSeveritySetting.Severity.values()[sev]);
			}
			return map;
		}
	}
	
	/**
	 * Store the singleton for the ValPrefManagerGlobal. This approach is used to avoid having to synchronize the
	 * ValPrefManagerGlobal.getDefault() method.
	 * 
	 * @author karasiuk
	 *
	 */
	private final static class Singleton {
		final static ValPrefManagerGlobal valPrefManagerGlobal = new ValPrefManagerGlobal();
	}

}
