//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.media;

import java.util.Arrays;
import java.util.Iterator;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;

import com.google.common.collect.Iterators;

import com.samskivert.util.IntListUtil;
import com.samskivert.util.StringUtil;

import com.samskivert.swing.Label;

import com.threerings.media.animation.Animation;
import com.threerings.media.animation.AnimationManager;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
import com.threerings.media.timer.MediaTimer;

import static com.threerings.media.Log.log;

/**
 * Coordinates interaction between a sprite and animation manager and the media host that hosts and
 * renders them. This class is a little fiddly because {@link MediaPanel} has been around a long
 * time and is thoroughly out in the wild and now that we need to abstract out a bunch of its
 * functionality, we're constrained by all the extant usages and derivations.
 */
public class MetaMediaManager
    implements MediaConstants, Iterable<AbstractMedia>
{
    public MetaMediaManager (FrameManager framemgr, MediaHost host)
    {
        // keep these for later
        _framemgr = framemgr;
        _host = host;

        // initialize our managers
        _animmgr.init(host, _remgr);
        _spritemgr.init(host, _remgr);
    }

    /**
     * Returns the frame manager with which we are coordinating.
     */
    public FrameManager getFrameManager ()
    {
        return _framemgr;
    }

    /**
     * Returns a reference to the animation manager used by this media panel.
     */
    public AnimationManager getAnimationManager ()
    {
        return _animmgr;
    }

    /**
     * Returns a reference to the sprite manager used by this media panel.
     */
    public SpriteManager getSpriteManager ()
    {
        return _spritemgr;
    }

    /**
     * Returns the region manager used to coordiante our dirty regions.
     */
    public RegionManager getRegionManager ()
    {
        return _remgr;
    }

    /**
     * Returns true if we are paused, false if we are running normally.
     */
    public boolean isPaused ()
    {
        return _paused;
    }

    /**
     * Pauses the sprites and animations that are currently active on this media panel. Also stops
     * listening to the frame tick while paused.
     */
    public void setPaused (boolean paused)
    {
        // sanity check
        if ((paused && (_pauseTime != 0)) || (!paused && (_pauseTime == 0))) {
            log.warning("Requested to pause when paused or vice-versa [paused=" + paused + "].");
            return;
        }

        _paused = paused;
        if (_paused) {
            // make a note of our pause time
            _pauseTime = _framemgr.getTimeStamp();

        } else {
            // let the animation and sprite managers know that we just warped into the future
            long delta = _framemgr.getTimeStamp() - _pauseTime;
            _animmgr.fastForward(delta);
            _spritemgr.fastForward(delta);

            // clear out our pause time
            _pauseTime = 0;
        }
    }

    /**
     * Returns a timestamp from the {@link MediaTimer} used to track time intervals for this media
     * panel. <em>Note:</em> this should only be called from the AWT thread.
     */
    public long getTimeStamp ()
    {
        return _framemgr.getTimeStamp();
    }

    /**
     * Adds a sprite to this panel.
     */
    public void addSprite (Sprite sprite)
    {
        _spritemgr.addSprite(sprite);
    }

    /**
     * @return true if the sprite is already added to this panel.
     */
    public boolean isManaged (Sprite sprite)
    {
        return _spritemgr.isManaged(sprite);
    }

    /**
     * Removes a sprite from this panel.
     */
    public void removeSprite (Sprite sprite)
    {
        _spritemgr.removeSprite(sprite);
    }

    /**
     * Removes all sprites from this panel.
     */
    public void clearSprites ()
    {
        _spritemgr.clearMedia();
    }

    /**
     * Adds an animation to this panel. Animations are automatically removed when they finish.
     */
    public void addAnimation (Animation anim)
    {
        _animmgr.registerAnimation(anim);
    }

    /**
     * @return true if the animation is already added to this panel.
     */
    public boolean isManaged (Animation anim)
    {
        return _animmgr.isManaged(anim);
    }

    /**
     * Aborts a currently running animation and removes it from this panel. Animations are normally
     * automatically removed when they finish.
     */
    public void abortAnimation (Animation anim)
    {
        _animmgr.unregisterAnimation(anim);
    }

    /**
     * Removes all animations from this panel.
     */
    public void clearAnimations ()
    {
        _animmgr.clearMedia();
    }

    /**
     * Called by the host to coordinate dirty region tracking. This should be supplied with the
     * number of dirty regions being painted on this tick and called just before painting them.
     */
    public void noteDirty (int regions)
    {
        _dirty[_tick] = regions;
    }

    /**
     * Our media front end should implement {@link FrameParticipant} and call this methed in their
     * {@link FrameParticipant#tick} method. They must also first check {@link #isPaused} and not
     * call this method if we are paused. As they will probably want to have willTick() and
     * didTick() calldown methods, we cannot handle pausedness for them.
     */
    public void tick (long tickStamp)
    {
        // now tick our animations and sprites
        _animmgr.tick(tickStamp);
        _spritemgr.tick(tickStamp);

        // if performance debugging is enabled,
        if (FrameManager._perfDebug.getValue()) {
            if (_perfLabel == null) {
                _perfLabel = new Label("", Label.OUTLINE, Color.white, Color.black,
                                       new Font("Arial", Font.PLAIN, 10));
            }
            if (_perfRect == null) {
                _perfRect = new Rectangle(5, 5, 0, 0);
            }

            StringBuilder perf = new StringBuilder();
            perf.append("[FPS: ");
            perf.append(_framemgr.getPerfTicks()).append("/");
            perf.append(_framemgr.getPerfTries());
            perf.append(" PM:");
            StringUtil.toString(perf, _framemgr.getPerfMetrics());
//             perf.append(" MP:").append(_dirtyPerTick);
            perf.append("]");

            String perfStatus = perf.toString();
            if (!_perfStatus.equals(perfStatus)) {
                _perfStatus = perfStatus;
                _perfLabel.setText(perfStatus);

                Graphics2D gfx = _host.createGraphics();
                if (gfx != null) {
                    _perfLabel.layout(gfx);
                    gfx.dispose();

                    // make sure the region we dirty contains the old and the new text (which we
                    // ensure by never letting the rect shrink)
                    Dimension psize = _perfLabel.getSize();
                    _perfRect.width = Math.max(_perfRect.width, psize.width);
                    _perfRect.height = Math.max(_perfRect.height, psize.height);
                    _remgr.addDirtyRegion(new Rectangle(_perfRect));
                }
            }
        } else {
            _perfRect = null;
        }
    }

    /**
     * Our media front end should implement {@link FrameParticipant} and call this methed in their
     * {@link FrameParticipant#needsPaint} method.
     */
    public boolean needsPaint ()
    {
        // compute our average dirty regions per tick
        if (_tick++ == 99) {
            _tick = 0;
            int dirty = IntListUtil.sum(_dirty);
            Arrays.fill(_dirty, 0);
            _dirtyPerTick = (float)dirty/100;
        }

        // regardless of whether or not we paint, we need to let our abstract media managers know
        // that we've gotten to the point of painting because they need to remain prepared to deal
        // with media changes that happen any time between the tick() and the paint() and thus need
        // to know when paint() happens
        _animmgr.willPaint();
        _spritemgr.willPaint();

        // if we have dirty regions, we need painting
        return _remgr.haveDirtyRegions();
    }

    /**
     * Renders the sprites and animations that intersect the supplied dirty region in the specified
     * layer.
     */
    public void paintMedia (Graphics2D gfx, int layer, Rectangle dirty)
    {
        if (layer == FRONT) {
            _spritemgr.paint(gfx, layer, dirty);
            _animmgr.paint(gfx, layer, dirty);
        } else {
            _animmgr.paint(gfx, layer, dirty);
            _spritemgr.paint(gfx, layer, dirty);
        }
    }

    /**
     * Renders our performance debugging information if enabled.
     */
    public void paintPerf (Graphics2D gfx)
    {
        if (_perfRect != null && FrameManager._perfDebug.getValue()) {
            gfx.setClip(null);
            _perfLabel.render(gfx, _perfRect.x, _perfRect.y);
        }
    }

    /**
     * If our host supports scrolling around in a virtual view, it should call this method when the
     * view origin changes.
     */
    public void viewLocationDidChange (int dx, int dy)
    {
        if (_perfRect != null) {
            Rectangle sdirty = new Rectangle(_perfRect);
            sdirty.translate(-dx, -dy);
            _remgr.addDirtyRegion(sdirty);
        }

        // let our sprites and animations know what's up
        _animmgr.viewLocationDidChange(dx, dy);
        _spritemgr.viewLocationDidChange(dx, dy);
    }

    public Iterator<AbstractMedia> iterator ()
    {
        return Iterators.concat(_spritemgr.enumerateSprites(), _animmgr.iterator());
    }

    /** The frame manager with whom we register. */
    protected FrameManager _framemgr;

    /** Our media host, so gracious and accomodating. */
    protected MediaHost _host;

    /** Used to accumulate and merge dirty regions on each tick. */
    protected RegionManager _remgr = new RegionManager();

    /** The animation manager in use by this panel. */
    protected AnimationManager _animmgr = new AnimationManager();

    /** The sprite manager in use by this panel. */
    protected SpriteManager _spritemgr = new SpriteManager();

    /** Whether we're currently paused. */
    protected boolean _paused;

    /** Used to track the clock time at which we were paused. */
    protected long _pauseTime;

    /** Used to keep metrics. */
    protected int[] _dirty = new int[200];

    /** Used to keep metrics. */
    protected int _tick;

    /** Used to keep metrics. */
    protected float _dirtyPerTick;

    // used to render performance metrics
    protected String _perfStatus = "";
    protected Label _perfLabel;
    protected Rectangle _perfRect;
}
