/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package csa.tiletools.particle;

import java.util.Vector;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import csa.gui.TimingTriggerer;
import csa.gui.TriggerCallback;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;

/** see also: http://buildnewgames.com/particle-systems/
 * http://www.java-gaming.org/topics/particle-editor-tool/28535/view.html
 *
 * @author salchr
 */
public class ParticleEngine {
    
    // only supported mode is PE_FIXE_SIZE_MODE
    public static int PE_FIX_SIZE_MODE = 1; // a fixed size is given to rendering image, all positions are in relation to top left, no negative positions are available! (out of bounds)
    public static int PE_CALS_SIZE_MODE = 2; // rendered image size is determined by min/max position an width height of the particles, each rendering may have a different size!

    
    public static int PE_UPDATE_MODE_TIMER = 0;
    public static int PE_UPDATE_MODE_MANUAL = 1; // not implemented
    
    public static int PARTICLE_TEXTURE = 0; // particles are defined via a texture
    
    
    ParticleEngineData config;
    Color background = Color.black;//new Color(0,0,0,110); 
    int width; // 
    int height;
    boolean needsRender = true;
    
    
    
    int mode = PE_FIX_SIZE_MODE;

    TriggerCallback tt; 
    boolean started = false;
    TimingTriggerer timer = TimingTriggerer.getTimer();
    
    BufferedImage[] image = new BufferedImage[3];
    int currentImage = 0; // one or two
    
    
    private Vector<ParticleEngineListener> mParticleEngineListener = new Vector<ParticleEngineListener>();
    public void addParticleEngineListener(ParticleEngineListener listener)
    {
        mParticleEngineListener.removeElement(listener);
        mParticleEngineListener.addElement(listener);
    }
    public void removeParticleEngineListener(ParticleEngineListener listener)
    {
        mParticleEngineListener.removeElement(listener);
    }
    public void fireParticleEngineUpdated()
    {
        for (int i=0; i<mParticleEngineListener.size(); i++)
        {
            mParticleEngineListener.elementAt(i).updated(this);
        }
    }
    
    // has a number of emitters
    // generates one image
    
    
    // emitters are particles
    // oldes particle has the highest index!
    // new ones are added to position 0
    Vector<Particle> particles = new Vector<Particle>();
    
    private ParticleEngine() {}
    
    public ParticleEngine(ParticleEngineData c, ParticleEngineListener listener)
    {
        config = c;
        width = config.mWidth; // 
        height = config.mHeight;
        
        addParticleEngineListener(listener);
    }
    
    public void start() 
    {
        image[0] = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        image[1] = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        image[2] = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // third image is only used to calculate additive coloring

        
        // if (config.updateMode == PE_UPDATE_MODE_TIMER)
        {
            started = true;
            timer.setResolution(10); //100*1ms resoltuion is 0,1seconds
            if (tt != null)
            {
                timer.removeTrigger(tt);
                tt = null;                
            }

            tt = new TriggerCallback()
            {
                @Override
                public void doIt(int state, Object o)
                {
                    if (update())
                    {
                        // only if update really happened!
                        fireParticleEngineUpdated();
                    }
                    if (started) timer.addTrigger(tt, config.mTickResolution, 0, null);
                }
            };
            timer.addTrigger(tt, config.mTickResolution, 0, null);
        }
    }
    
    public void stop() 
    {
//        if (config.updateMode == PE_UPDATE_MODE_TIMER)
        {
            timer.removeTrigger(tt);
            tt = null;
            started = false;
            particles.clear();
            
            image[0] = null;
            image[1] = null;
            image[2] = null;
        }
    }
    
    public void addParticle(Particle p)
    {
        particles.insertElementAt(p, 0);
    }

    // does NOT render, only updates the current settings for the next rendering of each particle
    // returns true if any one particle was changed in any way
    public boolean update()
    {
        if (!started) return false;
        boolean updated = false;
        for (int i=particles.size()-1; i>=0; i--)
        {
            Particle p = particles.elementAt(i);
            // emitters will emit new particles during update!
            boolean  updatedThis = p.update();
            updated = updated || updatedThis;
            
            if (p instanceof ParticleEmitter)
            {
                i = particles.indexOf(p); // correct index, if emitter emitted new Particels!, Remember new partciles are allways added to front!
            }
            if (!p.isActive())
            {
                particles.remove(p);
                ParticleEmitter.cacheParticle(p); // reuse particle if possible!
            }
        }
        if (updated) 
            needsRender = true;
        return updated;
    }
    
    public BufferedImage getCurrentImage()
    {
        return image[currentImage];
    }
    
    // does no update
    // generates image, displays nothing
    // returns true if the actual image that will be gotten from getCurrentImage() was changed since last call
    public boolean render()
    {
        // todo: seek better algorithm for addtive blending!

        if (!started) return false;
        if (!needsRender) return false;

        boolean changed = false;

        if (mode == PE_FIX_SIZE_MODE)
        {
            // look if there are ANY particles that need additive blending
            // if so, we use the third image as a step in between (much slower)
            // all renderings will then use a different rendering
            
            boolean hasAdditiveRendering = false;
            
            for (int i=particles.size()-1; i>=0; i--)
            {
                Particle p = particles.elementAt(i);
                if (p.config.mcolorAdditive)
                {
                    hasAdditiveRendering = true;
                }
            }
            resetRendererFM(hasAdditiveRendering);
            
            for (int i=particles.size()-1; i>=0; i--)
            {
                Particle p = particles.elementAt(i);
                if (p instanceof ParticleEmitter) continue;
                boolean rendered = false;
                if (hasAdditiveRendering)
                    rendered = renderOneParticleFMAdditive(p);
                else
                    rendered = renderOneParticleFM(p);
                changed = changed || rendered;
            }

            if (changed)
            {
                if (hasAdditiveRendering)
                {
                    // final copy of additive rendering to background layer!
                    BufferedImage work = image[(currentImage+1)%2];
                    Graphics2D g2 = (Graphics2D) work.getGraphics();
                    g2.drawImage(image[2], 0, 0, null);
                }
                currentImage = (currentImage+1)%2;
           }
        }
        needsRender = false;
        return changed;
    }
    
    void resetRendererFM(boolean hasAdditiveRendering)
    {
        // emptyResultImage 
        // just clear the non current image, alpha 0
        BufferedImage work = image[(currentImage+1)%2];
        
        Graphics2D graphics = (Graphics2D) work.getGraphics();
        graphics.setColor(background);
        graphics.fillRect(0, 0, width, height); 

        // if additive rendering must be done - clear the "work" image
        if (hasAdditiveRendering)
        {
            BufferedImage addplane = image[2];
            Graphics2D g2 = (Graphics2D) addplane.getGraphics();

            // clear it BLACK
            g2.setComposite(AlphaComposite.Clear); 
            g2.setColor(background);
            g2.fillRect(0, 0, width, height); 
            g2.setComposite(AlphaComposite.SrcOver);
        }
    }
    
    boolean renderOneParticleFM(Particle p)
    {
        // do the render due to parameters of particle
        if (oob(p)) return false;
        BufferedImage work = image[(currentImage+1)%2];
        Graphics2D g2 = (Graphics2D) work.getGraphics();
        g2.drawImage(p.getCurrentRendering(), (int)p.xD, (int)p.yD, null);
        return true;
    }

    boolean renderOneParticleFMAdditive(Particle p)
    {
        // do the render due to parameters of particle
        if (oob(p)) return false;

        BufferedImage addplane = image[2];
        BufferedImage addImage = p.getCurrentRendering();
        
        if (!p.config.mcolorAdditive)
        {
            Graphics2D g2 = (Graphics2D) addplane.getGraphics();
            g2.drawImage(p.getCurrentRendering(), (int)p.xD, (int)p.yD, null);
            return true;
        }
        
        // there is no "composite" setting for additive rendering
        // we have to do it by hand... each pixel... S L O W ...
        for (int y=0; y<p.height; y++)
        {
            int yy = ((int)p.yD)+y;
            if (yy<0) continue;
            if (yy>height) continue;
            for (int x=0; x<p.width; x++)
            {
                int xx = ((int)p.xD)+x;
                if (xx<0) continue;
                if (xx>width) continue;

                int addy = addImage.getRGB(x, y);
                if ( ( (addy&0xff000000) == 0) || ((addy&0xffffff) == 0) ) continue; // if there is nothing to add...

                int source = addplane.getRGB(xx, yy);
                
                if (source == 0xffffffff) continue; // if it is white allready, what is there left to add?
                
                int a = ((source >> 24) & 0xFF)+((addy >> 24) & 0xFF);
                int r = ((source >> 16) & 0xFF)+((addy >> 16) & 0xFF);
                int g = ((source >> 8) & 0xFF)+((addy >> 8) & 0xFF);
                int b = (source & 0xFF)+(addy & 0xFF);

                // what color is the "strongest"
                int max = r;
                if (g>max) 
                    max = g;
                else 
                    if (b>max) 
                        max = b;
                
                if (a < 255)
                {
                    // if alpha is smaller than max, than the color
                    // must be set in weighing of the rgb values
                    // scaling to the highest color
                    if (max>255)
                    {
                        float maxFactor = (float) (((float)255)/((float)max));
                    
                        r = (int) (r * maxFactor);
                        g = (int) (g * maxFactor);
                        b = (int) (b * maxFactor);
                    }
                }
                else
                {
                    // if alpha > 255, than set it to 255
                    // if alpha > max color
                    // than we allow colors to exceed the 255 range
                    // colors might be distorted to be white!

                    if (max < a) 
                    {
                        max = a;
                        float maxFactor = (float) (((float)255)/((float)max));

                        r = (int) (r * maxFactor);
                        g = (int) (g * maxFactor);
                        b = (int) (b * maxFactor);
                    }
                    a = 255;
                    if (r>255) r = 255;
                    if (g>255) g = 255;
                    if (b>255) b = 255;
                }

                int c  = b | (g << 8) | (r << 16) | (a << 24);
                addplane.setRGB(xx, yy, c);
            }
        }        
        return true;
    }

    // out of bounds?
    boolean oob(Particle p)
    {
        int x = (int) p.xD;
        int y = (int) p.yD;
        
        if (x>width) return true;
        if (y>height) return true;
        
        if (x+p.width<0) return true; 
        if (y+p.height<0) return true;

        return false;
    }
    
}
