//
// $Id: AnimatedPanel.java,v 1.18 2002/03/08 01:37:51 mdb Exp $

package com.threerings.media.animation;

import java.awt.AWTException;
import java.awt.BufferCapabilities;
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.ImageCapabilities;
import java.awt.Rectangle;

import java.awt.image.BufferStrategy;
import java.awt.image.VolatileImage;

import java.util.List;

import com.samskivert.util.Histogram;
import com.samskivert.util.StringUtil;

import com.threerings.media.Log;
import com.threerings.media.sprite.SpriteManager;

/**
 * The animated panel provides a useful extensible implementation of a
 * {@link Canvas} that implements the {@link AnimatedView} interface. It
 * takes care of automatically creating an animation manager and a sprite
 * manager to manage the animations that take place within this panel. If
 * a sprite manager is not needed, {@link #needsSpriteManager} can be
 * overridden to prevent it from being created.
 *
 * <p> Users of the animated panel must be sure to call {@link #stop} when
 * their panel is no longer being displayed and {@link #start} to start it
 * back up again when it is once again being displayed. These methods
 * deactivate and reactivate the underlying animation manager.
 *
 * <p> Sub-classes should override {@link #render} to draw their
 * panel-specific contents.
 */
public class AnimatedPanel extends Canvas implements AnimatedView
{
    /**
     * Constructs an animated panel.
     */
    public AnimatedPanel ()
    {
        // create our animation manager
        _animmgr = new AnimationManager(this);

        // create a sprite manager if we haven't been requested not to
        if (needsSpriteManager()) {
            _spritemgr = new SpriteManager();
            _animmgr.setSpriteManager(_spritemgr);
        }
    }

    /**
     * Derived classes can override this method to prevent the automatic
     * creation of a sprite manager to work with the animation manager
     * that is managing this panel.
     */
    protected boolean needsSpriteManager ()
    {
        return true;
    }        

    /**
     * Starts up the animation manager associated with this panel.
     */
    public void start ()
    {
        _animmgr.start();
    }

    /**
     * Shuts down the animation manager associated with this panel.
     */
    public void stop ()
    {
        _animmgr.stop();
    }

    /**
     * Instructs the view to begin scrolling with the specified velocities
     * in milliseconds per pixel. A setting of zero indicates that
     * scrolling should be deactivated in that direction. Negative values
     * mean that the view should be scrolled one pixel in the opposite
     * direction in the positive number of milliseconds.
     */
    public void setScrolling (int msppx, int msppy)
    {
        // set our scrolling parameters
        _msppx = msppx;
        _msppy = msppy;
        _lastx = _lasty = 0;

        // if the velocities are zero, stop the scrolling, otherwise make
        // a note of the time at which scrolling started
        if (msppx == 0 && msppy == 0) {
            _stime = 0;
            _animmgr.setScrolling(0);

        } else {
            _stime = System.currentTimeMillis();

            // set our scrolling speed to the (absolute value of the)
            // lower of the two velocities (but not to zero if either one
            // is zero)
            if (_msppx == 0) {
                _animmgr.setScrolling(Math.abs(msppy));
            } else if (_msppy == 0) {
                _animmgr.setScrolling(Math.abs(msppx));
            } else {
                _animmgr.setScrolling(
                    Math.min(Math.abs(msppx), Math.abs(msppy)));
            }
        }
    }

    // documentation inherited
    public void paint (Graphics g)
    {
        // we don't paint directly any more, we pass the dirty rect to the
        // animation manager to queue up along with our next repaint
        Rectangle dirty = g.getClipBounds();
        // we need to adjust the dirty rect by our viewport offset before
        // passing it to the animation manager
        dirty.translate(_tx, _ty);
        _animmgr.addDirtyRect(dirty);
    }

    // documentation inherited
    public void update (Graphics g)
    {
        // the normal Canvas.update() fills itself with its background
        // color before calling paint() which we definitely don't want
        paint(g);
    }

    // documentation inherited
    public void validate ()
    {
        super.validate();

        // if we change size, clear out our old back buffer
        _backimg = null;

        // figure out our viewport offsets
        Dimension size = getSize(), vsize = getViewSize();
        _tx = (vsize.width - size.width)/2;
        _ty = (vsize.height - size.height)/2;

//         Log.info("Size: " + size + ", vsize: " + vsize +
//                  ", tx: " + _ty + ", ty: " + _ty + ".");
    }

    // documentation inherited
    public void invalidateRect (Rectangle invalidRect)
    {
        // pass it on to the animation manager
        _animmgr.addDirtyRect(invalidRect);
    }

    // documentation inherited
    public void paintImmediately (List invalidRects)
    {
        // no use in painting if we're not showing or if we've not yet
        // been validated
        if (!isValid() || !isShowing()) {
            return;
        }

        // track how long it was since we were last painted
        long now = System.currentTimeMillis();
//         if (_last != 0) {
//             int delta = (int)(now-_last);
//             _histo.addValue(delta);

//             // dump the histogram every ten seconds
//             if (_last % (10*1000) < 50) {
//                 Log.info("Render histogram.");
//                 Log.info(StringUtil.toMatrixString(_histo.getBuckets(), 10, 3));
//             }
//         }
//         _last = now;

        int width = getWidth(), height = getHeight();

        // if scrolling is enabled, determine the scrolling delta to be
        // used and do the business
        int dx = 0, dy = 0;
        if (_stime != 0) {
            // compute the total distance scrolled since we started (to
            // avoid rounding errors)
            // long now = System.currentTimeMillis();

            // determine how many pixels further along we've moved and
            // make a note of our latest position
            if (_msppx != 0) {
                int xdist = (int)((now - _stime) / _msppx);
                dx = (xdist - _lastx);
                _lastx = xdist;
            }
            if (_msppy != 0) {
                int ydist = (int)((now - _stime) / _msppy);
                dy = (ydist - _lasty);
                _lasty = ydist;
            }

            // and add invalid rectangles for the exposed areas
            if (dx > 0) {
                Rectangle dirty = new Rectangle(width - dx, 0, dx, height);
                dirty.translate(_tx, _ty);
                invalidRects.add(dirty);
            } else if (dx < 0) {
                Rectangle dirty = new Rectangle(0, 0, -dx, height);
                dirty.translate(_tx, _ty);
                invalidRects.add(dirty);
            }
            if (dy > 0) {
                Rectangle dirty = new Rectangle(0, height - dy, width, dy);
                dirty.translate(_tx, _ty);
                invalidRects.add(dirty);
            } else if (dy < 0) {
                Rectangle dirty = new Rectangle(0, 0, width, -dy);
                dirty.translate(_tx, _ty);
                invalidRects.add(dirty);
            }

            // make sure we're actually scrolling before telling people
            // about it
            if (dx != 0 || dy != 0) {
                // if we are working with a sprite manager, let it know
                // that we're about to scroll out from under its sprites
                // and allow it to provide us with more dirty rects
                if (_spritemgr != null) {
                    _spritemgr.viewWillScroll(dx, dy, invalidRects);
                }

                // let our derived classes do whatever they need to do to
                // prepare to be scrolled
                viewWillScroll(dx, dy, now, invalidRects);
            }
        }

        // if we didn't scroll and have no invalid rects, there's no need
        // to repaint anything
        if (invalidRects.size() == 0 && dx == 0 && dy == 0) {
            return;
        }

        // create our off-screen buffer if necessary
        GraphicsConfiguration gc = getGraphicsConfiguration();
        if (_backimg == null) {
            createBackBuffer(gc);
        }

        // render into our back buffer
        do {
            // make sure our back buffer hasn't disappeared
            int valres = _backimg.validate(gc);

            // if we've changed resolutions, recreate the buffer
            if (valres == VolatileImage.IMAGE_INCOMPATIBLE) {
                Log.info("Back buffer incompatible, recreating.");
                createBackBuffer(gc);
            }

            Graphics g = null;
            try {
                g = _backimg.getGraphics();

                // if the image wasn't A-OK, we need to rerender the whole
                // business rather than just the dirty parts
                if (valres != VolatileImage.IMAGE_OK) {
                    invalidRects.clear();
                    invalidRects.add(new Rectangle(_tx, _ty, width, height));
                    Log.info("Lost back buffer, redrawing.");

                } else if (dx != 0 || dy != 0) {
                    // if it was OK, we may need to do some scrolling
                    g.copyArea(0, 0, width, height, -dx, -dy);
                }

                // translate into happy space
                g.translate(-_tx, -_ty);

                // now do our actual rendering
                render((Graphics2D)g, invalidRects);

                // translate back out of happy space
                g.translate(_tx, _ty);

            } finally {
                g.dispose();
            }

            // draw the back buffer to the screen
            try {
                g = getGraphics();

                // if we're scrolling, we've got to copy the whole image
                // to the screen, otherwise we can just copy the dirty
                // regions
                if (dx != 0 || dy != 0) {
                    g.drawImage(_backimg, 0, 0, null);

                } else {
                    // iterate through the invalid rectangles, copying
                    // those areas from the back buffer to the display
                    int isize = invalidRects.size();
                    for (int i = 0; i < isize; i++) {
                        Rectangle rect = (Rectangle)invalidRects.get(i);
                        // we have to translate out of view coordinates
                        // before doing the actual copy
                        rect.translate(-_tx, -_ty);
                        g.setClip(rect);
                        g.drawImage(_backimg, 0, 0, null);
                    }
                }

            } finally {
                if (g != null) {
                    g.dispose();
                }
            }

        } while (_backimg.contentsLost());
    }

    /**
     * This is called, when the view is scrolling, just before a call to
     * {@link #render}. The animated panel will take care of scrolling the
     * contents of the offscreen buffer, but the derived class will need
     * to do whatever is necessary to prepare to repaint the exposed
     * regions as well as update it's own internal state accordingly.
     *
     * @param dx the distance (in pixels) that the view will scroll in the
     * x direction.
     * @param dy the distance (in pixels) that the view will scroll in the
     * y direction.
     * @param now the current time, provided because we have it and
     * scrolling views are likely to want to use it in calculating stuff.
     * @param invalidRects the list of invalid rectangles which will be
     * redrawn; rectangles can be added to this list if necessary.
     */
    protected void viewWillScroll (int dx, int dy, long now, List invalidRects)
    {
        // nothing to do here
    }

    /**
     * Derived classes that wish to operate in a coordinate system based
     * on a view size that is larger or smaller than the viewport size
     * (the actual dimensions of the animated panel) can override this
     * method and return the desired size of the view. The animated panel
     * will take this size into account and translate into the view
     * coordinate system before calling {@link #render}.
     */
    protected Dimension getViewSize ()
    {
        return getSize();
    }

    /**
     * Requests that the supplied list of invalid rectangles be redrawn in
     * the supplied graphics context. The rectangles will be in the view
     * coordinate system (which may differ from screen coordinates, see
     * {@link #getViewSize}. Sub-classes should override this method to do
     * the actual rendering for their display.
     */
    protected void render (Graphics2D gfx, List invalidRects)
    {
        // nothing for now
    }

    /**
     * Creates the off-screen buffer used to perform double buffered
     * rendering of the animated panel.
     */
    protected void createBackBuffer (GraphicsConfiguration gc)
    {
        _backimg = gc.createCompatibleVolatileImage(getWidth(), getHeight());
    }

    /** The animation manager we use in this panel. */
    protected AnimationManager _animmgr;

    /** The sprite manager in use by this panel. */
    protected SpriteManager _spritemgr;

    /** The image used to render off-screen. */
    protected VolatileImage _backimg;

    /** Our viewport offsets. */
    protected int _tx, _ty;

    /** The scrolling velocity in milliseconds per pixel. */
    protected int _msppx, _msppy;

    /** The time at which the scrolling velocity was set. */
    protected long _stime;

    /** Used to determine how many pixels we've scrolled since the
     * scrolling velocity was last set. */
    protected int _lastx, _lasty;

    /** The last time we were rendered. */
    protected long _last;

    /** A histogram for tracking how frequently we're rendered. */
    protected Histogram _histo = new Histogram(0, 1, 100);

    /** The number of buffers to use when rendering. */
    protected static final int BUFFER_COUNT = 2;
}
