/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.richfaces.skin;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import javax.el.ELContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.context.FacesContext;
import javax.faces.el.ReferenceSyntaxException;

import org.ajax4jsf.Messages;
import org.ajax4jsf.resource.util.URLToStreamHelper;
import org.ajax4jsf.util.ELUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Implementation of {@link SkinFactory} with building skins from properties
 * files.
 * 
 * @author shura
 * 
 */
public class SkinFactoryImpl extends SkinFactory {


	/**
	 * Name of web application init parameter for current default
	 * {@link javax.faces.render.RenderKit } interaction. by default -
	 * org.exadel.chameleon.RENDERKIT_DEFINITION . TODO Possible Values
	 */
	public static final String RENDER_KIT_PARAMETER = "org.ajax4jsf.RENDERKIT_DEFINITION";

	/**
	 * Resource Uri for properties file with default values of skin parameters.
	 */
	private static final String DEFAULT_SKIN_PATH = "META-INF/skins/%s.skin.properties";

	private static final String USER_SKIN_PATH = "%s.skin.properties";

	//	private static final String DEFAULT_CONFIGURATION_RESOURCE = "META-INF/skins/DEFAULT.configuration.properties";
	/**
	 * Name of default skin . "DEFAULT" in this realisation.
	 */
	private static final String DEFAULT_SKIN_NAME = "DEFAULT";

	/**
	 * Path in jar to pre-defined vendor and custom user-defined skins
	 * definitions. in this realisation "META-INF/skins/" for vendor , "" -
	 * user-defined.
	 */
	private static final String[] SKINS_PATHS = {
			DEFAULT_SKIN_PATH, USER_SKIN_PATH };
	private static final String[] THEME_PATHS = {
		"META-INF/themes/%s.theme.properties", "%s.theme.properties" };
//	private static final String[] DEFAULT_SKIN_PATHS = { DEFAULT_SKIN_PATH };

//	private static final String[] CONFIGURATIONS_PATHS = {
//			"META-INF/skins/%s.configuration.properties",
//			"%s.configuration.properties" };
//	private static final String[] DEFAULT_CONFIGURATION_PATHS = { "META-INF/skins/DEFAULT.configuration.properties" };

	private Map<String,Skin> skins = new HashMap<String,Skin>();
	private Map<String,Skin> baseSkins = new HashMap<String,Skin>();
	private Map<String,Properties> sourceProperties = new HashMap<String,Properties>();
	private Map<String,Theme> themes = new HashMap<String,Theme>();

//	private Properties defaultSkinProperties = null;
	private String skinName = null;
	private ValueExpression skinBinding = null;
	private String baseSkinName = null;
	private ValueExpression baseSkinBinding = null;
	private static final Log log = LogFactory.getLog(SkinFactoryImpl.class);

	private static final String A4J_BASE_SKIN_PARAMETER = "org.ajax4jsf.BASE_SKIN";

	private static final String A4J_SKIN_PARAMETER = "org.ajax4jsf.SKIN";

	protected Skin getSkinByName(FacesContext facesContext, Object currentSkinOrName, boolean isBase) {
		if (null == currentSkinOrName) {
			throw new SkinNotFoundException(Messages
					.getMessage(Messages.NULL_SKIN_NAME_ERROR));
		}
		Skin currentSkin = null;
		// user binding return skin instance.
		if (currentSkinOrName instanceof Skin) {
			currentSkin = (Skin) currentSkinOrName;
		} else {
			String currentSkinName = currentSkinOrName.toString();

			Map<String, Skin> skinsMap = (isBase ? baseSkins : skins);
			synchronized (skinsMap) {
				currentSkin = (Skin) skinsMap.get(currentSkinName);
				// LAZY creation for skins, since, in case of EL expressions
				// for skin name, we don't can know all names of existing skins.
				if (currentSkin == null) {
					if (log.isDebugEnabled()) {
						log.debug(Messages.getMessage(
								Messages.CREATE_SKIN_INFO, currentSkinName));
					}
					currentSkin = buildSkin(facesContext, currentSkinName,
							isBase);
					skinsMap.put(currentSkinName, currentSkin);
				}
			}
		}
		return currentSkin;
	}

	public Skin getDefaultSkin(FacesContext context) {
		return getSkinByName(context, DEFAULT_SKIN_NAME, false);
	}

	public Skin getSkin(FacesContext context) {
		// TODO - cache skin for current thread ? or for current Faces Lifecycle
		// Phase ?
		Object currentSkinOrName = getSkinOrName(context, false);
		return getSkinByName(context, currentSkinOrName, false);
	}

	public Skin getBaseSkin(FacesContext context) {
		Object currentSkinOrName = getSkinOrName(context, true);
		return getSkinByName(context, currentSkinOrName, true);
	}

//	protected Properties getDefaultSkinProperties() {
//		if (defaultSkinProperties == null) {
//			defaultSkinProperties = loadProperties(DEFAULT_SKIN_NAME,DEFAULT_SKIN_PATHS);
//		}
//		return defaultSkinProperties;
//	}

	/**
	 * Calculate name for current skin. For EL init parameter store value
	 * binding for speed calculations.
	 * 
	 * @param context
	 * @param useBase
	 * @return name of currens skin from init parameter ( "DEFAULT" if no
	 *         parameter ) or {@link Skin } as result of evaluation EL
	 *         expression.
	 */
	protected Object getSkinOrName(FacesContext context, boolean useBase) {
		// Detect skin name
		ValueExpression binding;
		String skin;

		synchronized (this) {
			if (useBase) {
				binding = baseSkinBinding;
				skin = baseSkinName;
			} else {
				binding = skinBinding;
				skin = skinName;
			}

			if (binding == null && skin == null) {
				String currentSkinName = context.getExternalContext()
						.getInitParameter(
								useBase ? BASE_SKIN_PARAMETER : SKIN_PARAMETER);
				if (null == currentSkinName) {
					// Check for a old ( deprecated ) parameter name.
					currentSkinName = context.getExternalContext()
							.getInitParameter(
									useBase ? A4J_BASE_SKIN_PARAMETER
											: A4J_SKIN_PARAMETER);
					if (null != currentSkinName) {
						log.warn("Init parameter for a skin name changed to "+ (useBase ? BASE_SKIN_PARAMETER
											: SKIN_PARAMETER));
					}

				}
				if (currentSkinName == null) {
					// not set - usr default.
					return DEFAULT_SKIN_NAME;
				}
				if (ELUtils.isValueReference(currentSkinName)) {
					// For EL expression as skin name
					binding = context.getApplication().getExpressionFactory().
						createValueExpression(context.getELContext(), 
							currentSkinName, Object.class);
				} else {
					skin = currentSkinName;
				}

				if (useBase) {
					baseSkinBinding = binding;
					baseSkinName = skin;
				} else {
					skinBinding = binding;
					skinName = skin;
				}
			}

			// }
		}
		if (binding != null) {
			return binding.getValue(context.getELContext());
		} else {
			return skin;
		}
	}

	private void processProperties(FacesContext context, Map<Object, Object> properties) {
	    ELContext elContext = context.getELContext();
	    // replace all EL-expressions by prepared ValueBinding ?
	    ApplicationFactory factory = (ApplicationFactory) FactoryFinder
	    .getFactory(FactoryFinder.APPLICATION_FACTORY);
	    Application app = factory.getApplication();
	    
	    for (Entry<Object, Object> entry : properties.entrySet()) {
		Object propertyObject = entry.getValue();
		if (propertyObject instanceof String) {
		    String property = (String) propertyObject;
		    if (ELUtils.isValueReference(property)) {
			ExpressionFactory expressionFactory = app.getExpressionFactory();
			entry.setValue(expressionFactory.createValueExpression(elContext, property, Object.class));
		    } else {
			entry.setValue(property);
		    }
		}
	    }
	}
	
	/**
	 * Factory method for build skin from properties files. for given skin name,
	 * search in classpath all resources with name 'name'.skin.properties and
	 * append in content to default properties. First, get it from
	 * META-INF/skins/ , next - from root package. for any place search order
	 * determined by {@link java.lang.ClassLoader } realisation.
	 * @param name
	 *            name for builded skin.
	 * @param defaultProperties
	 * 
	 * @return skin instance for current name
	 * @throws SkinNotFoundException -
	 *             if no skin properies found for name.
	 */
	protected Skin buildSkin(FacesContext context, String name, boolean isBase)
			throws SkinNotFoundException {
		Properties skinParams;
		synchronized (sourceProperties) {
			skinParams = sourceProperties.get(name);
			if (skinParams == null) {
				skinParams = loadProperties(name, SKINS_PATHS);
				processProperties(context, skinParams);
				// skinParams = Collections.unmodifiableMap(skinParams);
				sourceProperties.put(name, skinParams);
			}
		}	    
	    BasicSkinImpl skinImpl;
	    if (DEFAULT_SKIN_NAME.equals(name)) {
		skinImpl = new DefaultSkinImpl(skinParams);
	    } else if (isBase) {
		skinImpl = new BaseSkinImpl(skinParams, this);
	    } else {
		skinImpl = new SkinImpl(skinParams, this);
	    }

	    return skinImpl;
	}

	/**
	 * @param name
	 * @param defaultProperties
	 * @return
	 * @throws SkinNotFoundException
	 * @throws FacesException
	 * @throws ReferenceSyntaxException
	 */
	protected Properties loadProperties(String name, String[] paths) throws SkinNotFoundException, FacesException {
		ClassLoader loader = getClassLoader();
		// Get properties for concrete skin.
		Properties skinProperties = new Properties();
		int loadedPropertiesCount = 0;
		for (int i = 0; i < paths.length; i++) {
			String skinPropertiesLocation = paths[i].replaceAll("%s", name);
			if(loadProperties(loader, skinProperties, skinPropertiesLocation)){
				loadedPropertiesCount++;
			}
		}
		if (loadedPropertiesCount == 0) {
			throw new SkinNotFoundException(Messages.getMessage(
					Messages.SKIN_NOT_FOUND_ERROR, name));
		}
		return skinProperties;
	}

	/**
	 * @return
	 */
	protected ClassLoader getClassLoader() {
		return Thread.currentThread().getContextClassLoader();
	}

	/**
	 * @param loader
	 * @param properties
	 * @param location
	 */
	protected boolean loadProperties(ClassLoader loader, Properties properties,
			String location) {
		boolean loaded = false;
		try {
			Enumeration<URL> resources = loader
					.getResources(location);
			while (resources.hasMoreElements()) {
				URL url = (URL) resources.nextElement();
				InputStream propertyStream = null;
				try {
					propertyStream = URLToStreamHelper.urlToStream(url);
					properties.load(propertyStream);
						loaded=true;
				} catch (IOException e) {
					log.warn(Messages
							.getMessage(Messages.SKIN_PROPERTIES_IO_ERROR),
							e);
					continue;
				} finally {
					if (null != propertyStream) {
						propertyStream.close();
					}
				}
			}
		} catch (IOException e) {
			// Do nothing - we can only log error, and continue to load next
			// property.
			if (log.isInfoEnabled()) {
				log.info(Messages
						.getMessage(Messages.SKIN_PROPERTIES_IO_ERROR), e);
			}
		}
		return loaded;
	}

	@Override
	public Theme getTheme(FacesContext facesContext, String name) {
		Theme theme = themes.get(name);
		if(null == theme){
			Properties properties;
			try {
				properties = loadProperties(name, THEME_PATHS);
			} catch (SkinNotFoundException e) {
				throw new ThemeNotFoundException(Messages.getMessage(
						Messages.THEME_NOT_FOUND_ERROR, name), e.getCause());
			}
			processProperties(facesContext, properties);
			theme = new ThemeImpl(properties);
			themes.put(name, theme);
		}
		return theme;
	}
}
