<?php
/* SVN FILE: $Id: tree.php 112 2008-06-19 11:57:33Z AD7six $ */
/**
 * Tree Helper.
 *
 * Used the generate nested representations of hierarchial data
 *
 * PHP versions 4 and 5
 *
 * Copyright (c) 2008, Andy Dawson
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 * @copyright            Copyright (c) 2008, Andy Dawson
 * @link                 www.ad7six.com
 * @package              cake-base
 * @subpackage           cake-base.app.views.helpers
 * @since                v 1.0
 * @version              $Revision: 112 $
 * @modifiedBy           $LastChangedBy: AD7six $
 * @lastModified         $Date: 2008-06-19 13:57:33 +0200 (Thu, 19 Jun 2008) $
 * @license              http://www.opensource.org/licenses/mit-license.php The MIT License
 */
/**
 * Tree helper
 *
 * Helper to generate tree representations of MPTT or recursively nested data
 */
class TreeHelper extends AppHelper
{
    /**
     * name property
     *
     * @var string 'Tree'
     * @access public
     */
    var $name = 'Tree';
    /**
     * settings property
     *
     * @var array
     * @access private
     */
    var $__settings = array();
    /**
     * typeAttributes property
     *
     * @var array
     * @access private
     */
    var $__typeAttributes = array();
    /**
     * typeAttributesNext property
     *
     * @var array
     * @access private
     */
    var $__typeAttributesNext = array();
    /**
     * itemAttributes property
     *
     * @var array
     * @access private
     */
    var $__itemAttributes = array();
    /**
     * helpers variable
     *
     * @var array
     * @access public
     */
    var $helpers = array(
        'Html'
    );
    /**
     * Tree generation method.
     *
     * Accepts the results of
     * 	find('all', array('fields' => array('lft', 'rght', 'whatever'), 'order' => 'lft ASC'));
     * 	children(); // if you have the tree behavior of course!
     * or 	findAllThreaded(); and generates a tree structure of the data.
     *
     * Settings (2nd parameter):
     *	'model' => name of the model (key) to look for in the data array. defaults to the first model for the current
     * controller. If set to false 2d arrays will be allowed/expected.
     *	'alias' => the array key to output for a simple ul (not used if element or callback is specified)
     *	'type' => type of output defaults to ul
     *	'itemType => type of item output default to li
     *	'id' => id for top level 'type'
     *	'class' => class for top level 'type'
     *	'element' => path to an element to render to get node contents.
     *	'callback' => callback to use to get node contents. e.g. array(&$anObject, 'methodName') or 'floatingMethod'
     *	'autoPath' =>  array($left, $right [$classToAdd = 'active']) if set any item in the path will have the class $classToAdd added. MPTT only.
     *	'left' => name of the 'lft' field if not lft. only applies to MPTT data
     *	'right' => name of the 'rght' field if not lft. only applies to MPTT data
     *	'depth' => used internally when running recursively, can be used to override the depth in either mode.
     *	'firstChild' => used internally when running recursively.
     *	'splitDepth' => if multiple "parallel" types are required, instead of one big type, nominate the depth to do so here
     *		example: useful if you have 30 items to display, and you'd prefer they appeared in the source as 3 lists of 10 to be able to
     *		style/float them.
     *	'splitCount' => the number of "parallel" types. defaults to 3
     *
     * @param array $data data to loop on
     * @param array $settings
     * @return string html representation of the passed data
     * @access public
     */
    function generate($data, $settings = array()) 
    {
        $this->__settings = array_merge(array(
            'model' => null,
            'alias' => 'name',
            'type' => 'ul',
            'itemType' => 'li',
            'id' => false,
            'class' => false,
            'element' => false,
            'callback' => false,
            'autoPath' => false,
            'left' => 'lft',
            'right' => 'rght',
            'depth' => 0,
            'firstChild' => true,
            'splitDepth' => false,
            'splitCount' => 3,
        ) , (array)$settings);
        if ($this->__settings['autoPath'] && !isset($this->__settings['autoPath'][2])) {
            $this->__settings['autoPath'][2] = 'active';
        }
        extract($this->__settings);
        $view = $this->_View;
        if ($model === null) {
            $model = Inflector::classify($view->params['models'][0]);
        }
        $stack = array();
        if ($depth == 0) {
            if ($class) {
                $this->addTypeAttribute('class', $class, null, 'previous');
            }
            if ($id) {
                $this->addTypeAttribute('id', $id, null, 'previous');
            }
        }
        $return = '';
        $__addType = true;
        foreach($data as $i => $result) {
            /* Allow 2d data arrays */
            if (!$model) {
                $result[$model] = $result;
            }
            /* BulletProof */
            if (!isset($result[$model][$left]) && !isset($result['children'])) {
                $result['children'] = array();
            }
            /* Close open items as appropriate */
            while ($stack && ($stack[count($stack) -1] < $result[$model][$right])) {
                array_pop($stack);
                $return.= "\r\n" . str_repeat("\t", count($stack) +1) . '</' . $type . '>';
                $return.= '</' . $itemType . '>';
            }
            /* Some useful vars */
            $hasChildren = $firstChild = $lastChild = $hasVisibleChildren = false;
            $numberOfDirectChildren = $numberOfTotalChildren = 0;
            if (isset($result['children'])) {
                if ($result['children']) {
                    $hasChildren = $hasVisibleChildren = true;
                    $numberOfDirectChildren = count($result['children']);
                }
                $prevRow = prev($data);
                if (!$prevRow) {
                    $firstChild = true;
                }
                next($data);
                $nextRow = next($data);
                if (!$nextRow) {
                    $lastChild = true;
                }
                prev($data);
            } elseif (isset($result[$model][$left])) {
                if ($result[$model][$left] != ($result[$model][$right]-1)) {
                    $hasChildren = true;
                    $numberOfTotalChildren = ($result[$model][$right]-$result[$model][$left]-1) /2;
                    if (isset($data[$i+1]) && $data[$i+1][$model][$right] < $result[$model][$right]) {
                        $hasVisibleChildren = true;
                    }
                }
                if (!isset($data[$i-1]) || ($data[$i-1][$model][$left] == ($result[$model][$left]-1))) {
                    $firstChild = true;
                }
                if (!isset($data[$i+1]) || ($stack && $stack[count($stack) -1] == ($result[$model][$right]+1))) {
                    $lastChild = true;
                }
            }
            $elementData = array(
                'data' => $result,
                'depth' => $depth ? $depth : count($stack) ,
                'hasChildren' => $hasChildren,
                'numberOfDirectChildren' => $numberOfDirectChildren,
                'numberOfTotalChildren' => $numberOfTotalChildren,
                'firstChild' => $firstChild,
                'lastChild' => $lastChild,
                'hasVisibleChildren' => $hasVisibleChildren
            );
            $this->__settings = array_merge($this->__settings, $elementData);
            /* Main Content */
            if ($element) {
                $content = $view->element($element, $elementData);
            } elseif ($callback) {
                list($content) = array_map($callback, array(
                    $elementData
                ));
            } else {
                $content = $result[$model][$alias];
            }
            if (!$content) {
                continue;
            }
            /* Prefix */
            if ($__addType) {
                $typeAttributes = $this->__attributes($type, array(
                    'data' => $elementData
                ));
                $return.= "\r\n" . str_repeat("\t", count($stack)) . '<' . $type . $typeAttributes . '>';
            }
            $itemAttributes = $this->__attributes($itemType, $elementData);
            $return.= "\r\n" . str_repeat("\t", count($stack) +1) . '<' . $itemType . $itemAttributes . '>';
            $return.= $content;
            /* Suffix */
            $__addType = false;
            if ($hasChildren) {
                if ($numberOfDirectChildren) {
                    $settings['depth'] = $depth+1;
                    $return.= $this->__suffix();
                    $return.= $this->generate($result['children'], $settings);
                    $return.= '</' . $itemType . '>';
                } elseif ($numberOfTotalChildren) {
                    $__addType = true;
                    $stack[] = $result[$model][$right];
                }
            } else {
                $return.= '</' . $itemType . '>';
                $return.= $this->__suffix();
            }
        }
        /* Cleanup */
        while ($stack) {
            array_pop($stack);
            $return.= "\r\n" . str_repeat("\t", count($stack) +1) . '</' . $type . '>';
            $return.= '</' . $itemType . '>';
        }
        $return.= "\r\n" . '</' . $type . '>' . "\r\n";
        return $return;
    }
    /**
     * addItemAttribute function
     *
     * Called to modify the attributes of the next <item> to be processed
     * Note that the content of a 'node' is processed before generating its wrapping <item> tag
     *
     * @param string $id
     * @param string $key
     * @param mixed $value
     * @access public
     * @return void
     */
    function addItemAttribute($id = '', $key = '', $value = null) 
    {
        if (!is_null($value)) {
            $this->__itemAttributes[$id][$key] = $value;
        } elseif (!(isset($this->__itemAttributes[$id]) && in_array($key, $this->__itemAttributes[$id]))) {
            $this->__itemAttributes[$id][] = $key;
        }
    }
    /**
     * addTypeAttribute function
     *
     * Called to modify the attributes of the next <type> to be processed
     * Note that the content of a 'node' is processed before generating its wrapping <type> tag (if appropriate)
     * An 'interesting' case is that of a first child with children. To generate the output
     * <ul> (1)
     *      <li>XYZ (3)
     *              <ul> (2)
     *                      <li>ABC...
     *                      ...
     *              </ul>
     *              ...
     * The processing order is indicated by the numbers in brackets.
     * attributes are allways applied to the next type (2) to be generated
     * to set properties of the holding type - pass 'previous' for the 4th param
     * i.e.
     * // Hide children (2)
     * $this->Tree->addTypeAttribute('style', 'display', 'hidden');
     * // give top level type (1) a class
     * $this->Tree->addTypeAttribute('class', 'hasHiddenGrandChildren', null, 'previous');
     *
     * @param string $id
     * @param string $key
     * @param mixed $value
     * @access public
     * @return void
     */
    function addTypeAttribute($id = '', $key = '', $value = null, $previousOrNext = 'next') 
    {
        $var = '__typeAttributes';
        $firstChild = isset($this->__settings['firstChild']) ? $this->__settings['firstChild'] : true;
        if ($previousOrNext == 'next' && $firstChild) {
            $var = '__typeAttributesNext';
        }
        if (!is_null($value)) {
            $this->{$var}[$id][$key] = $value;
        } elseif (!(isset($this->{$var}[$id]) && in_array($key, $this->{$var}[$id]))) {
            $this->{$var}[$id][] = $key;
        }
    }
    /**
     * suffix method
     *
     * Used to close and reopen a ul/ol to allow easier listings
     *
     * @access private
     * @return void
     */
    function __suffix() 
    {
        static $__splitCount = 0;
        static $__splitCounter = 0;
        extract($this->__settings);
        if ($splitDepth) {
            if ($depth == $splitDepth-1) {
                $total = $numberOfDirectChildren ? $numberOfDirectChildren : $numberOfTotalChildren;
                if ($total) {
                    $__splitCounter = 0;
                    $__splitCount = $total/$splitCount;
                    $rounded = (int)$__splitCount;
                    if ($rounded < $__splitCount) {
                        $__splitCount = $rounded+1;
                    }
                }
            }
            if ($depth == $splitDepth) {
                $__splitCounter++;
                if (($__splitCounter%$__splitCount) == 0) {
                    return '</' . $type . '><' . $type . '>';
                }
            }
        }
        return;
    }
    /**
     * attributes function
     *
     * Logic to apply styles to tags.
     *
     * @param mixed $rType
     * @param array $elementData
     * @access private
     * @return void
     */
    function __attributes($rType, $elementData = array() , $clear = true) 
    {
        extract($this->__settings);
        if ($rType == $type) {
            $attributes = $this->__typeAttributes;
            if ($clear) {
                $this->__typeAttributes = $this->__typeAttributesNext;
                $this->__typeAttributesNext = array();
            }
        } else {
            $attributes = $this->__itemAttributes;
            $this->__itemAttributes = array();
            if ($clear) {
                $this->__itemAttributes = array();
            }
        }
        if ($autoPath && $depth) {
            if ($this->__settings['data'][$model][$left] < $autoPath[0] && $this->__settings['data'][$model][$right] > $autoPath[1]) {
                $attributes['class'][] = $autoPath[2];
            } elseif (isset($autoPath[3]) && $this->__settings['data'][$model][$left] == $autoPath[0]) {
                $attributes['class'][] = $autoPath[3];
            }
        }
        if ($attributes) {
            foreach($attributes as $type => $values) {
                foreach($values as $key => $val) {
                    if (is_array($val)) {
                        $attributes[$type][$key] = '';
                        foreach($val as $vKey => $v) {
                            $attributes[$type][$key][$vKey].= $vKey . ':' . $v;
                        }
                        $attributes[$type][$key] = implode(';', $attributes[$type][$key]);
                    }
                    if (is_string($key)) {
                        $attributes[$type][$key] = $key . ':' . $val . ';';
                    }
                }
                $attributes[$type] = $type . '="' . implode(' ', $attributes[$type]) . '"';
            }
            return ' ' . implode(' ', $attributes);
        }
        return '';
    }
}
?>