/**
 * 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import javax.faces.FacesException;
import javax.faces.context.FacesContext;
import javax.imageio.ImageIO;
import javax.servlet.ServletContext;

import org.ajax4jsf.Messages;
import org.ajax4jsf.resource.util.URLToStreamHelper;
import org.ajax4jsf.util.base64.Codec;
import org.ajax4jsf.webapp.WebXml;
import org.apache.commons.digester.Digester;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Produce instances of InternetResource's for any types - jar resource, dynamic
 * created image, component-incapsulated etc. Realised as singleton class to
 * support cache, configuration etc.
 * 
 * @author shura (latest modification by $Author: alexsmirnov $)
 * @version $Revision: 1.1.2.1 $ $Date: 2007/01/09 18:56:58 $
 * 
 */
public class ResourceBuilderImpl extends InternetResourceBuilder {

	private static final Log log = LogFactory.getLog(ResourceBuilderImpl.class);

	private static final String DATA_SEPARATOR = "/DATA/";
	private static final String DATA_BYTES_SEPARATOR = "/DATB/";

	private static final Pattern DATA_SEPARATOR_PATTERN = Pattern
			.compile("/DAT(A|B)/");

	private static Map<String, ResourceRenderer> renderers;

	private static ResourceRenderer defaultRenderer = new MimeRenderer(null);

	/**
	 * keep resources instances . TODO - put this map to application-scope
	 * attribute, for support clastering environment.
	 */
	private Map<String, InternetResource> resources = Collections.synchronizedMap(new HashMap<String, InternetResource>());

	private long _startTime;

	private Codec codec;

	private static final ResourceRenderer scriptRenderer = new ScriptRenderer();
	
	private static final ResourceRenderer styleRenderer = new StyleRenderer();

	static {
		renderers = new HashMap<String, ResourceRenderer>();
		// append known renderers for extentions.
		renderers.put(".gif", new GifRenderer());
		ResourceRenderer renderer = new JpegRenderer();
		renderers.put(".jpeg", renderer);
		renderers.put(".jpg", renderer);
		renderers.put(".png", new PngRenderer());
		renderers.put(".js", scriptRenderer);
		renderers.put(".css", styleRenderer);
		renderers.put(".log", new LogfileRenderer());
		renderers.put(".html", new HTMLRenderer());
		renderers.put(".xhtml", new MimeRenderer("application/xhtml+xml"));
		renderers.put(".xml", new MimeRenderer("text/xml"));
		renderers.put(".xcss", new TemplateCSSRenderer());
		renderers.put(".swf", new MimeRenderer("application/x-shockwave-flash"));
		// renderers.put(".htc",new BehaviorRenderer());
		// set in-memory caching ImageIO
		ImageIO.setUseCache(false);

	}

	public WebXml getWebXml(FacesContext context) {
		WebXml webXml = WebXml.getInstance(context);
		if (null == webXml) {
			throw new FacesException(
					"Resources framework is not initialised, check web.xml for Filter configuration");
		}
		return webXml;
	}

	public ResourceBuilderImpl() {
		super();
		_startTime = System.currentTimeMillis();
		codec = new Codec();
	}

	/**
	 * @throws FacesException
	 */
	protected void registerResources() throws FacesException {
		try {
			ClassLoader loader = Thread.currentThread().getContextClassLoader();
			Enumeration<URL> e = loader
					.getResources("META-INF/resources-config.xml");
			while (e.hasMoreElements()) {
				URL resource = e.nextElement();
				registerConfig(resource);
			}
		} catch (IOException e) {
			throw new FacesException(e);
		}
	}

	private void registerConfig(URL resourceConfig) {
		try {
			if (log.isDebugEnabled()) {
				log.debug("Process resources configuration file "
						+ resourceConfig.toExternalForm());
			}

			InputStream in = URLToStreamHelper.urlToStream(resourceConfig);
			try {
				Digester digester = new Digester();
				digester.setValidating(false);
				digester.setEntityResolver(new EntityResolver() {
					// Dummi resolver - alvays do nothing
					public InputSource resolveEntity(String publicId,
							String systemId) throws SAXException, IOException {
						return new InputSource(new StringReader(""));
					}
				});
				digester.setNamespaceAware(false);
				digester.setUseContextClassLoader(true);
				digester.push(this);
				digester.addObjectCreate("resource-config/resource", "class",
						JarResource.class);
				digester.addObjectCreate("resource-config/resource/renderer",
						"class", HTMLRenderer.class);
				digester.addCallMethod(
						"resource-config/resource/renderer/content-type",
						"setContentType", 0);
				digester.addSetNext("resource-config/resource/renderer",
						"setRenderer", ResourceRenderer.class.getName());
				digester.addCallMethod("resource-config/resource/name",
						"setKey", 0);
				digester.addCallMethod("resource-config/resource/path",
						"setPath", 0);
				digester.addCallMethod("resource-config/resource/cacheable",
						"setCacheable", 0);
				digester.addCallMethod(
						"resource-config/resource/session-aware",
						"setSessionAware", 0);
				digester.addCallMethod("resource-config/resource/property",
						"setProperty", 2);
				digester.addCallParam("resource-config/resource/property/name",
						0);
				digester.addCallParam(
						"resource-config/resource/property/value", 1);
				digester.addCallMethod("resource-config/resource/content-type",
						"setContentType", 0);
				digester.addSetNext("resource-config/resource", "addResource",
						InternetResource.class.getName());
				digester.parse(in);
			} finally {
				in.close();
			}
		} catch (IOException e) {
			throw new FacesException(e);
		} catch (SAXException e) {
			throw new FacesException(e);
		}
	}

	/**
	 */
	public void init() throws FacesException {
		// TODO - mace codec configurable.
		registerResources();
	}

	/**
	 * Base point for creating resource. Must detect type and build appropriate
	 * instance. Currently - make static resource for ordinary request, or
	 * instance of class.
	 * 
	 * @param base
	 *            base object for resource ( resourcess in classpath will be get
	 *            relative to it package )
	 * @param path
	 *            key - path to resource, resource class name etc.
	 * @return
	 * @throws FacesException
	 */
	public InternetResource createResource(Object base, String path)
			throws FacesException {
		// TODO - detect type of resource ( for example, resources location path
		// in Skin
		try {
			return getResource(path);
		} catch (ResourceNotFoundException e) {
			try {
				return getResource(buildKey(base, path));
			} catch (ResourceNotFoundException e1) {
				if (log.isDebugEnabled()) {
					log.debug(Messages.getMessage(Messages.BUILD_RESOURCE_INFO,
							path));
				}
			}
		}
		// path - is class name ?
		InternetResource res;
		try {
			Class<?> resourceClass = Class.forName(path);
			res = createDynamicResource(path, resourceClass);
		} catch (Exception e) {
			try {
				res = createJarResource(base, path);
			} catch (ResourceNotFoundException ex) {
				res = createStaticResource(path);
			}
			// TODO - if resource not found, create static ?
		}
		return res;
	}

	private String buildKey(Object base, String path) {
		if (path.startsWith("/")) {
			return path.substring(1);
		}
		if (null == base) {
			return path;
		}
		StringBuffer packageName = new StringBuffer(base.getClass()
				.getPackage().getName().replace('.', '/'));
		return packageName.append("/").append(path).toString();
	}

	public String getUri(InternetResource resource, FacesContext context,
			Object storeData) {
		StringBuffer uri = new StringBuffer();// ResourceServlet.DEFAULT_SERVLET_PATH).append("/");
		uri.append(resource.getKey());
		// append serialized data as Base-64 encoded request string.
		if (storeData != null) {
			try {
				byte[] objectData;
				if (storeData instanceof byte[]) {
					objectData = (byte[]) storeData;
					uri.append(DATA_BYTES_SEPARATOR);
				} else {
					ByteArrayOutputStream dataSteram = new ByteArrayOutputStream(
							1024);
					ObjectOutputStream objStream = new ObjectOutputStream(
							dataSteram);
					objStream.writeObject(storeData);
					objStream.flush();
					objStream.close();
					dataSteram.close();
					objectData = dataSteram.toByteArray();
					uri.append(DATA_SEPARATOR);
				}
				byte[] dataArray = encrypt(objectData);
				uri.append(new String(dataArray, "ISO-8859-1"));

				// / byte[] objectData = dataSteram.toByteArray();
				// / uri.append("?").append(new
				// String(Base64.encodeBase64(objectData),
				// / "ISO-8859-1"));
			} catch (Exception e) {
				// Ignore errors, log it
				log.error(Messages
						.getMessage(Messages.QUERY_STRING_BUILDING_ERROR), e);
			}
		}
		
		boolean isGlobal = !resource.isSessionAware();
		
		String resourceURL = getWebXml(context).getFacesResourceURL(context,
				uri.toString(), isGlobal);// context.getApplication().getViewHandler().getResourceURL(context,uri.toString());
		if (!isGlobal) {
			resourceURL = context.getExternalContext().encodeResourceURL(
					resourceURL);
		}
		if (log.isDebugEnabled()) {
			log.debug(Messages.getMessage(Messages.BUILD_RESOURCE_URI_INFO,
					resource.getKey(), resourceURL));
		}
		return resourceURL;// context.getExternalContext().encodeResourceURL(resourceURL);

	}

	/**
	 * @param key
	 * @return
	 */
	public InternetResource getResourceForKey(String key)
			throws ResourceNotFoundException {

		Matcher matcher = DATA_SEPARATOR_PATTERN.matcher(key);
		if (matcher.find()) {
			int data = matcher.start();
			key = key.substring(0, data);
		}

		return getResource(key);
	}

	public Object getResourceDataForKey(String key) {
		Object data = null;
		String dataString = null;
		Matcher matcher = DATA_SEPARATOR_PATTERN.matcher(key);
		if (matcher.find()) {
			if (log.isDebugEnabled()) {
				log.debug(Messages.getMessage(
						Messages.RESTORE_DATA_FROM_RESOURCE_URI_INFO, key,
						dataString));
			}
			int dataStart = matcher.end();
			dataString = key.substring(dataStart);
			byte[] objectArray = null;
			byte[] dataArray;
			try {
				dataArray = dataString.getBytes("ISO-8859-1");
				objectArray = decrypt(dataArray);
			} catch (UnsupportedEncodingException e1) {
				// default encoding always presented.
			}
			if ("B".equals(matcher.group(1))) {
				data = objectArray;
			} else {
				try {
					ObjectInputStream in = new ObjectInputStream(
							new ByteArrayInputStream(objectArray));
					data = in.readObject();
				} catch (StreamCorruptedException e) {
					log.error(Messages
							.getMessage(Messages.STREAM_CORRUPTED_ERROR), e);
				} catch (IOException e) {
					log.error(Messages
							.getMessage(Messages.DESERIALIZE_DATA_INPUT_ERROR),
							e);
				} catch (ClassNotFoundException e) {
					log
							.error(
									Messages
											.getMessage(Messages.DATA_CLASS_NOT_FOUND_ERROR),
									e);
				}
			}
		}

		return data;
	}

	public InternetResource getResource(String path)
			throws ResourceNotFoundException {

		InternetResource internetResource = (InternetResource) resources
				.get(path);
		if (null == internetResource) {
			throw new ResourceNotFoundException("Resource not registered : "
					+ path);
		} else {
			return internetResource;
		}
	}

	public void addResource(InternetResource resource) {
		resources.put(resource.getKey(), resource);
		ResourceRenderer renderer = resource.getRenderer(null);
		if (renderer == null) {
			setRenderer(resource, resource.getKey());
		}
	}

	public void addResource(String key, InternetResource resource) {
		resources.put(key, resource);
		resource.setKey(key);
		// TODO - set renderer ?
	}

	// public String getFacesResourceKey(HttpServletRequest request) {
	// return getWebXml(context).getFacesResourceKey(request);
	// }

	public String getFacesResourceURL(FacesContext context, String Url, boolean isGlobal) {
		return getWebXml(context).getFacesResourceURL(context, Url, isGlobal);
	}

	/**
	 * Build resource for link to static context in webapp.
	 * 
	 * @param path
	 * @return
	 * @throws FacesException
	 */
	protected InternetResource createStaticResource(String path)
			throws ResourceNotFoundException, FacesException {
		FacesContext context = FacesContext.getCurrentInstance();
		if (null != context) {
			if (context.getExternalContext().getContext() instanceof ServletContext) {
				ServletContext servletContext = (ServletContext) context
						.getExternalContext().getContext();
				InputStream in = servletContext.getResourceAsStream(path);
				if (null != in) {
					InternetResourceBase res = new StaticResource(path);
					setRenderer(res, path);
					res.setLastModified(new Date(getStartTime()));
					addResource(path, res);
					try {
						in.close();
					} catch (IOException e) {
					}
					return res;
				}
			}
		}
		throw new ResourceNotFoundException(Messages.getMessage(
				Messages.STATIC_RESOURCE_NOT_FOUND_ERROR, path));
	}

	private void setRenderer(InternetResource res, String path)
			throws FacesException {
		int lastPoint = path.lastIndexOf('.');
		if (lastPoint > 0) {
			String ext = path.substring(lastPoint);
			ResourceRenderer resourceRenderer = (ResourceRenderer) renderers
					.get(ext);
			if (null != resourceRenderer) {
				res.setRenderer(resourceRenderer);
			} else {
				if (log.isDebugEnabled()) {
					log.debug(Messages.getMessage(
							Messages.NO_RESOURCE_REGISTERED_ERROR_2, path,
							renderers.keySet()));
				}

				// String mimeType = servletContext.getMimeType(path);
				res.setRenderer(defaultRenderer);
			}
		}
	}

	/**
	 * Create resurce to send from classpath relative to base class.
	 * 
	 * @param base
	 * @param path
	 * @return
	 * @throws FacesException
	 */
	protected InternetResource createJarResource(Object base, String path)
			throws ResourceNotFoundException, FacesException {
		String key = buildKey(base, path);
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		if (null != loader.getResource(key)) {
			JarResource res = new JarResource(key);
			setRenderer(res, path);
			res.setLastModified(new Date(getStartTime()));
			addResource(key, res);
			return res;
		} else {
			throw new ResourceNotFoundException(Messages.getMessage(
					Messages.NO_RESOURCE_EXISTS_ERROR, key));
		}

	}

	/**
	 * Create resource by instatiate given class.
	 * 
	 * @param path
	 * @param instatiate
	 * @return
	 */
	protected InternetResource createDynamicResource(String path,
			Class<?> instatiate) throws ResourceNotFoundException {
		if (InternetResource.class.isAssignableFrom(instatiate)) {
			InternetResource resource;
			try {
				resource = (InternetResource) instatiate.newInstance();
				addResource(path, resource);
			} catch (Exception e) {
				String message = Messages.getMessage(
						Messages.INSTANTIATE_RESOURCE_ERROR, instatiate
								.toString());
				log.error(message, e);
				throw new ResourceNotFoundException(message, e);
			}
			return resource;
		}
		throw new FacesException(Messages
				.getMessage(Messages.INSTANTIATE_CLASS_ERROR));
	}

	/**
	 * Create resource by instatiate {@link UserResource} class with given
	 * properties ( or got from cache ).
	 * 
	 * @param cacheable
	 * @param session
	 * @param mime
	 * @return
	 * @throws FacesException
	 */
	public InternetResource createUserResource(boolean cacheable,
			boolean session, String mime) throws FacesException {
		String path = getUserResourceKey(cacheable, session, mime);
		InternetResource userResource;
		try {
			userResource = getResource(path);
		} catch (ResourceNotFoundException e) {
			userResource = new UserResource(cacheable, session, mime);
			addResource(path, userResource);
		}
		return userResource;
	}

	/**
	 * Generate resource key for user-generated resource with given properties.
	 * 
	 * @param cacheable
	 * @param session
	 * @param mime
	 * @return
	 */
	private String getUserResourceKey(boolean cacheable, boolean session,
			String mime) {
		StringBuffer pathBuffer = new StringBuffer(UserResource.class.getName());
		pathBuffer.append(cacheable ? "/c" : "/n");
		pathBuffer.append(session ? "/s" : "/n");
		if (null != mime) {
			pathBuffer.append('/').append(mime.hashCode());
		}
		String path = pathBuffer.toString();
		return path;
	}

	/**
	 * @return Returns the startTime for application.
	 */
	public long getStartTime() {
		return _startTime;
	}

	protected byte[] encrypt(byte[] src) {
		try {
			Deflater compressor = new Deflater(Deflater.BEST_SPEED);
			byte[] compressed = new byte[src.length + 100];
			compressor.setInput(src);
			compressor.finish();
			int totalOut = compressor.deflate(compressed);
			byte[] zipsrc = new byte[totalOut];
			System.arraycopy(compressed, 0, zipsrc, 0, totalOut);
			compressor.end();
			return codec.encode(zipsrc);
		} catch (Exception e) {
			throw new FacesException("Error encode resource data", e);
		}
	}

	protected byte[] decrypt(byte[] src) {
		try {
			byte[] zipsrc = codec.decode(src);
			Inflater decompressor = new Inflater();
			byte[] uncompressed = new byte[zipsrc.length * 5];
			decompressor.setInput(zipsrc);
			int totalOut = decompressor.inflate(uncompressed);
			byte[] out = new byte[totalOut];
			System.arraycopy(uncompressed, 0, out, 0, totalOut);
			decompressor.end();
			return out;
		} catch (Exception e) {
			throw new FacesException("Error decode resource data", e);
		}
	}

	@Override
	public ResourceRenderer getScriptRenderer() {
		return scriptRenderer;
	}

	@Override
	public ResourceRenderer getStyleRenderer() {
		return styleRenderer;
	}
}
