package org.gbcpainter.view.draw;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import org.gbcpainter.game.levels.Level;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferStrategy;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A redraw job that draws each view component separately
* <p/>
* This job uses a {@link java.util.concurrent.ScheduledThreadPoolExecutor} to execute the redraw at fixed ratio
*
* @author Lorenzo Pellegrini
*/
@ThreadSafe
public abstract class ComposableRedrawJob<T> implements RedrawJob, Runnable {
private final static long MILLIS_FADEOUT = 1200;
private final static float INITIAL_FADEOUT = 0.2f;
@NotNull
private final Level level;
@NotNull
private final java.util.List<T> components;
@NotNull
private final Canvas drawingCanvas;
@NotNull
@GuardedBy ( "readWriteLock" )
private final ScheduledThreadPoolExecutor executors;
@GuardedBy ( "readWriteLock" )
private final Set<DrawListener> eventObservers = new HashSet<>( 1 );
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
@GuardedBy ( "readWriteLock" )
private int millisTimeout = 0;
@Nullable
@GuardedBy ( "readWriteLock" )
private Throwable executionError = null;
@Nullable
@GuardedBy ( "readWriteLock" )
private ScheduledFuture<?> scheduled = null;
@GuardedBy ( "readWriteLock" )
private volatile boolean paused = true;
@GuardedBy ( "readWriteLock" )
private volatile boolean terminated = false;
@GuardedBy ( "readWriteLock" )
private long millisStartFadeout = 0;
public ComposableRedrawJob( @NotNull Canvas whereTo, @NotNull Level level, int refreshPerSecond, @NotNull java.util.List<T> components ) {
this.drawingCanvas = whereTo;
this.millisTimeout = 1000 / refreshPerSecond;
this.level = level;
this.components = new LinkedList<>( components );
this.executors = new ScheduledThreadPoolExecutor( 1 );
}
@Override
public void pauseDraw( final boolean pause ) {
this.writeLock.lock();
try {
this.paused = pause;
if ( pause ) {
if ( this.scheduled != null ) {
this.executors.remove( this );
this.scheduled.cancel( false );
this.scheduled = null;
}
} else {
if ( this.scheduled == null ) {
this.scheduled = this.executors.scheduleWithFixedDelay( this, 0, this.millisTimeout, TimeUnit.MILLISECONDS );
}
}
} finally {
this.writeLock.unlock();
}
}
@Override
public boolean isPaused() {
this.readLock.lock();
try {
return this.paused;
} finally {
this.readLock.unlock();
}
}
@Override
public void terminate() {
this.writeLock.lock();
try {
if ( ! this.terminated ) {
this.terminated = true;
if ( this.scheduled != null ) {
this.executors.remove( this );
this.scheduled.cancel( false );
this.scheduled = null;
}
this.executors.shutdown();
this.executors.purge();
}
} finally {
this.writeLock.unlock();
}
}
@Override
public boolean isTerminated() {
this.readLock.lock();
try {
return this.terminated;
} finally {
this.readLock.unlock();
}
}
@Override
public int getRefreshPerSecond() {
this.readLock.lock();
try {
return 1000 / this.millisTimeout;
} finally {
this.readLock.unlock();
}
}
@Override
public void setRefreshPerSecond( final int refresh ) {
this.writeLock.lock();
try {
this.millisTimeout = 1000 / refresh;
if ( this.scheduled != null ) {
this.executors.remove( this );
this.scheduled.cancel( false );
this.scheduled = this.executors.scheduleWithFixedDelay( this, 0, this.millisTimeout, TimeUnit.MILLISECONDS );
}
} finally {
this.writeLock.unlock();
}
}
@Override
public boolean hasFailed() {
return this. getError() != null;
}
@Override
public long startFadeOut() {
this.writeLock.lock();
try {
this.millisStartFadeout = System.currentTimeMillis();
return MILLIS_FADEOUT;
} finally {
this.writeLock.unlock();
}
}
@Override
@Nullable
public Throwable getError() {
this.readLock.lock();
try {
return this.executionError;
} finally {
this.readLock.unlock();
}
}
protected void setError( @Nullable Throwable error ) {
this.writeLock.lock();
try {
this.executionError = error;
if ( error != null ) {
this.pauseDraw( true );
}
} finally {
this.writeLock.unlock();
}
}
@Override
public void addLevelStopListener( @NotNull final DrawListener callback ) {
this.writeLock.lock();
try {
this.eventObservers.add( callback );
} finally {
this.writeLock.unlock();
}
}
@Override
public void removeLevelStopListener( @NotNull final DrawListener toBeRemoved ) {
this.writeLock.lock();
try {
this.eventObservers.remove( toBeRemoved );
} finally {
this.writeLock.unlock();
}
}
@Override
public void run() {
if ( this.isDrawable() ) {
BufferStrategy bs;
Graphics2D g2;
boolean mustCreateBuffer;
do {
g2 = null;
mustCreateBuffer = false;
bs = this.getBufferStrategy();
if ( bs == null ) {
if ( ! this.isDrawable() ) {
return;
}
mustCreateBuffer = true;
} else {
g2 = this.getStrategyGraphics( bs );
if ( g2 == null ) {
return;
}
}
if ( mustCreateBuffer ) {
if ( ! this.createBufferStrategy() ) {
return;
}
}
if ( bs == null ) {
bs = this.getBufferStrategy();
}
try {
if ( g2 == null ) {
this.readLock.lock();
try {
g2 = (Graphics2D) ( ( bs == null ) ? this.drawingCanvas.getGraphics() : this.getStrategyGraphics( bs ) );
} finally {
this.readLock.unlock();
}
}
if ( g2 != null ) {
g2.setColor( this.drawingCanvas.getBackground() );
g2.fillRect( 0, 0, this.drawingCanvas.getWidth(), this.drawingCanvas.getHeight() );
g2.setColor( this.drawingCanvas.getForeground() );
this.doDraw( g2 );
this.applyFadeoutEffect( g2, this.getFadeoutStartTime() );
}
} catch ( Exception e ) {
this.setError( e );
this.doCallback( this.getObservers() );
return;
} finally {
if ( g2 != null && bs != null ) {
g2.dispose();
}
}
if ( bs != null ) {
bs.show();
}
} while ( bs != null && bs.contentsLost() && this.isDrawable() );
}
}
private void doDraw( @NotNull Graphics2D g2d ) throws Exception {
final Rectangle previousClip = g2d.getClipBounds();
for (T element : this.components) {
final Rectangle where = this.getComponentRect( element );
if ( where != null ) {
g2d.setTransform( new AffineTransform() );
g2d.setClip( where.x, where.y, where.width, where.height );
this.drawComponent( element, g2d );
}
}
g2d.setTransform( new AffineTransform() );
g2d.setClip( previousClip );
}
private long getFadeoutStartTime() {
this.readLock.lock();
try {
return this.millisStartFadeout;
} finally {
this.readLock.unlock();
}
}
protected void applyFadeoutEffect( @NotNull Graphics2D g2d, long millisStartTime ) {
final long currentTime = System.currentTimeMillis();
if ( currentTime < ( millisStartTime + MILLIS_FADEOUT ) ) {
final float fadeAlpha = INITIAL_FADEOUT + ( 1.0f - INITIAL_FADEOUT ) * ( (float) ( currentTime - millisStartTime ) / MILLIS_FADEOUT );
g2d.setColor( new Color( 0, 0, 0, fadeAlpha ) );
Rectangle bounds = g2d.getClipBounds();
if ( bounds != null ) {
g2d.fillRect( bounds.x, bounds.y, bounds.width, bounds.height );
} else {
g2d.fillRect( 0, 0, this.drawingCanvas.getWidth() - 1, this.drawingCanvas.getHeight() - 1 );
}
}
}
/**
* Draws the component on screen
*
* The Graphics object is clipped to the position specified from {@link #getComponentRect(Object)}
*
* @param component The component to be drawn
* @param g2d The grphics object
*
* @throws Exception If an error occurs
*/
protected abstract void drawComponent( @NotNull T component, @NotNull Graphics2D g2d ) throws Exception;
/**
* Returns the clip of the component
*
* @param component The component whose size is required
*
* @return The size of the component or null if the element is hidden
*/
@Nullable
protected abstract Rectangle getComponentRect( @NotNull T component );
@NotNull
protected Level getLevel() {
return this.level;
}
@NotNull
protected Set<DrawListener> getObservers() {
this.readLock.lock();
try {
return new HashSet<>( this.eventObservers );
} finally {
this.readLock.unlock();
}
}
protected void doCallback( @NotNull Iterable<DrawListener> observers ) {
for (DrawListener exec : observers) {
try {
exec.drawStopped( this, this.level );
} catch ( Exception ignored ) {
}
}
}
protected boolean isDrawable() {
this.readLock.lock();
try {
return this.drawingCanvas.isVisible() && ( ! this.isPaused() ) && ( ! this.isTerminated() );
} finally {
this.readLock.unlock();
}
}
@Nullable
protected BufferStrategy getBufferStrategy() {
this.readLock.lock();
try {
if ( ! this.isDrawable() ) {
return null;
}
return this.drawingCanvas.getBufferStrategy();
} finally {
this.readLock.unlock();
}
}
protected boolean createBufferStrategy() {
try {
EventQueue.invokeAndWait( new Runnable() {
@Override
public void run() {
ComposableRedrawJob.this.createBufferStrategyDirect();
}
} );
} catch ( Exception e ) {
return false;
}
return true;
}
private void createBufferStrategyDirect() {
this.readLock.lock();
try {
if ( ! this.isDrawable() ) {
throw new IllegalStateException();
}
this.drawingCanvas.createBufferStrategy( 2 );
} catch ( IllegalStateException e ) {
throw new IllegalStateException();
} finally {
this.readLock.unlock();
}
}
@Nullable
protected Graphics2D getStrategyGraphics( @NotNull final BufferStrategy strategy ) {
final Graphics2D[] holder = { null };
try {
EventQueue.invokeAndWait( new Runnable() {
@Override
public void run() {
ComposableRedrawJob.this.readLock.lock();
try {
if ( ! ComposableRedrawJob.this.isDrawable() ) {
holder[0] = null;
} else {
holder[0] = (Graphics2D) strategy.getDrawGraphics();
}
} catch ( IllegalStateException e ) {
ComposableRedrawJob.this.createBufferStrategyDirect();
BufferStrategy strategyRec = ComposableRedrawJob.this.getBufferStrategy();
if ( strategyRec != null ) {
holder[0] = ComposableRedrawJob.this.getStrategyGraphics( strategyRec );
} else {
holder[0] = null;
}
} finally {
ComposableRedrawJob.this.readLock.unlock();
}
}
} );
} catch ( Exception e ) {
holder[0] = null;
}
return holder[0];
}
@Override
@NonNls
public String toString() {
this.readLock.lock();
try {
return "ComposableRedrawJob{" +
"level=" + this.level +
", components=" + this.components +
", drawingCanvas=" + this.drawingCanvas +
", executors=" + this.executors +
", eventObservers=" + this.eventObservers +
", millisTimeout=" + this.millisTimeout +
", executionError=" + this.executionError +
", scheduled=" + this.scheduled +
", paused=" + this.paused +
", terminated=" + this.terminated +
", millisStartFadeout=" + this.millisStartFadeout +
", readWriteLock=" + this.readWriteLock +
", readLock=" + this.readLock +
", writeLock=" + this.writeLock +
'}';
} finally {
this.readLock.unlock();
}
}
@Override
public void finalize() throws Throwable {
this.terminate();
super.finalize();
}
}