//
// $Id: DirtyItemList.java,v 1.10 2002/02/06 23:14:56 mdb Exp $

package com.threerings.miso.scene;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;

import com.samskivert.util.HashIntMap;
import com.samskivert.util.StringUtil;

import com.threerings.media.sprite.Sprite;
import com.threerings.media.tile.ObjectTile;

import com.threerings.media.Log;

/**
 * The dirty item list keeps track of dirty sprites and object tiles
 * in a scene.
 */
public class DirtyItemList
{
    /**
     * Appends the dirty sprite at the given coordinates to the dirty
     * item list.
     */
    public void appendDirtySprite (
        MisoCharacterSprite sprite, int x, int y, Rectangle dirtyRect)
    {
        _items.add(new DirtyItem(sprite, null, null, x, y, dirtyRect));
    }

    /**
     * Appends the dirty object tile at the given coordinates to the
     * dirty item list.
     */
    public void appendDirtyObject (
        ObjectTile tile, Shape bounds, Shape footprint,
        int x, int y, Rectangle dirtyRect)
    {
        _items.add(new DirtyItem(tile, bounds, footprint, x, y, dirtyRect));
    }

    /**
     * Returns the dirty item at the given index in the list.
     */
    public DirtyItem get (int idx)
    {
        return (DirtyItem)_items.get(idx);
    }

    /**
     * Returns an array of the {@link DirtyItem} objects in the list
     * sorted in proper rendering order.
     */
    public DirtyItem[] sort ()
    {
        int size = size();

        if (DEBUG_SORT) {
            Log.info("Sorting dirty item list [size=" + size + "].");
        }

        // get items sorted by increasing origin x-coordinate
        DirtyItem[] xitems = new DirtyItem[size];
        _items.toArray(xitems);
        // if we've only got one item, bail out now
        if (xitems.length < 2) {
            return xitems;
        }
        Arrays.sort(xitems, ORIGIN_X_COMP);

        if (DEBUG_SORT) {
            Log.info("Sorted by x-origin " +
                     "[items=" + DirtyItemList.toString(xitems) + "].");
        }

        // get items sorted by increasing origin y-coordinate
        DirtyItem[] yitems = new DirtyItem[size];
        _items.toArray(yitems);
        Arrays.sort(yitems, ORIGIN_Y_COMP);

        if (DEBUG_SORT) {
            Log.info("Sorted by y-origin " +
                     "[items=" + DirtyItemList.toString(yitems) + "].");
        }

        // sort items into proper render order
        DirtyItem[] ritems = new DirtyItem[size];
        _items.toArray(ritems);
        Arrays.sort(ritems, new RenderComparator(xitems, yitems));

        if (DEBUG_SORT) {
            Log.info("Sorted for render " +
                     "[items=" + toString(ritems) + "].");
        }

        return ritems;
    }

    /**
     * Returns the number of items in the dirty item list.
     */
    public int size ()
    {
        return _items.size();
    }

    /**
     * Clears all items in the list.
     */
    public void clear ()
    {
        _items.clear();
    }

    /**
     * Returns an abbreviated string representation of the two given dirty
     * items describing each by only its origin coordinates.  Intended for
     * debugging purposes.
     */
    protected static String toString (DirtyItem a, DirtyItem b)
    {
        return toString(new DirtyItem[] { a, b });
    }

    /**
     * Returns an abbreviated string representation of the given dirty
     * items describing each by only its origin coordinates.  Intended for
     * debugging purposes.
     */
    protected static String toString (DirtyItem[] items)
    {
        StringBuffer buf = new StringBuffer();
        buf.append("[");
        for (int ii = 0; ii < items.length; ii++) {
            buf.append("(ox=").append(items[ii].ox);
            buf.append(", oy=").append(items[ii].oy).append(")");
            if (ii < (items.length - 1)) {
                buf.append(", ");
            }
        }
        return buf.append("]").toString();
    }

    /**
     * A class to hold the items inserted in the dirty list along with
     * all of the information necessary to render their dirty regions
     * to the target graphics context when the time comes to do so.
     */
    public static class DirtyItem
    {
        /** The dirtied object; one of either a sprite or an object tile. */
        public Object obj;

        /** The bounds of the dirty item if it's an object tile. */
        public Shape bounds;

        /** The footprint of the dirty item if it's an object tile and
         * we're drawing footprints. */
        public Shape footprint;

        /** The origin tile coordinates. */
        public int ox, oy;

        /** The leftmost tile coordinates. */
        public int lx, ly;

        /** The rightmost tile coordinates. */
        public int rx, ry;

        /** The dirty rectangle. */
        public Rectangle dirtyRect;

        /**
         * Constructs a dirty item.
         */
        public DirtyItem (Object obj, Shape bounds, Shape footprint,
                          int x, int y, Rectangle dirtyRect)
        {
            this.obj = obj;
            this.bounds = bounds;
            this.footprint = footprint;
            this.ox = x;
            this.oy = y;
            this.dirtyRect = dirtyRect;

            // calculate the item's leftmost and rightmost tiles.
            // note that sprites occupy only a single tile, so
            // leftmost and rightmost tiles are equivalent
            lx = rx = ox;
            ly = ry = oy;
            if (obj instanceof ObjectTile) {
                ObjectTile tile = (ObjectTile)obj;
                lx -= (tile.getBaseWidth() - 1);
                ry -= (tile.getBaseHeight() - 1);
            }
        }

        /**
         * Paints the dirty item to the given graphics context.  Only
         * the portion of the item that falls within the given dirty
         * rectangle is actually drawn.
         */
        public void paint (Graphics2D gfx, Rectangle clip)
        {
            Shape oclip = gfx.getClip();

            // clip the draw region to the dirty portion of the item
            gfx.clip(clip);

            // if there's a footprint, paint that
            if (footprint != null) {
                gfx.setColor(Color.black);
                gfx.draw(footprint);
            }

            // paint the item
            if (obj instanceof Sprite) {
                ((Sprite)obj).paint(gfx);
            } else {
                ((ObjectTile)obj).paint(gfx, bounds);
            }

            // restore original clipping region
            gfx.setClip(oclip);
        }

        // documentation inherited
        public boolean equals (Object other)
        {
            // we're never equal to something that's not our kind
            if (!(other instanceof DirtyItem)) {
                return false;
            }

            // sprites are equivalent if they're the same sprite
            DirtyItem b = (DirtyItem)other;
            if ((obj instanceof Sprite) && (b.obj instanceof Sprite)) {
                return (obj == b.obj);
            }

            // object-to-object or object-to-sprite are distinguished
            // simply by origin tile coordinate since they can never
            // occupy the same tile
            return (ox == b.ox && oy == b.oy);
        }

        /**
         * Returns a string representation of the dirty item.
         */
        public String toString ()
        {
            StringBuffer buf = new StringBuffer();
            buf.append("[obj=").append(obj);
            buf.append(", ox=").append(ox);
            buf.append(", oy=").append(oy);
            buf.append(", lx=").append(lx);
            buf.append(", ly=").append(ly);
            buf.append(", rx=").append(rx);
            buf.append(", ry=").append(ry);
            return buf.append("]").toString();
        }
    }

    /**
     * A comparator class for use in sorting dirty items in ascending
     * origin x- or y-axis coordinate order.
     */
    protected static class OriginComparator implements Comparator
    {
        /**
         * Constructs an origin comparator that sorts dirty items in
         * ascending order based on their origin coordinate on the given
         * axis.
        */
        public OriginComparator (int axis)
        {
            _axis = axis;
        }

        // documentation inherited
        public int compare (Object a, Object b)
        {
            DirtyItem da = (DirtyItem)a;
            DirtyItem db = (DirtyItem)b;
            return (_axis == X_AXIS) ? (da.ox - db.ox) : (da.oy - db.oy);
        }

        // documentation inherited
        public boolean equals (Object obj)
        {
	    return (obj == this);
        }

        /** The axis this comparator sorts on. */
        protected int _axis;
    }

    /**
     * A comparator class for use in sorting the dirty sprites and
     * objects in a scene in ascending x- and y-coordinate order
     * suitable for rendering in the isometric view with proper visual
     * results.
     */
    protected static class RenderComparator implements Comparator
    {
        /**
         * Constructs a render comparator with the given list of
         * pre-sorted dirty item lists for each axis.
         */
        public RenderComparator (DirtyItem[] xitems, DirtyItem[] yitems)
        {
            _xitems = xitems;
            _yitems = yitems;
        }

        // documentation inherited
        public int compare (Object a, Object b)
        {
            DirtyItem da = (DirtyItem)a;
            DirtyItem db = (DirtyItem)b;

            // check for partitioning objects on the y-axis
            int result = comparePartitioned(Y_AXIS, da, db);
            if (result != 0) {
                if (DEBUG_COMPARE) {
                    String items = DirtyItemList.toString(da, db);
                    Log.info("compare: Y-partitioned " +
                             "[result=" + result + ", items=" + items + "].");
                }
                return result;
            }
            
            // check for partitioning objects on the x-axis
            result = comparePartitioned(X_AXIS, da, db);
            if (result != 0) {
                if (DEBUG_COMPARE) {
                    String items = DirtyItemList.toString(da, db);
                    Log.info("compare: X-partitioned " +
                             "[result=" + result + ", items=" + items + "].");
                }
                return result;
            }

            // use normal iso-ordering check
            result = compareNonPartitioned(da, db);
            if (DEBUG_COMPARE) {
                String items = DirtyItemList.toString(da, db);
                Log.info("compare: non-partitioned " +
                         "[result=" + result + ", items=" + items + "].");
            }

            return result;
        }

        // documentation inherited
        public boolean equals (Object obj)
        {
	    return (obj == this);
        }

        /**
         * Returns whether two dirty items have a partitioning object
         * between them on the given axis.
         */
        protected int comparePartitioned (
            int axis, DirtyItem da, DirtyItem db)
        {
            // prepare for the partitioning check
            DirtyItem[] sitems;
            Comparator comp;
            boolean swapped = false;
            switch (axis) {
            case X_AXIS:
                if (da.ox == db.ox) {
                    // can't be partitioned if there's no space between
                    return 0;
                }

                // order items for proper comparison
                if (da.ox > db.ox) {
                    DirtyItem temp = da;
                    da = db;
                    db = temp;
                    swapped = true;
                }

                // use the axis-specific sorted array
                sitems = _xitems;
                comp = ORIGIN_X_COMP;
                break;

            case Y_AXIS:
            default:
                if (da.oy == db.oy) {
                    // can't be partitioned if there's no space between
                    return 0;
                }

                // order items for proper comparison
                if (da.oy > db.oy) {
                    DirtyItem temp = da;
                    da = db;
                    db = temp;
                    swapped = true;
                }

                // use the axis-specific sorted array
                sitems = _yitems;
                comp = ORIGIN_Y_COMP;
                break;
            }

            // get the bounding item indices and the number of
            // potentially-partitioning dirty items
            int aidx = Arrays.binarySearch(sitems, da, comp);
            int bidx = Arrays.binarySearch(sitems, db, comp);
            int size = bidx - aidx - 1;

            // check each potentially partitioning item
            int startidx = aidx + 1, endidx = startidx + size;
            for (int pidx = startidx; pidx < endidx; pidx++) {
                DirtyItem dp = sitems[pidx];
                if (dp.obj instanceof MisoCharacterSprite) {
                    // character sprites can't partition things
                    continue;
                } else if ((dp.obj == da.obj) ||
                           (dp.obj == db.obj)) {
                    // can't be partitioned by ourselves
                    continue;
                }

                // perform the actual partition check for this object
                switch (axis) {
                case X_AXIS:
                    if (dp.ly >= da.ry &&
                        dp.ry <= db.ly &&
                        dp.lx > da.rx &&
                        dp.rx < db.lx) {
                        return (swapped) ? 1 : -1;
                    }

                case Y_AXIS:
                default:
                    if (dp.lx <= db.ox &&
                        dp.rx >= da.lx &&
                        dp.ry > da.oy &&
                        dp.oy < db.ry) {
                        return (swapped) ? 1 : -1;
                    }
                }
            }

            // no partitioning object found
            return 0;
        }

        /**
         * Compares the two dirty items assuming there are no partitioning
         * objects between them.
         */
        protected int compareNonPartitioned (DirtyItem da, DirtyItem db)
        {
            if (da.ox == db.ox &&
                da.oy == db.oy) {
                if (da.equals(db)) {
                    // render level is equal if we're the same sprite
                    // or an object at the same location
                    return 0;
                }

                if ((da.obj instanceof MisoCharacterSprite) &&
                    (db.obj instanceof MisoCharacterSprite)) {
                    // we're comparing two sprites co-existing on the same
                    // tile, so study their fine coordinates
                    MisoCharacterSprite as = (MisoCharacterSprite)da.obj;
                    MisoCharacterSprite bs = (MisoCharacterSprite)db.obj;

                    int ahei = as.getFineX() + as.getFineY();
                    int bhei = bs.getFineX() + bs.getFineY();
                    int diff = ahei - bhei;
                    // if they're at the same vertical row of intra-tile
                    // tiles, just use hashCode() for consistency
                    return (diff != 0) ? diff :
                        (as.hashCode() - bs.hashCode());
                }
            }

            // establish a consistent ordering between objects.  see the
            // diagram at "narya/docs/miso/render_sort_diagram.png" for
            // more information.
            if (da.lx > db.ox) {
                return 1;
            } else if (da.ry > db.oy) {
                return (db.lx > da.ox) ? -1 : 1;
            }
            return -1;
        }

        /** The items in the dirty item list sorted by ascending x origin. */
        protected DirtyItem[] _xitems;

        /** The items in the dirty item list sorted by ascending y origin. */
        protected DirtyItem[] _yitems;
    }

    /** Whether to log debug info when comparing pairs of dirty items. */
    protected static final boolean DEBUG_COMPARE = false;

    /** Whether to log debug info for the main dirty item sorting algorithm. */
    protected static final boolean DEBUG_SORT = false;

    /** Constants used to denote axis sorting constraints. */
    protected static final int X_AXIS = 0;
    protected static final int Y_AXIS = 1;

    /** The comparator used to sort dirty items in ascending origin
     * x-coordinate order. */
    protected static final Comparator ORIGIN_X_COMP =
        new OriginComparator(X_AXIS);

    /** The comparator used to sort dirty items in ascending origin
     * y-coordinate order. */
    protected static final Comparator ORIGIN_Y_COMP =
        new OriginComparator(Y_AXIS);

    /** The list of dirty items. */
    protected ArrayList _items = new ArrayList();
}
