/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.gvt.renderer;

import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.RenderContext;
import java.lang.ref.SoftReference;
import java.util.Collection;

import org.apache.batik.ext.awt.geom.RectListManager;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.ext.awt.image.PadMode;
import org.apache.batik.ext.awt.image.renderable.Filter;
import org.apache.batik.ext.awt.image.rendered.CachableRed;
import org.apache.batik.ext.awt.image.rendered.PadRed;
import org.apache.batik.ext.awt.image.rendered.TileCacheRed;
import org.apache.batik.ext.awt.image.rendered.TranslateRed;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.HaltingThread;

/**
 * Simple implementation of the Renderer that simply does static
 * rendering in an offscreen buffer image.
 *
 * @author <a href="mailto:vincent.hardy@eng.sun.com">Vincent Hardy</a>
 * @version $Id$
 */
public class StaticRenderer implements ImageRenderer {

    /**
     * Tree this Renderer paints.
     */
    protected GraphicsNode      rootGN;
    protected Filter            rootFilter;
    protected CachableRed       rootCR;
    protected SoftReference     lastCR;
    protected SoftReference     lastCache;

    /**
     * Flag for double buffering.
     */
    protected boolean isDoubleBuffered = false;

    /**
     * Offscreen image where the Renderer does its rendering
     */
    protected WritableRaster currentBaseRaster;
    protected WritableRaster currentRaster;
    protected BufferedImage  currentOffScreen;

    protected WritableRaster workingBaseRaster;
    protected WritableRaster workingRaster;
    protected BufferedImage  workingOffScreen;

    protected int offScreenWidth;
    protected int offScreenHeight;

    /**
     * Passed to the GVT tree to describe the rendering environment
     */
    protected RenderingHints renderingHints;
    protected AffineTransform usr2dev;

    protected static RenderingHints defaultRenderingHints;
    static {
        defaultRenderingHints = new RenderingHints(null);
        defaultRenderingHints.put(RenderingHints.KEY_ANTIALIASING,
                                  RenderingHints.VALUE_ANTIALIAS_ON);

        defaultRenderingHints.put(RenderingHints.KEY_INTERPOLATION,
                                  RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    }

    /**
     * @param rh Hints for rendering.
     * @param at Starting user to device coordinate system transform.
     */
    public StaticRenderer(RenderingHints rh,
                          AffineTransform at){
        renderingHints = new RenderingHints(null);
        renderingHints.add(rh);
        usr2dev = new AffineTransform(at);
    }

    /**
     * Creates a new StaticRenderer object.
     */
    public StaticRenderer(){
        renderingHints = new RenderingHints(null);
        renderingHints.add(defaultRenderingHints);
        usr2dev = new AffineTransform();
    }


    /**
     * Disposes all resources of this renderer.
     */
    public void dispose() {
        rootGN     = null;
        rootFilter = null;
        rootCR     = null;

        workingOffScreen = null;
        workingBaseRaster = null;
        workingRaster = null;

        currentOffScreen = null;
        currentBaseRaster = null;
        currentRaster = null;

        renderingHints = null;
        lastCache = null;
        lastCR = null;
    }

    /**
     * This associates the given GVT Tree with this renderer.
     * Any previous tree association is forgotten.
     * Not certain if this should be just GraphicsNode, or CanvasGraphicsNode.
     */
    public void setTree(GraphicsNode rootGN){
        this.rootGN = rootGN;
        rootFilter  = null;
        rootCR      = null;

        workingOffScreen = null;
        workingRaster = null;

        currentOffScreen = null;
        currentRaster = null;

        // renderingHints = new RenderingHints(defaultRenderingHints);
    }

    /**
     * @return the GVT tree associated with this renderer
     */
    public GraphicsNode getTree(){
        return rootGN;
    }

    /**
     * @param rh Set of rendering hints to use for future renderings
     */
    public void setRenderingHints(RenderingHints rh) {
        renderingHints = new RenderingHints(null);
        renderingHints.add(rh);

        rootFilter = null;
        rootCR     = null;

        workingOffScreen = null;
        workingRaster = null;

        currentOffScreen = null;
        currentRaster = null;
    }

    /**
     * @return the RenderingHints which the Renderer is using for its
     *         rendering
     */
    public RenderingHints getRenderingHints() {
        return renderingHints;
    }

    /**
     * Sets the transform from the current user space (as defined by
     * the top node of the GVT tree, to the associated device space.
     *
     * @param usr2dev the new user space to device space transform. If null,
     *        the identity transform will be set.
     */
    public void setTransform(AffineTransform usr2dev){
        if (this.usr2dev.equals(usr2dev))
            return;

        if(usr2dev == null)
            this.usr2dev = new AffineTransform();
        else
            this.usr2dev = new AffineTransform(usr2dev);

        rootCR = null;
    }

    /**
     * Returns the transform from the current user space (as defined
     * by the top node of the GVT tree) to the device space.
     */
    public AffineTransform getTransform(){
        return usr2dev;
    }

    /**
     * Returns true if the Renderer is currently doubleBuffering is
     * rendering requests.  If it is then getOffscreen will only
     * return completed renderings (or null if nothing is available).
     */
    public boolean isDoubleBuffered(){
        return isDoubleBuffered;
    }

    /**
     * Turns on/off double buffering in renderer.  Turning off
     * double buffering makes it possible to see the ongoing results
     * of a render operation.
     *
     * @param isDoubleBuffered the new value for double buffering
     */
    public void setDoubleBuffered(boolean isDoubleBuffered){
        if (this.isDoubleBuffered == isDoubleBuffered)
            return;

        this.isDoubleBuffered = isDoubleBuffered;
        if (isDoubleBuffered) {
            // Now double buffering, so make sure they can't see work buffers.
            currentOffScreen  = null;
            currentBaseRaster = null;
            currentRaster     = null;
        } else {
            // No longer double buffering so join work and current buffers.
            currentOffScreen  = workingOffScreen;
            currentBaseRaster = workingBaseRaster;
            currentRaster     = workingRaster;
        }
    }


    /**
     * Update the size of the image to be returned by getOffScreen.
     * Note that this change will not be reflected by calls to
     * getOffscreen until either clearOffScreen has completed (when
     * isDoubleBuffered is false) or reapint has completed (when
     * isDoubleBuffered is true).
     *
     */
    public void updateOffScreen(int width, int height) {
        offScreenWidth  = width;
        offScreenHeight = height;
    }

    /**
     * Returns the current offscreen image.
     *
     * The exact symantics of this vary base on the value of
     * isDoubleBuffered.  If isDoubleBuffered is false this will
     * return the image currently being worked on as soon as it is
     * available.
     *
     * if isDoubleBuffered is false this will return the most recently
     * completed result of repaint.
     */
    public BufferedImage getOffScreen() {
        if (rootGN == null)
            return null;

        return currentOffScreen;
    }

    /**
     * Sets up and clears the current offscreen buffer.
     *
     * When not double buffering one should call this method before
     * calling getOffscreen to get the offscreen being drawn into.
     * This ensures the buffer is up to date and doesn't contain junk.
     *
     * When double buffering this call can effectively be skipped,
     * since getOffscreen will only refect the new rendering after
     * repaint completes.
     */
    public void clearOffScreen() {

        // No need to clear in double buffer case people will
        // only see it when it is done...
        if (isDoubleBuffered)
            return;

        updateWorkingBuffers();
        if ((rootCR == null)           ||
            (workingBaseRaster == null))
            return;

        ColorModel     cm         = rootCR.getColorModel();
        WritableRaster syncRaster = workingBaseRaster;

        // Ensure only one thread works on baseRaster at a time...
        synchronized (syncRaster) {
            BufferedImage bi = new BufferedImage
                (cm, workingBaseRaster, cm.isAlphaPremultiplied(), null);
            Graphics2D g2d = bi.createGraphics();
            g2d.setComposite(AlphaComposite.Clear);
            g2d.fillRect(0, 0, bi.getWidth(), bi.getHeight());
            g2d.dispose();
        }
    }


    /**
     * Repaints the associated GVT tree under <code>area</code>.
     *
     * If double buffered is true and this method completes cleanly it
     * will set the result of the repaint as the image returned by
     * getOffscreen otherwise the old image will still be returned.
     * If double buffered is false it is possible some effects of
     * the failed rendering will be visible in the image returned
     * by getOffscreen.
     *
     * @param area region to be repainted, in the current user space
     * coordinate system.
     */
    public void repaint(Shape area) {
        if (area == null) return;
        RectListManager rlm = new RectListManager();
        rlm.add(usr2dev.createTransformedShape(area).getBounds());
        repaint(rlm);
    }

    /**
     * Repaints the associated GVT tree under the list of <code>areas</code>.
     *
     * If double buffered is true and this method completes cleanly it
     * will set the result of the repaint as the image returned by
     * getOffscreen otherwise the old image will still be returned.
     * If double buffered is false it is possible some effects of
     * the failed rendering will be visible in the image returned
     * by getOffscreen.
     *
     * @param areas a List of regions to be repainted, in the current
     * user space coordinate system.
     */
    public void repaint(RectListManager areas) {

        if (areas == null)
            return;

        // System.out.println("Renderer Repainting");

        // long t0 = System.currentTimeMillis();

        CachableRed cr;
        WritableRaster syncRaster;
        WritableRaster copyRaster;

        // While we are synchronized pull all the relavent info out
        // of member variables into local variables.
        updateWorkingBuffers();
        if ((rootCR == null)           ||
            (workingBaseRaster == null))
            return;

        cr = rootCR;
        syncRaster = workingBaseRaster;
        copyRaster = workingRaster;

        Rectangle srcR = rootCR.getBounds();
        Rectangle dstR = workingRaster.getBounds();
        if ((dstR.x < srcR.x) ||
            (dstR.y < srcR.y) ||
            (dstR.x+dstR.width  > srcR.x+srcR.width) ||
            (dstR.y+dstR.height > srcR.y+srcR.height))
            cr = new PadRed(cr, dstR, PadMode.ZERO_PAD, null);

        // Ensure only one thread works on baseRaster at a time...
        synchronized (syncRaster) {
            cr.copyData(copyRaster);
        }

        if (!HaltingThread.hasBeenHalted()) {
            // Swap the buffers if the rendering completed cleanly.
            BufferedImage tmpBI = workingOffScreen;

            workingBaseRaster = currentBaseRaster;
            workingRaster     = currentRaster;
            workingOffScreen  = currentOffScreen;

            currentRaster     = copyRaster;
            currentBaseRaster = syncRaster;
            currentOffScreen  = tmpBI;

            // System.out.println("Current offscreen : " + currentOffScreen);
        }
    }

    /**
     * Flush any cached image data.
     */
    public void flush() {
        if (lastCache == null) return;
        Object o = lastCache.get();
        if (o == null) return;

        TileCacheRed tcr = (TileCacheRed)o;
        tcr.flushCache(tcr.getBounds());
    }

    /**
     * Flush a list of rectangles of cached image data.
     */
    public void flush(Collection areas) {
        AffineTransform at = getTransform();
        for (Object area : areas) {
            Shape s = (Shape) area;
            Rectangle r = at.createTransformedShape(s).getBounds();
            flush(r);
        }
    }

    /**
     * Flush a rectangle of cached image data.
     */
    public void flush(Rectangle r) {
        if (lastCache == null) return;
        Object o = lastCache.get();
        if (o == null) return;

        TileCacheRed tcr = (TileCacheRed)o;
        r = (Rectangle)r.clone();
        r.x -= Math.round((float)usr2dev.getTranslateX());
        r.y -= Math.round((float)usr2dev.getTranslateY());
        // System.out.println("Flushing Rect:" + r);
        tcr.flushCache(r);
    }

    protected CachableRed setupCache(CachableRed img) {
        if ((lastCR == null) ||
            (img != lastCR.get())) {
            lastCR    = new SoftReference(img);
            lastCache = null;
        }

        Object o = null;
        if (lastCache != null)
            o = lastCache.get();
        if (o != null)
            return (CachableRed)o;

        img       = new TileCacheRed(img);
        lastCache = new SoftReference(img);
        return img;
    }

    protected CachableRed renderGNR() {
        AffineTransform at, rcAT;
        at = usr2dev;
        rcAT = new AffineTransform(at.getScaleX(), at.getShearY(),
                                   at.getShearX(), at.getScaleY(),
                                   0, 0);

        RenderContext rc = new RenderContext(rcAT, null, renderingHints);

        RenderedImage ri = rootFilter.createRendering(rc);
        if (ri == null)
            return null;

        CachableRed ret;
        ret = GraphicsUtil.wrap(ri);
        ret = setupCache(ret);

        int dx = Math.round((float)at.getTranslateX());
        int dy = Math.round((float)at.getTranslateY());
        ret = new TranslateRed(ret, ret.getMinX()+dx, ret.getMinY()+dy);
        ret = GraphicsUtil.convertTosRGB(ret);

        return ret;
    }


    /**
     * Internal method used to synchronize local state in response to
     * various set methods.
     */
    protected void updateWorkingBuffers() {
        if (rootFilter == null) {
            rootFilter = rootGN.getGraphicsNodeRable(true);
            rootCR = null;
        }

        rootCR = renderGNR();
        if (rootCR == null) {
            // No image to display so clear everything out...
            workingRaster = null;
            workingOffScreen = null;
            workingBaseRaster = null;

            currentOffScreen = null;
            currentBaseRaster = null;
            currentRaster = null;
            return;
        }

        SampleModel sm = rootCR.getSampleModel();
        int         w  = offScreenWidth;
        int         h  = offScreenHeight;

        int tw = sm.getWidth();
        int th = sm.getHeight();
        w = (((w+tw-1)/tw)+1)*tw;
        h = (((h+th-1)/th)+1)*th;

        if ((workingBaseRaster == null) ||
            (workingBaseRaster.getWidth()  < w) ||
            (workingBaseRaster.getHeight() < h)) {

            sm = sm.createCompatibleSampleModel(w, h);

            workingBaseRaster
                = Raster.createWritableRaster(sm, new Point(0,0));
        }

        int tgx = -rootCR.getTileGridXOffset();
        int tgy = -rootCR.getTileGridYOffset();
        int xt, yt;
        if (tgx>=0) xt = tgx/tw;
        else        xt = (tgx-tw+1)/tw;
        if (tgy>=0) yt = tgy/th;
        else        yt = (tgy-th+1)/th;

        int xloc = xt*tw - tgx;
        int yloc = yt*th - tgy;

        // System.out.println("Info: [" +
        //                    xloc + "," + yloc + "] [" +
        //                    tgx  + "," + tgy  + "] [" +
        //                    xt   + "," + yt   + "] [" +
        //                    tw   + "," + th   + "]");
        // This raster should be aligned with cr's tile grid.
        workingRaster = workingBaseRaster.createWritableChild
          (0, 0, w, h, xloc, yloc, null);

        workingOffScreen =  new BufferedImage
          (rootCR.getColorModel(),
           workingRaster.createWritableChild (0, 0, offScreenWidth,
                                           offScreenHeight, 0, 0, null),
           rootCR.getColorModel().isAlphaPremultiplied(), null);


        if (!isDoubleBuffered) {
            currentOffScreen  = workingOffScreen;
            currentBaseRaster = workingBaseRaster;
            currentRaster     = workingRaster;
        }
    }
}
