<?php

require_once 'Horde/DataTree.php';

/**
 * Horde_Relationship API.
 *
 * Copyright 2003-2004, Jeroen Huinink <j.huinink@wanadoo.nl>
 * Copyright 2003-2004, Chuck Hagenbuch <chuck@horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * $Horde: framework/Relationship/Relationship/Manager.php,v 1.2.2.3 2005/07/03 05:48:03 selsky Exp $
 *
 * @author  Jeroen Huinink <j.huinink@wanadoo.nl>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @since   Horde 3.0
 * @package Horde_Relationship
 */
class Relationship_Manager {

    /**
     * A datatree instance for storage.
     *
     * @var DataTree
     */
    var $_datatree;

    /**
     * The subclass of DataTreeObject to instantiate relationship
     * objects as.
     *
     * @var string
     */
    var $_relationshipObject = 'DataTreeObject_Relationship';

    /**
     * Creates or returns the global Relationship_Manager instance.
     *
     * This method must be invoked as: $manager = &Relationship_Manager::singleton()
     *
     * @return Relationship_Manager  The concrete Relationship_Manager
     *                               reference or false on an error.
     */
    function &singleton()
    {
        static $manager;

        if (!isset($manager)) {
            $manager = new Relationship_Manager();
        }

        return $manager;
    }

    /**
     * Constructor.
     *
     * @access private
     */
    function Relationship_Manager()
    {
        global $conf;

        if (empty($conf['datatree']['driver'])) {
            Horde::fatal('You must configure a DataTree backend to use Relationships.');
        }

        $driver = $conf['datatree']['driver'];
        $this->_datatree = &DataTree::singleton($driver,
                                                array_merge(Horde::getDriverConfig('datatree', $driver),
                                                            array('group' => 'horde.relationship')));
    }

    /**
     * Create a relationship between the passed array of objectIds.
     *
     * @param array $objects  An array of objectIds to create a relationship between.
     *
     * @return DataTreeObject_Relationship  The new relationship object.
     */
    function &addRelationship($objects)
    {
        if (count($objects) < 2) {
            return PEAR::raiseError('Relationships are between 2 or more objects.');
        }

        $relationship = &$this->newRelationship();
        if (is_a($relationship, 'PEAR_Error')) {
            return $relationship;
        }

        $relationship->addObjects($objects);
        $result = $relationship->save();
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        return $relationship;
    }

    /**
     * Returns a new relationship object.
     *
     * @return DataTreeObject_Relationship  A new relationship object.
     */
    function &newRelationship()
    {
        $relationship = &new $this->_relationshipObject(md5(microtime()));
        $relationship->setDataTree($this->_datatree);
        $relationship->set('owner', Auth::getAuth());

        return $relationship;
    }

    /**
     * This function returns all relationships that include the passed
     * $objectId.
     *
     * @param string $objectId  The GUID of the object we're looking for
     *                          relationships with.
     * @param mixed $class      What class should the relationship be
     *                          instantiated as? The default (or false) is the
     *                          value of $_relationshipObject.
     *                          A value of NULL means to determine the class
     *                          from the DataTree attributes.
     *
     * @return array  The list of relationships (DataTreeObject_Relationship).
     */
    function &listRelationships($objectId, $class = false)
    {
        /* Get relationships from the DataTree backend. */
        $relationshipIds = $this->_datatree->getByAttributes($this->_buildCriteria($objectId),
                                                             DATATREE_ROOT, true, false);

        return $this->getRelationships(array_keys($relationshipIds), $class);
    }

    /**
     * Returns an array of DataTreeObject_Relationship objects
     * corresponding to the given set of unique IDs, with the details
     * retrieved appropriately.
     *
     * @param array $ids    The array of ids to retrieve.
     * @param mixed $class  What class should the relationship be instantiated
     *                      as? The default (or false) is the value of
     *                      $_relationshipObject.
     *                      A value of NULL means to determine the class from
     *                      the DataTree attributes.
     *
     * @return array  The requested relationships.
     */
    function &getRelationships($ids, $class = false)
    {
        if ($class === false) {
            $class = $this->_relationshipObject;
        }
        return $this->_datatree->getObjects($ids, $class);
    }

    /**
     * Returns a DataTreeObject_Relationship object corresponding to
     * the given unique ID, with the details retrieved appropriately.
     *
     * @param string $id    The DataTree name of the relationship to fetch.
     * @param mixed $class  What class should the relationship be instantiated
     *                      as? The default (or false) is the value of
     *                      $_relationshipObject.
     *                      A value of NULL means to determine the class from
     *                      the DataTree attributes.
     *
     * @return DataTreeObject_Relationship  The requested relationship.
     */
    function &getRelationship($id, $class = false)
    {
        if ($class === false) {
            $class = $this->_relationshipObject;
        }
        return $this->_datatree->getObject($id, $class);
    }

    /**
     * Build a DataTree criteria query structure.
     *
     * @access private
     *
     * @param string|array $objectIds  The objectIds we're searching for.
     */
    function _buildCriteria($objectIds)
    {
        if (is_array($objectIds)) {
            $criteria = array();
            foreach ($objectIds as $objectId) {
                $c = array(array('field' => 'name', 'op' => '=', 'test' => 'objects'),
                           array('field' => 'value', 'op' => '=', 'test' => $objectId));
                $criteria[] = array('AND' => $c);
            }
            $criteria = array('OR' => $criteria);
        } else {
            $criteria = array('AND' => array(array('field' => 'name', 'op' => '=', 'test' => 'objects'),
                                             array('field' => 'value', 'op' => '=', 'test' => $objectIds)));
        }

        return $criteria;
    }

}

/**
 * Extension of the DataTreeObject class for storing Relationship
 * information in the DataTree driver. If you want to store
 * specialized Relationship information, you should extend this class
 * instead of extending DataTreeObject directly.
 *
 * @package Horde_Relationship
 */
class DataTreeObject_Relationship extends DataTreeObject {

    /**
     * Add an object to this relationship. Relationships are between
     * 2+ objects, no top limit (theoretically).
     *
     * @param string|array $objectIds  The new object or objects to add to
     *                                 this relationship.
     * @param boolean $update          Save the relationship automatically?
     */
    function addObjects($objectIds, $update = true)
    {
        if (empty($this->data['objects'])) {
            $this->data['objects'] = array();
        }

        if (!is_array($objectIds)) {
            $objectIds = array($objectIds);
        }
        foreach ($objectIds as $objectId) {
            $this->data['objects'][] = $objectId;
        }

        if ($update) {
            if (count($this->data['objects']) < 2) {
                return PEAR::raiseError('Relationships are between 2 or more objecst. The relationship has not been saved.');
            }
            return $this->save();
        }

        return true;
    }

    /**
     * Remove an object from this relationship. If removing the object
     * would mean that this relationship only contains 0 or 1 objects,
     * the relationship will be deleted.
     *
     * Deletions always take effect immediately because of the
     * potential to delete the relationship itself.
     *
     * @param string $objectId  The object to remove from the relationship.
     */
    function deleteObject($objectId)
    {
        if (empty($this->data['objects']) || count($this->data['objects']) < 3) {
            return $this->delete();
        }

        $key = array_search($objectId, $this->data['objects']);
        if ($key === false) {
            return PEAR::raiseError($key . ' not present in this relationship');
        }

        unset($this->data['objects'][$key]);
        if ($update) {
            return $this->save();
        }

        return true;
    }

    /**
     * Maps this object's attributes from the data array into a format
     * that we can store in the attributes storage backend.
     *
     * @access protected
     *
     * @return array  The attributes array.
     */
    function _toAttributes()
    {
        // Default to no attributes.
        $attributes = array();

        foreach ($this->data as $key => $value) {
            if (is_array($value)) {
                foreach ($value as $k => $v) {
                    $attributes[] = array('name' => $key,
                                          'key' => $k,
                                          'value' => $v);
                }
            } else {
                $attributes[] = array('name' => $key,
                                      'key' => '',
                                      'value' => $value);
            }
        }

        return $attributes;
    }

    /**
     * Takes in a list of attributes from the backend and maps it to
     * our internal data array.
     *
     * @access protected
     *
     * @param array $attributes   The list of attributes from the backend
     *                            (attribute name, key, and value).
     */
    function _fromAttributes($attributes, $permsonly = false)
    {
        foreach ($attributes as $a) {
            if (!empty($a['key'])) {
                $this->data[$a['name']][$a['key']] = $a['value'];
            } else {
                $this->data[$a['name']] = $a['value'];
            }
        }
    }

}
