/*
 * 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.
 */

/* $Id: AbstractRed.java 1804124 2017-08-04 14:13:54Z ssteiner $ */

package org.apache.xmlgraphics.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.xmlgraphics.image.GraphicsUtil;

// CSOFF: LocalVariableName
// CSOFF: MultipleVariableDeclarations
// CSOFF: NeedBraces
// CSOFF: NoWhitespaceAfter
// CSOFF: WhitespaceAround

/**
 * 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.
 *
 * @version $Id: AbstractRed.java 1804124 2017-08-04 14:13:54Z ssteiner $
 *
 * Originally authored by Thomas DeWeese.
 */
public abstract class AbstractRed implements CachableRed {

    protected Rectangle   bounds;
    protected Vector      srcs;
    protected Map         props;
    protected SampleModel sm;
    protected ColorModel  cm;
    protected int tileGridXOff;
    protected int tileGridYOff;
    protected int tileWidth;
    protected int tileHeight;
    protected int minTileX;
    protected int minTileY;
    protected int numXTiles;
    protected int 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;
        int y1;
        int maxTileX;
        int 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);

        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(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(
                "Nonexistent 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(
                "Nonexistent 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 isIntPack =
            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 (isIntPack) {
                    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);
        }
    }
}

