//
// $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.Comparator;
import java.util.List;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;

import com.google.common.collect.Lists;

import com.samskivert.util.ObserverList;
import com.samskivert.util.SortableArrayList;
import com.samskivert.util.Tuple;
import com.samskivert.util.ObserverList.ObserverOp;

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

/**
 * Manages, ticks, and paints {@link AbstractMedia}.
 */
public abstract class AbstractMediaManager
    implements MediaConstants
{
    /**
     * Provides this media manager with its host and region manager.
     */
    public void init (MediaHost host, RegionManager remgr)
    {
        _host = host;
        _remgr = remgr;
    }

    /**
     * Returns region manager in use by this manager.
     */
    public RegionManager getRegionManager ()
    {
        return _remgr;
    }

    /**
     * Creates a graphics that can be used to compute metrics or whatever else a media might need.
     * The caller should call {@link Graphics#dispose} on the returned object when it is done
     * with it.
     */
    public Graphics2D createGraphics ()
    {
        return _host.createGraphics();
    }

    /**
     * Must be called every frame so that the media can be properly updated.
     */
    public void tick (long tickStamp)
    {
        _tickStamp = tickStamp;
        tickAllMedia(tickStamp);
        dispatchNotifications();
        // we clear our tick stamp when we're about to be painted, this lets us handle situations
        // when yet more media is slipped in between our being ticked and our being painted
    }

    /**
     * This will always be called prior to the {@link #paint} calls for a particular tick. Because
     * it is possible that there will be no dirty regions and thus no calls to {@link #paint} this
     * method exists so that the media manager can guarantee that it will be notified when all
     * ticking is complete and the painting phase has begun.
     */
    public void willPaint ()
    {
        // now that we're done ticking, we can safely clear this
        _tickStamp = 0;
    }

    /**
     * Renders all registered media in the given layer that intersect the supplied clipping
     * rectangle to the given graphics context.
     *
     * @param layer the layer to render; one of {@link #FRONT}, {@link #BACK}, or {@link #ALL}.
     * The front layer contains all animations with a positive render order; the back layer
     * contains all animations with a negative render order; all, both.
     */
    public void paint (Graphics2D gfx, int layer, Shape clip)
    {
        for (int ii = 0, nn = _media.size(); ii < nn; ii++) {
            AbstractMedia media = _media.get(ii);
            int order = media.getRenderOrder();
            try {
                if (((layer == ALL) || (layer == FRONT && order >= 0) ||
                     (layer == BACK && order < 0)) && clip.intersects(media.getBounds())) {
                    media.paint(gfx);
                }

            } catch (Exception e) {
                log.warning("Failed to render media", "media", media, e);
            }
        }
    }

    /**
     * If the manager is paused for some length of time, it should be fast forwarded by the
     * appropriate number of milliseconds.  This allows media to smoothly pick up where they left
     * off rather than abruptly jumping into the future, thinking that some outrageous amount of
     * time passed since their last tick.
     */
    public void fastForward (long timeDelta)
    {
        if (_tickStamp > 0) {
            log.warning("Egads! Asked to fastForward() during a tick.", new Exception());
        }

        for (int ii = 0, nn = _media.size(); ii < nn; ii++) {
            _media.get(ii).fastForward(timeDelta);
        }
    }

    /**
     * Returns true if the specified media is being managed by this media manager.
     */
    public boolean isManaged (AbstractMedia media)
    {
        return _media.contains(media);
    }

    /**
     * Called by a {@link VirtualMediaPanel} when the view that contains our media is scrolled.
     *
     * @param dx the scrolled distance in the x direction (in pixels).
     * @param dy the scrolled distance in the y direction (in pixels).
     */
    public void viewLocationDidChange (int dx, int dy)
    {
        // let our media know
        for (int ii = 0, ll = _media.size(); ii < ll; ii++) {
            _media.get(ii).viewLocationDidChange(dx, dy);
        }
    }

    /**
     * Called by a {@link AbstractMedia} when its render order has changed.
     */
    public void renderOrderDidChange (AbstractMedia media)
    {
        if (_tickStamp > 0) {
            log.warning("Egads! Render order changed during a tick.", new Exception());
        }

        _media.remove(media);
        _media.insertSorted(media, RENDER_ORDER);
    }

    /**
     * Calls {@link AbstractMedia#tick} on all media to give them a chance to move about, change
     * their look, generate dirty regions, and so forth.
     */
    protected void tickAllMedia (long tickStamp)
    {
        // we use _tickpos so that it can be adjusted if media is added or removed during the tick
        // dispatch
        for (_tickpos = 0; _tickpos < _media.size(); _tickpos++) {
            tickMedia(_media.get(_tickpos), tickStamp);
        }
        _tickpos = -1;
    }

    /**
     * Inserts the specified media into this manager, return true on success.
     */
    protected boolean insertMedia (AbstractMedia media)
    {
        if (_media.contains(media)) {
            log.warning("Attempt to insert media more than once [media=" + media + "].",
                        new Exception());
            return false;
        }

        media.init(this);
        int ipos = _media.insertSorted(media, RENDER_ORDER);

        // if we've started our tick but have not yet painted our media, we need to take care that
        // this newly added media will be ticked before our upcoming render
        if (_tickStamp > 0L) {
            if (_tickpos == -1) {
                // if we're done with our own call to tick(), we need to tick this new media
                tickMedia(media, _tickStamp);
            } else if (ipos <= _tickpos) {
                // otherwise, we're in the middle of our call to tick() and we only need to tick
                // this guy if he's being inserted before our current tick position (if he's
                // inserted after our current position, we'll get to him as part of this tick
                // iteration)
                _tickpos++;
                tickMedia(media, _tickStamp);
            }
        }

        return true;
    }

    /** A helper function used to call {@link AbstractMedia#tick}. */
    protected final void tickMedia (AbstractMedia media, long tickStamp)
    {
        if (media._firstTick == 0L) {
            media.willStart(tickStamp);
        }
        media.tick(tickStamp);
    }

    /**
     * Removes the specified media from this manager, return true on success.
     */
    protected boolean removeMedia (AbstractMedia media)
    {
        int mpos = _media.indexOf(media);
        if (mpos != -1) {
            _media.remove(mpos);
            media.invalidate();
            media.shutdown();
            // if we're in the middle of ticking, we need to adjust the _tickpos if necessary
            if (mpos <= _tickpos) {
                _tickpos--;
            }
            return true;
        }
        log.warning("Attempt to remove media that wasn't inserted [media=" + media + "].");
        return false;
    }

    /**
     * Clears all media from the manager and calls {@link AbstractMedia#shutdown} on each. This
     * does not invalidate the media's vacated bounds; it is assumed that it will be ok.
     */
    protected void clearMedia ()
    {
        if (_tickStamp > 0) {
            log.warning("Egads! Requested to clearMedia() during a tick.", new Exception());
        }

        for (int ii = _media.size() - 1; ii >= 0; ii--) {
            _media.remove(ii).shutdown();
        }
    }

    /**
     * Queues the notification for dispatching after we've ticked all the media.
     */
    public void queueNotification (ObserverList<Object> observers, ObserverOp<Object> event)
    {
        _notify.add(new Tuple<ObserverList<Object>,ObserverOp<Object>>(observers, event));
    }

    /** Type safety jockeying. */
    protected abstract SortableArrayList<? extends AbstractMedia> createMediaList ();

    /**
     * Dispatches all queued media notifications.
     */
    protected void dispatchNotifications ()
    {
        for (int ii = 0, nn = _notify.size(); ii < nn; ii++) {
            Tuple<ObserverList<Object>,ObserverOp<Object>> tuple = _notify.get(ii);
            tuple.left.apply(tuple.right);
        }
        _notify.clear();
    }

    /** Used to sort media by render order. */
    protected static final Comparator<AbstractMedia> RENDER_ORDER =
        new Comparator<AbstractMedia>() {
        public int compare (AbstractMedia am1, AbstractMedia am2) {
            return am1.renderCompareTo(am2);
        }
    };

    /** The media host we're working with. */
    protected MediaHost _host;

    /** The region manager. */
    protected RegionManager _remgr;

    /** List of observers to notify at the end of the tick. */
    protected List<Tuple<ObserverList<Object>,ObserverOp<Object>>> _notify = Lists.newArrayList();

    /** Our render-order sorted list of media. */
    @SuppressWarnings("unchecked") protected SortableArrayList<AbstractMedia> _media =
        (SortableArrayList<AbstractMedia>)createMediaList();

    /** The position in our media list that we're ticking (while in the middle of a call to {@link
     * #tick}) otherwise -1. */
    protected int _tickpos = -1;

    /** The tick stamp if the manager is in the midst of a call to {@link #tick}, else 0. */
    protected long _tickStamp;
}
