/*
 * @(#)ColorCyclingMemoryImageSource.java  1.1  2010-08-03
 *
 * Copyright (c) 2009-2010 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.ilbm;

import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.ImageConsumer;
import java.awt.image.IndexColorModel;
import java.awt.image.MemoryImageSource;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
import javax.swing.Timer;

/**
 * ColorCyclingMemoryImageSource.
 *
 * @author Werner Randelshofer
 * @version 1.1 2010-08-03 Added method putProperties. Added support for
 * blended color cycles.
 * <br>1.0.1 2010-11-08 Fixed color cycling rate.
 * <br>1.0 2009-12-17 Created.
 */
public class ColorCyclingMemoryImageSource extends MemoryImageSource {

    private int width;
    private int height;
    private ColorModel model;
    private Object pixels;
    private int pixeloffset;
    private int pixelscan;
    private Hashtable properties;
    private ArrayList<ColorCycle> colorCycles = new ArrayList<ColorCycle>();
    private Timer timer;
    private HashSet<ImageConsumer> consumers = new HashSet<ImageConsumer>();
    /** Whether color cycling is available. */
    private boolean isColorCyclingAvailable;
    /** Whether color cycling is started. */
    private boolean isStarted;
    /** Whether color cycles are blended. */
    private boolean isBlendedColorCycling;
    private volatile ColorModel cycledModel;

    /**
     * Constructs an ImageProducer object which uses an array of bytes
     * to produce data for an Image object.
     * @param w the width of the rectangle of pixels
     * @param h the height of the rectangle of pixels
     * @param cm the specified <code>ColorModel</code>
     * @param pix an array of pixels
     * @param off the offset into the array of where to store the
     *        first pixel
     * @param scan the distance from one row of pixels to the next in
     *        the array
     * @see java.awt.Component#createImage
     */
    public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
            byte[] pix, int off, int scan) {
        super(w, h, cm, pix, off, scan);
        initialize(w, h, cm, (Object) pix, off, scan, new Hashtable());
    }

    /**
     * Constructs an ImageProducer object which uses an array of bytes
     * to produce data for an Image object.
     * @param w the width of the rectangle of pixels
     * @param h the height of the rectangle of pixels
     * @param cm the specified <code>ColorModel</code>
     * @param pix an array of pixels
     * @param off the offset into the array of where to store the
     *        first pixel
     * @param scan the distance from one row of pixels to the next in
     *        the array
     * @param props a list of properties that the <code>ImageProducer</code>
     *        uses to process an image
     * @see java.awt.Component#createImage
     */
    public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
            byte[] pix, int off, int scan,
            Hashtable<?, ?> props) {
        super(w, h, cm, pix, off, scan, props);
        initialize(w, h, cm, (Object) pix, off, scan, props);
    }

    /**
     * Constructs an ImageProducer object which uses an array of integers
     * to produce data for an Image object.
     * @param w the width of the rectangle of pixels
     * @param h the height of the rectangle of pixels
     * @param cm the specified <code>ColorModel</code>
     * @param pix an array of pixels
     * @param off the offset into the array of where to store the
     *        first pixel
     * @param scan the distance from one row of pixels to the next in
     *        the array
     * @see java.awt.Component#createImage
     */
    public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
            int[] pix, int off, int scan) {
        super(w, h, cm, pix, off, scan);
        initialize(w, h, cm, (Object) pix, off, scan, null);
    }

    /**
     * Constructs an ImageProducer object which uses an array of integers
     * to produce data for an Image object.
     * @param w the width of the rectangle of pixels
     * @param h the height of the rectangle of pixels
     * @param cm the specified <code>ColorModel</code>
     * @param pix an array of pixels
     * @param off the offset into the array of where to store the
     *        first pixel
     * @param scan the distance from one row of pixels to the next in
     *        the array
     * @param props a list of properties that the <code>ImageProducer</code>
     *        uses to process an image
     * @see java.awt.Component#createImage
     */
    public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
            int[] pix, int off, int scan,
            Hashtable<?, ?> props) {
        super(w, h, cm, pix, off, scan, props);
        initialize(w, h, cm, (Object) pix, off, scan, props);
    }

    private void initialize(int w, int h, ColorModel cm,
            Object pix, int off, int scan, Hashtable props) {
        width = w;
        height = h;
        model = cm;
        pixels = pix;
        pixeloffset = off;
        pixelscan = scan;
        if (props == null) {
            props = new Hashtable();
        }
        properties = props;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public ColorModel getColorModel() {
        return model;
    }

    public Hashtable getProperties() {
        return properties;
    }

    @Override
    public synchronized void newPixels(byte[] newpix, ColorModel newmodel,
            int offset, int scansize) {
        this.pixels = newpix;
        this.model = newmodel;
        this.pixeloffset = offset;
        this.pixelscan = scansize;
        super.newPixels(newpix, cycledModel == null ? newmodel : cycledModel, offset, scansize);
    }

    /**
     * Changes to a new int array to hold the pixels for this image.
     * If the animation flag has been turned on through the setAnimated()
     * method, then the new pixels will be immediately delivered to any
     * ImageConsumers that are currently interested in the data for
     * this image.
     * @param newpix the new pixel array
     * @param newmodel the specified <code>ColorModel</code>
     * @param offset the offset into the array
     * @param scansize the distance from one row of pixels to the next in
     * the array
     * @see #newPixels(int, int, int, int, boolean)
     * @see #setAnimated
     */
    @Override
    public synchronized void newPixels(int[] newpix, ColorModel newmodel,
            int offset, int scansize) {
        this.pixels = newpix;
        this.model = newmodel;
        this.pixeloffset = offset;
        this.pixelscan = scansize;
        super.newPixels(newpix, cycledModel == null ? newmodel : cycledModel, offset, scansize);
    }

    public void addColorCycle(ColorCycle cc) {
        colorCycles.add(cc);
    }

    @Override
    public void addConsumer(ImageConsumer ic) {
        super.addConsumer(ic);
        consumers.add(ic);
        if (isColorCyclingAvailable && isStarted && consumers.size() > 0) {
            startAnimationTimer();
        }
    }

    @Override
    public void removeConsumer(ImageConsumer ic) {
        super.removeConsumer(ic);
        consumers.remove(ic);
        if (isColorCyclingAvailable && consumers.size() == 0) {
            stopAnimationTimer();
        }
    }

    @Override
    public void setAnimated(boolean b) {
        super.setAnimated(b);
        isColorCyclingAvailable = b;

        if (isColorCyclingAvailable && consumers.size() > 0 && isStarted) {
            startAnimationTimer();
        } else {
            stopAnimationTimer();
        }
    }

    /** Starts or stops color cycling. */
    public void setColorCyclingStarted(boolean b) {
        isStarted = b;
        if (isColorCyclingAvailable && consumers.size() > 0 && isStarted) {
            startAnimationTimer();
        } else {
            stopAnimationTimer();
        }
    }

    /** Returns true if color cycling is on. */
    public boolean isColorCyclingStarted() {
        return isStarted;
    }

    /** Starts color cycling. */
    public void start() {
        setColorCyclingStarted(true);
    }

    /** Stops color cycling. */
    public void stop() {
        setColorCyclingStarted(false);
    }

    public boolean isStarted() {
        return isColorCyclingStarted();
    }

    private synchronized void startAnimationTimer() {
        if (timer != null) {
            return;
        }
        if (model instanceof IndexColorModel) {
            final IndexColorModel icm = (IndexColorModel) model;
            final int[] rgbs = new int[icm.getMapSize()];
            icm.getRGBs(rgbs);

            // Calculate the timer delay
            int delay = 1000;
            int i = 0;
            if (isBlendedColorCycling) {
                for (ColorCycle cc : colorCycles) {
                    if (cc.isActive()) {
                        // Note: we divide 1000 by 4
                        // 2 for Nyquist Theorem (double sample rate)
                        // 2 for blending
                        int ccDelay = 1000 / 4 * cc.getTimeScale() / cc.getRate();
                        if (ccDelay < delay) {
                            delay = Math.max(1, ccDelay);
                        }
                    }
                }
                delay = Math.max(delay, 1000 / 60); // throttle at 60 fps
            } else {
                for (ColorCycle cc : colorCycles) {
                    if (cc.isActive()) {
                        // Note: we divide 1000 by 2 (=double sampling rate)
                        // because of Nyquist theorem
                        int ccDelay = 1000 / 2 * cc.getTimeScale() / cc.getRate();
                        if (ccDelay < delay) {
                            delay = Math.max(1, ccDelay);
                        }
                    }
                }
            }

            timer = new Timer(delay, new ActionListener() {

                private int[] previousCycled = new int[rgbs.length];
                private int[] cycled = new int[rgbs.length];
                long startTime = System.currentTimeMillis();

                @Override
                public void actionPerformed(ActionEvent evt) {
                    long now = System.currentTimeMillis();
                    System.arraycopy(rgbs, 0, cycled, 0, rgbs.length);
                    for (ColorCycle cc : colorCycles) {
                        cc.doCycle(cycled, now - startTime);
                    }
                    // We only fire new pixels, if the cycles have changed
                    if (!Arrays.equals(previousCycled, cycled)) {
                        ColorCyclingMemoryImageSource.super.newPixels((byte[]) pixels, //
                                cycledModel = new IndexColorModel(8, cycled.length, cycled, 0, false, -1, DataBuffer.TYPE_BYTE),
                                pixeloffset,
                                pixelscan);
                    }
                    // swap cycled colors
                    int[] tmp = previousCycled;
                    previousCycled = cycled;
                    cycled = tmp;
                }
            });
            timer.setRepeats(true);
            timer.start();
        }
    }

    private synchronized void stopAnimationTimer() {
        if (timer != null) {
            timer.stop();
            timer = null;
            cycledModel = null;
            // Reset colors to their initial state
            ColorCyclingMemoryImageSource.super.newPixels((byte[]) pixels, //
                    model,
                    pixeloffset,
                    pixelscan);
        }

    }

    /** Creates a BufferedImage which shares its pixel data with this memory image source. */
    public BufferedImage toBufferedImage() {
        DataBuffer buf = (pixels instanceof byte[]) ?//
                new DataBufferByte((byte[]) pixels, pixelscan * height, pixeloffset) ://
                new DataBufferInt((int[]) pixels, pixelscan * height, pixeloffset);
        SampleModel sm = model.createCompatibleSampleModel(width, height);
        WritableRaster raster = Raster.createWritableRaster(sm, buf, new Point());
        return new BufferedImage(model, raster, false, properties);
    }

    public boolean isColorCyclingAvailable() {
        return isColorCyclingAvailable;
    }

    public void putProperties(Hashtable props) {
        properties.putAll(props);
    }

    public void setBlendedColorCycling(boolean newValue) {
        isBlendedColorCycling = newValue;
        for (ColorCycle cc : colorCycles) {
            cc.setBlended(newValue);
        }
        if (isStarted()) {
            stop();
            start();
        }
    }

    public boolean isBlendedColorCycling() {
        return isBlendedColorCycling;
    }
}
