/*
 * Copyright (c) 2010-11, Oracle and/or its affiliates. All rights reserved.
 */

package com.sun.javafx.tk.quantum;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicBoolean;

import com.sun.glass.ui.View;
import com.sun.prism.render.CompletionListener;
import com.sun.prism.render.RenderJob;

/**
 * Manages the collection and rendering of dirty scenes. This class has
 * methods which may be called from one of several threads, depending
 * on the method.
 *
 * <ul>
 *     <li>createInstance: Called by QuantumToolkit once during initialization</li>
 *     <li>getInstance: May be called from any thread</li>
 *     <li>hasDirty: May be called from any thread</li>
 *     <li>isReleased: May be called from any thread</li>
 *     <li>releaseScene: Called only from the Render Thread</li>
 *     <li>releaseBarrier: Called from the Render Thread and FX Thread</li>
 *     <li>addDirtyScene: Called only on the FX Thread</li>
 *     <li>removeDirtyScene: Called only on the FX Thread</li>
 *     <li>liveRepaintRenderJob: Called only on the FX Thread</li>
 *     <li>renderAll: Called only on the FX Thread</li>
 * </ul>
 *
 * Assertions have been added to each method to verify whether the calling
 * thread is the expected thread.
 *
 * @author  Morris Meyer
 */
public class PaintCollector  {

    private static final boolean verbose =
        AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
            @Override public Boolean run() {
                return Boolean.getBoolean("quantum.verbose");
            }
        });

    private static final boolean pulseDebug =
        AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
            @Override public Boolean run() {
                return Boolean.getBoolean("quantum.pulse");
            }
        });
    
    /**
     * The QuantumRenderer and FX application thread barrier. Ultimately,
     * the rendering thread and FX thread should be running almost
     * entirely concurrently, only meeting up for a short synchronization
     * period, but otherwise being completely concurrent. However, until
     * we are ready to go "full threaded", we need to run these two
     * threads somewhat serially. When we come to "present" we can release
     * the FX thread to carry on its processing, but while doing all the
     * software rendering and such we need, for the moment, to block the
     * FX thread from continuing on to the next event.
     *
     * <p>This barrier is used to ensure that the renderAll method does
     * not return (and thus, the FX thread doesn't continue processing)
     * until the render thread has reached a safe point and releases
     * the FX thread.</p>
     */
    private final CyclicBarrier    pulseBarrier = new CyclicBarrier(2);

    /**
     * Contains a list of all of the dirty scenes. This list is populated
     * only from the FX Thread in consequence of a call to addDirtyScene,
     * or cleared from the FX Thread in consequence of a call to renderAll
     * or removeDirtyScene. It is only ever accessed (both read and write!)
     * from the FX thread.
     */
    private final List<GlassScene> uploadScenes = new ArrayList<GlassScene>();
    
    /*
     * container for potentially synchronous PresentingPainter scenes
     *
     * note: only prism-d3d pipeline is actually synchronous.
     */
    private final List<GlassScene> syncScenes   = new ArrayList<GlassScene>();
    
    /*
     *
     */
    private final List<GlassScene> dirtyScenes  = new ArrayList<GlassScene>();
    private final List<GlassScene> dirtyMarker  = new ArrayList<GlassScene>();
    private final List<GlassScene> dirtyWork    = new ArrayList<GlassScene>();

    /**
     * Indicates whether this PaintCollector has any dirty scenes that
     * need to be processed. This is used by the QuantumToolkit to detect
     * in the postPulse() method whether there are dirty scenes. If there
     * are, then the postPulse will potentially post a new pulse event.
     * Updated from the FX Thread, but may be read from any thread.
     */
    private final AtomicBoolean    hasDirty     = new AtomicBoolean(false);

    /**
     * Indicates whether the rendering operations are all completed. This
     * is used for debugging purposes at present in the QuantumToolkit
     * checkFxUserThread method.
     */
    private final AtomicBoolean    released     = new AtomicBoolean(false);

    /**
     * A reference to the toolkit. This is supplied in the constructor.
     * Although a Toolkit.getToolkit() call and cast to QuantumToolkit
     * could be used, it is somewhat cleaner to simply supply these
     * parameters in the constructor and not reach out to static state.
     */
    private final QuantumToolkit   toolkit;

    /**
     * A reference to the QuantumRenderer. This is supplied in
     * the constructor.
     */
    private final QuantumRenderer  renderer;

    private ViewPainter            viewPainter;

    /**
     * Indicates whether there have been potentially synchronous scenes
     * that have been painted. If so this is posted to the Toolkit, which
     * has the choice based on OS and capabilities of the <code>GraphicsPipeline</code>
     * to push a pulse onto the FX application event queue and fire off the
     * next animation frame.  This allows FX to align its animation frames to
     * vsync on systems that support it.
     */
    private boolean                needsHint;
        
    // singleton constructor
    private PaintCollector(QuantumToolkit qt, QuantumRenderer qr) {
        toolkit  = qt;
        renderer = qr;
    }

    private static PaintCollector collector;
    
    protected static PaintCollector createInstance(QuantumToolkit toolkit) {
        collector = new PaintCollector(toolkit, QuantumRenderer.getInstance());
        
        return collector;
    }
    
    public static PaintCollector getInstance() { 
        return collector;
    }
            
    /**
     * Gets whether there are any dirty scenes that need to be rendered. If
     * true, then a subsequent pulse event and renderAll call is required.
     *
     * @return Whether there are any dirty scenes that need to be rendered.
     */
    public AtomicBoolean hasDirty() {
        return hasDirty;
    }
    
    /**
     * Gets whether all rendering is completed.
     *
     * @return Whether all rendering is completed.
     */
    public boolean isReleased() {
        return released.get();
    }

    protected QuantumToolkit toolkit() {
        return toolkit;
    }

    protected static String sceneSize(GlassScene scene) {
        if (scene instanceof ViewScene) {
            View view = ((ViewScene)scene).getPlatformView();
            return (" scene: " + scene.hashCode() + " @ (" + view.getWidth() + "," + view.getHeight() + ")");
        } else {
            GlassStage stage = scene.glassStage;
            EmbeddedStage estage = (EmbeddedStage)stage;
            return (" scene: " + scene.hashCode() + ")");
        }
    }

    /**
     * Adds a dirty scene to the PaintCollector for subsequent processing.
     * This method simply makes the PaintCollector aware of this new
     * scene and ensure it gets processed on the next call to renderAll.
     *
     * The next QuantumToolkit Glass timer generated pulse or PaintCollector
     * rendering vsync hinted pulse will process these dirty scenes.
     *
     * <p>This method must only be called on the FX Thread</p>
     * 
     * @param scene    The scene which is dirty. This must not be null.
     */
    void addDirtyScene(GlassScene scene) {
        if (verbose) {
            System.err.println("PC.addDirtyScene: " + System.nanoTime() + sceneSize(scene));
        }
        synchronized (this) {
            if (dirtyScenes.contains(scene) == false) {
                dirtyScenes.add(scene);
            }
            hasDirty.set(true);
        }
    }

    /**
     * Removes a scene from the dirtyScene list. If the given scene
     * was previously added with a call to addDirtyScene, it will
     * be removed. Potentially this means that after this call the
     * PaintCollector will no longer have any dirty scenes and will
     * no longer require a repaint.
     * 
     * <p>This method is typically called when a scene is removed
     * from a stage, or when visible becomes false.
     * </p>
     *
     * <p>This method must only be called on the FX Thread</p>
     *
     * @param scene    The scene which is no longer dirty. Must not be null.
     */
    void removeDirtyScene(GlassScene scene) {
        if (verbose) {
            System.err.println("PC.removeDirtyScene: " + sceneSize(scene));
        }
        synchronized (this) {
            dirtyScenes.remove(scene);
            hasDirty.set(dirtyScenes.isEmpty() == false);
        }
    }
    
    void checkThreads() {
        assert Thread.currentThread().equals(toolkit.getFxUserThread()) == false;
    }

    /**
     * Called by the <code>PresentingPainter</code> and 
     * <code>UploadingPainter</code> when it has finished
     * rendering the scene, but only if this is not a live-repaint
     * scenario.
     *
     * @param scene The scene to release. Must not be null.
     */
    void releaseScene(GlassScene scene) {
        synchronized (this) {
            if (dirtyMarker.remove(scene)) {
                if (dirtyMarker.isEmpty()) {
                    releaseBarrier();
                    released.set(true);
                }
            }
        }
    }
    
    void releaseBarrier() {
        try {
            pulseBarrier.await();
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        } catch (BrokenBarrierException bbe) {
            bbe.printStackTrace();
        }
    }
    
    private final CompletionListener rendered = new CompletionListener() {
        @Override public void done(RenderJob job) {
            checkThreads();

            PaintRenderJob prj = (PaintRenderJob)job;
            final GlassScene scene = prj.getScene();
            if (pulseDebug) {
                System.err.println("PC: rendered: now: " + System.nanoTime() + sceneSize(scene));
            }
            synchronized (PaintCollector.this) {
                dirtyMarker.remove(scene);
                scene.clearDirty();
                scene.frameRendered();        
            }
            if (pulseDebug) {
                System.err.println("PC: rendered: remain: " + dirtyMarker.size());
                for (GlassScene gs : dirtyMarker) {
                    System.err.println("REMAIN: " + sceneSize(gs));
                }
            }

            synchronized (PaintCollector.this) {
                if (dirtyMarker.isEmpty()) {

                    toolkit.endPulseRunning();

                    if (needsHint) {
                        toolkit.vsyncHint();
                    }

                    if (released.get() == false) {
                        releaseBarrier();
                    }
                    
                    if (pulseDebug) {
                        System.err.println("PC: rendered: renderAll: " + System.nanoTime());
                    }
                }
            }
        }
    };

    protected CompletionListener getRendered() {
        return (rendered);
    }

    private final CompletionListener viewRepainted  = new CompletionListener() {
        @Override public void done(final RenderJob rj) {
            viewPainter.liveRepaint.set(false);
            if (verbose) {
                System.err.println("LR.end: " + System.nanoTime());
            }
        }
    };

    /**
     * This is a essentially a pulse without the animation run 
     * and the sceneListeners pulse. It only repaints the 
     * scene at new dimensions.
     */
    public void liveRepaintRenderJob(final GlassScene scene) {
        if (scene instanceof ViewScene) {
            final ViewScene view = (ViewScene)scene;

            synchronized (this) {
                viewPainter = view.getPen().getPainter();

                if (viewPainter.liveRepaint.getAndSet(true) == false) {
                    renderer.submit(new RenderJob((Runnable)viewPainter, viewRepainted));

                    if (verbose) {
                        System.err.println("LR.start: " + System.nanoTime() + sceneSize(scene));
                    }
                }
            }
        }
    }
    
    void renderAll() {
        if (pulseDebug) {
            System.err.println("PC.renderAll(" + dirtyScenes.size() + "): " + System.nanoTime());
        }

        /**
         * If we don't have any dirty scenes to process, end the pulse.
         */
        if (hasDirty.get() == false) {
            toolkit.endPulseRunning();
            return;
        }

        GlassScene gs = null;
        synchronized (this) {
            pulseBarrier.reset();
            released.set(false);
            for (int i = 0, n = dirtyScenes.size(); i < n; i++) {
                gs = dirtyScenes.get(i);
                dirtyMarker.add(gs);
                dirtyWork.add(gs);
            }
            dirtyScenes.clear();
            uploadScenes.clear();
            syncScenes.clear();
            for (int i = 0, n = dirtyWork.size(); i < n; i++) {
                gs = dirtyWork.get(i);

                if (gs.isSynchronous()) {
                    /* PresentingPainter */
                    syncScenes.add(gs);
                } else {
                    /* EmbeddedPainter, UploadingPainter */
                    uploadScenes.add(gs);
                }

            }
        }

        if (uploadScenes.isEmpty() && syncScenes.isEmpty()) {
            /* nothing to do - avoid the barrier */
            toolkit.endPulseRunning();
            return;
        }

        /*
         * Only post the vsync hint if there are synchronous scenes
         */
        needsHint = !syncScenes.isEmpty();

        /*
         * order the upload scenes before the presentable scenes, as on systems
         * that support vsync (prism-d3d) could wind up waiting around for vsync.
         */
        for (int i = 0, n = uploadScenes.size(); i < n; i++) {
            gs = uploadScenes.get(i);
            try {
                gs.repaint();
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        for (int i = 0, n = syncScenes.size(); i < n; i++) {
            gs = syncScenes.get(i);
            try {
                gs.repaint();
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        try {
            releaseBarrier();
        } finally {
            synchronized (this) {
                dirtyWork.clear();
                assert dirtyMarker.isEmpty();
                hasDirty.set(false);
            }
        }
    }
}
