<?php

// Available import/export formats.
/** @constant CATEGORY_FORMAT_TREE List every category in an array,
    similar to PEAR/html/menu.php */
define('CATEGORY_FORMAT_TREE', 1);

/** @constant CATEGORY_FORMAT_FETCH List every category in an array
    child-parent. Comes from driver pear/sql */
define('CATEGORY_FORMAT_FETCH', 2);

/** @constant CATEGORY_FORMAT_FLAT Get a full list - an array of keys */
define('CATEGORY_FORMAT_FLAT', 3);

/** @constant CATEGORY_FORMAT_3D Use a specific format, handled by
    the CategoryTree:: class.

    $data[0][0]['name'] = 'Root';    $data[0][0]['p'] = '0.0';
    $data[1][1]['name'] = 'dir1';    $data[1][1]['p'] = '0.0';
    $data[2][2]['name'] = 'subdir1'; $data[2][2]['p'] = '1.1';
    $data[3][3]['name'] = 'data1';   $data[3][3]['p'] = '2.2';
    $data[3][4]['name'] = 'data2';   $data[3][4]['p'] = '2.2';
    $data[3][5]['name'] = 'data3';   $data[3][5]['p'] = '2.2';
    $data[2][6]['name'] = 'subdir2'; $data[2][6]['p'] = '1.1';
    $data[1][7]['name'] = 'dir2';    $data[1][7]['p'] = '0.0';
    $data[2][8]['name'] = 'subdir3'; $data[2][8]['p'] = '1.7';
    $data[2][9]['name'] = 'subdir4'; $data[2][9]['p'] = '1.7';
*/
define('CATEGORY_FORMAT_3D', 4);

/**
 * The Category:: class provides a common abstracted interface into
 * the various backends for the Horde Categories system.
 *
 * A category is just a title that is saved in the page for the null
 * driver or can be saved in a database to be accessed from
 * everywhere. Every category must have a different name (for a same
 * groupid). A category may have different parent categories.
 *
 * Required values for $params:
 * groupid: define each group of categories we want to build.
 *
 * $Horde: horde/lib/Category.php,v 1.88 2003/08/01 14:30:05 chuck Exp $
 *
 * Copyright 1999-2003 Stephane Huther <shuther@bigfoot.com>
 * Copyright 2001-2003 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.
 *
 * @author  Stephane Huther <shuther@bigfoot.com>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.88 $
 * @since   Horde 2.1
 * @package horde.category
 */
class Category {

    /**
     * Array of all categories: indexed by id. The format is:
     * array(id => 'name' => name, 'parent' => parent).
     * @var array $_categories
     */
    var $_categories = array();

    /**
     * A hash that can be used to map a full category name
     * (parent:child:category) to that category's unique ID.
     * @var array $_nameMap
     */
    var $_nameMap = array();

    /**
     * Hash containing connection parameters.
     * @var array $_params
     */
    var $_params = array();

    /**
     * Constructor
     *
     * @param array $params  A hash containing any additional configuration or
     *                       connection parameters a subclass might need.
     *                       We always need 'groupid', a string that defines the prefix
     *                       for each set of hierarchical data.
     */
    function Category($params = null)
    {
        $this->_params = $params;
    }

    /**
     * Does the current categories backend have persistent storage?
     *
     * @return boolean  True if there is persistent storage, false if not.
     */
    function isPersistent()
    {
        return false;
    }

    /**
     * Remove a category
     *
     * @param string $category   The category to remove.
     */
    function removeCategory($category)
    {
        if (is_a($category, 'CategoryObject')) {
            $category = $category->getName();
        }

        if (!$this->exists($category)) {
            return PEAR::raiseError('Does not exist');
        }

        $id = $this->getCategoryId($category);
        $pid = $this->getParent($category);
        $order = $this->_categories[$id]['order'];
        unset($this->_categories[$id]);
        unset($this->_nameMap[$id]);

        // Shift down the order positions
        $this->_reorderCategory($pid, $order);

        return $id;
    }

    /**
     * Move a category to a new parent.
     *
     * @param mixed  $category   The category to move.
     * @param string $newparent  The new parent category. Defaults to the root.
     */
    function moveCategory($category, $newparent = null)
    {
        $cid = $this->getCategoryId($category);
        if (is_a($cid, 'PEAR_Error')) {
            return PEAR::raiseError(sprintf('Category to move does not exist: %s', $cid->getMessage()));
        }

        if (!is_null($newparent)) {
            $pid = $this->getCategoryId($newparent);
            if (is_a($pid, 'PEAR_Error')) {
                return PEAR::raiseError(sprintf('New parent category does not exist: %s', $pid->getMessage()));
            }
        } else {
            $pid = '-1';
        }

        $this->_categories[$cid]['parent'] = $pid;

        return true;
    }

    /**
     * Change a category's name.
     *
     * @param mixed $old_category        The old category.
     * @param string $new_category_name  The new category_name.
     */
    function renameCategory($old_category, $new_category_name)
    {
        /* Check whether the category exists at all */
        if (!$this->exists($old_category)) {
            return PEAR::raiseError('Does not exist');
        }

        /* Check for duplicates - get parent and create new category name */
        $parent = $this->getCategoryName($this->getParent($old_category));
        if ($this->exists($parent . ':' . $new_category_name)) {
            return PEAR::raiseError('Duplicate name');
        }

        /* Replace the old category name with the new one in the cache */
        $old_category_id = $this->getCategoryId($old_category);
        $this->_categories[$old_category_id]['name'] = $new_category_name;

        return true;
    }

    /**
     * Change order of subcategories within a category.
     *
     * @param string $parents The parent category id string path.
     * @param mixed  $order   Specific new order position or an array containing
     *                        the new positions for the given $parents category.
     * @param int    $cid     If provided indicates insertion of a new child to
     *                        the category to avoid incrementing it when
     *                        shifting up all other children's order. If not
     *                        provided indicates deletion, so shift all other
     *                        positions down one.
     */
    function _reorderCategory($pid, $order = null, $cid = null)
    {
        if (!is_array($order) && !is_null($order)) { // Single update (add/del)
            if (is_null($cid)) { // No category id given so shuffle down
                foreach ($this->_categories as $c_key => $c_val) {
                    if ($this->_categories[$c_key]['parent'] == $pid
                        && $this->_categories[$c_key]['order'] > $order) {
                        $this->_categories[$c_key]['order']--;
                    }
                }
            } else { // We have a category id so shuffle up
                foreach ($this->_categories as $c_key => $c_val) {
                    if ($c_key != $cid  && $this->_categories[$c_key]['parent'] == $pid
                    && $this->_categories[$c_key]['order'] >= $order) {
                        $this->_categories[$c_key]['order']++;
                    }
                }
            }
        } elseif (is_array($order) && !empty($order)) { // Multi update
            foreach ($order as $order_position => $cid) {
                $this->_categories[$cid]['order'] = $order_position;
            }
        }
    }

    /**
     * Return a CategoryObject (or subclass) object of the data in the
     * category defined by $category.
     *
     * @param string $category        The category to fetch:
     *                                'parent:sub-parent:category-name'.
     * @param optional string $class  Subclass of CategoryObject to use.
     *                                Defaults to CategoryObject.
     */
    function &getCategory($category, $class = 'CategoryObject')
    {
        if (!class_exists($class)) {
            return PEAR::raiseError($class . ' not found');
        }

        if (empty($category)) {
            return PEAR::raiseError($category . ' not found');
        }
        $this->_load($category);
        if (!$this->exists($category)) {
            return PEAR::raiseError($category . ' not found');
        }

        $categoryOb = &new $class($category);
        /* If the class has a _fromAttributes method, load data from the
           attributes backend. */
        if (method_exists($categoryOb, '_fromAttributes')) {
            $attributes = $this->getCategoryAttributes($this->getCategoryId($category));
            if (is_a($attributes, 'PEAR_Error')) {
                return $attributes;
            }
            $categoryOb->_fromAttributes($attributes);
        } else {
            /* Otherwise load it from the old data storage field. */
            $categoryOb->data = $this->getCategoryData($this->getCategoryId($category));
        }
        $categoryOb->order = $this->getCategoryOrder($category);
        return $categoryOb;
    }

    /**
     * Return a CategoryObject (or subclass) object of the data in the
     * category with the category ID $id.
     *
     * @param integer $id             A category id.
     * @param optional string $class  The subclass of CategoryObject to use.
     *                                Defaults to CategoryObject.
     */
    function &getCategoryById($id, $class = 'CategoryObject')
    {
        return $this->getCategory($this->getCategoryName($id), $class);
    }

    /**
     * Return an array of CategoryObject (or subclass) objects
     * corresponding to the categories in $ids, with the category
     * names as the keys of the array.
     *
     * @param array $ids              An array of category ids.
     * @param optional string $class  The subclass of CategoryObject to use.
     *                                Defaults to CategoryObject.
     */
    function &getCategories($ids, $class = 'CategoryObject')
    {
        if (!class_exists($class)) {
            return PEAR::raiseError($class . ' not found');
        }

        $result = $this->_loadById($ids);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        // Fetch data in here somewhere;
        $data = $this->getCategoryAttributes($ids);

        $categories = array();
        foreach ($ids as $id) {
            $name = $this->getCategoryName($id);
            if (!empty($name) && !empty($data[$id])) {
                $categories[$name] = &new $class($name);
                $categories[$name]->_fromAttributes($data[$id]);
                $categories[$name]->order = $this->getCategoryOrder($name);
            }
        }

        return $categories;
    }

    /**
     * Export a list of categories.
     *
     * @param constant $format             Format of the export
     * @param optional string  $startleaf  The name of the leaf from which we
     *                                     start the export tree.
     * @param optional boolean $reload     Re-load the requested chunk? Defaults
     *                                     to false (only what is currently
     *                                     loaded).
     * @param optional string  $rootname   The label to use for the root element
     *                                     (defaults to '-1').
     *
     * @return mixed  The tree representation of the categories, or a PEAR_Error
     *                on failure.
     */
    function get($format, $startleaf = '-1', $reload = false, $rootname = '-1')
    {
        $this->_load($startleaf, $reload);
        $out = array();

        switch ($format) {
        case CATEGORY_FORMAT_TREE:
            $this->_extractAllLevelTree($out, $this->getCategoryId($startleaf));
            break;

        case CATEGORY_FORMAT_FLAT:
            $this->_extractAllLevelList($out, $this->getCategoryId($startleaf));
            if (!empty($out['-1'])) {
                $out['-1'] = $rootname;
            }
            break;

        case CATEGORY_FORMAT_3D:
            $out2 = $this->get(CATEGORY_FORMAT_TREE, $startleaf, false);
            $id = 0;
            $this->_map3d($out, $out2, 0, $id, 0, $rootname);
            break;

        default:
            return PEAR::raiseError('Not supported');
        }

        return $out;
    }

    /**
     * Export a list of categories just like get() above, but uses a category
     * id to fetch the list of categories.
     *
     * @param constant $format             Format of the export.
     * @param optional string  $startleaf  The category id the leaf from which
     *                                     we start the export tree.
     * @param optional boolean $reload     Reload the requested chunk? Defaults
     *                                     to false (only what is currently
     *                                     loaded).
     * @param optional string  $rootname   The label to use for the root element
     *                                     (defaults to '-1').
     *
     * @return mixed  The tree representation of the categories, or a PEAR_Error
     *                on failure.
     */
    function getById($format, $startleaf = '-1', $reload = false, $rootname = '-1')
    {
        $this->_load($this->getCategoryName($startleaf), $reload);
        $out = array();

        switch ($format) {
        case CATEGORY_FORMAT_TREE:
            $this->_extractAllLevelTree($out, $startleaf);
            break;

        case CATEGORY_FORMAT_FLAT:
            $this->_extractAllLevelList($out, $startleaf);
            if (!empty($out['-1'])) {
                $out['-1'] = $rootname;
            }
            break;

        case CATEGORY_FORMAT_3D:
            $out2 = $this->get(CATEGORY_FORMAT_TREE, $this->getCategoryName($startleaf), false);
            $id = 0;
            $this->_map3d($out, $out2, 0, $id, 0, $rootname);
            break;

        default:
            return PEAR::raiseError('Not supported');
        }

        return $out;
    }

    /**
     * Used by the get function to handle CATEGORY_FORMAT_3D.
     *
     * @param array   $out   Array that will contain the result.
     * @param array   $arr   Array from get(CATEGORY_FORMAT_TREE).
     * @param integer $depth Depth of the child.
     * @param integer $id    Kind of auto increment value.
     * @param integer $pId   $id of the parent, the depth will be $depth - 1.
     *
     * @access private
     * @see get()
     */
    function _map3d(&$out, $arr, $depth, &$id, $pId, $rootname = 'root')
    {
        foreach ($arr as $cid => $val) {
            if (0 == $depth) {
                $pDepth = 0;
            } else {
                $pDepth = $depth - 1;
            }

            if ('-1' == $cid) {
                $name = $rootname;
            } else {
                $name = $this->getCategoryName($cid);
                $name = strstr($name, ':') ? substr($name, strrpos($name, ':') + 1) : $name;
            }

            $out[$depth][$id]['p'] = $pDepth . '.' . $pId;
            $out[$depth][$id]['id'] = $cid;
            $out[$depth][$id]['name'] = $name;

            if (!isset($out['x']) || $depth > $out['x']) {
                $out['x'] = $depth;
            }
            if (!isset($out['y']) || $id > $out['y']) {
                $out['y'] = $id;
            }

            $id = $id + 1;
            if (is_array($val)) {
                $this->_map3d($out, $val, $depth + 1, $id, $id - 1);
            }
        }
    }

    /**
     * Import a list of categories. Used by drivers to populate the internal
     * $_categories array.
     *
     * @access private
     *
     * @param integer $format   Format of the import (CATEGORY_FORMAT_*).
     * @param array   $data     The data to import.
     * @param string  $charset  The charset to convert the category name from.
     */
    function set($format, $data, $charset = null)
    {
        switch ($format) {
        case CATEGORY_FORMAT_FETCH:
            $cats = array();
            $cids = array();
            foreach ($data as $cat) {
                if (!is_null($charset)) {
                    $cat[1] = String::convertCharset($cat[1], $charset);
                }
                $cids[$cat[0]] = $cat[1];
                $cparents[$cat[0]] = $cat[2];
                $corders[$cat[0]] = $cat[3];
            }
            foreach ($cids as $id => $name) {
                $this->_categories[$id]['name'] = $name;
                $this->_categories[$id]['order'] = $corders[$id];
                if (!empty($cparents[$id])) {
                    $parents = explode(':', substr($cparents[$id], 1));
                    $par = $parents[count($parents) - 1];
                    $this->_categories[$id]['parent'] = $par;
                    $this->_nameMap[$id] = '';
                    foreach ($parents as $parID) {
                        if (!empty($this->_nameMap[$id])) {
                            $this->_nameMap[$id] .= ':';
                        }
                        $this->_nameMap[$id] .= $cids[$parID];
                    }
                    $this->_nameMap[$id] .= ':' . $name;
                } else {
                    $this->_categories[$id]['parent'] = '-1';
                    $this->_nameMap[$id] = $name;
                }
            }
            break;

        default:
            return PEAR::raiseError('Not supported');
        }

        return true;
    }

    /**
     * Extract one level of categories for a parent leaf and sorted first by
     * their category order, and then name. This function is a way to get a
     * collection of node's children.
     *
     * @param string optional $parent  Name of the parent from which to start.
     *
     * @return array
     */
    function _extractOneLevel($leaf = '-1')
    {
        $out = array();
        foreach ($this->_categories as $id => $categoryVals) {
            if ($categoryVals['parent'] == $leaf) {
                $out[$id] = $categoryVals;
            }
        }

        uasort($out, array($this, '_cmp'));
        return $out;
    }

    /**
     * Extract all levels of categories, starting from a given parent leaf in
     * the categories tree.
     *
     * @param array $out                This is an iterating function, so $out
     *                                  is passed by reference to contain the
     *                                  result.
     * @param string optional  $parent  The name of the parent from which to
     *                                  begin.
     * @param integer optional $level   Number of levels of depth to check.
     *
     * Note: if nothing is returned that means there is no child, but don't
     * forget to add the parent if any subsequent operations are required!
     */
    function _extractAllLevelTree(&$out, $parent = '-1', $level = -1)
    {
        if ($level == 0) {
            return false;
        }

        $out[$parent] = true;

        $k = $this->_extractOneLevel($parent);
        foreach ($k as $category => $v) {
            if (!is_array($out[$parent])) {
                $out[$parent] = array();
            }
            $out[$parent][$category] = true;
            $this->_extractAllLevelTree($out[$parent], $category, $level - 1);
        }
    }

    /**
     * Extract all levels of categories, starting from any parent in the tree.
     *
     * Returned array format: array(parent => array(child => true))
     *
     * @param array $out                This is an iterating function, so $out
     *                                  is passed by reference to contain the
     *                                  result.
     * @param optional string  $parent  The name of the parent from which to
     *                                  begin.
     * @param optional integer $level   Number of levels of depth to check.
     */
    function _extractAllLevelList(&$out, $parent = '-1', $level = -1)
    {
        if ($level == 0) {
            return false;
        }

        // This is redundant most of the time, so make sure we need to
        // do it.
        if (empty($out[$parent])) {
            $out[$parent] = $this->getCategoryName($parent);
        }

        $k = array_keys($this->_extractOneLevel($parent));
        foreach ($k as $category) {
            $out[$category] = $this->getCategoryName($category);
            $this->_extractAllLevelList($out, $category, $level - 1);
        }
    }

    /**
     * Get a $child's direct parent ID.
     *
     * @param string $child  Get the parent of this category.
     *
     * @return integer  The unique ID of the parent.
     */
    function getParent($child)
    {
        if (is_a($child, 'CategoryObject')) {
            $child = $child->getName();
        }
        $id = $this->getCategoryId($child);
        if (is_a($id, 'PEAR_Error')) {
            return $id;
        }

        return $this->_categories[$id]['parent'];
    }

    /**
     * Get a list of parents all the way up to the root category for $child.
     *
     * @param mixed   $child   The name of the child
     * @param boolean $getids  If true, return parent IDs; otherwise, return
     *                         names.
     *
     * @return array  [child] [parent] in a tree format.
     */
    function getParents($child, $getids = false)
    {
        $pid = $this->getParent($child);
        if (is_a($pid, 'PEAR_Error')) {
            return new PEAR_Error('Parents not found');
        }
        $pname = $this->getCategoryName($pid);
        if ($getids) {
            $ret = array($pid => true);
        } else {
            $ret = array($pname => true);
        }

        if ($pid != '-1') {
            if ($getids) {
                $ret[$pid] = $this->getParents($pname, $getids);
            } else {
                $ret[$pname] = $this->getParents($pname, $getids);
            }
        }

        return $ret;
    }

    /**
     * Get a parent-id string (id:cid format) for the specified category.
     *
     * @param mixed $category  The category to return a parent string for.
     */
    function getParentIdString($category)
    {
        $pids = array();
        $ptree = $this->getParents($category, true);
        while ((list($id, $parent) = each($ptree)) && is_array($parent)) {
            array_unshift($pids, ':' . $id);
            $ptree = $parent;
        }

        return implode('', $pids);
    }

    /**
     * Get the number of children a category has, only counting immediate
     * children, not grandchildren, etc.
     *
     * @param optional mixed $parent  Either the category object or the
     *                                category name for which to count the
     *                                children, defaults to the root ('-1').
     *
     * @return integer
     */
    function getNumberOfChildren($parent = '-1')
    {
        if (is_a($parent, 'CategoryObject')) {
            $parent = $parent->getName();
        }
        $out = $this->_extractOneLevel($parent);
        return is_array($out) ? count($out) : 0;
    }

    /**
     * Check if a category exists or not. The category -1 always exists.
     *
     * @param mixed $category  The name of the category.
     *
     * @return boolean  True if the category exists, false otherwise.
     */
    function exists($category)
    {
        if (empty($category)) {
            return false;
        }
        if (is_a($category, 'CategoryObject')) {
            $category = $category->getName();
        } elseif (is_array($category)) {
            $category = implode(':', $category);
        }

        if ($category == '-1') {
            return true;
        }

        $idMap = array_flip($this->_nameMap);

        if (isset($idMap[$category])) {
            return true;
        }

        $this->_load($category);
        $idMap = array_flip($this->_nameMap);
        return isset($idMap[$category]);
    }

    /**
     * Get the name of a category from a category id.
     *
     * @param integer $id  The category id for which to look up the name.
     *
     * @return string
     */
    function getCategoryName($id)
    {
        /* If no id or if id is a PEAR error, return null. */
        if (empty($id) || is_a($id, 'PEAR_Error')) {
            return null;
        }

        /* If checking name of root, return -1. */
        if ($id == '-1') {
            return '-1';
        }

        /* If found in the name map, return the name. */
        if (isset($this->_nameMap[$id])) {
            return $this->_nameMap[$id];
        }

        /* Not found in name map, try loading this id into the name map. */
        $this->_loadById($id);

        /* If id loaded return the name, otherwise return null. */
        return isset($this->_nameMap[$id]) ?
            $this->_nameMap[$id] :
            null;
    }

    /**
     * Get the id of a category from a category name.
     *
     * @param mixed $name  Either the category object, an array containing the
     *                     category path elements or the category name for which
     *                     to look up the id.
     *
     * @return string
     */
    function getCategoryId($name)
    {
        /* Check if $name is not a string. */
        if (is_a($name, 'CategoryObject')) {
            /* Category object, get the string name. */
            $name = $name->getName();
        } elseif (is_array($name)) {
            /* Path array, implode to get the string name. */
            $name = implode(':', $name);
        }

        /* If checking id of root, return -1. */
        if ($name == '-1') {
            return '-1';
        }

        /* Check if the name actually exists, if not return PEAR error. */
        if (!$this->exists($name)) {
            return PEAR::raiseError('Does not exist');
        }

        /* Flip the name map to look up the id using the name as key. */
        $idMap = array_flip($this->_nameMap);
        return $idMap[$name];
    }

    /**
     * Get the order position of a category.
     *
     * @param mixed $child  Either the category object or the category name.
     * 
     * @return mixed  The category's order position or a PEAR error on failure.
     */
    function getCategoryOrder($child)
    {
        if (is_a($child, 'CategoryObject')) {
            $child = $child->getName();
        }
        $id = $this->getCategoryId($child);
        if (is_a($id, 'PEAR_Error')) {
            return $id;
        }

        return isset($this->_categories[$id]['order']) ?
            $this->_categories[$id]['order'] :
            null;
    }

    /**
     * Replace all the ':' in a category name with '.'.
     *
     * @access public
     *
     * @param string $name  The name of the category.
     * 
     * @return string  The encoded category name.
     */
    function encodeName($name)
    {
        return str_replace(':', '.', $name);
    }

    /**
     * Get the short name of a category, returns only the last portion of the
     * full category name. For display purposes only.
     *
     * @access public
     * @static
     *
     * @param string $category_name  The name of the category.
     * 
     * @return string  The category's short name.
     */
    function getShortName($category_name)
    {
        /* If there are several components to the name, explode and get the last
           one, otherwise just return the name. */
        if (strstr($category_name, ':')) {
            return array_pop(explode(':', $category_name));
        } else {
            return $category_name;
        }
    }

    /**
     * Add a category
     *
     * @param string   $name  The short category name.
     * @param integer  $id    The new category's unique ID.
     * @param integer  $pid   The unique ID of the category's parent.
     * @param integer  $order The ordering data for the category.
     *
     * @access protected
     */
    function _addCategory($name, $id, $pid, $order = '')
    {
        $this->_categories[$id] = array('name' => $name,
                                        'parent' => $pid,
                                        'order' => $order);
        $this->_nameMap[$id] = $name;

        /* Shift along the order positions. */
        $this->_reorderCategory($pid, $order, $id);

        return true;
    }

    /**
     * Sort two categories by their category_order field, and if that is the
     * same, alphabetically (case insensitive) by name.
     *
     * You never call this function; it's used in uasort() calls. Do NOT use
     * usort(); you'll lose key => value associations.
     *
     * @param array $a  The first category
     * @param array $b  The second category
     *
     * @return integer  1 if $a should be first,
     *                 -1 if $b should be first,
     *                  0 if they are entirely equal.
     */
    function _cmp($a, $b)
    {
        if ($a['order'] > $b['order']) {
            return 1;
        } elseif ($a['order'] < $b['order']) {
            return -1;
        } else {
            return strcasecmp($a['name'], $b['name']);
        }
    }

    /**
     * Attempts to return a concrete Category instance based on $driver.
     *
     * @param mixed $driver  The type of concrete Category subclass to return.
     *                       This is based on the storage driver ($driver). The
     *                       code is dynamically included. If $driver is an array,
     *                       then we will look in $driver[0]/lib/Category/ for
     *                       the subclass implementation named $driver[1].php.
     * @param array $params  (optional) A hash containing any additional
     *                       configuration or connection parameters a subclass
     *                       might need.
     *                       here, we need 'groupid' = a string that defines
     *                       top-level categories of categories.
     *
     * @return object Category  The newly created concrete Category instance,
     *                          or false on an error.
     */
    function &factory($driver, $params = null)
    {
        $driver = basename($driver);

        if (is_null($params)) {
            $params = Horde::getDriverConfig('category', $driver);
        }

        if (empty($driver) || (strcmp($driver, 'none') == 0)) {
            $driver = 'null';
        }

        if (!empty($app)) {
            require_once $GLOBALS['registry']->getParam('fileroot', $app) . '/lib/Category/' . $driver . '.php';
        } elseif (@file_exists(dirname(__FILE__) . '/Category/' . $driver . '.php')) {
            require_once dirname(__FILE__) . '/Category/' . $driver . '.php';
        } else {
            @include_once 'Horde/Category/' . $driver . '.php';
        }
        $class = 'Category_' . $driver;
        if (class_exists($class)) {
            return new $class($params);
        } else {
            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
        }
    }

    /**
     * Attempts to return a reference to a concrete Category instance based on
     * $driver. It will only create a new instance if no Category instance with
     * the same parameters currently exists.
     *
     * This should be used if multiple category sources (and, thus, multiple
     * Category instances) are required.
     *
     * This method must be invoked as: $var = &Category::singleton()
     *
     * @param mixed $driver           Type of concrete Category subclass to
     *                                return, based on storage driver ($driver).
     *                                The code is dynamically included. If
     *                                $driver is an array, then look in
     *                                $driver[0]/lib/Category/ for subclass
     *                                implementation named $driver[1].php.
     * @param optional array $params  A hash containing any additional
     *                                configuration or connection parameters a
     *                                subclass might need.
     *
     * @return object Category  The concrete Category reference, or false on an
     *                          error.
     */
    function &singleton($driver, $params = null)
    {
        static $instances;
        if (!isset($instances)) {
            $instances = array();
        }

        if (is_null($params)) {
            $params = Horde::getDriverConfig('category', $driver);
        }

        $signature = serialize(array($driver, $params));
        if (!isset($instances[$signature])) {
            $instances[$signature] = &Category::factory($driver, $params);
        }

        return $instances[$signature];
    }

}

/**
 * Class that can be extended to save arbitrary information as part of a
 * category. The Groups system makes use of this; the CategoryObject_Group class
 * is an example of an extension of this class with specialized methods.
 *
 * @author  Stephane Huther <shuther@bigfoot.com>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.88 $
 * @since   Horde 2.1
 * @package horde.category
 */
class CategoryObject {

    /**
     * Key-value hash that will be serialized.
     * @see getData()
     * @var array $data
     */
    var $data = array();

    /**
     * The unique name of this category. These names have the same requirements
     * as other category names - they must be unique, etc.
     * @var string $name.
     */
    var $name;

    /**
     * If this category has ordering data, store it here.
     * @var integer $order
     */
    var $order = null;

    /**
     * CategoryObject constructor. Just sets the $name parameter.
     *
     * @param string $name The category name.
     */
    function CategoryObject($name)
    {
        $this->name = $name;
    }

    /**
     * Get the name of this category.
     *
     * @return string The category name.
     */
    function getName()
    {
        return $this->name;
    }

    /**
     * Get the short name of this category. For display purposes only.
     *
     * @return string The category's short name.
     */
    function getShortName()
    {
        return Category::getShortName($this->name);
    }

    /**
     * Get a pointer/accessor to the data array.
     * 
     * @return array  A reference to the internal data array.
     */
    function &getData()
    {
        return $this->data;
    }

    /**
     * Get one of the attributes of the object, or null if it isn't
     * defined.
     *
     * @param string $attribute  The attribute to get.
     *
     * @return mixed  The value of the attribute, or null.
     */
    function get($attribute)
    {
        return isset($this->data[$attribute])
            ? $this->data[$attribute]
            : null;
    }

    /**
     * Set one of the attributes of the object.
     *
     * @param string $attribute  The attribute to set.
     * @param mixed  $value      The value for $attribute.
     */
    function set($attribute, $value)
    {
        $this->data[$attribute] = $value;
    }

}
