<?php
/*
 *  $Id: Project.php 1097 2011-05-17 17:10:32Z mrook $
 *
 * 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.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information please see
 * <http://phing.info>.
 */

include_once 'phing/system/io/PhingFile.php';
include_once 'phing/util/FileUtils.php';
include_once 'phing/TaskAdapter.php';
include_once 'phing/util/StringHelper.php';
include_once 'phing/BuildEvent.php';
include_once 'phing/input/DefaultInputHandler.php';

/**
 *  The Phing project class. Represents a completely configured Phing project.
 *  The class defines the project and all tasks/targets. It also contains
 *  methods to start a build as well as some properties and FileSystem
 *  abstraction.
 *
 * @author    Andreas Aderhold <andi@binarycloud.com>
 * @author    Hans Lellelid <hans@xmpl.org>
 * @version   $Revision: 1097 $
 * @package   phing
 */
class Project {

    // Logging level constants.
    const MSG_DEBUG = 4;
    const MSG_VERBOSE = 3;
    const MSG_INFO = 2;
    const MSG_WARN = 1;
    const MSG_ERR = 0;
    
    /** contains the targets */
    private $targets         = array();
    /** global filterset (future use) */
    private $globalFilterSet = array();
    /**  all globals filters (future use) */
    private $globalFilters   = array();
    
    /** Project properties map (usually String to String). */
    private $properties = array();
    
    /**
     * Map of "user" properties (as created in the Ant task, for example).
     * Note that these key/value pairs are also always put into the
     * project properties, so only the project properties need to be queried.
     * Mapping is String to String.
     */
    private $userProperties = array();
    
    /**
     * Map of inherited "user" properties - that are those "user"
     * properties that have been created by tasks and not been set
     * from the command line or a GUI tool.
     * Mapping is String to String.
     */
    private $inheritedProperties = array();
    
    /** task definitions for this project*/
    private $taskdefs = array();
    
    /** type definitions for this project */
    private $typedefs = array();
    
    /** holds ref names and a reference to the referred object*/
    private $references = array();
    
    /** The InputHandler being used by this project. */
    private $inputHandler;
    
    /* -- properties that come in via xml attributes -- */
    
    /** basedir (PhingFile object) */
    private $basedir;
    
    /** the default target name */
    private $defaultTarget = 'all';
    
    /** project name (required) */
    private $name;
    
    /** project description */
    private $description;

    /** require phing version */
    private $phingVersion;

    /** a FileUtils object */
    private $fileUtils;
    
    /**  Build listeneers */
    private $listeners = array();

    /**
     *  Constructor, sets any default vars.
     */
    function __construct() {
        $this->fileUtils = new FileUtils();
        $this->inputHandler = new DefaultInputHandler();
    }

    /**
     * Sets the input handler
     */
    public function setInputHandler(InputHandler $handler) {
        $this->inputHandler = $handler;
    }

    /**
     * Retrieves the current input handler.
     */
    public function getInputHandler() {
        return $this->inputHandler;
    }

    /** inits the project, called from main app */
    function init() {
        // set builtin properties
        $this->setSystemProperties();
        
        // load default tasks
        $taskdefs = Phing::getResourcePath("phing/tasks/defaults.properties");
        
        try { // try to load taskdefs
            $props = new Properties();
            $in = new PhingFile((string)$taskdefs);

            if ($in === null) {
                throw new BuildException("Can't load default task list");
            }
            $props->load($in);

            $enum = $props->propertyNames();
            foreach($enum as $key) {
                $value = $props->getProperty($key);
                $this->addTaskDefinition($key, $value);
            }
        } catch (IOException $ioe) {
            throw new BuildException("Can't load default task list");
        }

        // load default tasks
        $typedefs = Phing::getResourcePath("phing/types/defaults.properties");

        try { // try to load typedefs
            $props = new Properties();
            $in    = new PhingFile((string)$typedefs);
            if ($in === null) {
                throw new BuildException("Can't load default datatype list");
            }
            $props->load($in);

            $enum = $props->propertyNames();
            foreach($enum as $key) {
                $value = $props->getProperty($key);
                $this->addDataTypeDefinition($key, $value);
            }
        } catch(IOException $ioe) {
            throw new BuildException("Can't load default datatype list");
        }
    }

    /** returns the global filterset (future use) */
    function getGlobalFilterSet() {
        return $this->globalFilterSet;
    }

    // ---------------------------------------------------------
    // Property methods
    // ---------------------------------------------------------
    
    /**
     * Sets a property. Any existing property of the same name
     * is overwritten, unless it is a user property.
     * @param string $name The name of property to set.
     *             Must not be <code>null</code>.
     * @param string $value The new value of the property.
     *              Must not be <code>null</code>.
     * @return void
     */
    public function setProperty($name, $value) {
    
        // command line properties take precedence
        if (isset($this->userProperties[$name])) {
            $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE);
            return;
        }

        if (isset($this->properties[$name])) {
            $this->log("Overriding previous definition of property " . $name, Project::MSG_VERBOSE);
        }

        $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG);
        $this->properties[$name] = $value;
    }

    /**
     * Sets a property if no value currently exists. If the property
     * exists already, a message is logged and the method returns with
     * no other effect.
     *
     * @param string $name The name of property to set.
     *             Must not be <code>null</code>.
     * @param string $value The new value of the property.
     *              Must not be <code>null</code>.
     * @since 2.0
     */
    public function setNewProperty($name, $value) {
        if (isset($this->properties[$name])) {
            $this->log("Override ignored for property " . $name, Project::MSG_DEBUG);
            return;
        }
        $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG);
        $this->properties[$name] = $value;
    }

    /**
     * Sets a user property, which cannot be overwritten by
     * set/unset property calls. Any previous value is overwritten.
     * @param string $name The name of property to set.
     *             Must not be <code>null</code>.
     * @param string $value The new value of the property.
     *              Must not be <code>null</code>.
     * @see #setProperty()
     */
    public function setUserProperty($name, $value) {
        $this->log("Setting ro project property: " . $name . " -> " . $value, Project::MSG_DEBUG);
        $this->userProperties[$name] = $value;
        $this->properties[$name] = $value;
    }

    /**
     * Sets a user property, which cannot be overwritten by set/unset
     * property calls. Any previous value is overwritten. Also marks
     * these properties as properties that have not come from the
     * command line.
     *
     * @param string $name The name of property to set.
     *             Must not be <code>null</code>.
     * @param string $value The new value of the property.
     *              Must not be <code>null</code>.
     * @see #setProperty()
     */
    public function setInheritedProperty($name, $value) {
        $this->inheritedProperties[$name] = $value;
        $this->setUserProperty($name, $value);
    }

    /**
     * Sets a property unless it is already defined as a user property
     * (in which case the method returns silently).
     *
     * @param name The name of the property.
     *             Must not be <code>null</code>.
     * @param value The property value. Must not be <code>null</code>.
     */
    private function setPropertyInternal($name, $value) {
        if (isset($this->userProperties[$name])) {
            $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE);
            return;
        }
        $this->properties[$name] = $value;
    }

    /**
     * Returns the value of a property, if it is set.
     *
     * @param string $name The name of the property.
     *             May be <code>null</code>, in which case
     *             the return value is also <code>null</code>.
     * @return string The property value, or <code>null</code> for no match
     *         or if a <code>null</code> name is provided.
     */
    public function getProperty($name) {
        if (!isset($this->properties[$name])) {
            return null;
        }
        $found = $this->properties[$name];
        // check to see if there are unresolved property references
        if (false !== strpos($found, '${')) {
          // attempt to resolve properties
          $found = $this->replaceProperties($found);
          // save resolved value
          $this->properties[$name] = $found;
        }
        return $found;
    }

    /**
     * Replaces ${} style constructions in the given value with the
     * string value of the corresponding data types.
     *
     * @param value The string to be scanned for property references.
     *              May be <code>null</code>.
     *
     * @return the given string with embedded property names replaced
     *         by values, or <code>null</code> if the given string is
     *         <code>null</code>.
     *
     * @exception BuildException if the given value has an unclosed
     *                           property name, e.g. <code>${xxx</code>
     */
    public function replaceProperties($value) {
        return ProjectConfigurator::replaceProperties($this, $value, $this->properties);
    }

    /**
     * Returns the value of a user property, if it is set.
     *
     * @param string $name The name of the property.
     *             May be <code>null</code>, in which case
     *             the return value is also <code>null</code>.
     * @return string  The property value, or <code>null</code> for no match
     *         or if a <code>null</code> name is provided.
     */
     public function getUserProperty($name) {
        if (!isset($this->userProperties[$name])) {
            return null;
        }
        return $this->userProperties[$name];
    }

    /**
     * Returns a copy of the properties table.
     * @return array A hashtable containing all properties
     *         (including user properties).
     */
    public function getProperties() {
        return $this->properties;
    }

    /**
     * Returns a copy of the user property hashtable
     * @return a hashtable containing just the user properties
     */
    public function getUserProperties() {
        return $this->userProperties;
    }

    /**
     * Copies all user properties that have been set on the command
     * line or a GUI tool from this instance to the Project instance
     * given as the argument.
     *
     * <p>To copy all "user" properties, you will also have to call
     * {@link #copyInheritedProperties copyInheritedProperties}.</p>
     *
     * @param Project $other the project to copy the properties to.  Must not be null.
     * @return void
     * @since phing 2.0
     */
    public function copyUserProperties(Project $other) {        
        foreach($this->userProperties as $arg => $value) {
            if (isset($this->inheritedProperties[$arg])) {
                continue;
            }
            $other->setUserProperty($arg, $value);
        }
    }

    /**
     * Copies all user properties that have not been set on the
     * command line or a GUI tool from this instance to the Project
     * instance given as the argument.
     *
     * <p>To copy all "user" properties, you will also have to call
     * {@link #copyUserProperties copyUserProperties}.</p>
     *
     * @param other the project to copy the properties to.  Must not be null.
     *
     * @since phing 2.0
     */
    public function copyInheritedProperties(Project $other) {
        foreach($this->userProperties as $arg => $value) {
            if ($other->getUserProperty($arg) !== null) {
                continue;
            }
            $other->setInheritedProperty($arg, $value);
        }        
    }
    
    // ---------------------------------------------------------
    //  END Properties methods
    // ---------------------------------------------------------


    function setDefaultTarget($targetName) {
        $this->defaultTarget = (string) trim($targetName);
    }

    function getDefaultTarget() {
        return (string) $this->defaultTarget;
    }

    /**
     * Sets the name of the current project
     *
     * @param    string   name of project
     * @return   void
     * @access   public
     * @author   Andreas Aderhold, andi@binarycloud.com
     */

    function setName($name) {
        $this->name = (string) trim($name);
        $this->setProperty("phing.project.name", $this->name);
    }

    /**
     * Returns the name of this project
     *
     * @return  string  projectname
     * @access  public
     * @author  Andreas Aderhold, andi@binarycloud.com
     */
    function getName() {
        return (string) $this->name;
    }

    /** Set the projects description */
    function setDescription($description) {
        $this->description = (string) trim($description);
    }

    /** return the description, null otherwise */
    function getDescription() {
        return $this->description;
    }

    /** Set the minimum required phing version **/
    function setPhingVersion($version) {
        $version = str_replace('phing', '', strtolower($version));
        $this->phingVersion = (string)trim($version);
    }

    /** Get the minimum required phing version **/
    function getPhingVersion() {
        if($this->phingVersion === null) {
            $this->setPhingVersion(Phing::getPhingVersion());
        }
        return $this->phingVersion;
    }

    /** Set basedir object from xml*/
    function setBasedir($dir) {
        if ($dir instanceof PhingFile) {
            $dir = $dir->getAbsolutePath();
        }

        $dir = $this->fileUtils->normalize($dir);

        $dir = new PhingFile((string) $dir);
        if (!$dir->exists()) {
            throw new BuildException("Basedir ".$dir->getAbsolutePath()." does not exist");
        }
        if (!$dir->isDirectory()) {
            throw new BuildException("Basedir ".$dir->getAbsolutePath()." is not a directory");
        }
        $this->basedir = $dir;
        $this->setPropertyInternal("project.basedir", $this->basedir->getAbsolutePath());
        $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE);
        
        // [HL] added this so that ./ files resolve correctly.  This may be a mistake ... or may be in wrong place.                
        chdir($dir->getAbsolutePath());
    }

    /**
     * Returns the basedir of this project
     *
     * @return  PhingFile  Basedir PhingFile object
     * @access  public
     * @throws  BuildException
     * @author  Andreas Aderhold, andi@binarycloud.com
     */
    function getBasedir() {
        if ($this->basedir === null) {            
            try { // try to set it
                $this->setBasedir(".");
            } catch (BuildException $exc) {
                throw new BuildException("Can not set default basedir. ".$exc->getMessage());
            }
        }
        return $this->basedir;
    }

    /**
     * Sets system properties and the environment variables for this project.
     * 
     * @return void
     */
    function setSystemProperties() {
        
        // first get system properties
        $systemP = array_merge( self::getProperties(), Phing::getProperties() );
        foreach($systemP as $name => $value) {
            $this->setPropertyInternal($name, $value);
        }
        
        // and now the env vars
        foreach($_SERVER as $name => $value) {
            // skip arrays
            if (is_array($value)) {
                continue;
            }
            $this->setPropertyInternal('env.' . $name, $value);
        }
        return true;
    }


    /**
     * Adds a task definition.
     * @param string $name Name of tag.
     * @param string $class The class path to use.
     * @param string $classpath The classpat to use.
     */
    function addTaskDefinition($name, $class, $classpath = null) {
        $name  = $name;
        $class = $class;
        if ($class === "") {
            $this->log("Task $name has no class defined.", Project::MSG_ERR);
        }  elseif (!isset($this->taskdefs[$name])) {
            Phing::import($class, $classpath);
            $this->taskdefs[$name] = $class;
            $this->log("  +Task definiton: $name ($class)", Project::MSG_DEBUG);
        } else {
            $this->log("Task $name ($class) already registerd, skipping", Project::MSG_VERBOSE);
        }
    }

    function getTaskDefinitions() {
        return $this->taskdefs;
    }

    /**
     * Adds a data type definition.
     * @param string $name Name of tag.
     * @param string $class The class path to use.
     * @param string $classpath The classpat to use.
     */
    function addDataTypeDefinition($typeName, $typeClass, $classpath = null) {    
        if (!isset($this->typedefs[$typeName])) {        
            Phing::import($typeClass, $classpath);
            $this->typedefs[$typeName] = $typeClass;
            $this->log("  +User datatype: $typeName ($typeClass)", Project::MSG_DEBUG);
        } else {
            $this->log("Type $typeName ($typeClass) already registerd, skipping", Project::MSG_VERBOSE);
        }
    }

    function getDataTypeDefinitions() {
        return $this->typedefs;
    }

    /** add a new target to the project */
    function addTarget($targetName, &$target) {
        if (isset($this->targets[$targetName])) {
            throw new BuildException("Duplicate target: $targetName");
        }
        $this->addOrReplaceTarget($targetName, $target);
    }

    function addOrReplaceTarget($targetName, &$target) {
        $this->log("  +Target: $targetName", Project::MSG_DEBUG);
        $target->setProject($this);
        $this->targets[$targetName] = $target;

        $ctx = $this->getReference("phing.parsing.context");
        $current = $ctx->getConfigurator()->getCurrentTargets();
        $current[$targetName] = $target;
    }

    function getTargets() {
        return $this->targets;
    }

    /**
     * Create a new task instance and return reference to it. This method is
     * sorta factory like. A _local_ instance is created and a reference returned to
     * that instance. Usually PHP destroys local variables when the function call
     * ends. But not if you return a reference to that variable.
     * This is kinda error prone, because if no reference exists to the variable
     * it is destroyed just like leaving the local scope with primitive vars. There's no
     * central place where the instance is stored as in other OOP like languages.
     *
     * [HL] Well, ZE2 is here now, and this is  still working. We'll leave this alone
     * unless there's any good reason not to.
     *
     * @param    string    $taskType    Task name
     * @return   Task                A task object
     * @throws   BuildException
     *           Exception
     */
    function createTask($taskType) {
        try {
            $classname = "";
            $tasklwr = strtolower($taskType);
            foreach ($this->taskdefs as $name => $class) {
                if (strtolower($name) === $tasklwr) {
                    $classname = $class;
                    break;
                }
            }
            
            if ($classname === "") {
                return null;
            }
            
            $cls = Phing::import($classname);
            
            if (!class_exists($cls)) {
                throw new BuildException("Could not instantiate class $cls, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)");
            }
            
            $o = new $cls();
    
            if ($o instanceof Task) {
                $task = $o;
            } else {
                $this->log ("  (Using TaskAdapter for: $taskType)", Project::MSG_DEBUG);
                // not a real task, try adapter
                $taskA = new TaskAdapter();
                $taskA->setProxy($o);
                $task = $taskA;
            }
            $task->setProject($this);
            $task->setTaskType($taskType);
            // set default value, can be changed by the user
            $task->setTaskName($taskType);
            $this->log ("  +Task: " . $taskType, Project::MSG_DEBUG);
        } catch (Exception $t) {
            throw new BuildException("Could not create task of type: " . $taskType, $t);
        }
        // everything fine return reference
        return $task;
    }

    /**
     * Create a datatype instance and return reference to it
     * See createTask() for explanation how this works
     *
     * @param   string   Type name
     * @return  object   A datatype object
     * @throws  BuildException
     *          Exception
     */
    function createDataType($typeName) {        
        try {
            $cls = "";
            $typelwr = strtolower($typeName);
            foreach ($this->typedefs as $name => $class) {
                if (strtolower($name) === $typelwr) {
                    $cls = StringHelper::unqualify($class);                                    
                    break;
                }
            }
            
            if ($cls === "") {
                return null;
            }
            
            if (!class_exists($cls)) {
                throw new BuildException("Could not instantiate class $cls, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)");
            }
            
            $type = new $cls();
            $this->log("  +Type: $typeName", Project::MSG_DEBUG);
            if (!($type instanceof DataType)) {
                throw new Exception("$class is not an instance of phing.types.DataType");
            }
            if ($type instanceof ProjectComponent) {
                $type->setProject($this);
            }
        } catch (Exception $t) {
            throw new BuildException("Could not create type: $typeName", $t);
        }
        // everything fine return reference
        return $type;
    }

    /**
     * Executes a list of targets
     *
     * @param   array  List of target names to execute
     * @return  void
     * @throws  BuildException
     */
    function executeTargets($targetNames) {
        foreach($targetNames as $tname) {
            $this->executeTarget($tname);
        }
    }

    /**
     * Executes a target
     *
     * @param   string  Name of Target to execute
     * @return  void
     * @throws  BuildException
     */
    function executeTarget($targetName) {

        // complain about executing void
        if ($targetName === null) {
            throw new BuildException("No target specified");
        }

        // invoke topological sort of the target tree and run all targets
        // until targetName occurs.
        $sortedTargets = $this->_topoSort($targetName, $this->targets);        

        $curIndex = (int) 0;
        $curTarget = null;
        do {
            try {
                $curTarget = $sortedTargets[$curIndex++];
                $curTarget->performTasks();
            } catch (BuildException $exc) {
                $this->log("Execution of target \"".$curTarget->getName()."\" failed for the following reason: ".$exc->getMessage(), Project::MSG_ERR);
                throw $exc;
            }
        } while ($curTarget->getName() !== $targetName);
    }


    function resolveFile($fileName, $rootDir = null) {
        if ($rootDir === null) {
            return $this->fileUtils->resolveFile($this->basedir, $fileName);
        } else {
            return $this->fileUtils->resolveFile($rootDir, $fileName);
        }
    }    

    /**
     * Topologically sort a set of Targets.
     * @param  $root is the (String) name of the root Target. The sort is
     *         created in such a way that the sequence of Targets until the root
     *         target is the minimum possible such sequence.
     * @param  $targets is a array representing a "name to Target" mapping
     * @return An array of Strings with the names of the targets in
     *         sorted order.
     */
    function _topoSort($root, &$targets) {

        $root     = (string) $root;
        $ret      = array();
        $state    = array();
        $visiting = array();

        // We first run a DFS based sort using the root as the starting node.
        // This creates the minimum sequence of Targets to the root node.
        // We then do a sort on any remaining unVISITED targets.
        // This is unnecessary for doing our build, but it catches
        // circular dependencies or missing Targets on the entire
        // dependency tree, not just on the Targets that depend on the
        // build Target.

        $this->_tsort($root, $targets, $state, $visiting, $ret);

        $retHuman = "";
        for ($i=0, $_i=count($ret); $i < $_i; $i++) {
            $retHuman .= $ret[$i]->toString()." ";
        }
        $this->log("Build sequence for target '$root' is: $retHuman", Project::MSG_VERBOSE);

        $keys = array_keys($targets);
        while($keys) {
            $curTargetName = (string) array_shift($keys);
            if (!isset($state[$curTargetName])) {
                $st = null;
            } else {
                $st = (string) $state[$curTargetName];
            }

            if ($st === null) {
                $this->_tsort($curTargetName, $targets, $state, $visiting, $ret);
            } elseif ($st === "VISITING") {
                throw new Exception("Unexpected node in visiting state: $curTargetName");
            }
        }

        $retHuman = "";
        for ($i=0,$_i=count($ret); $i < $_i; $i++) {
            $retHuman .= $ret[$i]->toString()." ";
        }
        $this->log("Complete build sequence is: $retHuman", Project::MSG_VERBOSE);

        return $ret;
    }

    // one step in a recursive DFS traversal of the target dependency tree.
    // - The array "state" contains the state (VISITED or VISITING or null)
    //   of all the target names.
    // - The stack "visiting" contains a stack of target names that are
    //   currently on the DFS stack. (NB: the target names in "visiting" are
    //    exactly the target names in "state" that are in the VISITING state.)
    // 1. Set the current target to the VISITING state, and push it onto
    //    the "visiting" stack.
    // 2. Throw a BuildException if any child of the current node is
    //    in the VISITING state (implies there is a cycle.) It uses the
    //    "visiting" Stack to construct the cycle.
    // 3. If any children have not been VISITED, tsort() the child.
    // 4. Add the current target to the Vector "ret" after the children
    //    have been visited. Move the current target to the VISITED state.
    //    "ret" now contains the sorted sequence of Targets upto the current
    //    Target.

    function _tsort($root, &$targets, &$state, &$visiting, &$ret) {
        $state[$root] = "VISITING";
        $visiting[]  = $root;

        if (!isset($targets[$root]) || !($targets[$root] instanceof Target)) {
            $target = null;
        } else {
            $target = $targets[$root];
        }

        // make sure we exist
        if ($target === null) {
            $sb = "Target '$root' does not exist in this project.";
            array_pop($visiting);
            if (!empty($visiting)) {
                $parent = (string) $visiting[count($visiting)-1];
                $sb .= "It is used from target '$parent'.";
            }
            throw new BuildException($sb);
        }

        $deps = $target->getDependencies();

        while($deps) {
            $cur = (string) array_shift($deps);
            if (!isset($state[$cur])) {
                $m = null;
            } else {
                $m = (string) $state[$cur];
            }
            if ($m === null) {
                // not been visited
                $this->_tsort($cur, $targets, $state, $visiting, $ret);
            } elseif ($m == "VISITING") {
                // currently visiting this node, so have a cycle
                throw $this->_makeCircularException($cur, $visiting);
            }
        }

        $p = (string) array_pop($visiting);
        if ($root !== $p) {
            throw new Exception("Unexpected internal error: expected to pop $root but got $p");
        }

        $state[$root] = "VISITED";
        $ret[] = $target;
    }

    function _makeCircularException($end, $stk) {
        $sb = "Circular dependency: $end";
        do {
            $c = (string) array_pop($stk);
            $sb .= " <- ".$c;
        } while($c != $end);
        return new BuildException($sb);
    }

    /**
     * Adds a reference to an object. This method is called when the parser
     * detects a id="foo" attribute. It passes the id as $name and a reference
     * to the object assigned to this id as $value
     */
    function addReference($name, $object) {
        if (isset($this->references[$name])) {
            $this->log("Overriding previous definition of reference to $name", Project::MSG_WARN);
        }
        $this->log("Adding reference: $name -> ".get_class($object), Project::MSG_DEBUG);
        $this->references[$name] = $object;
    }

    /**
     * Returns the references array.
     * @return array
     */
    function getReferences() {
        return $this->references;
    }
    
    /**
     * Returns a specific reference.
     * @param string $key The reference id/key.
     * @return Reference or null if not defined
     */
    function getReference($key)
    {
        if (isset($this->references[$key])) {
            return $this->references[$key];
        }
        return null; // just to be explicit
    }

    /**
     * Abstracting and simplifyling Logger calls for project messages
     */
    function log($msg, $level = Project::MSG_INFO) {
        $this->logObject($this, $msg, $level);
    }

    function logObject($obj, $msg, $level) {
        $this->fireMessageLogged($obj, $msg, $level);
    }

    function addBuildListener(BuildListener $listener) {
        $this->listeners[] = $listener;
    }

    function removeBuildListener(BuildListener $listener) {
        $newarray = array();
        for ($i=0, $size=count($this->listeners); $i < $size; $i++) {
            if ($this->listeners[$i] !== $listener) {
                $newarray[] = $this->listeners[$i];
            }
        }
        $this->listeners = $newarray;
    }

    function getBuildListeners() {
        return $this->listeners;
    }

    function fireBuildStarted() {
        $event = new BuildEvent($this);        
        foreach($this->listeners as $listener) {
            $listener->buildStarted($event);
        }
    }

    function fireBuildFinished($exception) {        
        $event = new BuildEvent($this);
        $event->setException($exception);
        foreach($this->listeners as $listener) {
            $listener->buildFinished($event);
        }
    }

    function fireTargetStarted($target) {
        $event = new BuildEvent($target);        
           foreach($this->listeners as $listener) {
            $listener->targetStarted($event);
        }
    }

    function fireTargetFinished($target, $exception) {
        $event = new BuildEvent($target);        
        $event->setException($exception);
        foreach($this->listeners as $listener) {
            $listener->targetFinished($event);
        }
    }

    function fireTaskStarted($task) {
        $event = new BuildEvent($task);        
        foreach($this->listeners as $listener) {
            $listener->taskStarted($event);
        }
    }

    function fireTaskFinished($task, $exception) {
        $event = new BuildEvent($task);        
        $event->setException($exception);
        foreach($this->listeners as $listener) {
            $listener->taskFinished($event);
        }
    }

    function fireMessageLoggedEvent($event, $message, $priority) {
        $event->setMessage($message, $priority);
        foreach($this->listeners as $listener) {
            $listener->messageLogged($event);
        }
    }

    function fireMessageLogged($object, $message, $priority) {
        $this->fireMessageLoggedEvent(new BuildEvent($object), $message, $priority);
    }
}
