/**
 * 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.ajax4jsf.resource;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Map;
import java.util.Properties;

import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.ajax4jsf.Messages;
import org.ajax4jsf.cache.Cache;
import org.ajax4jsf.cache.CacheConfigurationLoader;
import org.ajax4jsf.cache.CacheException;
import org.ajax4jsf.cache.CacheFactory;
import org.ajax4jsf.cache.CacheLoader;
import org.ajax4jsf.cache.CacheManager;
import org.ajax4jsf.cache.ServletContextInitMap;
import org.ajax4jsf.resource.util.URLToStreamHelper;
import org.ajax4jsf.webapp.BaseFilter;
import org.ajax4jsf.webapp.CacheContent;
import org.ajax4jsf.webapp.WebXml;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class InternetResourceService implements CacheLoader,
		CacheConfigurationLoader {
	private static final Log log = LogFactory
			.getLog(InternetResourceService.class);

	static final String ENABLE_CACHING_PARAMETER = "enable-cache";

	private static final String RESOURCE_LIFECYCLE_PARAMETER = "org.ajax4jsf.RESOURCE_LIFECYCLE";

	private FilterConfig filterConfig;

	private boolean cacheEnabled = true;

	private Cache cache = null;

	// private ServletCacheAdministrator cacheAdmin;

	private FacesContextFactory contextFactory;

	// private RenderKitFactory renderKitFactory;
	private String lifecycleClass;

	private ResourceLifecycle lifecycle;

	private InternetResourceBuilder resourceBuilder;

	private WebXml webXml;

	public InternetResourceService() {
	}

	public void setCacheEnabled(boolean b) {
		cacheEnabled = b;
	}

	public void init(FilterConfig config) throws ServletException {
		filterConfig = config;
		ServletContext servletContext = config.getServletContext();
		if ("false".equalsIgnoreCase(config
				.getInitParameter(ENABLE_CACHING_PARAMETER))) {
			setCacheEnabled(false);
			// this.cacheEnabled = false;
			// this.cacheAdmin = null;
		} else {
			// this.cacheAdmin = ServletCacheAdministrator.getInstance(
			// servletContext, cacheProperties);
			try {
				CacheManager cacheManager = CacheManager.getInstance();
				Map<String, String> env = new ServletContextInitMap(servletContext);
				CacheFactory cacheFactory = cacheManager.getCacheFactory(env);
				this.cache = cacheFactory.createCache(env, this, this);
			} catch (CacheException e) {
				throw new FacesException(e.getMessage(), e);
			}
		}
		// Create Resource-specific Faces Lifecycle instance.
		lifecycleClass = servletContext
				.getInitParameter(RESOURCE_LIFECYCLE_PARAMETER);
		if (lifecycleClass != null) {
			ClassLoader classLoader = Thread.currentThread()
					.getContextClassLoader();
			try {
				Class<?> clazz = classLoader.loadClass(lifecycleClass);
				lifecycle = (ResourceLifecycle) clazz.newInstance();
			} catch (Exception e) {
				throw new FacesException(
						"Error create instance of resource Lifecycle "
								+ lifecycleClass, e);
			}
		} else {
			lifecycle = new ResourceLifecycle();
		}
		webXml = new WebXml();
		webXml.init(servletContext, filterConfig.getFilterName());
		if (log.isDebugEnabled()) {
			log.debug("Resources service initialized");
		}
	}

	public boolean serviceResource(HttpServletRequest httpServletRequest,
			HttpServletResponse httpServletResponse) throws ServletException,
			IOException {
		String resourceKey = webXml.getFacesResourceKey(httpServletRequest);
		if (null != resourceKey) {
			serviceResource(resourceKey, httpServletRequest,
					httpServletResponse);
			return true;
		}
		return false;
	}

	public void serviceResource(String resourceKey, HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		InternetResource resource;// getInternetResource(request);
		try {
			resource = getResourceBuilder().getResourceForKey(resourceKey);
		} catch (ResourceNotFoundException e) {
			throw new ServletException(e);
		}
		Object resourceDataForKey = getResourceBuilder().getResourceDataForKey(
				resourceKey);

		ResourceContext resourceContext = getResourceContext(resource, request,
				response);
		resourceContext.setResourceData(resourceDataForKey);
		try {

			if (resource.isCacheable(resourceContext) && this.cacheEnabled) {
				// Test for client request modification time.
				try {
					long ifModifiedSince = request
							.getDateHeader("If-Modified-Since");
					if (ifModifiedSince >= 0) {
						// Test for modification. 1000 ms due to round
						// modification
						// time to seconds.
						long lastModified = resource.getLastModified(
								resourceContext).getTime() - 1000;
						if (lastModified <= ifModifiedSince) {
							response.setStatus(304);
							return;
						}
					}
				} catch (IllegalArgumentException e) {
					log
							.warn(
									Messages
											.getMessage(Messages.PARSING_IF_MODIFIED_SINCE_WARNING),
									e);
				}
				String cacheKey = resourceKey;
				CachedResourceContext cachedResourceContext = new CachedResourceContext(
						resourceContext);

				CacheContext cacheLoaderContext = new CacheContext(
						cachedResourceContext, resource);

				try {
					CacheContent content = (CacheContent) cache.get(cacheKey,
							cacheLoaderContext);
					if (log.isDebugEnabled()) {
						log
								.debug(Messages.getMessage(
										Messages.GET_CONTENT_FROM_CACHE_INFO,
										cacheKey));
					}
					content.sendHeaders(response);
					// Correct expires date for resource.
					long expired = resource.getExpired(resourceContext);
					if (expired < 0 ) {
						expired = InternetResource.DEFAULT_EXPIRE;
					} 
						response.setDateHeader("Expires", System
								.currentTimeMillis()
								+ expired);
//						response.addHeader("Cache-control", "max-age="
//								+ (expired / 1000L));
					if (!request.getMethod().equals("HEAD")) {
						content.send(response);
					}
					content.flush(response);
				} catch (CacheException e) {
					log.error(
							Messages.getMessage(Messages.SEND_RESOURCE_ERROR),
							e);
					throw new ServletException(Messages.getMessage(
							Messages.SEND_RESOURCE_ERROR_2, e.getMessage()), e);
				}
			} else {
				getLifecycle().send(resourceContext, resource);
				// sendResource(resource, request, response,
				// resourceDataForKey);
			}

		} finally {
			resourceContext.release();
		}
	}

	/**
	 * @param resource
	 * @param request
	 * @param response
	 * @return
	 * @throws ServletException
	 * @throws FacesException
	 */
	protected ResourceContext getResourceContext(InternetResource resource,
			HttpServletRequest request, HttpServletResponse response)
			throws FacesException {
		FacesContext facesContext = null;
		ResourceContext resourceContext;
		if (resource.requireFacesContext()) {
			facesContext = getFacesContext(request, response);
			resourceContext = new FacesResourceContext(facesContext);
		} else {
			resourceContext = new ServletResourceContext(getServletContext(),
					request, response);
		}
		return resourceContext;
	}

	/**
	 * Get properties file from classpath
	 * 
	 * @param name
	 * @return
	 */
	protected Properties getProperties(String name) {
		Properties properties = new Properties();
		InputStream props = URLToStreamHelper.urlToStreamSafe(BaseFilter.class.getResource(name));
		if (null != props) {
			try {
				properties.load(props);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				log.warn(Messages.getMessage(Messages.READING_PROPERTIES_ERROR,
						name), e);
			} finally {
				try {
					props.close();
				} catch (IOException e) {
					// Can be ignored
				}
			}
		}
		return properties;

	}

	/**
	 * @return Returns the servletContext.
	 */
	protected ServletContext getServletContext() {
		return filterConfig.getServletContext();
	}

	/**
	 * @return the lifecycle
	 * @throws ServletException
	 */
	protected ResourceLifecycle getLifecycle() throws FacesException {
		return lifecycle;
	}

	/**
	 * @return the contextFactory
	 */
	protected synchronized FacesContextFactory getContextFactory() {
		if (contextFactory == null) {
			contextFactory = (FacesContextFactory) FactoryFinder
					.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
		}

		return contextFactory;
	}

	protected FacesContext getFacesContext(ServletRequest request,
			ServletResponse response) throws FacesException {
		return getContextFactory().getFacesContext(getServletContext(),
				request, response, getLifecycle());
	}

	/**
	 * @return the resourceBuilder
	 */
	protected InternetResourceBuilder getResourceBuilder() {
		if (resourceBuilder == null) {
			// Create resource builder for this filter.
			resourceBuilder = InternetResourceBuilder.getInstance();
		}
		return resourceBuilder;
	}

	public Object load(Object key, Object context) throws CacheException {
		CacheContext cacheKey = (CacheContext) context;
		CachedResourceContext resourceContext = cacheKey.getResourceContext();
		try {
			getLifecycle().send(resourceContext, cacheKey.getResource());
		} catch (IOException e) {
			throw new CacheException(e.getMessage(), e);
		}
		// TODO - set refresh interval ?
		// cache.put(cacheKey, resourceContext.getContent());
		return resourceContext.getContent();
	}

	public Properties loadProperties(String name) {
		return getProperties(name);
	}

}
