/*******************************************************************************
 * Copyright (c) 2007 Exadel, Inc. and Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is 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:
 *     Exadel, Inc. and Red Hat, Inc. - initial API and implementation
 ******************************************************************************/ 
package org.jboss.tools.common.resref.core;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

public abstract class ResourceReferenceList {
	
	//Fix for JBIDE-2979
	private static final String CODING = "UTF-16";//$NON-NLS-1$
	
	ResourceReferenceListListener[] listeners = new ResourceReferenceListListener[0];
	
	protected abstract QualifiedName getPropertyName();
	
	public void addChangeListener(ResourceReferenceListListener listener) {
		int i = getListenerIndex(listener);
		if(i >= 0) return;
		ResourceReferenceListListener[] ls = new ResourceReferenceListListener[listeners.length + 1];
		System.arraycopy(listeners, 0, ls, 0, listeners.length);
		ls[listeners.length] = listener;
		listeners = ls;
	}
	
	public void removeChangeListener(ResourceReferenceListListener listener) {
		int i = getListenerIndex(listener);
		if(i < 0) return;
		ResourceReferenceListListener[] ls = new ResourceReferenceListListener[listeners.length - 1];
		if(i > 0) System.arraycopy(listeners, 0, ls, 0, i);
		if(i < ls.length) System.arraycopy(listeners, i + 1, ls, i, ls.length - i);
		listeners = ls;
	}

	private int getListenerIndex(ResourceReferenceListListener listener) {
		for (int i = 0; i < listeners.length; i++) {
			if(listeners[i] == listener) return i;
		}
		return -1;
	}
	
	/**
	 * Returns all resources for current file
	 * @param file
	 * @return <code>ResourceReference</code> array that might be empty, never returns <code>null</code>
	 */
	public ResourceReference[] getAllResources(IFile file) {
		Set<String> locations = new HashSet<String>();
		List<ResourceReference> css = new ArrayList<ResourceReference>();
		
		if(file.getProject() != null) {
			String[] dcss = getDeclaredResources(file.getProject());
			for (int i = 0; i < dcss.length; i++) {
				ResourceReference ref = new ResourceReference(dcss[i], ResourceReference.PROJECT_SCOPE);
				locations.add(dcss[i]);
				css.add(ref);
			}
		}
		IResource parent = file.getParent();
		int depth = 0;
		while(parent instanceof IFolder) {
			String[] dcss = getDeclaredResources(parent);
			for (int i = 0; i < dcss.length; i++) {
				if(locations.contains(dcss[i])) continue;
				ResourceReference ref = new ResourceReference(dcss[i], ResourceReference.FOLDER_SCOPE);
				ref.setDepth(depth);
				locations.add(dcss[i]);
				css.add(ref);
			}
			parent = parent.getParent();
			depth++;
		}
		String[] dcss = getDeclaredResources(file);
		for (int i = 0; i < dcss.length; i++) {
			if(locations.contains(dcss[i])) continue;
			ResourceReference ref = new ResourceReference(dcss[i], ResourceReference.FILE_SCOPE);
			locations.add(dcss[i]);
			css.add(ref);
		}
		return css.toArray(new ResourceReference[css.size()]);		
	}
	
	private String[] getDeclaredResources(IResource resource) {
	    /*
	     * Property value
	     */
	    String s = null;
	    /*
	     * https://jira.jboss.org/jira/browse/JBIDE-3211
	     * Reading preferences from project scope from .settings
	     */
	    IScopeContext projectScope = new ProjectScope(resource.getProject());
	    IEclipsePreferences root = projectScope
	    	.getNode(ResourceReferencePlugin.PLUGIN_ID);
	    if (root != null) {
		/*
		 * Create subnode for file/folder/project
		 */
		IPath path = resource.getProjectRelativePath();
		String nodeString = path.toString();
		Preferences node = root.node(nodeString);
		if (null != node) {
		    s = node.get(getPropertyName().getLocalName(), ""); //$NON-NLS-1$
		}
	    }
		if (s == null || s.length() == 0) {
			/*
			 * Old preferences format property value.
			 */
			String old = null;
			try {
				old = resource.getPersistentProperty(getPropertyName());
			} catch (CoreException e) {
				/*
				 * Ignore, there is no properties for this resource.
				 */
			}
			if (old == null || old.length() == 0) {
				return new String[0];
			} else {
				/*
				 * If there is property stored in the old preferences format
				 * return this value.
				 */
				return decodeResourceString(old);
			}
		} else {
			return decodeResourceString(s);
		}
	}
	
	//Fix for JBIDE-2979
	private static String[] decodeResourceString(String resource) {
		String[] results = resource.split("[,;]");
		String[] returnValues = new String [results.length]; 
		try {
			for (int i=0;i<results.length;i++) {	
				returnValues[i] = URLDecoder.decode(results[i].trim(), CODING);	
			}
		} catch (UnsupportedEncodingException e) {
			ResourceReferencePlugin.getPluginLog().logError(e);
			//in case of exception we return undecoded values
			return results;
		}
		return returnValues;
	}
	
	public void setAllResources(IFile file, ResourceReference[] entries) {
	    	IResource changed = null;
		boolean b = setDeclaredResources(file, entries, ResourceReference.FILE_SCOPE, 0);
		if(b) changed = file;
		IResource parent = file.getParent();
		int depth = 0;
		while(parent instanceof IFolder) {
			b = setDeclaredResources(parent, entries, ResourceReference.FOLDER_SCOPE, depth);
			if(b) {
			    changed = parent;
			}
			parent = parent.getParent();
			depth++;
		}
		if(file.getProject() != null) {
			int scope = ResourceReference.PROJECT_SCOPE;
			if(file.getParent() == file.getProject()) scope = 10;
			b = setDeclaredResources(file.getProject(), entries, scope, 0);
			if(b) {
			    changed = file.getProject();
			}
		}
		if(changed != null) fire(changed.getFullPath());
	}

	private boolean setDeclaredResources(IResource resource, ResourceReference[] entries, int scope, int depth) {
	    /*
	     * https://jira.jboss.org/jira/browse/JBIDE-3211
	     * Storing project preferences to project scope in .settings
	     */
	    IScopeContext projectScope = new ProjectScope(resource.getProject());
	    IEclipsePreferences root = projectScope
	    	.getNode(ResourceReferencePlugin.PLUGIN_ID);
	    if (root != null) {
		/*
		 * Get subnode for file/folder/project
		 */
		IPath path = resource.getProjectRelativePath();
		String nodeString = path.toString();
		Preferences node = root.node(nodeString);
		String oldValue = node.get(getPropertyName().getLocalName(), ""); //$NON-NLS-1$
		String newValue = encodeDeclaredResources(entries, scope, depth);
		if (oldValue.equals(newValue)) {
		    return false;
		}
		node.put(getPropertyName().getLocalName(), newValue);
		try {
		    root.flush();
		} catch (BackingStoreException e) {
		    ResourceReferencePlugin.getPluginLog().logError(e);
		}
	    }
	    return true;
	}
	
	private String encodeDeclaredResources(ResourceReference[] entries, int scope, int depth) {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < entries.length; i++) {
			int s = entries[i].getScope();
			if(scope < 10 && s != scope) continue;
			if(scope == 10 && s == ResourceReference.FILE_SCOPE) continue;
			if(scope == ResourceReference.FOLDER_SCOPE && entries[i].getDepth() != depth) continue;
			if(sb.length() > 0) 
				sb.append(';');
			try {
				sb.append(URLEncoder.encode(entries[i].getLocationAndProperties(),CODING));
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return sb.toString();
	}
	
	void fire(IPath path) {
		ResourceReferenceListListener[] ls = listeners;
		for (int i = 0; i < ls.length; i++) {
			IPath listenedPath = ls[i].getPath();
			if(listenedPath != null && path.isPrefixOf(listenedPath)) {
				ls[i].changed(this);
			}
		}
	}
	
	/*
	 * Handle opened external files
	 */
	TreeMap allExternalResources = null;
	
	private TreeMap getAllExternalResources() {
		if (allExternalResources == null) {
			allExternalResources = new TreeMap();
			/*
			 * Property value
			 */
			String s = null;
			/*
			 * https://jira.jboss.org/jira/browse/JBIDE-3211 
			 * Reading global preferences from instance scope.
			 */
			Preferences root = InstanceScope.INSTANCE.getNode(ResourceReferencePlugin.PREFERENCES_QUALIFIER);
			if (null != root) {
				Preferences node = root.node(ResourceReferencePlugin.PLUGIN_ID);
				s = node.get(getPropertyName().getLocalName(), ""); //$NON-NLS-1$
			}
			/*
			 * If there is property stored in the old preferences format and
			 * there are no other properties old value will be returned
			 */
			if (s != null && s.length() > 0) {
				parseExternalResources(s);
			} else {
				/*
				 * Old preferences format property value.
				 */
				String old = null;
				try {
					old =ResourcesPlugin.getWorkspace().getRoot()
							.getPersistentProperty(getPropertyName());
				} catch (CoreException e) {
					/*
					 * Ignore, there is no properties for this resource.
					 */
				}
				if (old != null && old.length() > 0) {
					parseExternalResources(old);
				}
			}
		}
		return allExternalResources;
	}
	
	private void parseExternalResources(String s) {
		StringTokenizer st = new StringTokenizer(s, "#"); //$NON-NLS-1$
		while(st.hasMoreTokens()) {
			String t = st.nextToken();
			int e = t.indexOf('=');
			String path = t.substring(0, e);
			String list = t.substring(e + 1);
			if(new File(path).exists()) {
			    allExternalResources.put(path, list);
			}
		}
	}

	private void setAllExternalResources() {
		StringBuffer sb = new StringBuffer();
		Iterator it = allExternalResources.keySet().iterator();
		while (it.hasNext()) {
			String path = it.next().toString();
			String list = (String) allExternalResources.get(path);
			if (path != null && list != null && new File(path).exists()) {
				if (sb.length() > 0)
					sb.append('#');
				sb.append(path).append('=').append(list);
			}
		}
		/*
		 * https://jira.jboss.org/jira/browse/JBIDE-3211 
		 * Storing global preferences to instance scope to
		 * ${workspace}\.metadata\.plugins\org.eclipse.core.runtime\
		 * .settings\org.jboss.tools.common.model.prefs
		 */
		
		Preferences root = InstanceScope.INSTANCE.getNode(ResourceReferencePlugin.PREFERENCES_QUALIFIER);
		if (null != root) {
			Preferences node = root.node(ResourceReferencePlugin.PLUGIN_ID);
			node.put(getPropertyName().getLocalName(), sb.toString());
			try {
				root.flush();
			} catch (BackingStoreException e) {
				ResourceReferencePlugin.getPluginLog().logError(e);
			}
		}
	}
	
	/**
	 * 
	 * @param path
	 * @return <code>ResourceReference</code> array that might be empty, never returns <code>null</code>
	 */
	public ResourceReference[] getAllResources(IPath path) {
		Set locations = new HashSet();
		List css = new ArrayList();
		IPath parent = path.removeLastSegments(1);
		int depth = 0;
		boolean isGlobal = path.equals(Platform.getLocation());
		int setScope = isGlobal ? ResourceReference.GLOBAL_SCOPE : ResourceReference.FILE_SCOPE;
		while(parent != null && parent.segmentCount() > 1) {
			String[] dcss = getDeclaredResources(path);
			for (int i = 0; i < dcss.length; i++) {
				if(locations.contains(dcss[i])) continue;
				ResourceReference ref = new ResourceReference(dcss[i],isGlobal ? ResourceReference.GLOBAL_SCOPE :  ResourceReference.FOLDER_SCOPE);
				if(isGlobal){
				    ref.setGlobal(true);
				}
				ref.setDepth(depth);
				locations.add(dcss[i]);
				css.add(ref);
			}
			parent = parent.removeLastSegments(1);
			depth++;
		}
		String[] dcss = getDeclaredResources(path);
		for (int i = 0; i < dcss.length; i++) {
			if(locations.contains(dcss[i])) continue;
			ResourceReference ref = new ResourceReference(dcss[i], setScope);
			if(setScope == ResourceReference.GLOBAL_SCOPE){
			    ref.setGlobal(true);
			}
			locations.add(dcss[i]);
			css.add(ref);
		}
		return (ResourceReference[])css.toArray(new ResourceReference[css.size()]);
	}

	private String[] getDeclaredResources(IPath path) {
	    String s = (String)getAllExternalResources().get(path.toString());
		if(s == null || s.length() == 0) {
			return new String[0];
		}else {
			return	decodeResourceString(s);
		}
	}

	public void setAllResources(IPath path, ResourceReference[] entries) {
		IPath changed = null;
		boolean b = false;
		int checkScope = path.equals(Platform.getLocation()) ? ResourceReference.GLOBAL_SCOPE : ResourceReference.FILE_SCOPE;

		b = setDeclaredResources(path, entries, checkScope, 0);
		if(b) {
			changed = path;
		}
		IPath parent = path.removeLastSegments(1);
		int depth = 0;
		while(parent != null && parent.segmentCount() > 1) {
			b = setDeclaredResources(parent, entries, ResourceReference.FOLDER_SCOPE, depth);
			if(b) {
				changed = parent;
			}
			parent = parent.removeLastSegments(1);
			depth++;
		}
		if(changed != null) {
			setAllExternalResources();
			fire(changed);
		}
	}

	private boolean setDeclaredResources(IPath path, ResourceReference[] entries, int scope, int depth) {
	    		String oldValue = (String)getAllExternalResources().get(path.toString());
			if(oldValue == null) {
			    oldValue = ""; //$NON-NLS-1$
			}
			String newValue = encodeDeclaredResources(entries, scope, depth);
			if(oldValue.equals(newValue)) return false;
			if(newValue == null || newValue.length() == 0) {
				getAllExternalResources().remove(path.toString());
			} else {
				getAllExternalResources().put(path.toString(), newValue);
			}
		return true;
	}

}
