/*******************************************************************************
 * Copyright (c) 2006, 2015 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
 * Baltasar Belyavsky (Texas Instruments) - [405744] PropertyManager causes many unnecessary file-writes into workspace metadata
 *******************************************************************************/
package org.eclipse.cdt.managedbuilder.internal.core;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.eclipse.cdt.managedbuilder.core.IBuildObject;
import org.eclipse.cdt.managedbuilder.core.IBuilder;
import org.eclipse.cdt.managedbuilder.core.IConfiguration;
import org.eclipse.cdt.managedbuilder.core.IManagedBuildInfo;
import org.eclipse.cdt.managedbuilder.core.IManagedProject;
import org.eclipse.cdt.managedbuilder.core.IResourceConfiguration;
import org.eclipse.cdt.managedbuilder.core.IResourceInfo;
import org.eclipse.cdt.managedbuilder.core.ITool;
import org.eclipse.cdt.managedbuilder.core.IToolChain;
import org.eclipse.cdt.managedbuilder.core.ManagedBuildManager;
import org.eclipse.cdt.managedbuilder.core.ManagedBuilderCorePlugin;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

/**
 * This class allows specifying BuildObject-specific persisted properties
 *
 */
public class PropertyManager {
	//	private static final String PROPS_PROPERTY = "properties";	//$NON-NLS-1$
	//	private static final QualifiedName propsSessionProperty = new QualifiedName(ManagedBuilderCorePlugin.getUniqueIdentifier(), PROPS_PROPERTY);

	private static final String NODE_NAME = "properties"; //$NON-NLS-1$

	private static PropertyManager fInstance;

	private LoaddedInfo fLoaddedInfo;

	private static class LoaddedInfo {
		private final IProject fProject;
		private final String fCfgId;
		// one of Map<String, String> or Map<String, Map<String, Properties>>
		private final Map<String, Object> fCfgPropertyMap;

		LoaddedInfo(IProject project, String cfgId, Map<String, Object> cfgPropertyMap) {
			fProject = project;
			fCfgId = cfgId;
			fCfgPropertyMap = cfgPropertyMap;
		}

		public IConfiguration getConfiguration() {
			return PropertyManager.getConfigurationFromId(fProject, fCfgId);
		}

		public IProject getProject() {
			return fProject;
		}

		public String getConfigurationId() {
			return fCfgId;
		}

		public Map<String, Object> getProperties() {
			return fCfgPropertyMap;
		}

		public boolean cfgMatch(IConfiguration cfg) {
			if (fCfgId == null || fProject == null)
				return false;

			if (!fCfgId.equals(cfg.getId()))
				return false;

			if (!fProject.equals(PropertyManager.getProject(cfg)))
				return false;

			return true;
		}
	}

	private PropertyManager() {
	}

	public static PropertyManager getInstance() {
		if (fInstance == null)
			fInstance = new PropertyManager();
		return fInstance;
	}

	protected void setProperty(IConfiguration cfg, IBuildObject bo, String prop, String value) {
		if (((Configuration) cfg).isPreference())
			return;
		Properties props = getProperties(cfg, bo);
		if (props != null) {
			props.setProperty(prop, value);
		}
	}

	protected String getProperty(IConfiguration cfg, IBuildObject bo, String prop) {
		if (((Configuration) cfg).isPreference())
			return null;
		Properties props = getProperties(cfg, bo);
		if (props != null)
			return props.getProperty(prop);
		return null;
	}

	protected Properties getProperties(IConfiguration cfg, IBuildObject bo) {
		return loadProperties(cfg, bo);
	}

	private LoaddedInfo getLoaddedInfo() {
		return fLoaddedInfo;
	}

	private synchronized void setLoaddedInfo(LoaddedInfo info) {
		fLoaddedInfo = info;
	}

	protected Map<String, Object> getLoaddedData(IConfiguration cfg) {
		LoaddedInfo info = getLoaddedInfo();
		if (info == null)
			return null;

		if (!info.cfgMatch(cfg))
			return null;

		return info.getProperties();
		//		Map map = null;
		//		IProject proj = null;
		//		try {
		//			if(!((Configuration)cfg).isPreference()){
		//				proj = cfg.getOwner().getProject();
		//				map = (Map)proj.getSessionProperty(propsSessionProperty);
		//			}
		//			if(map == null){
		//				map = new HashMap();
		//				if(proj != null){
		//					proj.setSessionProperty(propsSessionProperty, map);
		//				}
		//			}
		//			map = (Map)map.get(cfg.getId());
		//		} catch (CoreException e) {
		//		}
		//		return map;
	}

	protected synchronized void clearLoaddedData(IConfiguration cfg) {
		if (((Configuration) cfg).isPreference())
			return;

		LoaddedInfo info = getLoaddedInfo();
		if (info == null)
			return;

		if (info.cfgMatch(cfg))
			setLoaddedInfo(null);
		//		IProject proj = cfg.getOwner().getProject();
		//		try {
		//			proj.setSessionProperty(propsSessionProperty, null);
		//		} catch (CoreException e) {
		//		}
	}

	private static IProject getProject(IConfiguration cfg) {
		IResource rc = cfg.getOwner();
		return rc != null ? rc.getProject() : null;
	}

	protected Properties loadProperties(IConfiguration cfg, IBuildObject bo) {
		Map<String, Object> map = getData(cfg);

		return getPropsFromData(map, bo);
	}

	protected Properties getPropsFromData(Map<String, Object> data, IBuildObject bo) {
		synchronized (data) {
			Object oVal = data.get(bo.getId());
			Properties props = null;
			if (oVal instanceof String) {
				props = stringToProps((String) oVal);
				data.put(bo.getId(), props);
			} else if (oVal instanceof Properties) {
				props = (Properties) oVal;
			}

			if (props == null) {
				props = new Properties();
				data.put(bo.getId(), props);
			}

			return props;
		}
	}

	protected void storeData(IConfiguration cfg) {
		Map<String, Object> map = getLoaddedData(cfg);

		if (map != null)
			storeData(cfg, map);
	}

	protected Properties mapToProps(Map<String, Object> map) {
		Properties props = null;
		if (map != null) {
			synchronized (map) {
				if (map.size() > 0) {
					props = new Properties();
					Set<Entry<String, Object>> entrySet = map.entrySet();
					for (Entry<String, Object> entry : entrySet) {
						String key = entry.getKey();
						String value = null;
						Object oVal = entry.getValue();
						if (oVal instanceof Properties) {
							value = propsToString((Properties) oVal);
						} else if (oVal instanceof String) {
							value = (String) oVal;
						}

						if (key != null && value != null)
							props.setProperty(key, value);
					}
				}
			}
		}

		return props;
	}

	protected String propsToString(Properties props) {
		if (props == null || props.size() == 0)
			return null;

		ByteArrayOutputStream stream = new ByteArrayOutputStream();
		try {
			props.store(stream, null);
		} catch (IOException e1) {
		}

		byte[] bytes = stream.toByteArray();

		String value = null;
		try {
			value = new String(bytes, "UTF-8"); //$NON-NLS-1$
		} catch (UnsupportedEncodingException e) {
			value = new String(bytes);
		}

		/* FIX for Bug 405744: Properties.store() always starts the serialzed string with
		 * a timestamp comment. That constantly changing comment causes the preference-store
		 * to perform many unnecessary file-writes into the workspace metadata, even when
		 * the properties don't change. The comment is ignored by Properties.load(), so
		 * just remove it here.
		 */
		String sep = System.getProperty("line.separator"); //$NON-NLS-1$
		while (value.charAt(0) == '#') {
			value = value.substring(value.indexOf(sep) + sep.length());
		}

		return value;
	}

	protected Properties stringToProps(String str) {
		Properties props = null;
		if (str != null) {
			props = new Properties();
			byte[] bytes;
			try {
				bytes = str.getBytes("UTF-8"); //$NON-NLS-1$
			} catch (UnsupportedEncodingException e) {
				bytes = str.getBytes();
			}

			ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
			try {
				props.load(stream);
			} catch (IOException e) {
				props = null;
			}
		}
		return props;
	}

	protected void storeData(IConfiguration cfg, Map<String, Object> map) {
		String str = null;
		Properties props = mapToProps(map);

		str = propsToString(props);

		storeString(cfg, str);
	}

	protected void storeString(IConfiguration cfg, String str) {
		Preferences prefs = getNode(cfg.getManagedProject());
		if (prefs != null) {
			if (str != null)
				prefs.put(cfg.getId(), str);
			else
				prefs.remove(cfg.getId());
			try {
				prefs.flush();
			} catch (BackingStoreException e) {
			}
		}
	}

	protected String loadString(IConfiguration cfg) {
		String str = null;
		Preferences prefs = getNode(cfg.getManagedProject());
		if (prefs != null)
			str = prefs.get(cfg.getId(), null);
		return str;
	}

	protected Preferences getNode(IManagedProject mProject) {
		//		return getProjNode(mProject);
		return getInstNode(mProject);
	}

	protected Preferences getProjNode(IManagedProject mProject) {
		IProject project = mProject.getOwner().getProject();
		if (project == null || !project.exists() || !project.isOpen())
			return null;

		Preferences prefs = new ProjectScope(project).getNode(ManagedBuilderCorePlugin.getUniqueIdentifier());
		if (prefs != null)
			return prefs.node(NODE_NAME);
		return null;
	}

	protected Preferences getInstNode(IManagedProject mProject) {
		Preferences prefs = InstanceScope.INSTANCE.getNode(ManagedBuilderCorePlugin.getUniqueIdentifier());
		if (prefs != null) {
			prefs = prefs.node(NODE_NAME);
			if (prefs != null)
				prefs = prefs.node(mProject.getId());
		}
		return prefs;
	}

	protected Map<String, Object> getData(IConfiguration cfg) {
		Map<String, Object> map = getLoaddedData(cfg);

		if (map == null) {
			map = loadData(cfg);

			setLoaddedData(cfg, map);
		}

		return map;
	}

	protected Map<String, Object> loadData(IConfiguration cfg) {
		Map<String, Object> map = null;
		String str = loadString(cfg);

		Properties props = stringToProps(str);

		map = propsToMap(props);

		if (map == null)
			map = new LinkedHashMap<>();

		return map;
	}

	protected Map<String, Object> propsToMap(Properties props) {
		if (props != null) {
			@SuppressWarnings({ "rawtypes", "unchecked" })
			Map<String, Object> map = new LinkedHashMap(props);
			return map;
		}
		return null;
	}

	private static IConfiguration getConfigurationFromId(IProject project, String id) {
		if (project == null || id == null)
			return null;
		IManagedBuildInfo bInfo = ManagedBuildManager.getBuildInfo(project, false);
		IConfiguration cfg = null;
		if (bInfo != null) {
			IManagedProject mProj = bInfo.getManagedProject();
			if (mProj != null) {
				cfg = mProj.getConfiguration(id);
			}
		}

		return cfg;
	}

	protected void setLoaddedData(IConfiguration cfg, Map<String, Object> data) {
		if (cfg.getOwner() == null)
			return;

		LoaddedInfo info = getLoaddedInfo();

		if (info != null) {
			if (info.cfgMatch(cfg)) {
				info = new LoaddedInfo(info.getProject(), info.getConfigurationId(), data);
				setLoaddedInfo(info);
				return;
			}

			IConfiguration oldCfg = info.getConfiguration();
			if (oldCfg != null) {
				storeData(oldCfg, info.getProperties());
			}
		}

		IProject proj = cfg.getOwner().getProject();
		info = new LoaddedInfo(proj, cfg.getId(), data);
		setLoaddedInfo(info);
	}

	public void setProperty(IConfiguration cfg, String key, String value) {
		setProperty(cfg, cfg, key, value);
	}

	public void setProperty(IResourceInfo rcInfo, String key, String value) {
		setProperty(rcInfo.getParent(), rcInfo, key, value);
	}

	public void setProperty(IToolChain tc, String key, String value) {
		setProperty(tc.getParent(), tc, key, value);
	}

	public void setProperty(ITool tool, String key, String value) {
		Configuration cfg = (Configuration) getConfiguration(tool);
		if (cfg.isPreference())
			return;
		setProperty(cfg, tool, key, value);
	}

	public void setProperty(IBuilder builder, String key, String value) {
		setProperty(getConfiguration(builder), builder, key, value);
	}

	public String getProperty(IConfiguration cfg, String key) {
		return getProperty(cfg, cfg, key);
	}

	public String getProperty(IResourceInfo rcInfo, String key) {
		return getProperty(rcInfo.getParent(), rcInfo, key);
	}

	public String getProperty(IToolChain tc, String key) {
		return getProperty(tc.getParent(), tc, key);
	}

	public String getProperty(ITool tool, String key) {
		return getProperty(getConfiguration(tool), tool, key);
	}

	public String getProperty(IBuilder builder, String key) {
		return getProperty(getConfiguration(builder), builder, key);
	}

	public void clearProperties(IManagedProject mProject) {
		if (mProject == null)
			return;

		IConfiguration cfgs[] = mProject.getConfigurations();
		for (int i = 0; i < cfgs.length; i++)
			clearLoaddedData(cfgs[i]);

		Preferences prefs = getNode(mProject);
		if (prefs != null) {
			try {
				Preferences parent = prefs.parent();
				prefs.removeNode();
				if (parent != null)
					parent.flush();
			} catch (BackingStoreException e) {
			}
		}
	}

	public void clearProperties(IConfiguration cfg) {
		if (cfg.getOwner() == null)
			return;

		clearLoaddedData(cfg);
		storeData(cfg, null);
	}

	private IConfiguration getConfiguration(IBuilder builder) {
		IToolChain tc = builder.getParent();
		if (tc != null)
			return tc.getParent();
		return null;
	}

	private IConfiguration getConfiguration(ITool tool) {
		IBuildObject p = tool.getParent();
		IConfiguration cfg = null;
		if (p instanceof IToolChain) {
			cfg = ((IToolChain) p).getParent();
		} else if (p instanceof IResourceConfiguration) {
			cfg = ((IResourceConfiguration) p).getParent();
		}
		return cfg;
	}

	public void serialize(IConfiguration cfg) {
		if (cfg.isTemporary() || cfg.getOwner() == null)
			return;

		storeData(cfg);
	}

	public void serialize() {
		LoaddedInfo info = getLoaddedInfo();
		IConfiguration cfg = info.getConfiguration();
		if (cfg != null) {
			serialize(cfg);

			clearLoaddedData(cfg);
		}
		//		IProject projects[] = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		//		for(int i = 0; i < projects.length; i++){
		//			IManagedBuildInfo info = ManagedBuildManager.getBuildInfo(projects[i], false);
		//			if(info != null && info.isValid() && info.getManagedProject() != null){
		//				IConfiguration cfgs[] = info.getManagedProject().getConfigurations();
		//				for(int j = 0; j < cfgs.length; j++){
		//					serialize(cfgs[j]);
		//				}
		//			}
		//		}
	}

}
