/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

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

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

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


// import org.apache.batik.ext.awt.image.DataBufferReclaimer;
// import java.awt.image.DataBufferInt;
// import java.awt.image.SinglePixelPackedSampleModel;


/**
 * This is an abstract base class that takes care of most of the
 * normal issues surrounding the implementation of the CachableRed
 * (RenderedImage) interface.  It tries to make no assumptions about
 * the subclass implementation.
 *
 * @author <a href="mailto:Thomas.DeWeeese@Kodak.com">Thomas DeWeese</a>
 * @version $Id: AbstractRed.java 1804130 2017-08-04 14:41:11Z ssteiner $
 */
public abstract class AbstractRed implements CachableRed {

    protected Rectangle   bounds;
    protected Vector      srcs;
    protected Map         props;
    protected SampleModel sm;
    protected ColorModel  cm;
    protected int         tileGridXOff, tileGridYOff;
    protected int         tileWidth,    tileHeight;
    protected int         minTileX,     minTileY;
    protected int         numXTiles,    numYTiles;

    /**
     * void constructor. The subclass must call one of the
     * flavors of init before the object becomes usable.
     * This is useful when the proper parameters to the init
     * method need to be computed in the subclasses constructor.
     */
    protected AbstractRed() {
    }


    /**
     * Construct an Abstract RenderedImage from a bounds rect and props
     * (may be null).  The srcs Vector will be empty.
     * @param bounds this defines the extent of the rable in the
     * user coordinate system.
     * @param props this initializes the props Map (may be null)
     */
    protected AbstractRed(Rectangle bounds, Map props) {
        init((CachableRed)null, bounds, null, null,
             bounds.x, bounds.y, props);
    }

    /**
     * Construct an Abstract RenderedImage from a source image and
     * props (may be null).
     * @param src will be the first (and only) member of the srcs
     * Vector. Src is also used to set the bounds, ColorModel,
     * SampleModel, and tile grid offsets.
     * @param props this initializes the props Map.  */
    protected AbstractRed(CachableRed src, Map props) {
        init(src, src.getBounds(), src.getColorModel(), src.getSampleModel(),
             src.getTileGridXOffset(),
             src.getTileGridYOffset(),
             props);
    }

    /**
     * Construct an Abstract RenderedImage from a source image, bounds
     * rect and props (may be null).
     * @param src will be the first (and only) member of the srcs
     * Vector. Src is also used to set the ColorModel, SampleModel,
     * and tile grid offsets.
     * @param bounds The bounds of this image.
     * @param props this initializes the props Map.  */
    protected AbstractRed(CachableRed src, Rectangle bounds, Map props) {
        init(src, bounds, src.getColorModel(), src.getSampleModel(),
             src.getTileGridXOffset(),
             src.getTileGridYOffset(),
             props);
    }

    /**
     * Construct an Abstract RenderedImage from a source image, bounds
     * rect and props (may be null).
     * @param src if not null, will be the first (and only) member
     * of the srcs Vector. Also if it is not null it provides the
     * tile grid offsets, otherwise they are zero.
     * @param bounds The bounds of this image.
     * @param cm The ColorModel to use. If null it will default to
     * ComponentColorModel.
     * @param sm The sample model to use. If null it will construct
     * a sample model the matches the given/generated ColorModel and is
     * the size of bounds.
     * @param props this initializes the props Map.  */
    protected AbstractRed(CachableRed src, Rectangle bounds,
                          ColorModel cm, SampleModel sm,
                          Map props) {
        init(src, bounds, cm, sm,
             (src==null)?0:src.getTileGridXOffset(),
             (src==null)?0:src.getTileGridYOffset(),
             props);
    }

    /**
     * Construct an Abstract Rable from a bounds rect and props
     * (may be null).  The srcs Vector will be empty.
     * @param src will be the first (and only) member of the srcs
     * Vector. Src is also used to set the ColorModel, SampleModel,
     * and tile grid offsets.
     * @param bounds this defines the extent of the rable in the
     * user coordinate system.
     * @param cm The ColorModel to use. If null it will default to
     * ComponentColorModel.
     * @param sm The sample model to use. If null it will construct
     * a sample model the matches the given/generated ColorModel and is
     * the size of bounds.
     * @param tileGridXOff The x location of tile 0,0.
     * @param tileGridYOff The y location of tile 0,0.
     * @param props this initializes the props Map.
     */
    protected AbstractRed(CachableRed src, Rectangle bounds,
                          ColorModel cm, SampleModel sm,
                          int tileGridXOff, int tileGridYOff,
                          Map props) {
        init(src, bounds, cm, sm, tileGridXOff, tileGridYOff, props);
    }

    /**
     * This is one of two basic init function (this is for single
     * source rendereds).
     * It is provided so subclasses can compute various values
     * before initializing all the state in the base class.
     * You really should call this method before returning from
     * your subclass constructor.
     *
     * @param src    The source for the filter
     * @param bounds The bounds of the image
     * @param cm     The ColorModel to use. If null it defaults to
     *               ComponentColorModel/ src's ColorModel.
     * @param sm     The Sample modle to use. If this is null it will
     *               use the src's sample model if that is null it will
     *               construct a sample model that matches the ColorModel
     *               and is the size of the whole image.
     * @param tileGridXOff The x location of tile 0,0.
     * @param tileGridYOff The y location of tile 0,0.
     * @param props  Any properties you want to associate with the image.
     */
    protected void init(CachableRed src, Rectangle   bounds,
                        ColorModel  cm,   SampleModel sm,
                        int tileGridXOff, int tileGridYOff,
                        Map props) {
        this.srcs         = new Vector(1);
        if (src != null) {
            this.srcs.add(src);
            if (bounds == null) bounds = src.getBounds();
            if (cm     == null) cm     = src.getColorModel();
            if (sm     == null) sm     = src.getSampleModel();
        }

        this.bounds       = bounds;
        this.tileGridXOff = tileGridXOff;
        this.tileGridYOff = tileGridYOff;

        this.props        = new HashMap();
        if(props != null){
            this.props.putAll(props);
        }

        if (cm == null)
            cm = new ComponentColorModel
                (ColorSpace.getInstance(ColorSpace.CS_GRAY),
                 new int [] { 8 }, false, false, Transparency.OPAQUE,
                 DataBuffer.TYPE_BYTE);

        this.cm = cm;

        if (sm == null)
            sm = cm.createCompatibleSampleModel(bounds.width, bounds.height);
        this.sm = sm;

        // Recompute tileWidth/Height, minTileX/Y, numX/YTiles.
        updateTileGridInfo();
    }

    /**
     * Construct an Abstract Rable from a List of sources a bounds rect
     * and props (may be null).
     * @param srcs This is used to initialize the srcs Vector.  All
     * the members of srcs must be CachableRed otherwise an error
     * will be thrown.
     * @param bounds this defines the extent of the rendered in pixels
     * @param props this initializes the props Map.
     */
    protected AbstractRed(List srcs, Rectangle bounds, Map props) {
        init(srcs, bounds, null, null, bounds.x, bounds.y, props);
    }

    /**
     * Construct an Abstract RenderedImage from a bounds rect,
     * ColorModel (may be null), SampleModel (may be null) and props
     * (may be null).  The srcs Vector will be empty.
     * @param srcs This is used to initialize the srcs Vector.  All
     * the members of srcs must be CachableRed otherwise an error
     * will be thrown.
     * @param bounds this defines the extent of the rendered in pixels
     * @param cm The ColorModel to use. If null it will default to
     * ComponentColorModel.
     * @param sm The sample model to use. If null it will construct
     * a sample model the matches the given/generated ColorModel and is
     * the size of bounds.
     * @param props this initializes the props Map.
     */
    protected AbstractRed(List srcs, Rectangle bounds,
                          ColorModel cm, SampleModel sm,
                          Map props) {
        init(srcs, bounds, cm, sm, bounds.x, bounds.y, props);
    }

    /**
     * Construct an Abstract RenderedImage from a bounds rect,
     * ColorModel (may be null), SampleModel (may be null), tile grid
     * offsets and props (may be null).  The srcs Vector will be
     * empty.
     * @param srcs This is used to initialize the srcs Vector.  All
     * the members of srcs must be CachableRed otherwise an error
     * will be thrown.
     * @param bounds this defines the extent of the rable in the
     * user coordinate system.
     * @param cm The ColorModel to use. If null it will default to
     * ComponentColorModel.
     * @param sm The sample model to use. If null it will construct
     * a sample model the matches the given/generated ColorModel and is
     * the size of bounds.
     * @param tileGridXOff The x location of tile 0,0.
     * @param tileGridYOff The y location of tile 0,0.
     * @param props this initializes the props Map.
     */
    protected AbstractRed(List srcs, Rectangle bounds,
                          ColorModel cm, SampleModel sm,
                          int tileGridXOff, int tileGridYOff,
                          Map props) {
        init(srcs, bounds, cm, sm, tileGridXOff, tileGridYOff, props);
    }

    /**
     * This is the basic init function.
     * It is provided so subclasses can compute various values
     * before initializing all the state in the base class.
     * You really should call this method before returning from
     * your subclass constructor.
     *
     * @param srcs   The list of sources
     * @param bounds The bounds of the image
     * @param cm     The ColorModel to use. If null it defaults to
     *               ComponentColorModel.
     * @param sm     The Sample modle to use. If this is null it will
     *               construct a sample model that matches the ColorModel
     *               and is the size of the whole image.
     * @param tileGridXOff The x location of tile 0,0.
     * @param tileGridYOff The y location of tile 0,0.
     * @param props  Any properties you want to associate with the image.
     */
    protected void init(List srcs, Rectangle bounds,
                        ColorModel cm, SampleModel sm,
                        int tileGridXOff, int tileGridYOff,
                        Map props) {
        this.srcs = new Vector();
        if(srcs != null){
            this.srcs.addAll(srcs);
        }

        if (srcs.size() != 0) {
            CachableRed src = (CachableRed)srcs.get(0);
            if (bounds == null) bounds = src.getBounds();
            if (cm     == null) cm     = src.getColorModel();
            if (sm     == null) sm     = src.getSampleModel();
        }

        this.bounds       = bounds;
        this.tileGridXOff = tileGridXOff;
        this.tileGridYOff = tileGridYOff;
        this.props        = new HashMap();
        if(props != null){
            this.props.putAll(props);
        }

        if (cm == null)
            cm = new ComponentColorModel
                (ColorSpace.getInstance(ColorSpace.CS_GRAY),
                 new int [] { 8 }, false, false, Transparency.OPAQUE,
                 DataBuffer.TYPE_BYTE);

        this.cm = cm;

        if (sm == null)
            sm = cm.createCompatibleSampleModel(bounds.width, bounds.height);
        this.sm = sm;

        // Recompute tileWidth/Height, minTileX/Y, numX/YTiles.
        updateTileGridInfo();
    }

    /**
     * This function computes all the basic information about the tile
     * grid based on the data stored in sm, and tileGridX/YOff.
     * It is responsible for updating tileWidth, tileHeight,
     * minTileX/Y, and numX/YTiles.
     */
    protected void updateTileGridInfo() {
        this.tileWidth  = sm.getWidth();
        this.tileHeight = sm.getHeight();

        int x1, y1, maxTileX, maxTileY;

        // This computes and caches important information about the
        // structure of the tile grid in general.
        minTileX = getXTile(bounds.x);
        minTileY = getYTile(bounds.y);

        x1       = bounds.x + bounds.width-1;     // Xloc of right edge
        maxTileX = getXTile(x1);
        numXTiles = maxTileX-minTileX+1;

        y1       = bounds.y + bounds.height-1;     // Yloc of right edge
        maxTileY = getYTile(y1);
        numYTiles = maxTileY-minTileY+1;
    }


    public Rectangle getBounds() {
        return new Rectangle(getMinX(),
                             getMinY(),
                             getWidth(),
                             getHeight());
    }

    public Vector getSources() {
        return srcs;
    }

    public ColorModel getColorModel() {
        return cm;
    }

    public SampleModel getSampleModel() {
        return sm;
    }

    public int getMinX() {
        return bounds.x;
    }
    public int getMinY() {
        return bounds.y;
    }

    public int getWidth() {
        return bounds.width;
    }

    public int getHeight() {
        return bounds.height;
    }

    public int getTileWidth() {
        return tileWidth;
    }

    public int getTileHeight() {
        return tileHeight;
    }

    public int getTileGridXOffset() {
        return tileGridXOff;
    }

    public int getTileGridYOffset() {
        return tileGridYOff;
    }

    public int getMinTileX() {
        return minTileX;
    }

    public int getMinTileY() {
        return minTileY;
    }

    public int getNumXTiles() {
        return numXTiles;
    }

    public int getNumYTiles() {
        return numYTiles;
    }

    public Object getProperty(String name) {
        Object ret = props.get(name);
        if (ret != null) return ret;
        for (Object src : srcs) {
            RenderedImage ri = (RenderedImage) src;
            ret = ri.getProperty(name);
            if (ret != null) return ret;
        }
        return null;
    }

    public String [] getPropertyNames() {
        Set keys = props.keySet();
        String[] ret  = new String[keys.size()];
        keys.toArray( ret );

//        Iterator iter = keys.iterator();
//        int i=0;
//        while (iter.hasNext()) {
//            ret[i++] = (String)iter.next();
//        }

        for (Object src : srcs) {
            RenderedImage ri = (RenderedImage) src;
            String[] srcProps = ri.getPropertyNames();
            if (srcProps.length != 0) {
                String[] tmp = new String[ret.length + srcProps.length];
                System.arraycopy(ret, 0, tmp, 0, ret.length);
                /// ??? System.arraycopy((tmp,ret.length,srcProps,0,srcProps.length);
                System.arraycopy(srcProps, 0, tmp, ret.length, srcProps.length);
                ret = tmp;
            }
        }

        return ret;
    }

    public Shape getDependencyRegion(int srcIndex, Rectangle outputRgn) {
        if ((srcIndex < 0) || (srcIndex > srcs.size()))
            throw new IndexOutOfBoundsException
                ("Nonexistant source requested.");

        // Return empty rect if they don't intersect.
        if ( ! outputRgn.intersects(bounds) )
            return new Rectangle();

        // We only depend on our source for stuff that is inside
        // our bounds...
        return outputRgn.intersection(bounds);
    }

    public Shape getDirtyRegion(int srcIndex, Rectangle inputRgn) {
        if (srcIndex != 0)
            throw new IndexOutOfBoundsException
                ("Nonexistant source requested.");

        // Return empty rect if they don't intersect.
        if ( ! inputRgn.intersects(bounds) )
            return new Rectangle();

        // Changes in the input region don't propogate outside our
        // bounds.
        return inputRgn.intersection(bounds);
    }


    // This is not included but can be implemented by the following.
    // In which case you _must_ reimplement getTile.
    // public WritableRaster copyData(WritableRaster wr) {
    //     copyToRaster(wr);
    //     return wr;
    // }

    public Raster getTile(int tileX, int tileY) {
        WritableRaster wr = makeTile(tileX, tileY);
        return copyData(wr);
    }

    public Raster getData() {
        return getData(bounds);
    }

    public Raster getData(Rectangle rect) {
        SampleModel smRet = sm.createCompatibleSampleModel
            (rect.width, rect.height);

        Point pt = new Point(rect.x, rect.y);
        WritableRaster wr = Raster.createWritableRaster(smRet, pt);

        // System.out.println("GD DB: " + wr.getDataBuffer().getSize());
        return copyData(wr);
    }

    /**
     * Returns the x index of tile under xloc.
     * @param  xloc the x location (in pixels) to get tile for.
     * @return The tile index under xloc (may be outside tile grid).
     */
    public final int getXTile(int xloc) {
        int tgx = xloc-tileGridXOff;
        // We need to round to -infinity...
        if (tgx>=0)
            return tgx/tileWidth;
        else
            return (tgx-tileWidth+1)/tileWidth;
    }

    /**
     * Returns the y index of tile under yloc.
     * @param  yloc the y location (in pixels) to get tile for.
     * @return The tile index under yloc (may be outside tile grid).
     */
    public final int getYTile(int yloc) {
        int tgy = yloc-tileGridYOff;
        // We need to round to -infinity...
        if (tgy>=0)
            return tgy/tileHeight;
        else
            return (tgy-tileHeight+1)/tileHeight;
    }

    /**
     * Copies data from this images tile grid into wr.  wr may
     * extend outside the bounds of this image in which case the
     * data in wr outside the bounds will not be touched.
     * @param wr Raster to fill with image data.
     */
    public void copyToRaster(WritableRaster wr) {
        int tx0 = getXTile(wr.getMinX());
        int ty0 = getYTile(wr.getMinY());
        int tx1 = getXTile(wr.getMinX()+wr.getWidth() -1);
        int ty1 = getYTile(wr.getMinY()+wr.getHeight()-1);

        if (tx0 < minTileX) tx0 = minTileX;
        if (ty0 < minTileY) ty0 = minTileY;

        if (tx1 >= minTileX+numXTiles) tx1 = minTileX+numXTiles-1;
        if (ty1 >= minTileY+numYTiles) ty1 = minTileY+numYTiles-1;

        final boolean is_INT_PACK =
            GraphicsUtil.is_INT_PACK_Data(getSampleModel(), false);

        for (int y=ty0; y<=ty1; y++)
            for (int x=tx0; x<=tx1; x++) {
                Raster r = getTile(x, y);
                if (is_INT_PACK)
                    GraphicsUtil.copyData_INT_PACK(r, wr);
                else
                    GraphicsUtil.copyData_FALLBACK(r, wr);
            }
    }


    // static DataBufferReclaimer reclaim = new DataBufferReclaimer();

    /**
     * This is a helper function that will create the tile requested
     * Including properly subsetting the bounds of the tile to the
     * bounds of the current image.
     * @param tileX The x index of the tile to be built
     * @param tileY The y index of the tile to be built
     * @return The tile requested
     * @exception IndexOutOfBoundsException if the requested tile index
     *   falles outside of the bounds of the tile grid for the image.
     */
    public WritableRaster makeTile(int tileX, int tileY) {
        if ((tileX < minTileX) || (tileX >= minTileX+numXTiles) ||
            (tileY < minTileY) || (tileY >= minTileY+numYTiles))
            throw new IndexOutOfBoundsException
                ("Requested Tile (" + tileX + ',' + tileY +
                 ") lies outside the bounds of image");

        Point pt = new Point(tileGridXOff+tileX*tileWidth,
                             tileGridYOff+tileY*tileHeight);

        WritableRaster wr;
        wr = Raster.createWritableRaster(sm, pt);
        // if (!(sm instanceof SinglePixelPackedSampleModel))
        //     wr = Raster.createWritableRaster(sm, pt);
        // else {
        //     SinglePixelPackedSampleModel sppsm;
        //     sppsm = (SinglePixelPackedSampleModel)sm;
        //     int stride = sppsm.getScanlineStride();
        //     int sz = stride*sppsm.getHeight();
        //
        //     int [] data = reclaim.request(sz);
        //     DataBuffer db = new DataBufferInt(data, sz);
        //
        //     reclaim.register(db);
        //
        //     wr = Raster.createWritableRaster(sm, db, pt);
        // }

        // System.out.println("MT DB: " + wr.getDataBuffer().getSize());

        int x0 = wr.getMinX();
        int y0 = wr.getMinY();
        int x1 = x0+wr.getWidth() -1;
        int y1 = y0+wr.getHeight()-1;

        if ((x0 < bounds.x) || (x1 >= (bounds.x+bounds.width)) ||
            (y0 < bounds.y) || (y1 >= (bounds.y+bounds.height))) {
            // Part of this raster lies outside our bounds so subset
            // it so it only advertises the stuff inside our bounds.
            if (x0 < bounds.x) x0 = bounds.x;
            if (y0 < bounds.y) y0 = bounds.y;
            if (x1 >= (bounds.x+bounds.width))  x1 = bounds.x+bounds.width-1;
            if (y1 >= (bounds.y+bounds.height)) y1 = bounds.y+bounds.height-1;

            wr = wr.createWritableChild(x0, y0, x1-x0+1, y1-y0+1,
                                        x0, y0, null);
        }
        return wr;
    }

    public static void copyBand(Raster         src, int srcBand,
                                WritableRaster dst, int dstBand) {
        Rectangle srcR = new Rectangle(src.getMinX(),  src.getMinY(),
                                       src.getWidth(), src.getHeight());
        Rectangle dstR = new Rectangle(dst.getMinX(),  dst.getMinY(),
                                       dst.getWidth(), dst.getHeight());

        Rectangle cpR  = srcR.intersection(dstR);

        int [] samples = null;
        for (int y=cpR.y; y< cpR.y+cpR.height; y++) {
            samples = src.getSamples(cpR.x, y, cpR.width, 1, srcBand, samples);
            dst.setSamples(cpR.x, y, cpR.width, 1, dstBand, samples);
        }
    }
}

