/*
 * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.imageio.plugins.common;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.RenderedImage;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

public abstract class SimpleRenderedImage implements RenderedImage {
    /** The X coordinate of the image's upper-left pixel. */
    protected int minX;

    /** The Y coordinate of the image's upper-left pixel. */
    protected int minY;

    /** The image's width in pixels. */
    protected int width;

    /** The image's height in pixels. */
    protected int height;

    /** The width of a tile. */
    protected int tileWidth;

    /** The height of a tile. */
    protected int tileHeight;

    /** The X coordinate of the upper-left pixel of tile (0, 0). */
    protected int tileGridXOffset = 0;

    /** The Y coordinate of the upper-left pixel of tile (0, 0). */
    protected int tileGridYOffset = 0;

    /** The image's SampleModel. */
    protected SampleModel sampleModel;

    /** The image's ColorModel. */
    protected ColorModel colorModel;

    /** The image's sources, stored in a Vector. */
    protected Vector<RenderedImage> sources = new Vector<RenderedImage>();

    /** A Hashtable containing the image properties. */
    protected Hashtable<String,Object> properties = new Hashtable<String,Object>();

    /** Returns the X coordinate of the leftmost column of the image. */
    public int getMinX() {
        return minX;
    }

    /**
     * Returns the X coordinate of the column immediately to the
     * right of the rightmost column of the image.  getMaxX() is
     * implemented in terms of getMinX() and getWidth() and so does
     * not need to be implemented by subclasses.
     */
    public final int getMaxX() {
        return getMinX() + getWidth();
    }

    /** Returns the X coordinate of the uppermost row of the image. */
    public int getMinY() {
        return minY;
    }

    /**
     * Returns the Y coordinate of the row immediately below the
     * bottom row of the image.  getMaxY() is implemented in terms of
     * getMinY() and getHeight() and so does not need to be
     * implemented by subclasses.
     */
    public final int getMaxY() {
        return getMinY() + getHeight();
    }

    /** Returns the width of the image. */
    public int getWidth() {
        return width;
    }

    /** Returns the height of the image. */
    public int getHeight() {
        return height;
    }

    /** Returns a Rectangle indicating the image bounds. */
    public Rectangle getBounds() {
        return new Rectangle(getMinX(), getMinY(), getWidth(), getHeight());
    }

    /** Returns the width of a tile. */
    public int getTileWidth() {
        return tileWidth;
    }

    /** Returns the height of a tile. */
    public int getTileHeight() {
        return tileHeight;
    }

    /**
     * Returns the X coordinate of the upper-left pixel of tile (0, 0).
     */
    public int getTileGridXOffset() {
        return tileGridXOffset;
    }

    /**
     * Returns the Y coordinate of the upper-left pixel of tile (0, 0).
     */
    public int getTileGridYOffset() {
        return tileGridYOffset;
    }

    /**
     * Returns the horizontal index of the leftmost column of tiles.
     * getMinTileX() is implemented in terms of getMinX()
     * and so does not need to be implemented by subclasses.
     */
    public int getMinTileX() {
        return XToTileX(getMinX());
    }

    /**
     * Returns the horizontal index of the rightmost column of tiles.
     * getMaxTileX() is implemented in terms of getMaxX()
     * and so does not need to be implemented by subclasses.
     */
    public int getMaxTileX() {
        return XToTileX(getMaxX() - 1);
    }

    /**
     * Returns the number of tiles along the tile grid in the
     * horizontal direction.  getNumXTiles() is implemented in terms
     * of getMinTileX() and getMaxTileX() and so does not need to be
     * implemented by subclasses.
     */
    public int getNumXTiles() {
        return getMaxTileX() - getMinTileX() + 1;
    }

    /**
     * Returns the vertical index of the uppermost row of tiles.  getMinTileY()
     * is implemented in terms of getMinY() and so does not need to be
     * implemented by subclasses.
     */
    public int getMinTileY() {
        return YToTileY(getMinY());
    }

    /**
     * Returns the vertical index of the bottom row of tiles.  getMaxTileY()
     * is implemented in terms of getMaxY() and so does not need to
     * be implemented by subclasses.
     */
    public int getMaxTileY() {
        return YToTileY(getMaxY() - 1);
    }

    /**
     * Returns the number of tiles along the tile grid in the vertical
     * direction.  getNumYTiles() is implemented in terms
     * of getMinTileY() and getMaxTileY() and so does not need to be
     * implemented by subclasses.
     */
    public int getNumYTiles() {
        return getMaxTileY() - getMinTileY() + 1;
    }

    /** Returns the SampleModel of the image. */
    public SampleModel getSampleModel() {
        return sampleModel;
    }

    /** Returns the ColorModel of the image. */
    public ColorModel getColorModel() {
        return colorModel;
    }

    /**
     * Gets a property from the property set of this image.  If the
     * property name is not recognized,
     * <code>java.awt.Image.UndefinedProperty</code> will be returned.
     *
     * @param name the name of the property to get, as a
     * <code>String</code>.  @return a reference to the property
     * <code>Object</code>, or the value
     * <code>java.awt.Image.UndefinedProperty.</code>
     */
    public Object getProperty(String name) {
        name = name.toLowerCase();
        Object value = properties.get(name);
        return value != null ? value : java.awt.Image.UndefinedProperty;
    }

    /**
     * Returns a list of the properties recognized by this image.  If
     * no properties are available, <code>null</code> will be
     * returned.
     *
     * @return an array of <code>String</code>s representing valid
     *         property names.
     */
    public String[] getPropertyNames() {
        String[] names = null;

        if(properties.size() > 0) {
            names = new String[properties.size()];
            int index = 0;

            Enumeration<String> e = properties.keys();
            while (e.hasMoreElements()) {
                String name = e.nextElement();
                names[index++] = name;
            }
        }

        return names;
    }

    /**
     * Returns an array of <code>String</code>s recognized as names by
     * this property source that begin with the supplied prefix.  If
     * no property names match, <code>null</code> will be returned.
     * The comparison is done in a case-independent manner.
     *
     * <p> The default implementation calls
     * <code>getPropertyNames()</code> and searches the list of names
     * for matches.
     *
     * @return an array of <code>String</code>s giving the valid
     * property names.
     */
    public String[] getPropertyNames(String prefix) {
        String[] propertyNames = getPropertyNames();
        if (propertyNames == null) {
            return null;
        }

        prefix = prefix.toLowerCase();

        ArrayList<String> names = new ArrayList<String>();
        for (int i = 0; i < propertyNames.length; i++) {
            if (propertyNames[i].startsWith(prefix)) {
                names.add(propertyNames[i]);
            }
        }

        if (names.size() == 0) {
            return null;
        }

        String[] prefixNames = names.toArray(new String[0]);
        return prefixNames;
    }

    // Utility methods.

    /**
     * Converts a pixel's X coordinate into a horizontal tile index
     * relative to a given tile grid layout specified by its X offset
     * and tile width.
     */
    public static int XToTileX(int x, int tileGridXOffset, int tileWidth) {
        x -= tileGridXOffset;
        if (x < 0) {
            x += 1 - tileWidth; // Force round to -infinity
        }
        return x/tileWidth;
    }

    /**
     * Converts a pixel's Y coordinate into a vertical tile index
     * relative to a given tile grid layout specified by its Y offset
     * and tile height.
     */
    public static int YToTileY(int y, int tileGridYOffset, int tileHeight) {
        y -= tileGridYOffset;
        if (y < 0) {
            y += 1 - tileHeight; // Force round to -infinity
        }
        return y/tileHeight;
    }

    /**
     * Converts a pixel's X coordinate into a horizontal tile index.
     * This is a convenience method.  No attempt is made to detect
     * out-of-range coordinates.
     *
     * @param x the X coordinate of a pixel.
     * @return the X index of the tile containing the pixel.
     */
    public int XToTileX(int x) {
        return XToTileX(x, getTileGridXOffset(), getTileWidth());
    }

    /**
     * Converts a pixel's Y coordinate into a vertical tile index.
     * This is a convenience method.  No attempt is made to detect
     * out-of-range coordinates.
     *
     * @param y the Y coordinate of a pixel.
     * @return the Y index of the tile containing the pixel.
     */
    public int YToTileY(int y) {
        return YToTileY(y, getTileGridYOffset(), getTileHeight());
    }

    /**
     * Converts a horizontal tile index into the X coordinate of its
     * upper left pixel relative to a given tile grid layout specified
     * by its X offset and tile width.
     */
    public static int tileXToX(int tx, int tileGridXOffset, int tileWidth) {
        return tx*tileWidth + tileGridXOffset;
    }

    /**
     * Converts a vertical tile index into the Y coordinate of
     * its upper left pixel relative to a given tile grid layout
     * specified by its Y offset and tile height.
     */
    public static int tileYToY(int ty, int tileGridYOffset, int tileHeight) {
        return ty*tileHeight + tileGridYOffset;
    }

    /**
     * Converts a horizontal tile index into the X coordinate of its
     * upper left pixel.  This is a convenience method.  No attempt is made
     * to detect out-of-range indices.
     *
     * @param tx the horizontal index of a tile.
     * @return the X coordinate of the tile's upper left pixel.
     */
    public int tileXToX(int tx) {
        return tx*tileWidth + tileGridXOffset;
    }

    /**
     * Converts a vertical tile index into the Y coordinate of its
     * upper left pixel.  This is a convenience method.  No attempt is made
     * to detect out-of-range indices.
     *
     * @param ty the vertical index of a tile.
     * @return the Y coordinate of the tile's upper left pixel.
     */
    public int tileYToY(int ty) {
        return ty*tileHeight + tileGridYOffset;
    }

    public Vector<RenderedImage> getSources() {
        return null;
    }

    /**
     * Returns the entire image in a single Raster.  For images with
     * multiple tiles this will require making a copy.
     *
     * <p> The returned Raster is semantically a copy.  This means
     * that updates to the source image will not be reflected in the
     * returned Raster.  For non-writable (immutable) source images,
     * the returned value may be a reference to the image's internal
     * data.  The returned Raster should be considered non-writable;
     * any attempt to alter its pixel data (such as by casting it to
     * WritableRaster or obtaining and modifying its DataBuffer) may
     * result in undefined behavior.  The copyData method should be
     * used if the returned Raster is to be modified.
     *
     * @return a Raster containing a copy of this image's data.
     */
    public Raster getData() {
        Rectangle rect = new Rectangle(getMinX(), getMinY(),
                                       getWidth(), getHeight());
        return getData(rect);
    }

    /**
     * Returns an arbitrary rectangular region of the RenderedImage
     * in a Raster.  The rectangle of interest will be clipped against
     * the image bounds.
     *
     * <p> The returned Raster is semantically a copy.  This means
     * that updates to the source image will not be reflected in the
     * returned Raster.  For non-writable (immutable) source images,
     * the returned value may be a reference to the image's internal
     * data.  The returned Raster should be considered non-writable;
     * any attempt to alter its pixel data (such as by casting it to
     * WritableRaster or obtaining and modifying its DataBuffer) may
     * result in undefined behavior.  The copyData method should be
     * used if the returned Raster is to be modified.
     *
     * @param bounds the region of the RenderedImage to be returned.
     */
    public Raster getData(Rectangle bounds) {
        // Get the image bounds.
        Rectangle imageBounds = getBounds();

        // Check for parameter validity.
        if(bounds == null) {
            bounds = imageBounds;
        } else if(!bounds.intersects(imageBounds)) {
            throw new IllegalArgumentException("The provided region doesn't intersect with the image bounds.");
        }

        // Determine tile limits for the prescribed bounds.
        int startX = XToTileX(bounds.x);
        int startY = YToTileY(bounds.y);
        int endX = XToTileX(bounds.x + bounds.width - 1);
        int endY = YToTileY(bounds.y + bounds.height - 1);

        // If the bounds are contained in a single tile, return a child
        // of that tile's Raster.
        if ((startX == endX) && (startY == endY)) {
            Raster tile = getTile(startX, startY);
            return tile.createChild(bounds.x, bounds.y,
                                    bounds.width, bounds.height,
                                    bounds.x, bounds.y, null);
        } else {
            // Recalculate the tile limits if the data bounds are not a
            // subset of the image bounds.
            if(!imageBounds.contains(bounds)) {
                Rectangle xsect = bounds.intersection(imageBounds);
                startX = XToTileX(xsect.x);
                startY = YToTileY(xsect.y);
                endX = XToTileX(xsect.x + xsect.width - 1);
                endY = YToTileY(xsect.y + xsect.height - 1);
            }

            // Create a WritableRaster of the desired size
            SampleModel sm =
                sampleModel.createCompatibleSampleModel(bounds.width,
                                                        bounds.height);

            // Translate it
            WritableRaster dest =
                Raster.createWritableRaster(sm, bounds.getLocation());

            // Loop over the tiles in the intersection.
            for (int j = startY; j <= endY; j++) {
                for (int i = startX; i <= endX; i++) {
                    // Retrieve the tile.
                    Raster tile = getTile(i, j);

                    // Create a child of the tile for the intersection of
                    // the tile bounds and the bounds of the requested area.
                    Rectangle tileRect = tile.getBounds();
                    Rectangle intersectRect =
                        bounds.intersection(tile.getBounds());
                    Raster liveRaster = tile.createChild(intersectRect.x,
                                                         intersectRect.y,
                                                         intersectRect.width,
                                                         intersectRect.height,
                                                         intersectRect.x,
                                                         intersectRect.y,
                                                         null);

                    // Copy the data from the child.
                    dest.setRect(liveRaster);
                }
            }

            return dest;
        }
    }

    /**
     * Copies an arbitrary rectangular region of the RenderedImage
     * into a caller-supplied WritableRaster.  The region to be
     * computed is determined by clipping the bounds of the supplied
     * WritableRaster against the bounds of the image.  The supplied
     * WritableRaster must have a SampleModel that is compatible with
     * that of the image.
     *
     * <p> If the raster argument is null, the entire image will
     * be copied into a newly-created WritableRaster with a SampleModel
     * that is compatible with that of the image.
     *
     * @param dest a WritableRaster to hold the returned portion of
     *        the image.
     * @return a reference to the supplied WritableRaster, or to a
     *         new WritableRaster if the supplied one was null.
     */
    public WritableRaster copyData(WritableRaster dest) {
        // Get the image bounds.
        Rectangle imageBounds = getBounds();

        Rectangle bounds;
        if (dest == null) {
            // Create a WritableRaster for the entire image.
            bounds = imageBounds;
            Point p = new Point(minX, minY);
            SampleModel sm =
                sampleModel.createCompatibleSampleModel(width, height);
            dest = Raster.createWritableRaster(sm, p);
        } else {
            bounds = dest.getBounds();
        }

        // Determine tile limits for the intersection of the prescribed
        // bounds with the image bounds.
        Rectangle xsect = imageBounds.contains(bounds) ?
            bounds : bounds.intersection(imageBounds);
        int startX = XToTileX(xsect.x);
        int startY = YToTileY(xsect.y);
        int endX = XToTileX(xsect.x + xsect.width - 1);
        int endY = YToTileY(xsect.y + xsect.height - 1);

        // Loop over the tiles in the intersection.
        for (int j = startY; j <= endY; j++) {
            for (int i = startX; i <= endX; i++) {
                // Retrieve the tile.
                Raster tile = getTile(i, j);

                // Create a child of the tile for the intersection of
                // the tile bounds and the bounds of the requested area.
                Rectangle tileRect = tile.getBounds();
                Rectangle intersectRect =
                    bounds.intersection(tile.getBounds());
                Raster liveRaster = tile.createChild(intersectRect.x,
                                                     intersectRect.y,
                                                     intersectRect.width,
                                                     intersectRect.height,
                                                     intersectRect.x,
                                                     intersectRect.y,
                                                     null);

                // Copy the data from the child.
                dest.setRect(liveRaster);
            }
        }

        return dest;
    }
}
