/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2005 - 2009 Works, Inc. All rights reserved.
 * http://www.works.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports 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 JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Licensed to Jaspersoft Corporation under a Contributer Agreement
 */
package net.sf.jasperreports.engine.base;

import java.awt.Graphics2D;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import net.sf.jasperreports.engine.JRConstants;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.JRPrintFrame;
import net.sf.jasperreports.engine.JRPrintImage;
import net.sf.jasperreports.engine.JRPrintPage;
import net.sf.jasperreports.engine.JRRenderable;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRVirtualizable;
import net.sf.jasperreports.engine.JRVirtualizationHelper;
import net.sf.jasperreports.engine.JRVirtualizer;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.fill.JRTemplateElement;
import net.sf.jasperreports.engine.fill.JRTemplatePrintElement;
import net.sf.jasperreports.engine.fill.JRVirtualizationContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A print page that can be virtualized to free heap memory.
 * 
 * @author John Bindel
 * @version $Id: JRVirtualPrintPage.java 3717 2010-04-09 10:01:33Z teodord $
 */
public class JRVirtualPrintPage implements JRPrintPage, JRVirtualizable, Serializable
{
	protected static final Log log = LogFactory.getLog(JRVirtualPrintPage.class);
	
	/**
	 * Identity objects are those that we want to replace when we devirtualize
	 * data. If object A was virtualized, and it is referenced outside the
	 * virtualized data, then we want to replace those references with object
	 * A', which is the version of the object that has been devirtualized. For
	 * example the Serialization mechanism creates a new version of the
	 * TextElement we want to be filled, but the bound object map references the
	 * original object A until we replace it with the new version A'.
	 */
	public static class ObjectIDPair implements Serializable {
		/**
		 * 
		 */
		private static final long serialVersionUID = JRConstants.SERIAL_VERSION_UID;

		private final Object o;

		private final int id;

		public ObjectIDPair(Object o) {
			this.o = o;
			this.id = System.identityHashCode(o);
		}

		/**
		 * Gets the object.
		 */
		public Object getObject() {
			return o;
		}

		/**
		 * Gets the identity of the object. The identity is the current object's
		 * identity hash code before we deserialize, but when we have
		 * deserialized it, the identity is that of the object that was
		 * serialized, not that of the newly deserialized object.
		 */
		public int getIdentity() {
			return id;
		}
	}

	/**
	 * Classes that want to deal with the identity data should implement this.
	 * The JRBaseFiller needs to do this.
	 */
	public static interface IdentityDataProvider {
		/**
		 * Get identity data that the provider later want to handle when the
		 * virtual object is paged in.
		 */
		ObjectIDPair[] getIdentityData(JRVirtualPrintPage page);

		/**
		 * Handle the identity data as necessary.
		 */
		void setIdentityData(JRVirtualPrintPage page,
				ObjectIDPair[] identityData);
	}

	private static final long serialVersionUID = JRConstants.SERIAL_VERSION_UID;

	private static final Random random = new Random(System.currentTimeMillis());

	private static short counter = 1;
	
	
	protected List elements = new ArrayList();

	/**
	 * A unique identifier that is useful for serialization and deserialization
	 * to some persistence mechanism.
	 */
	private String uid;

	/**
	 * The object that does the virtualization work.
	 */
	private transient JRVirtualizer virtualizer;

	/**
	 * The filler object which has our identity data.
	 */
	private transient IdentityDataProvider[] identityProviders;

	protected JRVirtualizationContext virtualizationContext;
	
	/**
	 * Constructs a virtualizable page.
	 */
	public JRVirtualPrintPage(JasperPrint printObject, JRVirtualizer virtualizer, JRVirtualizationContext virtualizationContext) {
		super();
		
		this.virtualizationContext = virtualizationContext;

		this.uid = makeUID(printObject);
		this.virtualizer = virtualizer;
		this.identityProviders = null;
		if (virtualizer != null) {
			virtualizer.registerObject(this);
		}
	}
	
	
	/**
	 * Make some unique identifier for this object.
	 */
	private static String makeUID(JasperPrint printObject) {
		synchronized (random) {
			return Integer.toString(System.identityHashCode(printObject)) + "_"
					+ (printObject.getPages().size()) + "_"
					+ Integer.toString(counter++) + "_"
					+ Integer.toString(random.nextInt());
		}
	}

	/**
	 * Make a new identifier for an object.
	 * 
	 * @return the new identifier
	 */
	private static String makeUID(JRVirtualPrintPage page) {
		synchronized (random) {
			return Integer.toString(System.identityHashCode(page)) + "_"
					+ Integer.toString(counter++) + "_"
					+ Integer.toString(random.nextInt());
		}
	}

	public final String getUID() {
		return this.uid;
	}

	public void setVirtualData(Object o)
	{
		elements = (List) o;
	}
	
	public Object getVirtualData()
	{
		return elements;
	}

	public void removeVirtualData() {
		elements = null;
	}

	public void setIdentityData(Object o) {
		if (identityProviders != null) {
			for (int i = 0; i < identityProviders.length; ++i) {
				identityProviders[i].setIdentityData(this, (ObjectIDPair[]) o);
			}
		}
	}

	public Object getIdentityData() {
		ObjectIDPair[] data;
		if (identityProviders != null) {
			if (identityProviders.length == 1) {
				data = identityProviders[0].getIdentityData(this);
			} else if (identityProviders.length > 1) {
				Set list = new HashSet();
				for (int i = 0; i < identityProviders.length; ++i) {
					ObjectIDPair[] pairs = identityProviders[i]
							.getIdentityData(this);
					if (pairs != null) {
						for (int j = 0; j < pairs.length; ++j) {
							list.add(pairs[j]);
						}
					}
				}
				data = (ObjectIDPair[]) list.toArray(new ObjectIDPair[list
						.size()]);
			} else {
				data = null;
			}
		} else {
			data = null;
		}

		return data;
	}

	public boolean isVirtualized() {
		return elements == null;
	}

	/**
	 * Sets the virtualizer.
	 */
	public void setVirtualizer(JRVirtualizer virtualizer) {
		this.virtualizer = virtualizer;
	}

	/**
	 * Gets the virtualizer.
	 */
	public JRVirtualizer getVirtualizer() {
		return this.virtualizer;
	}

	public void addIdentityDataProvider(IdentityDataProvider p) {
		if (identityProviders == null) {
			identityProviders = new IdentityDataProvider[] { p };
		} else {
			IdentityDataProvider[] newList = new IdentityDataProvider[identityProviders.length + 1];
			System.arraycopy(identityProviders, 0, newList, 0,
					identityProviders.length);
			newList[identityProviders.length] = p;
			identityProviders = newList;
		}
	}

	public void removeIdentityDataProvider(IdentityDataProvider p) {
		if (identityProviders != null) {
			int idx;
			for (idx = 0; idx < identityProviders.length; ++idx) {
				if (identityProviders[idx] == p) {
					IdentityDataProvider[] newList = new IdentityDataProvider[identityProviders.length - 1];
					System.arraycopy(identityProviders, 0, newList, 0, idx);
					int remaining = identityProviders.length - idx - 1;
					if (remaining > 0) {
						System.arraycopy(identityProviders, idx + 1, newList,
								idx, remaining);
					}
					identityProviders = newList;
					break;
				}
			}
		}
	}

	public List getElements()
	{
		ensureVirtualData();
		return elements;
	}

	protected void ensureVirtualData()
	{
		if (this.virtualizer != null)
		{			
			this.virtualizer.requestData(this);
		}
	}

	public void setElements(List elements) {
		cleanVirtualData();
		this.elements = elements;
		cacheInContext(this.elements);
	}

	protected void cleanVirtualData()
	{
		if (this.virtualizer != null)
		{
			this.virtualizer.clearData(this);
		}
	}

	
	public void addElement(JRPrintElement element)
	{
		ensureVirtualData();
		elements.add(element);
		cacheInContext(element);
	}
	
	
	/**
	 * Dummy image renderer that only stores the ID of a cached renderer.
	 * When a page gets serialized, all image renderers that are cached in the
	 * virtualization context are replaced with dummy renderers that only store the ID.
	 * When a page gets deserialized, the original renderers are restored from the 
	 * virtualization context based on the ID.
	 */
	protected static class JRIdHolderRenderer implements JRRenderable, Serializable
	{
		private static final long serialVersionUID = JRConstants.SERIAL_VERSION_UID;
		
		protected final String id;
		
		protected JRIdHolderRenderer(JRRenderable renderer)
		{
			this.id = renderer.getId();
		}

		public String getId()
		{
			return id;
		}

		public byte getType()
		{
			return TYPE_IMAGE;
		}

		public byte getImageType()
		{
			return IMAGE_TYPE_UNKNOWN;
		}

		public Dimension2D getDimension() throws JRException
		{
			return null;
		}

		public byte[] getImageData() throws JRException
		{
			return null;
		}

		public void render(Graphics2D grx, Rectangle2D rectanle) throws JRException
		{
		}
	}
	
	
	protected static class JRIdHolderTemplateElement extends JRTemplateElement
	{
		private static final long serialVersionUID = JRConstants.SERIAL_VERSION_UID;
		
		protected JRIdHolderTemplateElement(String id)
		{
			super(id);
		}
	}
	
	private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		// we do not use the original ID as the source object might still be
		// alive in the JVM
		String oldUid = (String) in.readObject();
		uid = makeUID(this);
		if (log.isDebugEnabled())
		{
			log.debug("Original uid " + oldUid + "; new uid " + uid);
		}
		
		virtualizationContext = (JRVirtualizationContext) in.readObject();
		
		int length = in.readInt();
		byte[] buffer = new byte[length];
		in.readFully(buffer);
		ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer, 0, buffer.length);
		ObjectInputStream elementsStream = new ObjectInputStream(inputStream);
		elements = (List) elementsStream.readObject();
		afterInternalization();
		
		setThreadVirtualizer();
	}

	
	private void writeObject(java.io.ObjectOutputStream out) throws IOException
	{
		ensureVirtualData();
		beforeExternalization();
		
		try
		{
			// maybe we should no longer serialize the id, as a new one is
			// generated on deserialization
			out.writeObject(uid);
			out.writeObject(virtualizationContext);

			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			ObjectOutputStream stream = new ObjectOutputStream(bout);
			stream.writeObject(elements);
			stream.flush();

			byte[] bytes = bout.toByteArray();
			out.writeInt(bytes.length);
			out.write(bytes);
		}
		finally
		{
			afterExternalization();
		}
	}

	
	private void setThreadVirtualizer()
	{
		JRVirtualizer threadVirtualizer = JRVirtualizationHelper.getThreadVirtualizer();
		if (threadVirtualizer != null)
		{
			virtualizer = threadVirtualizer;
			virtualizer.registerObject(this);
		}
	}


	protected void finalize() throws Throwable //NOSONAR
	{
		if (virtualizer != null)
		{
			virtualizer.deregisterObject(this);
		}
		
		super.finalize();
	}
	
	
	/**
	 * Returns all the elements on the page, including the ones placed inside
	 * {@link JRPrintFrame frames}.
	 * 
	 * @return all the elements on the page
	 */
	protected List getDeepElements()
	{
		List deepElements = new ArrayList(elements.size());
		collectDeepElements(elements, deepElements);
		return deepElements;
	}

	protected void collectDeepElements(List elementsList, List deepElements)
	{
		for (Iterator it = elementsList.iterator(); it.hasNext();)
		{
			JRPrintElement element = (JRPrintElement) it.next();
			deepElements.add(element);
			
			if (element instanceof JRPrintFrame)
			{
				JRPrintFrame frame = (JRPrintFrame) element;
				collectDeepElements(frame.getElements(), deepElements);
			}
		}
	}


	public void beforeExternalization()
	{
		setElementsExternalData();
	}


	protected void setElementsExternalData()
	{
		traverseDeepElements(new ExternalizationElementVisitor());
	}


	protected void setExternalizationRenderer(JRPrintImage image)
	{
		JRRenderable renderer = image.getRenderer();
		if (renderer != null && virtualizationContext.hasCachedRenderer(renderer.getId()))
		{
			image.setRenderer(new JRIdHolderRenderer(renderer));
		}
	}


	protected void cacheInContext(List elementList)
	{
		if (elementList != null && !elementList.isEmpty())
		{
			for (Iterator it = elementList.iterator(); it.hasNext();)
			{
				JRPrintElement element = (JRPrintElement) it.next();
				cacheInContext(element);
			}
		}
	}

	
	protected void cacheInContext(JRPrintElement element)
	{
		if (element instanceof JRTemplatePrintElement)
		{
			JRTemplatePrintElement templateElement = (JRTemplatePrintElement) element;
			JRTemplateElement template = templateElement.getTemplate();
			if (template != null)
			{
				virtualizationContext.cacheTemplate(template);
			}
		}
		
		if (element instanceof JRPrintFrame)
		{
			JRPrintFrame frame = (JRPrintFrame) element;
			cacheInContext(frame.getElements());
		}
	}

	public void afterInternalization()
	{
		restoreElementsData();
	}


	protected void restoreElementsData()
	{
		traverseDeepElements(new InternalizationElementVisitor());
	}


	public JRVirtualizationContext getContext()
	{
		return virtualizationContext;
	}


	public void afterExternalization()
	{
		restoreElementsData();
	}
	
	
	/**
	 * Traverses all the elements on the page, including the ones placed inside
	 * {@link JRPrintFrame frames}.
	 * 
	 * @param visitor element visitor
	 */
	protected void traverseDeepElements(ElementVisitor visitor)
	{
		traverseDeepElements(visitor, elements);
	}

	protected void traverseDeepElements(ElementVisitor visitor, List elementsList)
	{
		for (Iterator it = elementsList.iterator(); it.hasNext();)
		{
			JRPrintElement element = (JRPrintElement) it.next();
			visitor.visitElement(element);
			
			if (element instanceof JRPrintFrame)
			{
				JRPrintFrame frame = (JRPrintFrame) element;
				traverseDeepElements(visitor, frame.getElements());
			}
		}
	}
	
	protected static interface ElementVisitor
	{
		void visitElement(JRPrintElement element);
	}
	
	
	protected class ExternalizationElementVisitor implements ElementVisitor
	{
		private final Map idTemplates = new HashMap();

		public void visitElement(JRPrintElement element)
		{
			// replacing element template with dummy template that only stores the template ID
			if (element instanceof JRTemplatePrintElement)
			{
				setExternalizationTemplate((JRTemplatePrintElement) element);
			}
			
			// replacing image renderer cached in the virtualization context 
			// with dummy renderer that only stores the renderer ID
			if (element instanceof JRPrintImage)
			{
				setExternalizationRenderer((JRPrintImage) element);
			}
		}

		protected void setExternalizationTemplate(JRTemplatePrintElement templateElement)
		{
			JRTemplateElement template = templateElement.getTemplate();
			if (template != null)
			{
				if (virtualizationContext.hasCachedTemplate(template.getId()))
				{
					String templateId = template.getId();
					JRIdHolderTemplateElement idTemplate = (JRIdHolderTemplateElement) idTemplates.get(templateId);
					if (idTemplate == null)
					{
						idTemplate = new JRIdHolderTemplateElement(templateId);
						idTemplates.put(templateId, idTemplate);
					}
					templateElement.setTemplate(idTemplate);
				}
				else
				{
					if (log.isDebugEnabled())
					{
						log.debug("Template " + template + " having id " + template.getId() + " not found in virtualization context cache");
					}
				}
			}
		}
	}
	
	protected class InternalizationElementVisitor implements ElementVisitor
	{

		public void visitElement(JRPrintElement element)
		{			
			if (element instanceof JRTemplatePrintElement)
			{
				// restore the cached element template from the virtualization context
				restoreTemplate((JRTemplatePrintElement) element);
			}
			
			if (element instanceof JRPrintImage)
			{
				// restore the cached image rendere from the virtualization context
				restoreRenderer((JRPrintImage) element);
			}
		}

		protected void restoreTemplate(JRTemplatePrintElement element)
		{
			JRTemplateElement template = element.getTemplate();
			if (template != null && template instanceof JRIdHolderTemplateElement)
			{
				JRTemplateElement cachedTemplate = virtualizationContext.getCachedTemplate(template.getId());
				if (cachedTemplate == null)
				{
					throw new JRRuntimeException("Template " + template.getId() + " not found in virtualization context.");
				}

				element.setTemplate(cachedTemplate);
			}
		}

		protected void restoreRenderer(JRPrintImage image)
		{
			JRRenderable renderer = image.getRenderer();
			if (renderer != null && renderer instanceof JRIdHolderRenderer)
			{
				JRRenderable cachedRenderer = virtualizationContext.getCachedRenderer(renderer.getId());
				if (cachedRenderer == null)
				{
					throw new JRRuntimeException("Renderer " + renderer.getId() + " not found in virtualization context.");
				}
				image.setRenderer(cachedRenderer);
			}
		}		
	}
}
