/*

   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.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Collection;
import java.util.Iterator;

import org.apache.batik.ext.awt.geom.RectListManager;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.HaltingThread;

/**
 * Simple implementation of the Renderer that supports dynamic updates.
 *
 * @author <a href="mailto:Thierry.Kormann@sophia.inria.fr">Thierry Kormann</a>
 * @version $Id$
 */
public class MacRenderer implements ImageRenderer {

    static final int COPY_OVERHEAD      = 1000;
    static final int COPY_LINE_OVERHEAD = 10;
    static final AffineTransform IDENTITY = new AffineTransform();

    protected RenderingHints renderingHints;
    protected AffineTransform usr2dev;

    protected GraphicsNode rootGN;

    protected int offScreenWidth;
    protected int offScreenHeight;
    protected boolean isDoubleBuffered;
    protected BufferedImage currImg;
    protected BufferedImage workImg;
    protected RectListManager damagedAreas;

    public static int IMAGE_TYPE = BufferedImage.TYPE_INT_ARGB_PRE;
    public static Color TRANSPARENT_WHITE = new Color(255, 255, 255, 0);

    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);
    }

    /**
     * Constructs a new dynamic renderer with the specified buffer image.
     */
    public MacRenderer() {
        renderingHints = new RenderingHints(null);
        renderingHints.add(defaultRenderingHints);
        usr2dev = new AffineTransform();
    }

    public MacRenderer(RenderingHints rh,
                       AffineTransform at){
        renderingHints = new RenderingHints(null);
        renderingHints.add(rh);
        if (at == null) usr2dev = new AffineTransform();
        else            usr2dev = new AffineTransform(at);
    }

    public void dispose() {
        rootGN  = null;
        currImg = null;
        workImg = null;
        renderingHints = null;
        usr2dev = null;
        if ( damagedAreas != null ){
            damagedAreas.clear();
        }
        damagedAreas = 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 treeRoot) {
        rootGN = treeRoot;
    }

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

    /**
     * Sets the transform from the current user space (as defined by
     * the top node of the GVT tree, to the associated device space.
     */
    public void setTransform(AffineTransform usr2dev) {
        if(usr2dev == null)
            this.usr2dev = new AffineTransform();
        else
            this.usr2dev = new AffineTransform(usr2dev);
        if (workImg == null) return;
        synchronized (workImg) {
            Graphics2D g2d = workImg.createGraphics();
            g2d.setComposite(AlphaComposite.Clear);
            g2d.fillRect(0, 0, workImg.getWidth(), workImg.getHeight());
            g2d.dispose();
        }
        damagedAreas = 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;
    }

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

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

    /**
     * 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) {
            workImg = null;  // start double buffer, split buffers
        } else {
            // No longer double buffering so delete second offscreen
            workImg = currImg;
            damagedAreas = null;
        }
    }

    /**
     * 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 currImg;
    }

    /**
     * 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 (workImg == null) return;

        synchronized (workImg) {
            Graphics2D g2d = workImg.createGraphics();
            g2d.setComposite(AlphaComposite.Clear);
            g2d.fillRect(0, 0, workImg.getWidth(), workImg.getHeight());
            g2d.dispose();
        }
        damagedAreas = null;
    }

    public void flush() {
        // Since we don't cache we don't need to flush
    }
    public void flush(Rectangle r) {
        // Since we don't cache we don't need to flush
    }

    /**
     * Flush a list of rectangles of cached image data.
     */
    public void flush(Collection areas) {
        // Since we don't cache we don't need to flush
    }

    protected void updateWorkingBuffers() {
        if (rootGN == null) {
            currImg = null;
            workImg = null;
            return;
        }

        int         w  = offScreenWidth;
        int         h  = offScreenHeight;
        if ((workImg == null)         ||
            (workImg.getWidth()  < w) ||
            (workImg.getHeight() < h)) {
            workImg = new BufferedImage(w, h, IMAGE_TYPE);
            // workImg = new BI(w, h, IMAGE_TYPE);
        }

        if (!isDoubleBuffered) {
            currImg = workImg;
        }
    }

    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 devRLM regions to be repainted, in the current
     * user space coordinate system.
     */
    // long lastFrame = -1;
    public void repaint(RectListManager devRLM) {
        if (devRLM == null)
            return;

        updateWorkingBuffers();
        if ((rootGN == null) || (workImg == null))
            return;

        try {
        // Ensure only one thread works on WorkImg at a time...
        synchronized (workImg) {
            Graphics2D g2d = GraphicsUtil.createGraphics
                (workImg, renderingHints);

            Rectangle dr;
            dr = new Rectangle(0, 0, offScreenWidth, offScreenHeight);

            if ((isDoubleBuffered) &&
                (currImg != null) &&
                (damagedAreas  != null)) {

                damagedAreas.subtract(devRLM, COPY_OVERHEAD,
                                      COPY_LINE_OVERHEAD);

                damagedAreas.mergeRects(COPY_OVERHEAD,
                                        COPY_LINE_OVERHEAD);

                Iterator iter = damagedAreas.iterator();
                g2d.setComposite(AlphaComposite.Src);
                while (iter.hasNext()) {
                    Rectangle r = (Rectangle)iter.next();
                    if (!dr.intersects(r)) continue;
                    r = dr.intersection(r);
                    g2d.setClip     (r.x, r.y, r.width, r.height);
                    g2d.setComposite(AlphaComposite.Clear);
                    g2d.fillRect    (r.x, r.y, r.width, r.height);
                    g2d.setComposite(AlphaComposite.SrcOver);
                    g2d.drawImage   (currImg, 0, 0, null);
                }
            }


            for (Object aDevRLM : devRLM) {
                Rectangle r = (Rectangle) aDevRLM;
                if (!dr.intersects(r)) continue;
                r = dr.intersection(r);
                g2d.setTransform(IDENTITY);
                g2d.setClip(r.x, r.y, r.width, r.height);
                g2d.setComposite(AlphaComposite.Clear);
                g2d.fillRect(r.x, r.y, r.width, r.height);
                g2d.setComposite(AlphaComposite.SrcOver);
                g2d.transform(usr2dev);
                rootGN.paint(g2d);
            }
            g2d.dispose();
        }
        } catch (Throwable t) { t.printStackTrace(); }
        if (HaltingThread.hasBeenHalted())
            return;

        // System.out.println("Dmg: "   + damagedAreas);
        // System.out.println("Areas: " + devRects);

        // Swap the buffers if the rendering completed cleanly.
        if (isDoubleBuffered) {
            BufferedImage tmpImg = workImg;
            workImg = currImg;
            currImg = tmpImg;
            damagedAreas = devRLM;
        }
    }
}
