/*
 * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
 * Copyright (c) 1998-2008, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
 * and the following disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
 * contributors may be used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.umd.cs.piccolo;

import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.SwingUtilities;
import javax.swing.Timer;

import edu.umd.cs.piccolo.activities.PActivity;
import edu.umd.cs.piccolo.activities.PActivityScheduler;
import edu.umd.cs.piccolo.util.PDebug;
import edu.umd.cs.piccolo.util.PNodeFilter;

/**
 * <b>PRoot</b> serves as the top node in Piccolo2D's runtime structure. The
 * PRoot responsible for running the main UI loop that processes input from
 * activities and external events.
 * <P>
 * 
 * @version 1.1
 * @author Jesse Grosjean
 */
public class PRoot extends PNode {

    /**
     * Allows for future serialization code to understand versioned binary
     * formats.
     */
    private static final long serialVersionUID = 1L;

    /**
     * The property name that identifies a change in the set of this root's
     * input sources (see {@link InputSource InputSource}). In any property
     * change event the new value will be a reference to the list of this root's
     * input sources, but old value will always be null.
     */
    public static final String PROPERTY_INPUT_SOURCES = "inputSources";

    /**
     * The property code that identifies a change in the set of this root's
     * input sources (see {@link InputSource InputSource}). In any property
     * change event the new value will be a reference to the list of this root's
     * input sources, but old value will always be null.
     */
    public static final int PROPERTY_CODE_INPUT_SOURCES = 1 << 14;

    /**
     * The property name that identifies a change in this node's interacting
     * state.
     *
     * @since 1.3
     */
    public static final String PROPERTY_INTERACTING_CHANGED = "INTERACTING_CHANGED_NOTIFICATION";

    /**
     * The property code that identifies a change in this node's interacting
     * state.
     *
     * @since 1.3
     */
    public static final int PROPERTY_CODE_INTERACTING_CHANGED = 1 << 13;

    /** Whether this not is currently processing inputs. */
    protected transient boolean processingInputs;

    /** Whether this node needs to have its inputs processed. */
    protected transient boolean processInputsScheduled;

    /** The number of interactions this node is currently participating in. */
    private transient int interacting;

    /**
     * The singleton instance of the default input manager.
     */
    private transient PInputManager defaultInputManager;

    /** The Input Sources that are registered to this node. */
    private final transient List inputSources;

    /**
     * Used to provide a consistent clock time to activities as they are being
     * processed.
     * 
     * Should it happen that an activity step take longer than a millisecond,
     * the next step will be unaffected by the change in clock had it used
     * System.currentMillis().
     */
    private transient long globalTime;

    /**
     * Object responsible for scheduling activities, regardless of where in the
     * scene they take place.
     */
    private final PActivityScheduler activityScheduler;

    /**
     * Construct a new PRoot(). Note the PCanvas already creates a basic scene
     * graph for you so often you will not need to construct your own roots.
     */
    public PRoot() {
        super();
        inputSources = new ArrayList();
        globalTime = System.currentTimeMillis();
        activityScheduler = new PActivityScheduler(this);
    }

    // ****************************************************************
    // Activities
    // ****************************************************************

    /**
     * Add an activity to the activity scheduler associated with this root.
     * Activities are given a chance to run during each call to the roots
     * <code>processInputs</code> method. When the activity has finished running
     * it will automatically get removed.
     * 
     * @param activity Activity that should be scheduled
     * @return whether it has been scheduled (always true)
     */
    public boolean addActivity(final PActivity activity) {
        getActivityScheduler().addActivity(activity);
        return true;
    }

    /**
     * Get the activity scheduler associated with this root.
     * 
     * @return associated scheduler
     */
    public PActivityScheduler getActivityScheduler() {
        return activityScheduler;
    }

    /**
     * Wait for all scheduled activities to finish before returning from this
     * method. This will freeze out user input, and so it is generally
     * recommended that you use PActivities.setTriggerTime() to offset
     * activities instead of using this method.
     */
    public void waitForActivities() {
        final PNodeFilter cameraWithCanvas = new CameraWithCanvasFilter();

        while (activityScheduler.getActivitiesReference().size() > 0) {
            processInputs();
            final Iterator i = getAllNodes(cameraWithCanvas, null).iterator();
            while (i.hasNext()) {
                final PCamera each = (PCamera) i.next();
                each.getComponent().paintImmediately();
            }
        }
    }

    /**
     * Since getRoot is handled recursively, and root is the lowest point in the
     * hierarchy, simply returns itself.
     * 
     * @return itself
     */
    public PRoot getRoot() {
        return this;
    }

    /**
     * Get the default input manager to be used when processing input events.
     * PCanvas's use this method when they forward new swing input events to the
     * PInputManager.
     * 
     * @return a singleton instance of PInputManager
     */
    public PInputManager getDefaultInputManager() {
        if (defaultInputManager == null) {
            defaultInputManager = new PInputManager();
            addInputSource(defaultInputManager);
        }
        return defaultInputManager;
    }

    /**
     * Return true if this root has been marked as interacting. If so the root
     * will normally render at a lower quality that is faster.
     * 
     * @since 1.3
     * @return true if this root has user interaction taking place
     */
    public boolean getInteracting() {
        return interacting > 0;
    }

    /**
     * Set if this root is interacting. If so the root will normally render at a
     * lower quality that is faster. Also repaints the root if the the
     * interaction has ended.
     * <p/>
     * This has similar functionality to the setInteracting method on Canvas,
     * but this is the appropriate place to mark interactions that may occur in
     * multiple canvases if this Root is shared.
     * 
     * @since 1.3
     * @param isInteracting True if this root has user interaction taking place
     * @see PCanvas#setInteracting(boolean)
     */
    public void setInteracting(final boolean isInteracting) {
        final boolean wasInteracting = getInteracting();

        if (isInteracting) {
            interacting++;
        }
        else {
            interacting--;
        }

        if (!isInteracting && !getInteracting()) {
            // force all the child cameras to repaint
            for (int i = 0; i < getChildrenCount(); i++) {
                final PNode child = getChild(i);
                if (child instanceof PCamera) {
                    child.repaint();
                }
            }

        }
        if (wasInteracting != isInteracting) {
            firePropertyChange(PROPERTY_CODE_INTERACTING_CHANGED, PROPERTY_INTERACTING_CHANGED, Boolean
                    .valueOf(wasInteracting), Boolean.valueOf(isInteracting));
        }
    }

    /**
     * Advanced. If you want to add additional input sources to the roots UI
     * process you can do that here. You will seldom do this unless you are
     * making additions to the Piccolo2D framework.
     * 
     * @param inputSource An input source that should be added
     */
    public void addInputSource(final InputSource inputSource) {
        inputSources.add(inputSource);
        firePropertyChange(PROPERTY_CODE_INPUT_SOURCES, PROPERTY_INPUT_SOURCES, null, inputSources);
    }

    /**
     * Advanced. If you want to remove the default input source from the roots
     * UI process you can do that here. You will seldom do this unless you are
     * making additions to the Piccolo2D framework.
     * 
     * @param inputSource input source that should no longer be asked about
     *            input events
     */
    public void removeInputSource(final InputSource inputSource) {
        if (inputSources.remove(inputSource)) {
            firePropertyChange(PROPERTY_CODE_INPUT_SOURCES, PROPERTY_INPUT_SOURCES, null, inputSources);
        }
    }

    /**
     * Returns a new timer. This method allows subclasses, such as PSWTRoot to
     * create custom timers that will be used transparently by the Piccolo2D
     * framework.
     * 
     * @param delay # of milliseconds before action listener is invoked
     * @param listener listener to be invoked after delay
     * 
     * @return A new Timer
     */
    public Timer createTimer(final int delay, final ActionListener listener) {
        return new Timer(delay, listener);
    }

    // ****************************************************************
    // UI Loop - Methods for running the main UI loop of Piccolo2D.
    // ****************************************************************

    /**
     * Get the global Piccolo2D time. This is set to System.currentTimeMillis()
     * at the beginning of the roots <code>processInputs</code> method.
     * Activities should usually use this global time instead of System.
     * currentTimeMillis() so that multiple activities will be synchronized.
     * 
     * @return time as recorded at the beginning of activity scheduling
     */
    public long getGlobalTime() {
        return globalTime;
    }

    /**
     * This is the heartbeat of the Piccolo2D framework. Pending input events
     * are processed. Activities are given a chance to run, and the bounds
     * caches and any paint damage is validated.
     */
    public void processInputs() {
        PDebug.startProcessingInput();
        processingInputs = true;

        globalTime = System.currentTimeMillis();
        if (inputSources.size() > 0) {
            final Iterator inputSourceIterator = inputSources.iterator();
            while (inputSourceIterator.hasNext()) {
                final InputSource each = (InputSource) inputSourceIterator.next();
                each.processInput();
            }
        }

        activityScheduler.processActivities(globalTime);
        validateFullBounds();
        validateFullPaint();

        processingInputs = false;
        PDebug.endProcessingInput();
    }

    /** {@inheritDoc} */
    public void setFullBoundsInvalid(final boolean fullLayoutInvalid) {
        super.setFullBoundsInvalid(fullLayoutInvalid);
        scheduleProcessInputsIfNeeded();
    }

    /** {@inheritDoc} */
    public void setChildBoundsInvalid(final boolean childLayoutInvalid) {
        super.setChildBoundsInvalid(childLayoutInvalid);
        scheduleProcessInputsIfNeeded();
    }

    /** {@inheritDoc} */
    public void setPaintInvalid(final boolean paintInvalid) {
        super.setPaintInvalid(paintInvalid);
        scheduleProcessInputsIfNeeded();
    }

    /** {@inheritDoc} */
    public void setChildPaintInvalid(final boolean childPaintInvalid) {
        super.setChildPaintInvalid(childPaintInvalid);
        scheduleProcessInputsIfNeeded();
    }

    /**
     * Schedule process inputs if needed.
     */
    public void scheduleProcessInputsIfNeeded() {
        /*
         * The reason for the special case here (when not in the event dispatch
         * thread) is that the SwingUtilitiles.invokeLater code below only
         * invokes later with respect to the event dispatch thread, it will
         * invoke concurrently with other threads.
         */
        if (!SwingUtilities.isEventDispatchThread()) {
            /*
             * Piccolo2D is not thread safe and should almost always be called
             * from the Swing event dispatch thread. It should only reach this
             * point when a new canvas is being created.
             */
            return;
        }

        PDebug.scheduleProcessInputs();

        if (!processInputsScheduled && !processingInputs
                && (getFullBoundsInvalid() || getChildBoundsInvalid() || getPaintInvalid() || getChildPaintInvalid())) {

            processInputsScheduled = true;
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    processInputs();
                    processInputsScheduled = false;
                }
            });
        }
    }

    private static final class CameraWithCanvasFilter implements PNodeFilter {
        public boolean accept(final PNode aNode) {
            return aNode instanceof PCamera && ((PCamera) aNode).getComponent() != null;
        }

        public boolean acceptChildrenOf(final PNode aNode) {
            return true;
        }
    }

    /**
     * This interfaces is for advanced use only. If you want to implement a
     * different kind of input framework then Piccolo2D provides you can hook it
     * in here.
     */
    public static interface InputSource {
        /** Causes the system to process any pending Input Events. */
        void processInput();
    }
}
