/*******************************************************************************
 * Copyright (c) 2007-2012 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.jst.jsp.bundle;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.jboss.tools.common.el.core.model.ELArgumentInvocation;
import org.jboss.tools.common.el.core.model.ELExpression;
import org.jboss.tools.common.el.core.model.ELInstance;
import org.jboss.tools.common.el.core.model.ELInvocationExpression;
import org.jboss.tools.common.el.core.model.ELModel;
import org.jboss.tools.common.el.core.model.ELPropertyInvocation;
import org.jboss.tools.common.el.core.parser.ELParser;
import org.jboss.tools.common.el.core.parser.ELParserUtil;
import org.jboss.tools.common.model.XModel;
import org.jboss.tools.common.model.event.XModelTreeEvent;
import org.jboss.tools.common.model.event.XModelTreeListener;
import org.jboss.tools.common.model.options.PreferenceModelUtilities;
import org.jboss.tools.common.model.project.IModelNature;
import org.jboss.tools.common.model.util.EclipseResourceUtil;
import org.jboss.tools.jst.jsp.JspEditorPlugin;
import org.jboss.tools.jst.jsp.i18n.MainLocaleProvider;
import org.jboss.tools.jst.jsp.preferences.IVpePreferencesPage;
import org.jboss.tools.jst.jsp.util.Constants;
import org.jboss.tools.jst.web.project.WebProject;
import org.jboss.tools.jst.web.project.list.WebPromptingProvider;

public class BundleMap {

	public static final String TITLE_ATTRIBUTE_NAME = "title"; //$NON-NLS-1$
	private static final String[] JSF_PROJECT_NATURES = {
		WebProject.JSF_NATURE_ID
	};
		
	private BundleMapListener[] bundleMapListeners = new BundleMapListener[0];
	private String[] javaSources;
    /*
     * Stores the current VPE locale.
     */
	private Locale locale;
	private BundleEntry[] bundles = new BundleEntry[0];
	private Map<String,UsedKey> usedKeys = new HashMap<String,UsedKey>();
    
    boolean showBundleUsageAsEL = JspEditorPlugin.getDefault().getPreferenceStore().getBoolean(
			IVpePreferencesPage.SHOW_RESOURCE_BUNDLES_USAGE_AS_EL); 

	XModelTreeListener modelListener = new ML();
	private IProject project;
	
	public void init(IEditorInput input){
		if (input instanceof IFileEditorInput) {
			project=((IFileEditorInput)input).getFile().getProject();
		}
		init(project);
	}
	
	public void init(IProject project){
		if (project!=null) {
			javaSources = getJavaProjectSrcLocations(project);
		}
		/*
		 * Initialize the locale with default value.
		 */
		locale = MainLocaleProvider.getInstance().getLocale(project);
		refreshRegisteredBundles();
		PreferenceModelUtilities.getPreferenceModel().addModelTreeListener(modelListener);
	}
	
	public void refreshRegisteredBundles() {
		if (hasJsfProjectNatureType()
				&& project!=null) {
			IModelNature modelNature = EclipseResourceUtil
					.getModelNature(project);
			if (modelNature != null) {
				XModel model = modelNature.getModel();
				List<Object> l = WebPromptingProvider.getInstance().getList(
						model, WebPromptingProvider.JSF_REGISTERED_BUNDLES,
						null, null);
				if (l != null && l.size() > 1 && (l.get(1) instanceof Map)) {
					Map<?, ?> map = (Map<?, ?>) l.get(1);
					/*
					 * Fix for https://jira.jboss.org/jira/browse/JBIDE-5218
					 * When updating f:view's locale attribute right after
					 * template creation - map of registered bundles is empty
					 * and couldn't be updated. To change bundle's locale they
					 * should be accessed through <code>bundles</code> variable.
					 */
					if (map.keySet().size() > 0) {
						Iterator<?> it = map.keySet().iterator();
						while (it.hasNext()) {
							String uri = it.next().toString();
							String prefix = map.get(uri).toString();
							int hash = (prefix + ":" + uri).hashCode(); //$NON-NLS-1$
							removeBundle(hash);
							addBundle(hash, prefix, uri, true);
						}
					} else if (bundles.length > 0) {
						for (int i = 0; i < bundles.length; i++) {
							String uri = bundles[i].uri;
							String prefix = bundles[i].prefix;
							int hash = (prefix + ":" + uri).hashCode(); //$NON-NLS-1$
							removeBundle(hash);
							addBundle(hash, prefix, uri, true);
						}
					}
				}
			}
		}
	}
	
	public void clearAll() {
		bundles = new BundleEntry[0];
	    usedKeys = new HashMap<String,UsedKey>();
	}
	
	public void dispose() {
		PreferenceModelUtilities.getPreferenceModel().removeModelTreeListener(modelListener);
	}	

	public boolean isShowBundleUsageAsEL() {
		return showBundleUsageAsEL;
	}

	private boolean hasJsfProjectNatureType() {
		boolean hasJsfProjectNatureType = false;
		try {
			if(project!=null) {
				if (project.exists() && project.isOpen()) {
					for (int i = 0; i < JSF_PROJECT_NATURES.length; i++) {
						if (project.hasNature(JSF_PROJECT_NATURES[i])){ 
							hasJsfProjectNatureType = true;
						break;
						}
					}
				}
			}
		} catch (CoreException e) {
			JspEditorPlugin.getPluginLog().logError(e);
		}
		return hasJsfProjectNatureType;
	}
	
	public boolean openBundle(String expression, String locale){
		List<ELInstance> is = parseJSFExpression(expression);
		if(is == null || is.size() == 0) return false;
		String prefix = null;
		String propertyName = null;
		for (ELInstance i: is) {
			ELExpression expr = i.getExpression();
			if(expr == null) continue;
			List<ELInvocationExpression> invs = expr.getInvocations();
			if(invs.size() > 0) {
				String[] values = getCall(invs.get(0));
				if(values != null) {
					prefix = values[0];
					propertyName = values[1];
					break;
				}
			}
		}
		if(prefix == null) return false;

		BundleEntry entry = getBundle(prefix);
		
		if(entry == null){
			if (hasJsfProjectNatureType()) {
				if (project!=null) {
					XModel model = EclipseResourceUtil.getModelNature(project).getModel();
					String prefix2 = prefix;
					if(propertyName != null && prefix != null) {
						prefix2 = prefix + "." + propertyName; //$NON-NLS-1$
					}
					WebPromptingProvider.getInstance().getList(model, WebPromptingProvider.JSF_BEAN_OPEN, prefix2, null);
				}
			}
			return false;
		}

		if (hasJsfProjectNatureType()) {
			Properties p = new Properties();
			p.put(WebPromptingProvider.BUNDLE, entry.uri);
			p.put(WebPromptingProvider.KEY, propertyName);
			if (locale != null) p.put(WebPromptingProvider.LOCALE, locale);
			p.put(WebPromptingProvider.FILE, project);
			String error = null;
			if (project!=null) {
				XModel model = EclipseResourceUtil.getModelNature(project).getModel();
				WebPromptingProvider.getInstance().getList(model, WebPromptingProvider.JSF_OPEN_KEY, entry.uri, p);
				error = p.getProperty(WebPromptingProvider.ERROR); 
			}
			return (error == null || error.length() == 0);
		}
		return false;
	}

	/**
	 * Gets the bundle file based on the locale 
	 * from the loaded resource bundle.
	 *
	 * @param uri the uri
	 * @return the bundle file
	 */
	public IFile getBundleFile(String uri){
		if(project == null || !project.isOpen()) {
			return null;
		}
		try {
			if(!project.hasNature(JavaCore.NATURE_ID)) {
				return null;
			}
			IJavaProject javaProject = JavaCore.create(project);		
			IClasspathEntry[] es = javaProject.getResolvedClasspath(true);
			for (int i = 0; i < es.length; i++) {
				if(es[i].getEntryKind() != IClasspathEntry.CPE_SOURCE) {
					continue;
				}
				IFile file = (IFile) project.getWorkspace().getRoot()
					.getFolder(es[i].getPath()).findMember("/" + getBundleFileName(uri)); //$NON-NLS-1$
				if(file != null && file.exists()) {
					return file;
				}
			}
		} catch (CoreException e) {
			JspEditorPlugin.getPluginLog().logError(e);
			return null;
		}
		return null;
	}
	
	/**
	 * Gets the bundle file name.
	 * <a href="https://jira.jboss.org/browse/JBIDE-6729">
	 * Related Jira</a>
	 *
	 * @param uri the uri
	 * @return the bundle file name
	 */
	private String getBundleFileName(String uri) {
		String resultUri = uri.replace('.','/');
		ResourceBundle bundle = getBundleByUrl(uri, locale);
		String localeString = bundle.getLocale().toString();
		if ((null != localeString) && (localeString.length() > 0)) {
			/*
			 * getLanguage() method in  ResourceBundle could return "en_us" string.
			 * Bundle's file is case sensitive thus country name 
			 * should be transformed to UpperCase. 
			 */
			String[] parts = localeString.split(Constants.UNDERSCORE);
			if (parts.length == 2) {
				parts[1] = parts[1].toUpperCase();
			}
			for (String part : parts) {
				resultUri += Constants.UNDERSCORE + part;
			}
		}
		resultUri += ".properties"; //$NON-NLS-1$
		return resultUri;
	}
	
	private ResourceBundle getBundleByUrl(String uri, Locale locale) {
		try {
			if (javaSources!=null) {
				File file;
				URL[] urls = new URL[javaSources.length];
				for (int i=0;i<javaSources.length;++i) {
					try {
						file = new File(javaSources[i]).getCanonicalFile();
						urls[i] = file.toURL();
					} catch (IOException ioe) {
						JspEditorPlugin.getDefault().logError(ioe);
						return null;
					}
				}
				
				ClassLoader classLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
				ResourceBundle bundle = ResourceBundle.getBundle(uri, locale, classLoader);
				return bundle;
			}
		} catch (MissingResourceException ex) {
		    // Ignore this exception
		}
	
		return null;
	}

	private static String[] getJavaProjectSrcLocations(IProject project) {
		return EclipseResourceUtil.getJavaProjectSrcLocations(project);
	}
	
	private void removeBundle(int hashCode, boolean refresh) {
		if (bundles.length == 0) {
			return;
		}
		int index = -1;
		for (int i = 0; i < bundles.length; i++) {
			if (hashCode == bundles[i].hashCode){
				index = i;
				break;
			}
		}
		if (index == -1) {
			return;
		}
		if (bundles.length == 1) {
			bundles = new BundleEntry[0];
			return;
		}
		BundleEntry[] newBundles = new BundleEntry[bundles.length - 1];
		System.arraycopy(bundles, 0, newBundles, 0, index);
		System.arraycopy(bundles, index + 1, newBundles, index, bundles.length - index - 1);
		bundles = newBundles;
		if (refresh) {
			refreshUsedKeys();
		}
	}

	public void removeBundle(int hashCode) {
		removeBundle(hashCode, true);
	}
	
	public void addBundle(int hashCode, String prefix, String uri,boolean refresh) {
		ResourceBundle bundle = getBundleByUrl(uri, locale);
		BundleEntry entry = new BundleEntry(bundle, uri, prefix, hashCode);
		if (bundle != null) {
			BundleEntry[] newBundles = new BundleEntry[bundles.length + 1];
			System.arraycopy(bundles, 0, newBundles, 0, bundles.length);
			bundles = newBundles;
			bundles[bundles.length - 1] = entry;
		}
		if (refresh) {
			refreshUsedKeys();
		}
	}
	
	public void changeBundle(int hashCode, String prefix, String uri){
		removeBundle(hashCode, false);
		addBundle(hashCode, prefix, uri, true);
	}
	
	private void changeBundleWithoutRefresh(int hashCode, String prefix, String uri){
		removeBundle(hashCode, false);
		addBundle(hashCode, prefix, uri, false);
	}

	private BundleEntry getBundle(String prefix) {
		if (prefix == null) {
			return null;
		}
		BundleEntry lastBundle = null;
		for (int i = 0; i < bundles.length; i++) {
			if (prefix.equals(bundles[i].prefix)) {
				lastBundle = bundles[i];
			}
		}
		return lastBundle;
	}
	
	public void refresh(){
		refreshRegisteredBundles();
		if (project!=null) {
			javaSources = getJavaProjectSrcLocations(project);
			UsedKey key;
			UsedKey[] array = new UsedKey[0];
			array = usedKeys.values().toArray(array);			
			
			for(int i=0; i<array.length;i++){
				key = (UsedKey)array[i];
				changeBundleWithoutRefresh(key.hashCode, key.prefix, key.uri);
			}
			refreshUsedKeys();
		}
	}
	
	private void refreshUsedKeys(){
		UsedKey keyValue;
		
		
		/* yradtsevich: Fix of JBIDE-5818. The map usedKey cannot be modified
		 * in the following foreach loop. Therefore the keys to remove
		 * are marked and removed after the loop. */
		List<String> keysToBeRemoved = new ArrayList<String>(0);

		Set<String> usedKeysSet  = this.usedKeys.keySet();
		for(String key : usedKeysSet){
			keyValue =this.usedKeys.get(key);
			BundleEntry entry = getBundle(keyValue.prefix);
			if(entry != null){
				String value;
				try{
					value = (String)entry.bundle.getObject(keyValue.key);
				}catch(MissingResourceException ex){
					value = null;
					fireBundleKeyChanged(keyValue.prefix, keyValue.key, value);
					// Fix of JBIDE-5818
					keysToBeRemoved.add(key);
					continue;
				}
				if((value == null && keyValue.value != null) || (value != null && keyValue.value == null)){
					keyValue.value = value;
					fireBundleKeyChanged(keyValue.prefix, keyValue.key, value);
					continue;
				}else if(value != null && keyValue.value != null && !value.equals(keyValue.value)){
					keyValue.value = value;
					fireBundleKeyChanged(keyValue.prefix, keyValue.key, value);
					continue;
				}
			} else{
				keyValue.value = null;
				fireBundleKeyChanged(keyValue.prefix, keyValue.key, null);
			}
		}

		// Fix of JBIDE-5818
		for (String key : keysToBeRemoved) {
			this.usedKeys.remove(key);
		}
	}
	
	private List<ELInstance> parseJSFExpression(String expression){
		ELParser parser = ELParserUtil.getDefaultFactory().createParser();
		ELModel model = parser.parse(expression);
		List<ELInstance> is = model.getInstances();
		return is;
	}
	
	public String getBundleValue(String name) {
		String bundleValue = name;
		if (!showBundleUsageAsEL) {
			List<ELInstance> is = parseJSFExpression(name);
			if (is != null) {
				StringBuffer sb = new StringBuffer();
				int index = 0;
				boolean parsingErrors = false;
				for (ELInstance i : is) {
					/*
					 * https://issues.jboss.org/browse/JBIDE-10531
					 * Getting the bundle value should not succeed 
					 * if the EL is missing any required token.
					 */
					if (!i.getErrors().isEmpty()) {
						parsingErrors = true;
						break;
					}
					int start = i.getStartPosition();
					sb.append(name.substring(index, start));
					index = start;
					if (i.getExpression() instanceof ELInvocationExpression) {
						ELInvocationExpression expr = 
								(ELInvocationExpression) i.getExpression();
						String[] values = getCall(expr);
						if (values != null) {
							String value = getBundleValue(values[0], values[1]);
							if (value != null) {
								sb.append(value);
								index = i.getEndPosition();
							}
						}
					}
					if (index < i.getEndPosition()) {
						/*
						 * https://jira.jboss.org/jira/browse/JBIDE-6064
						 * CA out of range error occurs sometimes
						 */
						if (name.length() > i.getEndPosition()) {
							sb.append(name.substring(index, i.getEndPosition()));
							index = i.getEndPosition();
						} else {
							sb.append(name.substring(index, name.length()));
							index = name.length();
						}
					}
				}
				if (!parsingErrors) {
					bundleValue = sb.append(name.substring(index)).toString();
				}
			}
		}
		return bundleValue;
	}
	
	String[] getCall(ELInvocationExpression expr) {
		if(expr == null) return null;
		ELInvocationExpression left = expr.getLeft();
		if(left == null) return null;
		String name = expr.getMemberName();
		if(name == null || name.length() == 0) return null;
		if(expr instanceof ELPropertyInvocation) {
			/*
			 * Simple EL like #{bundle.key} goes here
			 */
			return new String[]{left.getText(), name};
		} else if(expr instanceof ELArgumentInvocation) {
			/*
			 * EL like #{bundle['compound.key']} goes here
			 */
			if(name.startsWith("\"") || name.startsWith("'")) { //$NON-NLS-1$ //$NON-NLS-2$
				name = name.substring(1);
			} else {
				/*
				 * https://issues.jboss.org/browse/JBIDE-10531
				 * EL with no quotes cannot be used to determine the bundle value.
				 */
				return null;
			}
			if(name.endsWith("\"") || name.endsWith("'")) { //$NON-NLS-1$ //$NON-NLS-2$
				name = name.substring(0, name.length() - 1);
			} else {
				/*
				 * https://issues.jboss.org/browse/JBIDE-10531
				 * EL with no quotes cannot be used to determine the bundle value.
				 */
				return null;
			}
			if (name.length() == 0) {
				return null;
			}
			return new String[] { left.getText(), name };
		}
		return null;
	}
	
	private String getBundleValue(String prefix, String propertyName) {
		String bundleValue = null;
		BundleEntry entry = getBundle(prefix);
		if (entry != null) {
			String name = prefix + "." + propertyName; //$NON-NLS-1$
			try {
				bundleValue = (String) entry.bundle.getObject(propertyName);
				if (!usedKeys.containsKey(name))
					usedKeys.put(name, new UsedKey(entry.uri, prefix,
							propertyName, bundleValue, entry.hashCode));
			} catch (MissingResourceException ex) {
				/*
				 * Null string will be returned.
				 */
			}
		}
		return bundleValue;
	}
	
	public void addBundleMapListener(BundleMapListener listener) {
		if (listener != null) {
			BundleMapListener[] newBundleMapListener = new BundleMapListener[bundleMapListeners.length + 1];
			System.arraycopy(bundleMapListeners, 0, newBundleMapListener, 0, bundleMapListeners.length);
			bundleMapListeners = newBundleMapListener;
			bundleMapListeners[bundleMapListeners.length - 1] = listener;
		}
	}
	
	public void removeBundleMapListener(BundleMapListener listener) {
		if (listener == null || bundleMapListeners.length == 0) return;
		int index = -1;
		for (int i = 0; i < bundleMapListeners.length; i++) {
			if (listener == bundleMapListeners[i]){
				index = i;
				break;
			}
		}
		if (index == -1) return;
		if (bundleMapListeners.length == 1) {
			bundleMapListeners = new BundleMapListener[0];
			return;
		}
		BundleMapListener[] newBundleMapListener = new BundleMapListener[bundleMapListeners.length - 1];
		System.arraycopy(bundleMapListeners, 0, newBundleMapListener, 0, index);
		System.arraycopy(bundleMapListeners, index + 1, newBundleMapListener, index, bundleMapListeners.length - index - 1);
		bundleMapListeners = newBundleMapListener;
	}
	
	private void fireBundleKeyChanged(String prefix, String key, String value) {
		for (int i = 0; i < bundleMapListeners.length; i++) {
			bundleMapListeners[i].bundleKeyChanged(prefix, key, value);
		}
	}
	
	public void updateShowBundleUsageAsEL(boolean showBundlesAsEL) {
		if(showBundleUsageAsEL != showBundlesAsEL) {
			showBundleUsageAsEL = showBundlesAsEL;
			refresh();
		}	
	}
	
	public void updateShowBundleUsageAsEL() {
		updateShowBundleUsageAsEL(JspEditorPlugin.getDefault().getPreferenceStore().getBoolean(
				IVpePreferencesPage.SHOW_RESOURCE_BUNDLES_USAGE_AS_EL));
	}
	
	public Locale getLocale() {
		return locale;
	}

	public void setLocale(Locale locale) {
		this.locale = locale;
	}
	
	static class Expression {
		public String prefix;
		public String propertyName;
	}

	/*
	 * https://jira.jboss.org/browse/JBIDE-6287
	 * It was required to get access to BundleEntry class.
	 */
	public static class BundleEntry {
		public ResourceBundle bundle;
		public String uri;
		public String prefix;
		public int hashCode;

		public BundleEntry(ResourceBundle bundle, String uri, String prefix,
				int hashCode) {
			this.bundle = bundle;
			this.uri = uri;
			this.prefix = prefix;
			this.hashCode = hashCode;
		}

		@Override
		public String toString() {
			return "BundleEntry [prefix=" + prefix + ", uri=" + uri + ", hashCode=" + hashCode //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					+ ", bundle="+ bundle +"]"; //$NON-NLS-1$ //$NON-NLS-2$
		}
		
	}

	static class UsedKey {
		public int hashCode;
		public String uri;
		public String prefix;
		public String key;
		public String value;

		public UsedKey(String uri, String prefix, String key, String value,
				int hashCode) {
			this.uri = uri;
			this.prefix = prefix;
			this.key = key;
			this.value = value;
			this.hashCode = hashCode;

		}
	}
	
	class ML implements XModelTreeListener {

		public void nodeChanged(XModelTreeEvent event) {
			updateShowBundleUsageAsEL();
		}

		public void structureChanged(XModelTreeEvent event) {
		}
		
	}

	/**
	 * @param showBundleUsageAsEL the showBundleUsageAsEL to set
	 */
	public void setShowBundleUsageAsEL(boolean showBundleUsageAsEL) {
		this.showBundleUsageAsEL = showBundleUsageAsEL;
	}
	
	public BundleEntry[] getBundles() {
		return bundles;
	}

}
