/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

package org.apache.batik.ext.awt.image.rendered;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBufferInt;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;

import org.apache.batik.ext.awt.image.GraphicsUtil;

/**
 * This filter simply tiles its tile starting from the upper
 * left corner of the tiled region.
 * 
 * @author <a href="mailto:vincent.hardy@eng.sun.com">Vincent Hardy</a>
 * @version $Id: TileRed.java,v 1.11 2003/04/11 13:58:01 vhardy Exp $
 */
public class TileRed extends AbstractRed implements TileGenerator {
    static final AffineTransform IDENTITY = new AffineTransform();

    /**
     * Area tiled by this filter. 
     */
    Rectangle tiledRegion;

    int xStep;
    int yStep;

    TileStore tiles;

    private RenderingHints  hints;

    final boolean is_INT_PACK;
    final boolean alphaPremult;

    /**
     * Tile
     */
    RenderedImage  tile   = null;
    WritableRaster raster = null;


    public TileRed(RenderedImage tile,
                   Rectangle tiledRegion) {
        this(tile, tiledRegion, tile.getWidth(), tile.getHeight(), null);
    }

    public TileRed(RenderedImage tile,
                   Rectangle tiledRegion,
                   RenderingHints hints) {
        this(tile, tiledRegion, tile.getWidth(), tile.getHeight(), hints);
    }

    public TileRed(RenderedImage tile, 
                   Rectangle tiledRegion,
                   int xStep, int yStep) {
        this(tile, tiledRegion, xStep, yStep, null);
    }

    public TileRed(RenderedImage tile, 
                   Rectangle tiledRegion,
                   int xStep, int yStep,
                   RenderingHints hints) {
        if(tiledRegion == null){
            throw new IllegalArgumentException();
        }

        if(tile == null){
            throw new IllegalArgumentException();
        }

        // org.apache.batik.test.gvt.ImageDisplay.showImage("Tile: ", tile);
        this.tiledRegion  = tiledRegion;
        this.xStep        = xStep;
        this.yStep        = yStep;
        this.hints        = hints;
        this.alphaPremult = false;

        SampleModel sm = fixSampleModel(tile, xStep, yStep, 
                                        tiledRegion.width,
                                        tiledRegion.height);
        ColorModel cm  = fixColorModel(tile, alphaPremult);

        double smSz   = AbstractTiledRed.getDefaultTileSize();
        smSz = smSz*smSz;

        double stepSz = (xStep*(double)yStep);
        // be prepaired to grow the default tile size quite a bit if
        // it means the image tile will fit in it...
        if (16.1*smSz > stepSz) {
            int xSz = xStep;
            int ySz = yStep;

            // If the pattern size is small then have multiple copies
            // in our tile.
            if (4*stepSz <= smSz) {
                int mult = (int)Math.ceil(Math.sqrt(smSz/stepSz));
                xSz *= mult;
                ySz *= mult;
            }
            // System.out.println("Using Raster for pattern");
            sm = sm.createCompatibleSampleModel(xSz, ySz);
            raster = Raster.createWritableRaster
                (sm, new Point(tile.getMinX(), tile.getMinY()));
        }
        
        is_INT_PACK = GraphicsUtil.is_INT_PACK_Data(sm, false);
        // System.out.println("Is INT PACK: " + is_INT_PACK);

        // Initialize our base class We set our bounds be we will
        // respond with data for any area we cover.  This is needed
        // because the userRegion passed into PatterPaintContext
        // doesn't account for stroke So we use that as a basis but
        // when the context asks us for stuff outside that region we
        // complie.
        init((CachableRed)null, tiledRegion, cm, sm, 
             tile.getMinX(), tile.getMinY(), null);

        if (raster != null) {
            WritableRaster fromRaster = raster.createWritableChild
                (tile.getMinX(), tile.getMinY(), 
                 xStep, yStep, tile.getMinX(), tile.getMinY(), null);

            // Fill one 'tile' of the input....
            fillRasterFrom(fromRaster, tile);
            fillOutRaster(raster);
        }
        else {
            this.tile        = new TileCacheRed(GraphicsUtil.wrap(tile));
        }
    }

    public WritableRaster copyData(WritableRaster wr) {
        int xOff = ((int)Math.floor(wr.getMinX()/xStep))*xStep;
        int yOff = ((int)Math.floor(wr.getMinY()/yStep))*yStep;
        int x0   = wr.getMinX()-xOff;
        int y0   = wr.getMinY()-yOff;
        int tx0 = getXTile(x0);
        int ty0 = getYTile(y0);
        int tx1 = getXTile(x0+wr.getWidth() -1);
        int ty1 = getYTile(y0+wr.getHeight()-1);

        for (int y=ty0; y<=ty1; y++)
            for (int x=tx0; x<=tx1; x++) {
                Raster r = getTile(x, y);
                r = r.createChild(r.getMinX(),      r.getMinY(), 
                                  r.getWidth(),     r.getHeight(),
                                  r.getMinX()+xOff, r.getMinY()+yOff, null);
                if (is_INT_PACK)
                    GraphicsUtil.copyData_INT_PACK(r, wr);
                else
                    GraphicsUtil.copyData_FALLBACK(r, wr);
            }
        return wr;
    }


    public Raster getTile(int x, int y) {
        
        if (raster!=null) {
            // We have a Single raster that we translate where needed
            // position.  So just offest appropriately.
            int tx = tileGridXOff+x*tileWidth;
            int ty = tileGridYOff+y*tileHeight;
            return raster.createTranslatedChild(tx, ty);
        }

        // System.out.println("Checking Cache [" + x + "," + y + "]");
        return genTile(x,y);
    }

    public Raster genTile(int x, int y) {
      // System.out.println("Cache Miss     [" + x + "," + y + "]");
        int tx = tileGridXOff+x*tileWidth;
        int ty = tileGridYOff+y*tileHeight;
        
        if (raster!=null) {
            // We have a Single raster that we translate where needed
            // position.  So just offest appropriately.
            return raster.createTranslatedChild(tx, ty);
        }

        Point pt = new Point(tx, ty);
        WritableRaster wr = Raster.createWritableRaster(sm, pt);
        fillRasterFrom(wr, tile);
        return wr;
    }

    public WritableRaster fillRasterFrom(WritableRaster wr, RenderedImage src){
        // System.out.println("Getting Raster : " + count + " " + wr.getMinX() + "/" + wr.getMinY() + "/" + wr.getWidth() + "/" + wr.getHeight());
        // System.out.println("Tile           : " + tile.getMinX() + "/" + tile.getMinY() + "/" + tile.getWidth() + "/" + tile.getHeight());

        ColorModel cm = getColorModel();
        BufferedImage bi
            = new BufferedImage(cm,
                                wr.createWritableTranslatedChild(0, 0),
                                cm.isAlphaPremultiplied(), null);

        Graphics2D g = GraphicsUtil.createGraphics(bi, hints);

        int minX = wr.getMinX();
        int minY = wr.getMinY();
        int maxX = wr.getWidth();
        int maxY = wr.getHeight();


        g.setComposite(AlphaComposite.Clear);
        g.setColor(new Color(0, 0, 0, 0));
        g.fillRect(0, 0, maxX, maxY);
        g.setComposite(AlphaComposite.SrcOver);

        g.translate(-minX, -minY);

        // Process initial translate so that tile is
        // painted to the left of the raster top-left 
        // corner on the first drawRenderedImage
        int x1 = src.getMinX()+src.getWidth()-1;
        int y1 = src.getMinY()+src.getHeight()-1;

        int tileTx = (int)Math.ceil(((minX-x1)/xStep))*xStep;
        int tileTy = (int)Math.ceil(((minY-y1)/yStep))*yStep;

        g.translate(tileTx, tileTy);

        int curX = tileTx - wr.getMinX() + src.getMinX();
        int curY = tileTy - wr.getMinY() + src.getMinY();

        // System.out.println("Wr: " + wr.getBounds());
        // System.out.println("Src : [" + src.getMinX() + ", " + 
        //                    src.getMinY()  + ", " + 
        //                    src.getWidth() + ", " +
        //                    src.getHeight() + "]");
        // System.out.println("tileTx/tileTy : " + tileTx + " / " + tileTy);
        minX = curX;
        while(curY < maxY) {
            while (curX < maxX) {
                // System.out.println("curX/curY : " + curX + " / " + curY);
                // System.out.println("transform : " + 
                //                    g.getTransform().getTranslateX() + 
                //                    " / " + 
                //                    g.getTransform().getTranslateY());
                GraphicsUtil.drawImage(g, src);
                curX += xStep;
                g.translate(xStep, 0);
                if (Thread.currentThread().isInterrupted())
                    return wr;
            }
            curY += yStep;
            g.translate(minX-curX, yStep);
            curX = minX;
        }
        
        /*g.setTransform(new AffineTransform());
        g.setPaint(colors[count++]);
        count %= colors.length;

        g.fillRect(0, 0, maxX, maxY);*/
        GraphicsUtil.coerceData(wr, src.getColorModel(), alphaPremult);
        return wr;
    }

    protected void fillOutRaster(WritableRaster wr) {
        if (is_INT_PACK)
            fillOutRaster_INT_PACK(wr);
        else
            fillOutRaster_FALLBACK(wr);
        
    }

    protected void fillOutRaster_INT_PACK(WritableRaster wr) {
        // System.out.println("Fast copyData");
        int x0 = wr.getMinX();
        int y0 = wr.getMinY();
        int width  = wr.getWidth();
        int height = wr.getHeight();

        SinglePixelPackedSampleModel sppsm;
        sppsm = (SinglePixelPackedSampleModel)wr.getSampleModel();

        final int     scanStride = sppsm.getScanlineStride();
        DataBufferInt db         = (DataBufferInt)wr.getDataBuffer();
        final int []  pixels     = db.getBankData()[0];
        final int     base =
            (db.getOffset() +
             sppsm.getOffset(x0-wr.getSampleModelTranslateX(),
                             y0-wr.getSampleModelTranslateY()));
        int step = xStep;
        for (int x=xStep; x<width; x+=step, step*=2) {
            int w = step;
            if (x+w > width) w = width-x;
            if (w >= 128) {
                int srcSP = base;
                int dstSP = base+x;
                for(int y=0; y<yStep; y++) {
                    System.arraycopy(pixels, srcSP, pixels, dstSP, w);
                    srcSP += scanStride;
                    dstSP += scanStride;
                }
            } else {
                int srcSP = base;
                int dstSP = base+x;
                for(int y=0; y<yStep; y++) {
                    int end = srcSP;
                    srcSP += w-1;
                    dstSP += w-1;
                    while(srcSP>=end)
                        pixels[dstSP--] = pixels[srcSP--];
                    srcSP+=scanStride+1;
                    dstSP+=scanStride+1;
                }
            }
        }

        step = yStep;
        for (int y=yStep; y<height; y+=step, step*=2) {
            int h = step;
            if (y+h > height) h = height-y;
            int dstSP = base+y*scanStride;
            System.arraycopy(pixels, base, pixels, dstSP, h*scanStride);
        }
    }

    protected void fillOutRaster_FALLBACK(WritableRaster wr) {
        // System.out.println("Fast copyData");
        int x0     = wr.getMinX();
        int y0     = wr.getMinY();
        int width  = wr.getWidth();
        int height = wr.getHeight();

        Object data = null;

        int step = xStep;
        for (int x=xStep; x<width; x+=step, step*=4) {
            int w = step;
            if (x+w > width) w = width-x;
            data = wr.getDataElements(0, 0, w, yStep, data);
            wr.setDataElements(x, 0, w, yStep, data);
            x+=w;

            if (x >= width) break;
            if (x+w > width) w = width-x;
            wr.setDataElements(x, 0, w, yStep, data);
            x+=w;

            if (x >= width) break;
            if (x+w > width) w = width-x;
            wr.setDataElements(x, 0, w, yStep, data);
        }

        step = yStep;
        for (int y=yStep; y<height; y+=step, step*=4) {
            int h = step;
            if (y+h > height) h = height-y;
            data = wr.getDataElements(0, 0, width, h, data);
            wr.setDataElements(0, y, width, h, data);
            y+=h;

            if (h >= height) break;
            if (y+h > height) h = height-y;
            wr.setDataElements(0, y, width, h, data);
            y+=h;

            if (h >= height) break;
            if (y+h > height) h = height-y;
            wr.setDataElements(0, y, width, h, data);
            y+=h;
        }
    }

    protected static ColorModel fixColorModel(RenderedImage src,
                                              boolean alphaPremult) {
        return GraphicsUtil.coerceColorModel(src.getColorModel(), 
                                             alphaPremult);
    }
    
    /**
     * This function 'fixes' the source's sample model.
     * right now it just ensures that the sample model isn't
     * much larger than my width.
     */
    protected static SampleModel fixSampleModel(RenderedImage src,
                                                int stepX, int stepY,
                                                int width, int height) {
        int defSz = AbstractTiledRed.getDefaultTileSize();
        SampleModel sm = src.getSampleModel();
        int w = sm.getWidth();
        if (w < defSz) w = defSz;
        if (w > stepX)  w = stepX;
        // if (w > width)  w = width;
        int h = sm.getHeight();
        if (h < defSz) h = defSz;
        if (h > stepY) h = stepY;
        // if (h > height) h = height;
        return sm.createCompatibleSampleModel(w, h);
    }
}
