/*
 *  
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program 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. 
 * 
 * This program 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 at /legal/license.txt). 
 * 
 * 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 Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.midp.chameleon;

import javax.microedition.lcdui.*;
import com.sun.midp.chameleon.skins.*;

/**
 * This class represents a "layer". A layer is an element used to comprise
 * a higher-level "window" (see CWindow.java). A layer has several properties
 * such as its visibility, whether it accepts input, its size, etc. A layer
 * also has content, such as its background and its foreground.
 */
public class CLayer {
    
    /** Flag indicating this layer is in need of repainting. */
    private boolean dirty;
    
    /** Array holding a bounding rectangle of an area needing repainting. */
    protected int[] dirtyBounds;
    
    /** Flag indicating if this layer has a transparent background or not. */
    protected boolean transparent;
    
    /** Flag indicating the current visibility state of this layer. */
    protected boolean visible;
    
    /** Flag indicating the ability of this layer to support key/pen input. */
    protected boolean supportsInput;

    /** 
     * Flag indicating this layer is either completely opaque, or not.
     * By default, a layer is not opaque, and thus requires the background
     * and any layers below it to be painted in addition to itself. However,
     * if a layer is opaque, it does not require anything it is obscuring to
     * be painted, just itself. All layers are opaque by default.
     */    
    protected boolean opaque = true;
    
    /** 
     * If this layer has a filled color background, 
     * the 0xrrggbbaa value of that color 
     */
    protected int   bgColor;
    
    /**
     * The image to use as a background for this layer if this layer
     * is not transparent and does not use a fill color.
     */
    protected Image[] bgImage;
    
    /**
     * If this layer is not transparent and uses a background image,
     * a flag indicating if that image should be tiled or otherwise centered.
     */
    protected boolean tileBG;
    
    /**
     * The window which owns this layer. If this layer has not been added
     * to a window or it has been removed from a window, the owner will be
     * null.
     */
    protected CWindow owner;

    /**
     * An array holding the bounds of this layer. The indices are
     * as follows:
     * 0 = layer's 'x' coordinate
     * 1 = layer's 'y' coordinate
     * 2 = layer's width
     * 3 = layer's height
     * The 'x' and 'y' coordinates are in the coordinate space of the
     * window which contains this layer.
     */
    public int[]   bounds;

    /** Constant used to reference the '0' index of the bounds array */
    public static final int X = 0;
    
    /** Constant used to reference the '1' index of the bounds array */
    public static final int Y = 1;
    
    /** Constant used to reference the '2' index of the bounds array */
    public static final int W = 2;
    
    /** Constant used to reference the '3' index of the bounds array */
    public static final int H = 3;
    
    /** 
     * When in the paint() routine, graphicsColor will be set to the
     * default graphics color. This is a convenience variable for
     * subclasses to easily modify and then reset the graphics color.
     */
    int graphicsColor;
    
    /**
     * When in the paint() routine, graphicsFont will be set to the
     * default graphics Font. This is a convenience variable for
     * subclasses to easily modify and then reset the graphics font.
     */
    Font graphicsFont;
    
    /**
     * Construct a default, transparent layer. As a result, the
     * 'transparent' value will be set to true.
     */
    public CLayer() {
        this((Image)null, -1);
    }
    
    /**
     * Construct a layer with the given background image if it is not null 
     * or with a background fill color.
     * The color should be in the 0xaarrggbb format. If the bgColor is 
     * invalid and bgImage is null, this layer will be transparent and 
     * equal to the no-argument constructor.
     *
     * @param bgImage The background image to use for this layer.
     * @param bgColor The color (0xaarrggbb) to use as the background fill
     */
    public CLayer(Image bgImage, int bgColor) 
    {
        if (bgImage != null) {
            this.bgImage = new Image[] { bgImage };
            this.tileBG = true;
            transparent = false;
        } else {
            transparent = (bgColor < 0);
        }
        this.bgColor = bgColor;
        initialize();
    }


    /**
     * Construct a layer with the given background images (if not null) 
     * or with a background fill color and border. The background images
     * should be a 9 element array, creating a 9-piece image background.
     * The color should be in the 0xaarrggbb format.  If the bgColor is 
     * invalid and bgImages are null, this layer will be transparent and 
     * equal to the no-argument constructor.
     *
     * @param bgImages The background image to use for this layer.
     * @param bgColor The color (0xaarrggbb) to use as the background fill
     */
    public CLayer(Image[] bgImages, int bgColor) {
        if (bgImages != null) {
            this.bgImage = new Image[bgImages.length];
            System.arraycopy(bgImages, 0, this.bgImage, 0, bgImages.length);
            this.tileBG = false;
            transparent = false;
        } else {
            transparent = (bgColor < 0);
        }
        this.bgColor = bgColor;
        initialize();
    }
    
    /**
     * Finish initialization of this CLayer. This can
     * be extended by subclasses. The dimensions of the
     * CLayer are stored in its bounds[]. The 'x' and 'y'
     * coordinates are in the coordinate space of the
     * window which contains this layer. By default, a layer is
     * located at the origin and is as large as the screen size.
     *
     * The X and Y coordinates represent the upper left position
     * of this CLayer in the containing CWindow's coordinate space.
     *
     */
    protected void initialize() {
        bounds = new int[4];
        bounds[X] = 0;
        bounds[Y] = 0;
        bounds[W] = ScreenSkin.WIDTH;
        bounds[H] = ScreenSkin.HEIGHT;
        
        dirtyBounds = new int[4];
        cleanDirtyRegions();
        
        // IMPL_NOTE : center the background image by default
    }
    
    /**
     * Establish a background. This method will evaluate the parameters
     * and create a background which is appropriate. If the image is non-null,
     * the image will be used to create the background. If the image is null,
     * the values for the colors will be used and the background will be
     * painted in fill color instead. If the image is null, and the background
     * color is a negative value, this layer will become transparent and no
     * background will be painted.
     *
     * @param bgImage the image to use for the background tile (or null)
     * @param tileBG If true, then tile the background image as necessary
     *               to fill a larger screen. If false, treat the image
     *               as fullsize.
     * @param bgColor if the image is null, use this color as a background
     *                fill color
     */
    public void setBackground(Image bgImage, boolean tileBG, int bgColor) {
        if (bgImage != null) {
            this.bgImage = new Image[] { bgImage };
            this.tileBG = tileBG;
            transparent = false;
        } else {
            this.bgImage = null;
            transparent = (bgColor < 0);
        }
        this.bgColor = bgColor;
        addDirtyRegion();
    }
    
    /**
     * Establish a background. This method will evaluate the parameters
     * and create a background which is appropriate. If the images are 
     * non-null, the images will be used to create a 9-piece background.
     * If the images are null, the value for the color will be used and
     * the background will be painted in fill color instead. If the images
     * are null, and the background color is a negative value, this layer
     * will become transparent and no background will be painted.
     *
     * @param bgImages an array containing a 9-piece image set to be used
     *                 as the background for this layer
     * @param bgColor if the images are null, use this color as a background
     *                fill color
     */
    public void setBackground(Image[] bgImages, int bgColor) {
        if (bgImages != null) {
            this.bgImage = new Image[bgImages.length];
            System.arraycopy(bgImages, 0, this.bgImage, 0, bgImages.length);
            this.tileBG = false;
            transparent = false;
        } else {
            this.bgImage = null;
            transparent = (bgColor < 0);
        }
        this.bgColor = bgColor;
        addDirtyRegion();            
    }

    /**
     * Establish the bounds of this layer. The coordinate space for
     * the 'x' and 'y' anchor will be interpreted in that of the window
     * which contains this layer.
     *
     * @param x The 'x' coordinate of this layer's origin
     * @param y The 'y' coordinate of this layer's origin
     * @param w The width of this layer
     * @param h The height of this layer
     */
    public void setBounds(int x, int y, int w, int h) {
        if (bounds == null) {
            bounds = new int[4];
        }
        bounds[X] = x;
        bounds[Y] = y;
        bounds[W] = w;
        bounds[H] = h;
    }
    
    /**
     * Return the bounds of this layer (in the coordinate space of
     * its parent Window). Returns a 4 element array, containing the
     * x, y, width, and height representing this layer's bounding
     * rectangle
     *
     * @return this layer's bounding rectangle in the coordinate space
     *         of its parent window
     */
    public int[] getBounds() {
        return bounds;
    }

    /**
     * Determine the current visibility of this layer. Note that this
     * state only pertains to the layer's visibility status within its
     * containing window. The window itself may or may not be actually
     * visible on the physical display.
     *
     * @return true if this layer is currently visible in a window
     */
    public boolean isVisible() {
        return visible;
    }

    /**
     * Toggle the visibility state of this layer within its containing
     * window.
     *
     * @param visible If true, this layer will be painted as part of its
     *                containing window, as well as receive events if it
     *                supports input.
     */
    public void setVisible(boolean visible) {
        this.visible = visible;
        addDirtyRegion();
    }

    /**
     * Determine if this layer is opaque
     *
     * @return true if this layer does not have any transparent
     *         or translucent areas
     */
    public boolean isOpaque() {
        return opaque;
    }
    
    /**
     * Set the opacity flag for this layer. True means that this
     * layer does not have any transparent or translucent areas
     *
     * @param opaque a flag indicating the layer's opacity
     */
    public void setOpaque(boolean opaque) {
        this.opaque = opaque;
    }
    
    /**
     * Returns true if this layer is in need of repainting.
     *
     * @return true if this layer is marked as 'dirty' and needs repainting.
     */
    public boolean isDirty() {
        return this.dirty;
    }
    
    /**
     * Mark this layer as being dirty.
     * By default, this will also mark the containing window (if there is one)
     * as being dirty as well.
     */    
    protected void setDirty() {
        setDirtyButNotNotifyOwner();
        if (owner != null) {
            owner.setDirty();
        }
    }

    /**
     * Mark this layer as being dirty
     * but don't mark the containing window.
     */
    protected void setDirtyButNotNotifyOwner() {
        this.dirty = true;
    }

    /** Clean any dirty regions of the layer. */
    protected void cleanDirtyRegions() {
        dirtyBounds[X] = dirtyBounds[Y]
            = dirtyBounds[W] = dirtyBounds[H] = -1;
    }

    /**
     * Determines whether dirty regions are empty.
     *
     * @return true if dirty regions are not set,
     *         false otherwise
     */
    protected boolean isEmptyDirtyRegions() {
        return dirtyBounds[X] == -1;
    }

    /** Clean any dirty regions of the layer and mark layer as not dirty. */
    protected void cleanDirty() {
        dirty = false;
        cleanDirtyRegions();
    }

    /**
     * Determine if this layer supports input, such as key and pen events.
     *
     * @return true if this layer supports handling input events
     */
    public boolean supportsInput() {
        return supportsInput;
    }

    /**
     * Toggle the ability of this layer to receive input.
     *
     * @param support If true, this layer will receive user input events
     *                (as long as the layer is also visible)
     */
    public void setSupportsInput(boolean support) {
        this.supportsInput = support;
    }

    /**
     * Handle input from a pen tap.
     *
     * Parameters describe the type of pen event and the x,y location in the
     * layer at which the event occurred.
     *
     * Important: the x,y location of the pen tap will already be translated
     * into the coordinate space of the layer.
     *
     * @param type the type of pen event
     * @param x the x coordinate of the event
     * @param y the y coordinate of the event
     * @return
     */
    public boolean pointerInput(int type, int x, int y) {
        return false;
    }

    /**
     * Handle key input from a keypad. Parameters describe
     * the type of key event and the platform-specific
     * code for the key. (Codes are translated using the
     * lcdui.Canvas)
     *
     * @param type the type of key event
     * @param code the numeric code assigned to the key
     * @return
     */
    public boolean keyInput(int type, int code) {
        return false;
    }

    /**
     * Handle input from some type of device-dependent
     * input method. This could be input from something
     * such as T9, or a phonebook lookup, etc.
     *
     * @param str the text to handle as direct input
     * @return
     */
    public boolean methodInput(String str) {
        return false;
    }

    /**
     * Utility method to determine if the given point lies within
     * the bounds of this layer. The point should be in the coordinate
     * space of this layer's containing CWindow.
     *
     * @param x the "x" coordinate of the point
     * @param y the "y" coordinate of the point
     * @return true if the coordinate lies in the bounds of this layer
     */
    public boolean containsPoint(int x, int y) {
        return (visible &&
                x >= bounds[X] && x <= (bounds[X] + bounds[W]) &&
                y >= bounds[Y] && y <= (bounds[Y] + bounds[H]));
    }

    /**
     * Utility method to determine if this layer wanna handle
     * the given point. By default the layer handles the point if it
     * lies within the bounds of this layer.  The point should be in
     * the coordinate space of this layer's containing CWindow.
     *
     * @param x the "x" coordinate of the point
     * @param y the "y" coordinate of the point
     * @return true if the coordinate lies in the bounds of this layer
     */
    public boolean handlePoint(int x, int y) {
        return containsPoint(x, y);
    }
    
    /**
     * Add this layer's entire area to be marked for repaint. Any pending
     * dirty regions will be cleared and the entire layer will be painted
     * on the next repaint.
     */
    public void addDirtyRegion() {
        if (CGraphicsQ.DEBUG) {
            System.err.println("Layer " + layerID() + ":");
            System.err.println("\tMarking entire layer dirty");
        }
        cleanDirtyRegions();
        setDirty();
    }
    
    /**
     * Add an area to be marked for repaint to this layer. This could
     * be needed for a variety of reasons, such as this layer being
     * obscured by another layer or window element. The new region should
     * be in the coordinate space of this layer.
     *
     * @param x the x coordinate of the region
     * @param y the y coordinate of the region
     * @param w the width of the region
     * @param h the height of the region
     * @return true if dirty region of the layer was changed,
     *         false otherwise
     */
    public boolean addDirtyRegion(int x, int y, int w, int h) {
        if (CGraphicsQ.DEBUG) {
            System.err.println("Layer " + this + ":");
            System.err.println("\tAdd dirty: " + x + ", "
                + y + ", " + w + ", " + h);
        }

        // The whole layer is dirty already
        if (isDirty() && isEmptyDirtyRegions()) {
            if (CGraphicsQ.DEBUG) {
                System.err.println(
                    "\tWhole layer is dirty already");
            }
            return false;
        }

        // Cache layer bounds
        int bw = bounds[W];
        int bh = bounds[H];
        int x2 = x + w;
        int y2 = y + h;

        // Dirty region can be outside of the layer
        if (x >= bw || y >= bh || x2 <= 0 || y2 <=0) {
            if (CGraphicsQ.DEBUG) {
                System.err.println(
                    "\tAdded region is outside of the layer");
            }
            return false;
        }

        boolean res = false;
        int dx, dy, dx2, dy2;
        if (isEmptyDirtyRegions()) {
            dx = x; dy = y;
            dx2 = x2; dy2 = y2;
        } else {
            dx = dirtyBounds[X];
            dy = dirtyBounds[Y];
            dx2 = dx + dirtyBounds[W];
            dy2 = dy + dirtyBounds[H];
            if (dx2 < x2) dx2 = x2;
            if (dy2 < y2) dy2 = y2;
            if (x < dx) dx = x;
            if (y < dy) dy = y;
        }

        // Lastly, we carefully restrict the dirty region
        // to be within the bounds of this layer
        if (dx < 0) dx = 0;
        if (dy < 0) dy = 0;
        if (dx2 > bw) dx2 = bw;
        if (dy2 > bh) dy2 = bh;

        // Update changed dirty region
        int dw = dx2 - dx;
        int dh = dy2 - dy;
        if (dirtyBounds[W] != dw || dirtyBounds[H] != dh) {
            if (dw == bw && dh == bh) {
                // The entire layer is dirty now
                cleanDirtyRegions();

                if (CGraphicsQ.DEBUG) {
                    System.err.println(
                        "\tThe entire layer became dirty");
                }
            } else {
                dirtyBounds[X] = dx;
                dirtyBounds[Y] = dy;
                dirtyBounds[W] = dw;
                dirtyBounds[H] = dh;

                if (CGraphicsQ.DEBUG) {
                    System.err.println(
                        "\tCurrent dirty: " + dirtyBounds[X] + ", "
                        + dirtyBounds[Y] + ", " + dirtyBounds[W] + ", "
                        + dirtyBounds[H]);
                }
            }
            res = true;
            setDirty();
        }

        return res;
    }

    /**
     * Subtract a layer area not needed for repaint of this layer.
     * It could be needed for a variety of reasons, such as layer being
     * overlapped with opague higher layer or window element.
     * The subtracted region should be in the coordinate space
     * of this layer.
     *
     * @param x the x coordinate of the region
     * @param y the y coordinate of the region
     * @param w the width of the region
     * @param h the height of the region
     * @return true if dirty region of the layer was changed,
     *         false otherwise
     */
    boolean subDirtyRegion(int x, int y, int w, int h) {
        if (!isDirty()) {
            return false;
        }

        if (CGraphicsQ.DEBUG) {
            System.err.println("Layer " + this + ":");
            System.err.println("\tSub dirty: " + x + ", "
                + y + ", " + w + ", " + h);
        }

        int x2 = x + w;
        int y2 = y + h;
        boolean res = false;
        int dx, dy, dx2, dy2;
        if (isEmptyDirtyRegions()) {
            dx = 0; dy = 0;
            dx2 = bounds[W];
            dy2 = bounds[H];
        } else {
            dx = dirtyBounds[X];
            dy = dirtyBounds[Y];
            dx2 = dx + dirtyBounds[W];
            dy2 = dy + dirtyBounds[H];
        }

        // Subtracted region can be outside of the dirty area
        if (x >= dx2 || y >= dy2 || x2 <= dx || y2 <=dy) {
            if (CGraphicsQ.DEBUG) {
                System.err.println(
                    "\tSubtracted region is outside of dirty area");
            }
            return false;
        }

        boolean insideVert = (y <= dy && y2 >= dy2);
        boolean insideHorz = (x <= dx && x2 >= dx2);

        if (insideVert) {
            if (dx >= x && dx < x2) dx = x2;
            else if (dx2 > x && dx2 <= x2) dx2 = x;
        } else if (insideHorz) {
            if (dy >= y && dy < y2) dy = y2;
            else if (dy2 > y && dy2 <= y2) dy2 = y;
        } else {
            // We can subtract only such a regions that result
            // of subtraction is a rectangular area
            if (CGraphicsQ.DEBUG) {
                System.err.println(
                    "\tSubtraction can't be done");
            }
            return false;

        }

        // Result of subtraction can be an empty dirty region
        if (dx >= dx2 || dy >= dy2) {
            cleanDirty();
            res = true;

            if (CGraphicsQ.DEBUG) {
                System.err.println(
                    "\tThe layer is no more dirty");
            }

        } else if (dirtyBounds[W] != dx2 - dx ||
                dirtyBounds[H] != dy2 - dy) {
            // Update changed dirty region
            dirtyBounds[X] = dx;
            dirtyBounds[Y] = dy;
            dirtyBounds[W] = dx2 - dx;
            dirtyBounds[H] = dy2 - dy;
            res = true;

            if (CGraphicsQ.DEBUG) {
                System.err.println(
                    "\tCurrent dirty: " + dirtyBounds[X] + ", "
                    + dirtyBounds[Y] + ", " + dirtyBounds[W] + ", "
                    + dirtyBounds[H]);
            }
        } else {
            if (CGraphicsQ.DEBUG) {
                System.err.println(
                    "\tDirty area has not been changed");
            }
        }

        return res;
    }


    /**
     * Request a repaint for the entire contents of this layer.
     */
    public void requestRepaint() {
        requestRepaint(0, 0, bounds[W], bounds[H]);
    }
    
    /**
     * Request a repaint for a specific region of this layer.
     *
     * @param x The 'x' coordinate of the upper left corner of the
     *          repaint region
     * @param y The 'y' coordinate of the upper right corner of the
     *          repaint region
     * @param w The width of the repaint region
     * @param h The height of the repaint region
     */
    public void requestRepaint(int x, int y, int w, int h) {
        addDirtyRegion(x, y, w, h);
        if (owner != null && visible) {
            // We request a repaint of our parent window after translating
            // the origin into the coordinate space of the window.
            owner.requestRepaint();
        }
    }
    
    /**
     * Paint this layer. This method should not generally be overridden by
     * subclasses. This method carefully stores the clip, translation, and
     * color before calling into subclasses. The graphics region will be
     * translated such that it is in this layer's coordinate space (0,0 is
     * the top left corner of this layer). The paintBackground() method will
     * be called first, then the paintBody() method. The graphics will then
     * carefully be put back into its original state before returning.
     * Subclasses should override the paintBody() method and do not generally 
     * need to do any translates or bother to undo any changes made to
     * the Graphics object.
     *
     * @param g The graphics object to use to paint this layer.
     */
    public void paint(Graphics g) {
        try {            
            // We first reset our dirty flag
            this.dirty = false;
            
            graphicsColor = g.getColor();
            graphicsFont = g.getFont();
            
            // Paint the background
            if (!transparent) {
                paintBackground(g);
            }
            
            // Just in case subclasses modified these values,
            // return them to standard
            g.setColor(graphicsColor);
            g.setFont(graphicsFont);
            
            // Paint the body
            paintBody(g);
            
            // Just in case subclasses modified these values,
            // return them to standard
            g.setColor(graphicsColor);
            g.setFont(graphicsFont);
            
            // We reset our dirty bounds region
            cleanDirtyRegions();
            
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    /** 
     * Paint the background of this layer. This method will automatically
     * handle a transparent layer, a layer with a background color (and/or
     * border color), and a layer with a background image (either centered
     * or tiled). Subclasses may override this method if they require a
     * more advanced background (note that the transparent value should be
     * set to false in order for the paintBackground() method to be called),
     * but subclasses must be careful to reset any modifications made to
     * the Graphics object before returning.
     *
     * @param g The Graphics object to use to paint the background.
     */
    protected void paintBackground(Graphics g) {        
        if (bgImage == null) {
            // background is null, just fill using the fill color
            g.setColor(bgColor);
            g.fillRect(g.getClipX(), g.getClipY(), 
                       g.getClipWidth(), g.getClipHeight());
        } else {
            if (bgImage.length == 1) {
                CGraphicsUtil.paintBackground(g, bgImage[0], tileBG, bgColor, 
                                              bounds[W], bounds[H]);
            } else if (bgImage.length == 9) {
                CGraphicsUtil.draw9pcsBackground(g, 0, 0, 
                                             bounds[W], bounds[H],
                                             bgImage);
            
            } else if (bgImage.length == 3) {
                CGraphicsUtil.draw3pcsBackground(g, 0, 0, bounds[W], bgImage);
            }
        }
    }

    /**
     * Paint the body or content of this layer. This method should be
     * overridden by subclasses. Note that the Graphics object will
     * already be translated into this layer's coordinate space. Subclasses
     * do not need to reset any changes made to the Graphics object as the
     * CLayer paint() routine will do that automatically.
     *
     * @param g The Graphics object to use to paint the content of this layer
     */
    protected void paintBody(Graphics g) {
    }


    public void update(CLayer[] mainLayers) {
        if (visible) {
            addDirtyRegion();
        }
    }

    /**
     * A String identifier used by subclasses (for debug purposes)
     * @return abbreviated class name of the layer instance
     */
    protected String layerID() {
        int i;
        String res = getClass().getName();
        if (res != null && (i = res.lastIndexOf('.')) > -1) {
            res = res.substring(i+1);
        }
        return res + "@" +
            Integer.toHexString(hashCode());
    }

    /**
     * Get the layer details including
     * its bound and dirty region information
     *
     * @return String with layer details
     */
    public String toString() {
        String res = layerID() + " [" +
            bounds[X] + ", " + bounds[Y] + ", " +
            bounds[W] + ", " + bounds[H] + "]";

        if (isDirty()) {
            res += ", dirty";
            if (!isEmptyDirtyRegions()) {
                res += " (" +
                    dirtyBounds[X] + ", " + dirtyBounds[Y] + ", " +
                    dirtyBounds[W] + ", " + dirtyBounds[H] + ")";
            }
        }
        res += ", opaque: " + (opaque ? 1 : 0);
        res += ", visible: " + (visible ? 1 : 0);
        res += ", transparent: " + (transparent ? 1 : 0);
        return res;
    }

}
