//
// $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.awt.Component;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;

import javax.swing.JComponent;

import com.samskivert.util.ResultListener;

/**
 * A generic timer class that can be rendered on screen.
 *
 * NOTE: This base class doesn't actually render anything, but it's still
 * useful for triggering user supplied callback functions.  Derived classes
 * are more than welcome to setup their own rendering, of course.
 */
public class TimerView
    implements FrameParticipant, HierarchyListener
{
    /**
     * Constructs a timer view that fires at the default rate.
     */
    public TimerView (FrameManager fmgr, JComponent host, Rectangle bounds)
    {
        // Cache the input arguments
        _fmgr = fmgr;
        _host = host;
        _bounds = new Rectangle(bounds);

        // Watch for changes in the timer's state so it can effectively
        // register or unregister with the frame manager as necessary
        _host.addHierarchyListener(this);
        checkFrameParticipation();
    }

    /**
     * Sets whether this timer should be rendered.
     */
    public void setEnabled (boolean enabled)
    {
        if (_enabled != enabled)
        {
            _enabled = enabled;
            invalidate();
        }
    }

    /**
     * Get the amount of time it takes to render digital changes in the
     * completion state.
     */
    public long getTransitionTime ()
    {
        return _transitionTime;
    }

    /**
     * Set the amount of time it takes to render digital changes in the
     * completion state (probably due to a timer reset).
     */
    public void setTransitionTime (long time)
    {
        _transitionTime = Math.max(0, time);
    }

    /**
     * Setup a warning to trigger after the timer is "warnPercent"
     * or more completed.  If "warner" is non-null, it will be
     * called at that time.
     */
    public void setWarning (float warnPercent, ResultListener<TimerView> warner)
    {
        // This warning hasn't triggered yet
        _warned = false;

        // Here are the details
        _warnPercent = warnPercent;
        _warner = warner;
    }

    /**
     * Remove any warning this timer might have had.
     */
    public void removeWarning ()
    {
        // Don't trigger any warnings
        _warned = false;
        _warnPercent = -1;
        _warner = null;
    }

    /** Test if the timer is running right now. */
    public boolean running ()
    {
        return _running;
    }

    /**
     * Start the timer running from the specified percentage complete,
     * to expire at the specified time.
     *
     * @param startPercent a value in [0f, 1f) indicating how much
     * hourglass time has already elapsed when the timer starts.
     * @param duration The time interval over which the timer would
     * run if it started at 0%.
     * @param finisher a listener that will be notified when the timer
     * finishes, or null if nothing should be notified.
     */
    public void start (float startPercent, long duration, ResultListener<TimerView> finisher)
    {
        // Sanity check input arguments
        if (startPercent < 0.0f || startPercent >= 1.0f) {
            throw new IllegalArgumentException(
                "Invalid starting percent " + startPercent);
        }
        if (duration < 0) {
            throw new IllegalArgumentException("Invalid duration " + duration);
        }

        // Stop any current processing
        stop();

        // Record the timer's full duration and effective start time
        _duration = duration;

        // Change the completion percent and make sure the starting
        // time gets updated on the next tick
        changeComplete(startPercent);
        _start = Long.MIN_VALUE;

        // Thank you sir; would you kindly take a chair in the waiting room?
        _finisher = finisher;

        // The warning and completion handlers haven't been triggered yet
        _warned = false;
        _completed = false;

        // Start things running
        _running = true;
    }

    /**
     * Reset the timer.
     */
    public void reset ()
    {
        // Stop processing permanently
        stop();

        // Reset the completion percent
        changeComplete(0f);
    }

    /**
     * Stop the timer.
     */
    public void stop ()
    {
        // Stop processing
        pause();

        // Prevent it from being unpaused
        _lastUpdate = Long.MIN_VALUE;
    }

    /**
     * Pause the timer from processing.
     */
    public void pause ()
    {
        // Stop processing
        _running = false;
    }

    /**
     * Unpause the timer.
     */
    public void unpause ()
    {
        // Don't unpause the timer if it wasn't paused to begin with
        if (_lastUpdate == Long.MIN_VALUE || _start == Long.MIN_VALUE) {
            return;
        }

        // Adjust the starting time when the timer next ticks
        _processUnpause = true;

        // Start things running again
        _running = true;
    }

    /**
     * Generate an unexpected change in the timer's completion and
     * set up the necessary details to render interpolation from the old
     * to new states
     */
    public void changeComplete (float complete)
    {
        // Store the new state
        _complete = complete;

        // Determine the percentage difference between the "actual"
        // completion state and the completion amount to render during
        // the smooth interpolation
        _renderOffset = _renderComplete - _complete;

        // When the timer next ticks, find out when this interpolation
        // should start
        _renderOffsetTime = Long.MIN_VALUE;
    }

    /**
     * Updates the timer for the current inputted time.
     */
    protected void update (long now)
    {
    }

    /**
     * Test if the warning was triggered and needs to be processed.
     */
    protected boolean triggeredWarning ()
    {
        // Trigger a warning if it exists, it hasn't occured yet,
        // and the timer has passed the warning threshold.
        return ((_warnPercent >= 0f) &&
                !_warned && (_warnPercent <= _complete));
    }

    /**
     * Test if the completion was triggered and needs to be processed.
     */
    protected boolean triggeredCompleted ()
    {
        return (!_completed && (1.0 <= _complete));
    }

    /**
     * Handle the trigger of the warning.
     */
    protected void handleWarning ()
    {
        // Remember that this warning was handled
        _warned = true;

        // Execute the warning listener if one was supplied
        if (_warner != null) {
            _warner.requestCompleted(this);
        }
    }

    /**
     * Handle the trigger of the timer's completion.
     */
    protected void handleCompleted ()
    {
        // Remember that the completion was handled
        _completed = true;

        // Stop the timer
        stop();

        // Handle the trigger function if one was supplied
        if (_finisher != null) {
            _finisher.requestCompleted(this);
        }
    }

    /**
     * Invalidates this view's bounds via the host component.
     */
    protected void invalidate ()
    {
        // Schedule the timer's location on screen to get repainted
        _invalidated = true;
        _host.repaint(_bounds.x, _bounds.y, _bounds.width, _bounds.height);
    }

    /**
     * Renders the timer to the given graphics context if enabled.
     */
    public void render (Graphics2D gfx)
    {
        // Paint the timer if its enabled
        if (_enabled) {
            paint(gfx, _renderComplete);
        }
        _invalidated = false;
    }

    /**
     * Paint the timer into the given graphics context at the inputted
     * percent complete (0f means just started, 1f means just finished).
     */
    public void paint (Graphics2D gfx, float complete)
    {
        // Inheriting classes will want to implement their own
        // version of this function.  Remember to call this one though!

        // Remember the completion level the last time the timer was painted
        _paintComplete = complete;
    }

    // documentation inherited
    public void tick (long now)
    {
        if (!_enabled) {
            return;
        }

        // Initialize the starting time if necessary
        if (_start == Long.MIN_VALUE) {
            _start = now - Math.round(_duration * _complete);
        }

        // Initialize the timestamp of the rendering error if necessary
        if (_renderOffsetTime == Long.MIN_VALUE) {
            _renderOffsetTime = now;
        }

        // If the timer was just unpaused, handle the updates that
        // need a timestamp
        if (_processUnpause) {
            _processUnpause = false;
            _start += now - _lastUpdate;
        }

        // Update the completion and triggers when the timer is running
        if (running())
        {
            // Figure out how what percent the timer has completed
            _lastUpdate = now;
            _complete = ((float) (_lastUpdate - _start)) / _duration;

            // Handle warning trigger if necessary
            if (triggeredWarning()) {
                handleWarning();
            }

            // Handle completion trigger if necessary
            if (triggeredCompleted()) {
                handleCompleted();
            }
        }

        // Add error to the render completion percent if the interpolation
        // hasn't finished yet
        _renderComplete = _complete;
        if (_renderOffsetTime + _transitionTime > now) {
            _renderComplete += _renderOffset *
                (1f - (now - _renderOffsetTime) / (float) _transitionTime);
            _renderComplete = Math.max(0f, Math.min(1f, _renderComplete));
        }

        // Possibly orce a repaint if the render level changed (highly
        // probable but not guaranteed)
        if (_renderComplete != _paintComplete)
        {
            // Always force a repaint when changing to or from a boundary
            if (_renderComplete <= 0f || _paintComplete <= 0f ||
                _renderComplete >= 1f || _paintComplete >= 1f)
            {
                invalidate();
            }

            // Also force a repaint if the completion state sufficiently
            // changed
            else if (Math.abs(_renderComplete - _paintComplete) >
                     _changeThreshold)
            {
                invalidate();
            }
        }
    }

    // documentation inherited
    public boolean needsPaint ()
    {
        // Always paint if the timer was invalidated
        return _invalidated;
    }

    // documentation inherited
    public Component getComponent ()
    {
        return null;
    }

    // documentation inherited
    public void hierarchyChanged (HierarchyEvent e)
    {
        checkFrameParticipation();
    }

    /** Check that the frame knows about the timer. */
    public void checkFrameParticipation ()
    {
        // Determine whether or not the timer should participate in
        // media ticks
        boolean participate = _host.isShowing();

        // Start participating if necessary
        if (participate && !_participating)
        {
            _fmgr.registerFrameParticipant(this);
            _participating = true;
        }

        // Stop participating if necessary
        else if (!participate && _participating)
        {
            _fmgr.removeFrameParticipant(this);
            _participating = false;
        }
    }

    /** The frame manager that manages our animated view. */
    protected FrameManager _fmgr;

    /** The media panel containing the view. */
    protected JComponent _host;

    /** The screen coordinates of the timer's bounding box. */
    protected Rectangle _bounds;

    /** Whether to render the timer. */
    protected boolean _enabled = true;

    /** Whether the timer is running. */
    protected boolean _running = false;

    /** Whether the timer is participating in media tick updates. */
    protected boolean _participating = false;

    /** The last time the timer updated while running. */
    protected long _lastUpdate = Long.MIN_VALUE;

    /** The total amount of time in the timer when it was 0% done. */
    protected long _duration;

    /** The timestamp when the timer effectively started. */
    protected long _start;

    /** The percent of the duration time completed. */
    protected float _complete = 0f;

    /** The difference between the old rendered completion percent and the
     * new completion percent the last time the completion percent had
     * an unexpected change.  In other words, this is the greatest amount
     * that _renderComplete will differ from _complete during an
     * state interpolation.
     */
    protected float _renderOffset = 0f;

    /** The timestamp of _renderOffset. */
    protected long _renderOffsetTime = Long.MIN_VALUE;

    /** The amount of time it takes to fully interpolation a render
     * transition from completion 0.0 to 1.0. */
    protected long _transitionTime = 0;

    /** The completion percent at which to render the timer. */
    protected float _renderComplete = 0f;

    /** The completion percent last time the timer was repainted. */
    protected float _paintComplete = -1;

    /**
     * The timer will not invalidate itself until the completion level
     * to render changes by at least this much from the previous time it
     * was invalidated.
     */
    protected float _changeThreshold = 0.0f;

    /** True if the timer has been invalidated since its last repaint. */
    protected boolean _invalidated;

    /** Trigger the warning when at least this much time has elapsed,
     * or -1 for no warning. */
    protected float _warnPercent = -1;

    /** True if the warning has already been processed. */
    protected boolean _warned;

    /** True if the completion has already been processed. */
    protected boolean _completed;

    /** True if the code should process everything required for an unpause
     * on the next tick(). */
    protected boolean _processUnpause = false;

    /** A listener to be notified when the timer finishes. */
    protected ResultListener<TimerView> _finisher;

    /** A listener to be notified when the warning time occurs. */
    protected ResultListener<TimerView> _warner;

    /** The default update date. */
    protected static final long DEFAULT_RATE = 100L;
}
