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.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;

public class JRVirtualPrintPage implements JRPrintPage, JRVirtualizable, Serializable {
  protected static final Log log = LogFactory.getLog(JRVirtualPrintPage.class);
  
  private static final long serialVersionUID = 10200L;
  
  public static class ObjectIDPair implements Serializable {
    private static final long serialVersionUID = 10200L;
    
    private final Object o;
    
    private final int id;
    
    public ObjectIDPair(Object o) {
      this.o = o;
      this.id = System.identityHashCode(o);
    }
    
    public Object getObject() {
      return this.o;
    }
    
    public int getIdentity() {
      return this.id;
    }
  }
  
  private static final Random random = new Random(System.currentTimeMillis());
  
  private static short counter = 1;
  
  protected List elements = new ArrayList();
  
  private String uid;
  
  private transient JRVirtualizer virtualizer;
  
  private transient IdentityDataProvider[] identityProviders;
  
  protected JRVirtualizationContext virtualizationContext;
  
  public JRVirtualPrintPage(JasperPrint printObject, JRVirtualizer virtualizer, JRVirtualizationContext virtualizationContext) {
    this.virtualizationContext = virtualizationContext;
    this.uid = makeUID(printObject);
    this.virtualizer = virtualizer;
    this.identityProviders = null;
    if (virtualizer != null)
      virtualizer.registerObject(this); 
  }
  
  private static String makeUID(JasperPrint printObject) {
    synchronized (random) {
      counter = (short)(counter + 1);
      return Integer.toString(System.identityHashCode(printObject)) + "_" + printObject.getPages().size() + "_" + Integer.toString(counter) + "_" + Integer.toString(random.nextInt());
    } 
  }
  
  public final String getUID() {
    return this.uid;
  }
  
  public void setVirtualData(Object o) {
    this.elements = (List)o;
  }
  
  public Object getVirtualData() {
    return this.elements;
  }
  
  public void removeVirtualData() {
    this.elements = null;
  }
  
  public void setIdentityData(Object o) {
    if (this.identityProviders != null)
      for (int i = 0; i < this.identityProviders.length; i++)
        this.identityProviders[i].setIdentityData(this, (ObjectIDPair[])o);  
  }
  
  public Object getIdentityData() {
    ObjectIDPair[] data;
    if (this.identityProviders != null) {
      if (this.identityProviders.length == 1) {
        data = this.identityProviders[0].getIdentityData(this);
      } else if (this.identityProviders.length > 1) {
        Set list = new HashSet();
        for (int i = 0; i < this.identityProviders.length; i++) {
          ObjectIDPair[] pairs = this.identityProviders[i].getIdentityData(this);
          if (pairs != null)
            for (int j = 0; j < pairs.length; j++)
              list.add(pairs[j]);  
        } 
        data = list.<ObjectIDPair>toArray(new ObjectIDPair[list.size()]);
      } else {
        data = null;
      } 
    } else {
      data = null;
    } 
    return data;
  }
  
  public boolean isVirtualized() {
    return (this.elements == null);
  }
  
  public void setVirtualizer(JRVirtualizer virtualizer) {
    this.virtualizer = virtualizer;
  }
  
  public JRVirtualizer getVirtualizer() {
    return this.virtualizer;
  }
  
  public void addIdentityDataProvider(IdentityDataProvider p) {
    if (this.identityProviders == null) {
      this.identityProviders = new IdentityDataProvider[] { p };
    } else {
      IdentityDataProvider[] newList = new IdentityDataProvider[this.identityProviders.length + 1];
      System.arraycopy(this.identityProviders, 0, newList, 0, this.identityProviders.length);
      newList[this.identityProviders.length] = p;
      this.identityProviders = newList;
    } 
  }
  
  public void removeIdentityDataProvider(IdentityDataProvider p) {
    if (this.identityProviders != null)
      for (int idx = 0; idx < this.identityProviders.length; idx++) {
        if (this.identityProviders[idx] == p) {
          IdentityDataProvider[] newList = new IdentityDataProvider[this.identityProviders.length - 1];
          System.arraycopy(this.identityProviders, 0, newList, 0, idx);
          int remaining = this.identityProviders.length - idx - 1;
          if (remaining > 0)
            System.arraycopy(this.identityProviders, idx + 1, newList, idx, remaining); 
          this.identityProviders = newList;
          break;
        } 
      }  
  }
  
  public List getElements() {
    ensureVirtualData();
    return this.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();
    this.elements.add(element);
    cacheInContext(element);
  }
  
  public static interface IdentityDataProvider {
    JRVirtualPrintPage.ObjectIDPair[] getIdentityData(JRVirtualPrintPage param1JRVirtualPrintPage);
    
    void setIdentityData(JRVirtualPrintPage param1JRVirtualPrintPage, JRVirtualPrintPage.ObjectIDPair[] param1ArrayOfObjectIDPair);
  }
  
  protected static class JRIdHolderRenderer implements JRRenderable, Serializable {
    private static final long serialVersionUID = 10200L;
    
    protected final String id;
    
    protected JRIdHolderRenderer(JRRenderable renderer) {
      this.id = renderer.getId();
    }
    
    public String getId() {
      return this.id;
    }
    
    public byte getType() {
      return 0;
    }
    
    public byte getImageType() {
      return 0;
    }
    
    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 = 10200L;
    
    protected JRIdHolderTemplateElement(String id) {
      super(id);
    }
  }
  
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    this.uid = (String)in.readObject();
    this.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);
    this.elements = (List)elementsStream.readObject();
    afterInternalization();
    setThreadVirtualizer();
  }
  
  private void writeObject(ObjectOutputStream out) throws IOException {
    ensureVirtualData();
    beforeExternalization();
    try {
      out.writeObject(this.uid);
      out.writeObject(this.virtualizationContext);
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream stream = new ObjectOutputStream(bout);
      stream.writeObject(this.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) {
      this.virtualizer = threadVirtualizer;
      this.virtualizer.registerObject(this);
    } 
  }
  
  protected void finalize() {
    if (this.virtualizer != null)
      this.virtualizer.deregisterObject(this); 
  }
  
  protected List getDeepElements() {
    List deepElements = new ArrayList(this.elements.size());
    collectDeepElements(this.elements, deepElements);
    return deepElements;
  }
  
  protected void collectDeepElements(List elementsList, List deepElements) {
    for (Iterator it = elementsList.iterator(); it.hasNext(); ) {
      JRPrintElement element = 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 && this.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 = it.next();
        cacheInContext(element);
      }  
  }
  
  protected void cacheInContext(JRPrintElement element) {
    if (element instanceof JRTemplatePrintElement) {
      JRTemplatePrintElement templateElement = (JRTemplatePrintElement)element;
      JRTemplateElement template = templateElement.getTemplate();
      if (template != null)
        this.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 this.virtualizationContext;
  }
  
  public void afterExternalization() {
    restoreElementsData();
  }
  
  protected void traverseDeepElements(ElementVisitor visitor) {
    traverseDeepElements(visitor, this.elements);
  }
  
  protected void traverseDeepElements(ElementVisitor visitor, List elementsList) {
    for (Iterator it = elementsList.iterator(); it.hasNext(); ) {
      JRPrintElement element = it.next();
      visitor.visitElement(element);
      if (element instanceof JRPrintFrame) {
        JRPrintFrame frame = (JRPrintFrame)element;
        traverseDeepElements(visitor, frame.getElements());
      } 
    } 
  }
  
  protected static interface ElementVisitor {
    void visitElement(JRPrintElement param1JRPrintElement);
  }
  
  protected class ExternalizationElementVisitor implements ElementVisitor {
    private final Map idTemplates = new HashMap();
    
    private final JRVirtualPrintPage this$0;
    
    public void visitElement(JRPrintElement element) {
      if (element instanceof JRTemplatePrintElement)
        setExternalizationTemplate((JRTemplatePrintElement)element); 
      if (element instanceof JRPrintImage)
        JRVirtualPrintPage.this.setExternalizationRenderer((JRPrintImage)element); 
    }
    
    protected void setExternalizationTemplate(JRTemplatePrintElement templateElement) {
      JRTemplateElement template = templateElement.getTemplate();
      if (template != null)
        if (JRVirtualPrintPage.this.virtualizationContext.hasCachedTemplate(template.getId())) {
          String templateId = template.getId();
          JRVirtualPrintPage.JRIdHolderTemplateElement idTemplate = (JRVirtualPrintPage.JRIdHolderTemplateElement)this.idTemplates.get(templateId);
          if (idTemplate == null) {
            idTemplate = new JRVirtualPrintPage.JRIdHolderTemplateElement(templateId);
            this.idTemplates.put(templateId, idTemplate);
          } 
          templateElement.setTemplate(idTemplate);
        } else if (JRVirtualPrintPage.log.isDebugEnabled()) {
          JRVirtualPrintPage.log.debug("Template " + template + " having id " + template.getId() + " not found in virtualization context cache");
        }  
    }
  }
  
  protected class InternalizationElementVisitor implements ElementVisitor {
    private final JRVirtualPrintPage this$0;
    
    public void visitElement(JRPrintElement element) {
      if (element instanceof JRTemplatePrintElement)
        restoreTemplate((JRTemplatePrintElement)element); 
      if (element instanceof JRPrintImage)
        restoreRenderer((JRPrintImage)element); 
    }
    
    protected void restoreTemplate(JRTemplatePrintElement element) {
      JRTemplateElement template = element.getTemplate();
      if (template != null && template instanceof JRVirtualPrintPage.JRIdHolderTemplateElement) {
        JRTemplateElement cachedTemplate = JRVirtualPrintPage.this.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 JRVirtualPrintPage.JRIdHolderRenderer) {
        JRRenderable cachedRenderer = JRVirtualPrintPage.this.virtualizationContext.getCachedRenderer(renderer.getId());
        if (cachedRenderer == null)
          throw new JRRuntimeException("Renderer " + renderer.getId() + " not found in virtualization context."); 
        image.setRenderer(cachedRenderer);
      } 
    }
  }
}
