/**
 * 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.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

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 javax.faces.el.ValueBinding;

import org.ajax4jsf.Messages;
import org.ajax4jsf.util.ELUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.richfaces.skin.Skin;
import org.richfaces.skin.SkinFactory;
import org.richfaces.skin.SkinNotFoundException;

/**
 * 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_PROPERTIES_RESOURCE = "META-INF/skins/DEFAULT.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 = {
			"META-INF/skins/%s.skin.properties", "%s.skin.properties" };
	private static final String[] DEFAULT_SKIN_PATHS = { DEFAULT_SKIN_PROPERTIES_RESOURCE };

	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 skins = Collections.synchronizedMap(new HashMap());
	private Properties defaultSkinProperties = null;
	private String skinName = null;
	private ValueBinding skinBinding = null;
	private String baseSkinName = null;
	private ValueBinding 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(Object currentSkinOrName) {
		if (null == currentSkinOrName) {
			throw new SkinNotFoundException(Messages
					.getMessage(Messages.NULL_SKIN_NAME_ERROR));
		}
		// user binding return skin instance.
		if (currentSkinOrName instanceof Skin) {
			return (Skin) currentSkinOrName;
		}
		String currentSkinName = currentSkinOrName.toString();
		Skin currentSkin = (Skin) skins.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(currentSkinName, getDefaultSkinProperties());
			skins.put(currentSkinName, currentSkin);
		}

		return currentSkin;
	}

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

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

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

	protected Properties getDefaultSkinProperties() {
		if (defaultSkinProperties == null) {
			defaultSkinProperties = loadDefaultProperties(DEFAULT_SKIN_PROPERTIES_RESOURCE);
		}
		return defaultSkinProperties;
	}

	/**
	 * 
	 */
	private Properties loadDefaultProperties(String path) {
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		Properties defaultProperties = new Properties();
		try {
			Enumeration properties = loader.getResources(path);
			while (properties.hasMoreElements()) {
				URL url = (URL) properties.nextElement();
				InputStream propertyStream = null;
				try {
					propertyStream = url.openStream();
					defaultProperties.load(propertyStream);
				} catch (IOException e) {
					// DO Nothing...
					log.info(Messages.getMessage(
							Messages.DEFAULT_PROPERTIES_NOT_FOUND_ERROR, path),
							e);
					continue;
				} finally {
					if (null != propertyStream) {
						propertyStream.close();
					}
				}
			}
		} catch (IOException e) {
			log.info(Messages.getMessage(
					Messages.DEFAULT_PROPERTIES_NOT_FOUND_ERROR, path), e);
		}
		return defaultProperties;
	}

	/**
	 * 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
		ValueBinding 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().createValueBinding(
							currentSkinName);
				} else {
					skin = currentSkinName;
				}
			}

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

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

	/**
	 * 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(String name, Properties defaultProperties)
			throws SkinNotFoundException {
		Map skinParams = loadProperties(name, defaultProperties, SKINS_PATHS);
		return new SkinImpl(skinParams);
	}

	/**
	 * @param name
	 * @param defaultProperties
	 * @return
	 * @throws SkinNotFoundException
	 * @throws FacesException
	 * @throws ReferenceSyntaxException
	 */
	protected Map loadProperties(String name, Properties defaultProperties,
			String[] paths) throws SkinNotFoundException, FacesException,
			ReferenceSyntaxException {
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		// Get properties for concrete skin.
		Properties skinProperties = new Properties(defaultProperties);
		int loadedPropertiesCount = 0;
		for (int i = 0; i < paths.length; i++) {
			try {
				String skinPropertiesLocation = paths[i].replaceAll("%s", name);
				Enumeration properties = loader
						.getResources(skinPropertiesLocation);
				while (properties.hasMoreElements()) {
					URL url = (URL) properties.nextElement();
					InputStream propertyStream = null;
					try {
						propertyStream = url.openStream();
						skinProperties.load(propertyStream);
						loadedPropertiesCount++;
					} 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);
				}
			}
		}
		// TODO - read skin from property file in WEB-INF directory.
		if (loadedPropertiesCount == 0) {
			throw new SkinNotFoundException(Messages.getMessage(
					Messages.SKIN_NOT_FOUND_ERROR, name));
		}
		// replace all EL-expressions by prepared ValueBinding ?
		ApplicationFactory factory = (ApplicationFactory) FactoryFinder
				.getFactory(FactoryFinder.APPLICATION_FACTORY);
		Application app = factory.getApplication();
		Map skinParams = new HashMap();
		for (Enumeration e = skinProperties.propertyNames(); e
				.hasMoreElements();) {
			String propertyName = (String) e.nextElement();
			String property = skinProperties.getProperty(propertyName);
			if (null != property) {
				int count = 0;
				while (property.startsWith("&")) {
					property = skinProperties
							.getProperty(property.substring(1));
					if (null == property) {
						throw new FacesException(Messages.getMessage(
								Messages.SKIN_ILLEGAL_REFERENCE, propertyName));
					}
					if (count++ > 1000) {
						throw new FacesException(Messages.getMessage(
								Messages.SKIN_CYCLIC_REFERENCE, propertyName));
					}
				}
				if (ELUtils.isValueReference(property)) {
					skinParams.put(propertyName, app
							.createValueBinding(property));
				} else {
					skinParams.put(propertyName, property);
				}
			}
		}
		return skinParams;
	}

}
