<?php

/**
 * Shortcut to ref, HTML mode
 *
 * @param   mixed $args
 * @return  void|string
 */
function r(){

  // arguments passed to this function
  $args = func_get_args();

  // options (operators) gathered by the expression parser;
  // this variable gets passed as reference to getInputExpressions(), which will store the operators in it
  $options = array();

  // names of the arguments that were passed to this function
  $expressions = ref::getInputExpressions($options);
  $capture = in_array('@', $options, true);

  // something went wrong while trying to parse the source expressions?
  // if so, silently ignore this part and leave out the expression info
  if(func_num_args() !== count($expressions))
    $expressions = null;

  // use HTML formatter only if we're not in CLI mode, or if return was requested
  $format = (php_sapi_name() !== 'cli') || $capture ? 'html' : 'cliText';

  // IE goes funky if there's no doctype
  if(!$capture && ($format === 'html') && !headers_sent() && (!ob_get_level() || ini_get('output_buffering')))
    print '<!DOCTYPE HTML><html><head><title>REF</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>';

  $ref = new ref($format);

  if($capture)
    ob_start();

  foreach($args as $index => $arg)
    $ref->query($arg, $expressions ? $expressions[$index] : null);

  // return the results if this function was called with the error suppression operator 
  if($capture)
    return ob_get_clean();
  
  // stop the script if this function was called with the bitwise not operator
  if(in_array('~', $options, true) && ($format === 'html')){
    print '</body></html>';
    exit(0);
  }  
}



/**
 * Shortcut to ref, plain text mode
 *
 * @param   mixed $args
 * @return  void|string
 */
function rt(){
  $args        = func_get_args();
  $options     = array();  
  $output      = '';
  $expressions = ref::getInputExpressions($options);
  $capture     = in_array('@', $options, true);  
  $ref         = new ref((php_sapi_name() !== 'cli') || $capture ? 'text' : 'cliText');  

  if(func_num_args() !== count($expressions))
    $expressions = null;

  if(!headers_sent())    
    header('Content-Type: text/plain; charset=utf-8');  

  if($capture)
    ob_start();  

  foreach($args as $index => $arg)
    $ref->query($arg, $expressions ? $expressions[$index] : null);

  if($capture)
    return ob_get_clean(); 

  if(in_array('~', $options, true))
    exit(0);  
}



/**
 * REF is a nicer alternative to PHP's print_r() / var_dump().
 *
 * @version  1.0
 * @author   digitalnature - http://digitalnature.eu
 */
class ref{

  const

    MARKER_KEY = '_phpRefArrayMarker_';
    


  protected static
  
    /**
     * CPU time used for processing
     *
     * @var  array
     */   
    $time = 0,

    /**
     * Configuration (+ default values)
     *
     * @var  array
     */     
    $config = array(

                // initially expanded levels (for HTML mode only)
                'expLvl'               => 1,

                // depth limit (0 = no limit);
                // this is not related to recursion
                'maxDepth'             => 6,

                // show the place where r() has been called from
                'showBacktrace'        => true,

                // display iterator contents
                'showIteratorContents' => false,

                // display extra information about resources
                'showResourceInfo'     => true,
                
                // display method and parameter list on objects
                'showMethods'          => true,

                // display private properties / methods
                'showPrivateMembers'   => false,

                // peform string matches (date, file, functions, classes, json, serialized data, regex etc.)
                // note: seriously slows down queries on large amounts of data
                'showStringMatches'    => true,

                // shortcut functions used to access the query method below;
                // if they are namespaced, the namespace must be present as well (methods are not supported)
                'shortcutFunc'         => array('r', 'rt'),

                // custom/external formatters (as associative array: format => className)
                'formatters'           => array(),                

                // stylesheet path (for HTML only);
                // 'false' means no styles
                'stylePath'            => '{:dir}/ref.css',

                // javascript path (for HTML only);
                // 'false' means no js                      
                'scriptPath'           => '{:dir}/ref.js',

                // display url info via cURL
                'showUrls'             => false,

                // stop evaluation after this amount of time (seconds)
                'timeout'              => 10,

                // whether to produce W3c-valid HTML,
                // or unintelligible, but optimized markup that takes less space
                'validHtml'            => false,
              ),

    /**
     * Some environment variables
     * used to determine feature support
     *
     * @var  array
     */ 
    $env = array(),

    /**
     * Timeout point
     *
     * @var  bool
     */ 
    $timeout = -1,

    $debug = array(
      'cacheHits' => 0,
      'objects'   => 0,
      'arrays'    => 0,
      'scalars'   => 0,
    );    


  protected

    /**
     * Output formatter of this instance
     *
     * @var  RFormatter
     */     
    $fmt = null,

    /**
     * Start time of the current instance
     *
     * @var  float
     */ 
    $startTime = 0,

    /**
     * Internally created objects
     *
     * @var  SplObjectStorage
     */ 
    $intObjects = null;



  /**
   * Constructor
   *
   * @param   string|RFormatter $format      Output format ID, or formatter instance defaults to 'html'
   */
  public function __construct($format = 'html'){

    static $didIni = false;

    if(!$didIni){
      $didIni = true;
      foreach(array_keys(static::$config) as $key){
        $iniVal = get_cfg_var('ref.' . $key);
        if($iniVal !== false)
          static::$config[$key] = $iniVal;
      }

    }

    if($format instanceof RFormatter){
      $this->fmt = $format;

    }else{
      $format = isset(static::$config['formatters'][$format]) ? static::$config['formatters'][$format] : 'R' . ucfirst($format) . 'Formatter';

      if(!class_exists($format, false))
        throw new \Exception(sprintf('%s class not found', $format));

      $this->fmt = new $format();
    }

    if(static::$env)
      return;

    static::$env = array(

      // php 5.4+ ?
      'is54'         => version_compare(PHP_VERSION, '5.4') >= 0,

      // php 5.4.6+ ?
      'is546'        => version_compare(PHP_VERSION, '5.4.6') >= 0,      

      // php 5.6+
      'is56'         => version_compare(PHP_VERSION, '5.6') >= 0,      

      // php 7.0+ ?
      'is7'          => version_compare(PHP_VERSION, '7.0') >= 0,      

      // curl extension running?
      'curlActive'   => function_exists('curl_version'),      

      // is the 'mbstring' extension active?
      'mbStr'        => function_exists('mb_detect_encoding'),

      // @see: https://bugs.php.net/bug.php?id=52469     
      'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0),
    );
  }



  /**
   * Enforce proper use of this class
   *
   * @param   string $name
   */
  public function __get($name){
    throw new \Exception(sprintf('No such property: %s', $name));
  }



  /**
   * Enforce proper use of this class
   *
   * @param   string $name
   * @param   mixed $value
   */
  public function __set($name, $value){
    throw new \Exception(sprintf('Cannot set %s. Not allowed', $name));
  }  



  /**
   * Generate structured information about a variable/value/expression (subject)
   *
   * Output is flushed to the screen
   *   
   * @param   mixed $subject
   * @param   string $expression
   */
  public function query($subject, $expression = null){

    if(static::$timeout > 0)
      return;

    $this->startTime = microtime(true);

    $this->intObjects = new \SplObjectStorage();
    
    $this->fmt->startRoot();
    $this->fmt->startExp();
    $this->evaluateExp($expression);
    $this->fmt->endExp();
    $this->evaluate($subject);    
    $this->fmt->endRoot();
    $this->fmt->flush();

    static::$time += microtime(true) - $this->startTime;     
  }




  /**
   * Executes a function the given number of times and returns the elapsed time.
   *
   * Keep in mind that the returned time includes function call overhead (including
   * microtime calls) x iteration count. This is why this is better suited for
   * determining which of two or more functions is the fastest, rather than
   * finding out how fast is a single function.
   *
   * @param   int $iterations      Number of times the function will be executed
   * @param   callable $function   Function to execute
   * @param   mixed &$output       If given, last return value will be available in this variable
   * @return  double               Elapsed time
   */
  public static function timeFunc($iterations, $function, &$output = null){
    
    $time = 0;

    for($i = 0; $i < $iterations; $i++){
      $start  = microtime(true);
      $output = call_user_func($function);
      $time  += microtime(true) - $start;
    }
    
    return round($time, 4);
  }



  /**
   * Timer utility
   *
   * First call of this function will start the timer.
   * The second call will stop the timer and return the elapsed time
   * since the timer started.
   *
   * Multiple timers can be controlled simultaneously by specifying a timer ID.
   *
   * @since   1.0   
   * @param   int $id          Timer ID, optional
   * @param   int $precision   Precision of the result, optional
   * @return  void|double      Elapsed time, or void if the timer was just started
   */
  public static function timer($id = 1, $precision = 4){

    static
      $timers = array();

    // check if this timer was started, and display the elapsed time if so
    if(isset($timers[$id])){
      $elapsed = round(microtime(true) - $timers[$id], $precision);
      unset($timers[$id]);
      return $elapsed;
    }

    // ID doesn't exist, start new timer
    $timers[$id] = microtime(true);
  }



  /**
   * Parses a DocBlock comment into a data structure.
   *
   * @link    http://pear.php.net/manual/en/standards.sample.php
   * @param   string $comment    DocBlock comment (must start with /**)
   * @param   string|null $key   Field to return (optional)
   * @return  array|string|null  Array containing all fields, array/string with the contents of
   *                             the requested field, or null if the comment is empty/invalid
   */
  public static function parseComment($comment, $key = null){

    $description = '';
    $tags        = array();
    $tag         = null;
    $pointer     = '';
    $padding     = 0;
    $comment     = preg_split('/\r\n|\r|\n/', '* ' . trim($comment, "/* \t\n\r\0\x0B"));

    // analyze each line
    foreach($comment as $line){

      // drop any wrapping spaces
      $line = trim($line);

      // drop "* "
      if($line !== '')
        $line = substr($line, 2);      

      if(strpos($line, '@') !== 0){

        // preserve formatting of tag descriptions,
        // because they may span across multiple lines
        if($tag !== null){
          $trimmed = trim($line);

          if($padding !== 0)
            $trimmed = static::strPad($trimmed, static::strLen($line) - $padding, ' ', STR_PAD_LEFT);
          else
            $padding = static::strLen($line) - static::strLen($trimmed);

          $pointer .= "\n{$trimmed}";
          continue;
        }
        
        // tag definitions have not started yet; assume this is part of the description text
        $description .= "\n{$line}";        
        continue;
      }  

      $padding = 0;
      $parts = explode(' ', $line, 2);

      // invalid tag? (should we include it as an empty array?)
      if(!isset($parts[1]))
        continue;

      $tag = substr($parts[0], 1);
      $line = ltrim($parts[1]);

      // tags that have a single component (eg. link, license, author, throws...);
      // note that @throws may have 2 components, however most people use it like "@throws ExceptionClass if whatever...",
      // which, if broken into two values, leads to an inconsistent description sentence
      if(!in_array($tag, array('global', 'param', 'return', 'var'))){
        $tags[$tag][] = $line;
        end($tags[$tag]);
        $pointer = &$tags[$tag][key($tags[$tag])];
        continue;
      }

      // tags with 2 or 3 components (var, param, return);
      $parts    = explode(' ', $line, 2);
      $parts[1] = isset($parts[1]) ? ltrim($parts[1]) : null;
      $lastIdx  = 1;

      // expecting 3 components on the 'param' tag: type varName varDescription
      if($tag === 'param'){
        $lastIdx = 2;
        if(in_array($parts[1][0], array('&', '$'), true)){
          $line     = ltrim(array_pop($parts));
          $parts    = array_merge($parts, explode(' ', $line, 2));        
          $parts[2] = isset($parts[2]) ? ltrim($parts[2]) : null;
        }else{
          $parts[2] = $parts[1];
          $parts[1] = null;
        }
      }  

      $tags[$tag][] = $parts;
      end($tags[$tag]);
      $pointer = &$tags[$tag][key($tags[$tag])][$lastIdx];
    }

    // split title from the description texts at the nearest 2x new-line combination
    // (note: loose check because 0 isn't valid as well)
    if(strpos($description, "\n\n")){
      list($title, $description) = explode("\n\n", $description, 2);

    // if we don't have 2 new lines, try to extract first sentence
    }else{  
      // in order for a sentence to be considered valid,
      // the next one must start with an uppercase letter    
      $sentences = preg_split('/(?<=[.?!])\s+(?=[A-Z])/', $description, 2, PREG_SPLIT_NO_EMPTY);

      // failed to detect a second sentence? then assume there's only title and no description text
      $title = isset($sentences[0]) ? $sentences[0] : $description;
      $description = isset($sentences[1]) ? $sentences[1] : '';
    }

    $title = ltrim($title);
    $description = ltrim($description);

    $data = compact('title', 'description', 'tags');

    if(!array_filter($data))
      return null;

    if($key !== null)
      return isset($data[$key]) ? $data[$key] : null;

    return $data;
  }



  /**
   * Split a regex into its components
   * 
   * Based on "Regex Colorizer" by Steven Levithan (this is a translation from javascript)
   *
   * @link     https://github.com/slevithan/regex-colorizer
   * @link     https://github.com/symfony/Finder/blob/master/Expression/Regex.php#L64-74
   * @param    string $pattern
   * @return   array
   */
  public static function splitRegex($pattern){

    // detection attempt code from the Symfony Finder component
    $maybeValid = false;
    if(preg_match('/^(.{3,}?)([imsxuADU]*)$/', $pattern, $m)) {
      $start = substr($m[1], 0, 1);
      $end   = substr($m[1], -1);

      if(($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}'))
        $maybeValid = true;
    }

    if(!$maybeValid)
      throw new \Exception('Pattern does not appear to be a valid PHP regex');

    $output              = array();
    $capturingGroupCount = 0;
    $groupStyleDepth     = 0;
    $openGroups          = array();
    $lastIsQuant         = false;
    $lastType            = 1;      // 1 = none; 2 = alternator
    $lastStyle           = null;

    preg_match_all('/\[\^?]?(?:[^\\\\\]]+|\\\\[\S\s]?)*]?|\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\\\]+|./', $pattern, $matches);

    $matches = $matches[0];

    $getTokenCharCode = function($token){
      if(strlen($token) > 1 && $token[0] === '\\'){
        $t1 = substr($token, 1);

        if(preg_match('/^c[A-Za-z]$/', $t1))
          return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1;

        if(preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1))
          return intval(substr($t1, 1), 16);

        if(preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1))
          return intval($t1, 8);

        $len = strlen($t1);

        if($len === 1 && strpos('cuxDdSsWw', $t1) !== false)
          return null;

        if($len === 1){
          switch ($t1) {
            case 'b': return 8;  
            case 'f': return 12; 
            case 'n': return 10; 
            case 'r': return 13; 
            case 't': return 9;  
            case 'v': return 11; 
            default: return $t1[0]; 
          }
        }
      }

      return ($token !== '\\') ? $token[0] : null;  
    };   

    foreach($matches as $m){

      if($m[0] === '['){
        $lastCC         = null;  
        $cLastRangeable = false;
        $cLastType      = 0;  // 0 = none; 1 = range hyphen; 2 = short class

        preg_match('/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts);

        array_shift($parts);
        list($opening, $content, $closing) = $parts;

        if(!$closing)
          throw new \Exception('Unclosed character class');

        preg_match_all('/[^\\\\-]+|-|\\\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)/', $content, $ccTokens);
        $ccTokens     = $ccTokens[0];
        $ccTokenCount = count($ccTokens);
        $output[]     = array('chr' => $opening);

        foreach($ccTokens as $i => $cm) {

          if($cm[0] === '\\'){
            if(preg_match('/^\\\\[cux]$/', $cm))
              throw new \Exception('Incomplete regex token');

            if(preg_match('/^\\\\[dsw]$/i', $cm)) {
              $output[]     = array('chr-meta' => $cm);
              $cLastRangeable  = ($cLastType !== 1);
              $cLastType       = 2;

            }elseif($cm === '\\'){
              throw new \Exception('Incomplete regex token');
              
            }else{
              $output[]       = array('chr-meta' => $cm);
              $cLastRangeable = $cLastType !== 1;
              $lastCC         = $getTokenCharCode($cm);
            }

          }elseif($cm === '-'){
            if($cLastRangeable){
              $nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[$i + 1] : false;

              if($nextToken){
                $nextTokenCharCode = $getTokenCharCode($nextToken[0]);

                if((!is_null($nextTokenCharCode) && $lastCC > $nextTokenCharCode) || $cLastType === 2 || preg_match('/^\\\\[dsw]$/i', $nextToken[0]))
                  throw new \Exception('Reversed or invalid range');

                $output[]       = array('chr-range' => '-');
                $cLastRangeable = false;
                $cLastType      = 1;
               
              }else{
                $output[] = $closing ? array('chr' => '-') : array('chr-range' => '-');
              }

            }else{
              $output[]        = array('chr' => '-');
              $cLastRangeable  = ($cLastType !== 1);
            }

          }else{
            $output[]       = array('chr' => $cm);
            $cLastRangeable = strlen($cm) > 1 || ($cLastType !== 1);
            $lastCC         = $cm[strlen($cm) - 1];
          }
        }

        $output[] = array('chr' => $closing);
        $lastIsQuant  = true;

      }elseif($m[0] === '('){
        if(strlen($m) === 2)
          throw new \Exception('Invalid or unsupported group type');
   
        if(strlen($m) === 1)
          $capturingGroupCount++;

        $groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1;
        $openGroups[]    = $m; // opening
        $lastIsQuant     = false;
        $output[]        = array("g{$groupStyleDepth}" => $m);

      }elseif($m[0] === ')'){
        if(!count($openGroups)) 
          throw new \Exception('No matching opening parenthesis');

        $output[]        = array('g' . $groupStyleDepth => ')');
        $prevGroup       = $openGroups[count($openGroups) - 1];
        $prevGroup       = isset($prevGroup[2]) ? $prevGroup[2] : '';
        $lastIsQuant     = !preg_match('/^[=!]/', $prevGroup);
        $lastStyle       = "g{$groupStyleDepth}";
        $lastType        = 0;
        $groupStyleDepth = ($groupStyleDepth !== 1) ? $groupStyleDepth - 1 : 5;

        array_pop($openGroups);
        continue;
      
      }elseif($m[0] === '\\'){
        if(isset($m[1]) && preg_match('/^[1-9]/', $m[1])){
          $nonBackrefDigits = '';
          $num = substr(+$m, 1);

          while($num > $capturingGroupCount){
            preg_match('/[0-9]$/', $num, $digits);
            $nonBackrefDigits = $digits[0] . $nonBackrefDigits;
            $num = floor($num / 10); 
          }

          if($num > 0){
            $output[] = array('meta' =>  "\\{$num}", 'text' => $nonBackrefDigits);

          }else{
            preg_match('/^\\\\([0-3][0-7]{0,2}|[4-7][0-7]?|[89])([0-9]*)/', $m, $pts);
            $output[] = array('meta' => '\\' . $pts[1], 'text' => $pts[2]);
          }

          $lastIsQuant = true;

        }elseif(isset($m[1]) && preg_match('/^[0bBcdDfnrsStuvwWx]/', $m[1])){
   
          if(preg_match('/^\\\\[cux]$/', $m))
            throw new \Exception('Incomplete regex token');

          $output[]    = array('meta' => $m);
          $lastIsQuant = (strpos('bB', $m[1]) === false);

        }elseif($m === '\\'){
          throw new \Exception('Incomplete regex token');
            
        }else{
          $output[]    = array('text' => $m);
          $lastIsQuant = true;
        }

      }elseif(preg_match('/^(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??$/', $m)){
        if(!$lastIsQuant)
          throw new \Exception('Quantifiers must be preceded by a token that can be repeated');

        preg_match('/^\{([0-9]+)(?:,([0-9]*))?/', $m, $interval);

        if($interval && (+$interval[1] > 65535 || (isset($interval[2]) && (+$interval[2] > 65535))))
          throw new \Exception('Interval quantifier cannot use value over 65,535');
        
        if($interval && isset($interval[2]) && (+$interval[1] > +$interval[2]))
          throw new \Exception('Interval quantifier range is reversed');
        
        $output[]     = array($lastStyle ? $lastStyle : 'meta' => $m);
        $lastIsQuant  = false;

      }elseif($m === '|'){
        if($lastType === 1 || ($lastType === 2 && !count($openGroups)))
          throw new \Exception('Empty alternative effectively truncates the regex here');

        $output[]    = count($openGroups) ? array("g{$groupStyleDepth}" => '|') : array('meta' => '|');
        $lastIsQuant = false;
        $lastType    = 2;
        $lastStyle   = '';
        continue;

      }elseif($m === '^' || $m === '$'){
        $output[]    = array('meta' => $m);
        $lastIsQuant = false;

      }elseif($m === '.'){
        $output[]    = array('meta' => '.');
        $lastIsQuant = true;
   
      }else{
        $output[]    = array('text' => $m);
        $lastIsQuant = true;
      }

      $lastType  = 0;
      $lastStyle = '';    
    }

    if($openGroups)
      throw new \Exception('Unclosed grouping');

    return $output;
  }



  /**
   * Set or get configuration options
   *
   * @param   string $key
   * @param   mixed|null $value
   * @return  mixed
   */
  public static function config($key, $value = null){

    if(!array_key_exists($key, static::$config))
      throw new \Exception(sprintf('Unrecognized option: "%s". Valid options are: %s', $key, implode(', ', array_keys(static::$config))));

    if($value === null)
      return static::$config[$key];

    if(is_array(static::$config[$key]))
      return static::$config[$key] = (array)$value;

    return static::$config[$key] = $value;
  }



  /**
   * Total CPU time used by the class
   *   
   * @param   int precision
   * @return  double
   */
  public static function getTime($precision = 4){
    return round(static::$time, $precision);
  }



  /**
   * Get relevant backtrace info for last ref call
   *   
   * @return  array|false
   */
  public static function getBacktrace(){

    // pull only basic info with php 5.3.6+ to save some memory
    $trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();

    while($callee = array_pop($trace)){

      // extract only the information we neeed
      $callee = array_intersect_key($callee, array_fill_keys(array('file', 'function', 'line'), false));
      extract($callee, EXTR_OVERWRITE);

      // skip, if the called function doesn't match the shortcut function name
      if(!$function || !in_array(mb_strtolower((string)$function), static::$config['shortcutFunc']))
        continue;

      return compact('file', 'function', 'line');
    }

    return false;
  }



  /**
   * Determines the input expression(s) passed to the shortcut function
   *
   * @param   array &$options   Optional, options to gather (from operators)
   * @return  array             Array of string expressions
   */
  public static function getInputExpressions(array &$options = null){    

    // used to determine the position of the current call,
    // if more queries calls were made on the same line
    static $lineInst = array();

    $trace = static::getBacktrace();

    if(!$trace)
      return array();

    extract($trace);

    $code     = file($file);
    $code     = $code[$line - 1]; // multiline expressions not supported!
    $instIndx = 0;
    $tokens   = token_get_all("<?php {$code}");

    // locate the caller position in the line, and isolate argument tokens
    foreach($tokens as $i => $token){

      // match token with our shortcut function name
      if(is_string($token) || ($token[0] !== T_STRING) || (strcasecmp($token[1], $function) !== 0))
        continue;

      // is this some method that happens to have the same name as the shortcut function?
      if(isset($tokens[$i - 1]) && is_array($tokens[$i - 1]) && in_array($tokens[$i - 1][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true))
        continue;

      // find argument definition start, just after '('
      if(isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '(')){
        $instIndx++;

        if(!isset($lineInst[$line]))
          $lineInst[$line] = 0;

        if($instIndx <= $lineInst[$line])
          continue;

        $lineInst[$line]++;

        // gather options
        if($options !== null){
          $j = $i - 1;
          while(isset($tokens[$j]) && is_string($tokens[$j]) && in_array($tokens[$j], array('@', '+', '-', '!', '~')))
            $options[] = $tokens[$j--];
        }  
       
        $lvl = $index = $curlies = 0;
        $expressions = array();

        // get the expressions
        foreach(array_slice($tokens, $i + 2) as $token){

          if(is_array($token)){
            if($token[0] !== T_COMMENT)
              $expressions[$index][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' ';

            continue;
          }

          if($token === '{')
            $curlies++;

          if($token === '}')
            $curlies--;        

          if($token === '(')
            $lvl++;

          if($token === ')')
            $lvl--;

          // assume next argument if a comma was encountered,
          // and we're not insde a curly bracket or inner parentheses
          if(($curlies < 1) && ($lvl === 0) && ($token === ',')){
            $index++;
            continue;
          }  

          // negative parentheses count means we reached the end of argument definitions
          if($lvl < 0){         
            foreach($expressions as &$expression)
              $expression = trim(implode('', $expression));

            return $expressions;
          }

          $expressions[$index][] = $token;      
        }

        break;
      }    
    }

    return array();
  }



  /**
   * Get all parent classes of a class
   *
   * @param   Reflector $class   Reflection object
   * @return  array              Array of ReflectionClass objects (starts with the ancestor, ends with the given class)
   */
  protected static function getParentClasses(\Reflector $class){

    $parents = array($class);
    while(($class = $class->getParentClass()) !== false)
      $parents[] = $class;
   
    return array_reverse($parents);
  }



  /**
   * Generate class / function info
   *
   * @param   Reflector $reflector      Class name or reflection object
   * @param   string $single            Skip parent classes
   * @param   Reflector|null $context   Object context (for methods)
   * @return  string
   */
  protected function fromReflector(\Reflector $reflector, $single = '', \Reflector $context = null){

    // @todo: test this
    $hash = var_export(func_get_args(), true);
    //$hash = $reflector->getName() . ';' . $single . ';' . ($context ? $context->getName() : '');

    if($this->fmt->didCache($hash)){
      static::$debug['cacheHits']++;
      return;
    }

    $items = array($reflector);

    if(($single === '') && ($reflector instanceof \ReflectionClass))
      $items = static::getParentClasses($reflector);

    $first = true;
    foreach($items as $item){

      if(!$first)
        $this->fmt->sep(' :: ');

      $first    = false;
      $name     = ($single !== '') ? $single : $item->getName();
      $comments = $item->isInternal() ? array() : static::parseComment($item->getDocComment());
      $meta     = array('sub' => array());
      $bubbles  = array();

      if($item->isInternal()){
        $extension = $item->getExtension();
        $meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf('Internal - part of %s (%s)', $extension->getName(), $extension->getVersion()) : 'Internal';
      
      }else{
        $comments = static::parseComment($item->getDocComment()); 

        if($comments)
          $meta += $comments;

        $meta['sub'][] = array('Defined in', basename($item->getFileName()) . ':' . $item->getStartLine());        
      }

      if(($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod)){
        if(($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName()))
          $meta['sub'][] = array('Inherited from', $item->getDeclaringClass()->getShortName());

        // @note: PHP 7 seems to crash when calling getPrototype on Closure::__invoke()
        if(($item instanceof \ReflectionMethod) && !$item->isInternal()){
          try{
            $proto = $item->getPrototype();
            $meta['sub'][] = array('Prototype defined by', $proto->class);
          }catch(\Exception $e){}
        }  

        $this->fmt->text('name', $name, $meta, $this->linkify($item));          
        continue;
      }
      
      // @todo: maybe - list interface methods
      if(!($item->isInterface() || (static::$env['is54'] && $item->isTrait()))){

        if($item->isAbstract())
          $bubbles[] = array('A', 'Abstract');

        if(static::$env['is7'] && $item->isAnonymous())
          $bubbles[] = array('?', 'Anonymous');        

        if($item->isFinal())
          $bubbles[] = array('F', 'Final');

        // php 5.4+ only
        if(static::$env['is54'] && $item->isCloneable())
          $bubbles[] = array('C', 'Cloneable');          

        if($item->isIterateable())
          $bubbles[] = array('X', 'Iterateable');                    
      
      }

      if($item->isInterface() && $single !== '')
        $bubbles[] = array('I', 'Interface');                            

      if($bubbles)
        $this->fmt->bubbles($bubbles);

      if($item->isInterface() && $single === '')
        $name .= sprintf(' (%d)', count($item->getMethods()));

      $this->fmt->text('name', $name, $meta, $this->linkify($item));          
    }

    $this->fmt->cacheLock($hash);  
  }



  /**
   * Generates an URL that points to the documentation page relevant for the requested context
   *
   * For internal functions and classes, the URI will point to the local PHP manual
   * if installed and configured, otherwise to php.net/manual (the english one)
   *
   * @param   Reflector $reflector    Reflector object (used to determine the URL scheme for internal stuff)
   * @param   string|null $constant   Constant name, if this is a request to linkify a constant
   * @return  string|null             URL
   */
  protected function linkify(\Reflector $reflector, $constant = null){

    static $docRefRoot = null, $docRefExt = null;

    // most people don't have this set
    if(!$docRefRoot)
      $docRefRoot = ($docRefRoot = rtrim(ini_get('docref_root'), '/')) ? $docRefRoot : 'http://php.net/manual/en';

    if(!$docRefExt)
      $docRefExt = ($docRefExt = ini_get('docref_ext')) ? $docRefExt : '.php';

    $phpNetSchemes = array(
      'class'     => $docRefRoot . '/class.%s'    . $docRefExt,
      'function'  => $docRefRoot . '/function.%s' . $docRefExt,
      'method'    => $docRefRoot . '/%2$s.%1$s'   . $docRefExt,
      'property'  => $docRefRoot . '/class.%2$s'  . $docRefExt . '#%2$s.props.%1$s',
      'constant'  => $docRefRoot . '/class.%2$s'  . $docRefExt . '#%2$s.constants.%1$s',      
    );

    $url  = null;    
    $args = array();

    // determine scheme
    if($constant !== null){
      $type = 'constant';
      $args[] = $constant;
    
    }else{
      $type = explode('\\', get_class($reflector)); 
      $type = strtolower(ltrim(end($type), 'Reflection'));

      if($type === 'object')
        $type = 'class';
    }

    // properties don't have the internal flag;
    // also note that many internal classes use some kind of magic as properties (eg. DateTime);
    // these will only get linkifed if the declared class is internal one, and not an extension :(
    $parent = ($type !== 'property') ? $reflector : $reflector->getDeclaringClass();

    // internal function/method/class/property/constant
    if($parent->isInternal()){
      $args[] = $reflector->name;

      if(in_array($type, array('method', 'property'), true))
        $args[] = $reflector->getDeclaringClass()->getName();

      $args = array_map(function($text){
        return str_replace('_', '-', ltrim(strtolower($text), '\\_'));
      }, $args);

      // check for some special cases that have no links
      $valid = (($type === 'method') || (strcasecmp($parent->name, 'stdClass') !== 0))
            && (($type !== 'method') || (($reflector->name === '__construct') || strpos($reflector->name, '__') !== 0));

      if($valid)
        $url = vsprintf($phpNetSchemes[$type], $args);

    // custom
    }else{
      switch(true){      

        // WordPress function;
        // like pretty much everything else in WordPress, API links are inconsistent as well;
        // so we're using queryposts.com as doc source for API
        case ($type === 'function') && class_exists('WP', false) && defined('ABSPATH') && defined('WPINC'):
          if(strpos($reflector->getFileName(), realpath(ABSPATH . WPINC)) === 0){
            $url = sprintf('http://queryposts.com/function/%s', urlencode(strtolower($reflector->getName())));
            break;
          }

        // @todo: handle more apps
      }      

    }

    return $url;
  }


  public static function getTimeoutPoint(){
    return static::$timeout;
  }


  public static function getDebugInfo(){
    return static::$debug;
  }



  protected function hasInstanceTimedOut(){

    if(static::$timeout > 0)
      return true;

    $timeout = static::$config['timeout'];

    if(($timeout > 0) && ((microtime(true) - $this->startTime) > $timeout))
      return (static::$timeout = (microtime(true) - $this->startTime));

    return false;
  }



  /**
   * Evaluates the given variable
   *
   * @param   mixed &$subject   Variable to query
   * @param   bool $specialStr  Should this be interpreted as a special string?
   * @return  mixed             Result (both HTML and text modes generate strings)
   */
  protected function evaluate(&$subject, $specialStr = false){    

    switch($type = gettype($subject)){

      // https://github.com/digitalnature/php-ref/issues/13
      case 'unknown type':
        return $this->fmt->text('unknown');
    
      // null value
      case 'NULL':
        return $this->fmt->text('null');

      // integer/double/float
      case 'integer':
      case 'double':
        return $this->fmt->text($type, $subject, $type);

      // boolean
      case 'boolean':
        $text = $subject ? 'true' : 'false';
        return $this->fmt->text($text, $text, $type);

      // arrays
      case 'array':

        // empty array?
        if(empty($subject)){
          $this->fmt->text('array');          
          return $this->fmt->emptyGroup();
        }  

        if(isset($subject[static::MARKER_KEY])){
          unset($subject[static::MARKER_KEY]);
          $this->fmt->text('array');
          $this->fmt->emptyGroup('recursion');          
          return;
        }

        // first recursion level detection;
        // this is optional (used to print consistent recursion info)
        foreach($subject as $key => &$value){

          if(!is_array($value))
            continue;

          // save current value in a temporary variable
          $buffer = $value;

          // assign new value
          $value = ($value !== 1) ? 1 : 2;
          
          // if they're still equal, then we have a reference            
          if($value === $subject){
            $value = $buffer;                      
            $value[static::MARKER_KEY] = true;
            $this->evaluate($value);
            return;
          }

          // restoring original value
          $value = $buffer;
        }        

        $this->fmt->text('array');
        $count = count($subject);
        if(!$this->fmt->startGroup($count))
          return;               

        $max = max(array_map('static::strLen', array_keys($subject)));
        $subject[static::MARKER_KEY] = true;
     
        foreach($subject as $key => &$value){
          
          // ignore our temporary marker
          if($key === static::MARKER_KEY)
            continue;

          if($this->hasInstanceTimedOut())
            break;

          $keyInfo = gettype($key);

          if($keyInfo === 'string'){
            $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
            $keyLen     = static::strLen($key);
            $keyLenInfo = $encoding && ($encoding !== 'ASCII') ? $keyLen . '; ' . $encoding : $keyLen;
            $keyInfo    = "{$keyInfo}({$keyLenInfo})";
          }else{
            $keyLen   = strlen($key);
          }

          $this->fmt->startRow();
          $this->fmt->text('key', $key, "Key: {$keyInfo}");
          $this->fmt->colDiv($max - $keyLen);
          $this->fmt->sep('=>');
          $this->fmt->colDiv();
          $this->evaluate($value, $specialStr);
          $this->fmt->endRow();
        }

        unset($subject[static::MARKER_KEY]);

        $this->fmt->endGroup();
        return;

      // resource
      case 'resource':
      case 'resource (closed)':
        $meta    = array();
        $resType = get_resource_type($subject);        

        $this->fmt->text('resource', strval($subject));

        if(!static::$config['showResourceInfo'])
          return $this->fmt->emptyGroup($resType);

        // @see: http://php.net/manual/en/resource.php
        // need to add more...
        switch($resType){

          // curl extension resource
          case 'curl':
            $meta = curl_getinfo($subject);
          break;

          case 'FTP Buffer':
            $meta = array(
              'time_out'  => ftp_get_option($subject, FTP_TIMEOUT_SEC),
              'auto_seek' => ftp_get_option($subject, FTP_AUTOSEEK),
            );

          break;

          // gd image extension resource
          case 'gd':            
            $meta = array(
               'size'       => sprintf('%d x %d', imagesx($subject), imagesy($subject)),
               'true_color' => imageistruecolor($subject),
            );

          break;

          case 'ldap link':
            $constants = get_defined_constants();

            array_walk($constants, function($value, $key) use(&$constants){
              if(strpos($key, 'LDAP_OPT_') !== 0)
                unset($constants[$key]);
            });

            // this seems to fail on my setup :(
            unset($constants['LDAP_OPT_NETWORK_TIMEOUT']);

            foreach(array_slice($constants, 3) as $key => $value)
              if(ldap_get_option($subject, (int)$value, $ret))
                $meta[strtolower(substr($key, 9))] = $ret;

          break;

          // mysql connection (mysql extension is deprecated from php 5.4/5.5)
          case 'mysql link':
          case 'mysql link persistent':
            $dbs = array();
            $query = @mysql_list_dbs($subject);
            while($row = @mysql_fetch_array($query))
              $dbs[] = $row['Database'];

            $meta = array(
              'host'             => ltrim(@mysql_get_host_info ($subject), 'MySQL host info: '),
              'server_version'   => @mysql_get_server_info($subject),
              'protocol_version' => @mysql_get_proto_info($subject),
              'databases'        => $dbs,
            );

          break;

          // mysql result
          case 'mysql result':
            while($row = @mysql_fetch_object($subject)){              
              $meta[] = (array)$row;

              if($this->hasInstanceTimedOut())
                break;
            }

          break;

          // stream resource (fopen, fsockopen, popen, opendir etc)
          case 'stream':
            $meta = stream_get_meta_data($subject);
          break;

        }

        if(!$meta)
          return $this->fmt->emptyGroup($resType);


        if(!$this->fmt->startGroup($resType))
          return;

        $max = max(array_map('static::strLen', array_keys($meta)));
        foreach($meta as $key => $value){
          $this->fmt->startRow();
          $this->fmt->text('resourceProp', ucwords(str_replace('_', ' ', $key)));
          $this->fmt->colDiv($max - static::strLen($key));
          $this->fmt->sep(':');
          $this->fmt->colDiv();
          $this->evaluate($value);          
          $this->fmt->endRow();
        }
        $this->fmt->endGroup();
        return;

      // string      
      case 'string':
        $length   = static::strLen($subject);       
        $encoding = static::$env['mbStr'] ? mb_detect_encoding($subject) : false;      
        $info     = $encoding && ($encoding !== 'ASCII') ? $length . '; ' . $encoding : $length;

        if($specialStr){
          $this->fmt->sep('"');
          $this->fmt->text(array('string', 'special'), $subject, "string({$info})");
          $this->fmt->sep('"');          
          return;
        }  
        
        $this->fmt->text('string', $subject, "string({$info})");        

        // advanced checks only if there are 3 characteres or more
        if(static::$config['showStringMatches'] && ($length > 2) && (trim($subject) !== '')){

          $isNumeric = is_numeric($subject);

          // very simple check to determine if the string could match a file path
          // @note: this part of the code is very expensive
          $isFile = ($length < 2048)
            && (max(array_map('strlen', explode('/', str_replace('\\', '/', $subject)))) < 128)
            && !preg_match('/[^\w\.\-\/\\\\:]|\..*\.|\.$|:(?!(?<=^[a-zA-Z]:)[\/\\\\])/', $subject);            

          if($isFile){
            try{
              $file  = new \SplFileInfo($subject);
              $flags = array();
              $perms = $file->getPerms();

              if(($perms & 0xC000) === 0xC000)       // socket
                $flags[] = 's';
              elseif(($perms & 0xA000) === 0xA000)   // symlink        
                $flags[] = 'l';
              elseif(($perms & 0x8000) === 0x8000)   // regular
                $flags[] = '-';
              elseif(($perms & 0x6000) === 0x6000)   // block special
                $flags[] = 'b';
              elseif(($perms & 0x4000) === 0x4000)   // directory
                $flags[] = 'd';
              elseif(($perms & 0x2000) === 0x2000)   // character special
                $flags[] = 'c';
              elseif(($perms & 0x1000) === 0x1000)   // FIFO pipe
                $flags[] = 'p';
              else                                   // unknown
                $flags[] = 'u';        

              // owner
              $flags[] = (($perms & 0x0100) ? 'r' : '-');
              $flags[] = (($perms & 0x0080) ? 'w' : '-');
              $flags[] = (($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-'));

              // group
              $flags[] = (($perms & 0x0020) ? 'r' : '-');
              $flags[] = (($perms & 0x0010) ? 'w' : '-');
              $flags[] = (($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-'));

              // world
              $flags[] = (($perms & 0x0004) ? 'r' : '-');
              $flags[] = (($perms & 0x0002) ? 'w' : '-');
              $flags[] = (($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-'));

              $size = is_dir($subject) ? '' : sprintf(' %.2fK', $file->getSize() / 1024);
              
              $this->fmt->startContain('file', true);
              $this->fmt->text('file', implode('', $flags) . $size);
              $this->fmt->endContain();

            }catch(\Exception $e){
              $isFile = false;
            }
          }

          // class/interface/function
          if(!preg_match('/[^\w+\\\\]/', $subject) && ($length < 96)){
            $isClass = class_exists($subject, false);
            if($isClass){
              $this->fmt->startContain('class', true);
              $this->fromReflector(new \ReflectionClass($subject));
              $this->fmt->endContain();
            }  

            if(!$isClass && interface_exists($subject, false)){
              $this->fmt->startContain('interface', true);
              $this->fromReflector(new \ReflectionClass($subject));
              $this->fmt->endContain('interface');
            }  

            if(function_exists($subject)){
              $this->fmt->startContain('function', true);
              $this->fromReflector(new \ReflectionFunction($subject));
              $this->fmt->endContain('function');
            }  
          }


          // skip serialization/json/date checks if the string appears to be numeric,
          // or if it's shorter than 5 characters
          if(!$isNumeric && ($length > 4)){

            // url
            if(static::$config['showUrls'] && static::$env['curlActive'] && filter_var($subject, FILTER_VALIDATE_URL)){
              $ch = curl_init($subject);
              curl_setopt($ch, CURLOPT_NOBODY, true);
              curl_exec($ch);
              $nfo = curl_getinfo($ch);
              curl_close($ch);

              if($nfo['http_code']){
                $this->fmt->startContain('url', true);
                $contentType = explode(';', $nfo['content_type']);
                $this->fmt->text('url', sprintf('%s:%d %s %.2fms (%d)', !empty($nfo['primary_ip']) ? $nfo['primary_ip'] : null, !empty($nfo['primary_port']) ? $nfo['primary_port'] : null, $contentType[0], $nfo['total_time'], $nfo['http_code']));
                $this->fmt->endContain();
              }

            }

            // date
            if(($length < 128) && static::$env['supportsDate'] && !preg_match('/[^A-Za-z0-9.:+\s\-\/]/', $subject)){
              try{
                $date   = new \DateTime($subject);
                $errors = \DateTime::getLastErrors();

                if(($errors['warning_count'] < 1) && ($errors['error_count'] < 1)){
                  $now    = new \Datetime('now');
                  $nowUtc = new \Datetime('now', new \DateTimeZone('UTC'));
                  $diff   = $now->diff($date);

                  $map = array(
                    'y' => 'yr',
                    'm' => 'mo',
                    'd' => 'da',                  
                    'h' => 'hr',
                    'i' => 'min',
                    's' => 'sec',
                  );

                  $timeAgo = 'now';
                  foreach($map as $k => $label){
                    if($diff->{$k} > 0){
                      $timeAgo = $diff->format("%R%{$k}{$label}");
                      break;
                    }
                  }

                  $tz   = $date->getTimezone();
                  $offs = round($tz->getOffset($nowUtc) / 3600);

                  if($offs > 0)
                    $offs = "+{$offs}";

                  $timeAgo .= ((int)$offs !== 0) ? ' ' . sprintf('%s (UTC%s)', $tz->getName(), $offs) : ' UTC';
                  $this->fmt->startContain('date', true);
                  $this->fmt->text('date', $timeAgo);
                  $this->fmt->endContain();
                  
                }  
              }catch(\Exception $e){
                // not a date
              }

            }

            // attempt to detect if this is a serialized string     
            static $unserializing = 0;
            $isSerialized = ($unserializing < 3)
              && (($subject[$length - 1] === ';') || ($subject[$length - 1] === '}'))              
              && in_array($subject[0], array('s', 'a', 'O'), true)
              && ((($subject[0] === 's') && ($subject[$length - 2] !== '"')) || preg_match("/^{$subject[0]}:[0-9]+:/s", $subject))
              && (($unserialized = @unserialize($subject)) !== false);

            if($isSerialized){
              $unserializing++;
              $this->fmt->startContain('serialized', true);
              $this->evaluate($unserialized);
              $this->fmt->endContain();
              $unserializing--;
            }

            // try to find out if it's a json-encoded string;
            // only do this for json-encoded arrays or objects, because other types have too generic formats                
            static $decodingJson = 0;
            $isJson = !$isSerialized && ($decodingJson < 3) && in_array($subject[0], array('{', '['), true);

            if($isJson){
              $decodingJson++;
              $data = json_decode($subject);

              // ensure created objects live enough for PHP to provide a unique hash
              if(is_object($data))
                $this->intObjects->attach($data);

              if($isJson = (json_last_error() === JSON_ERROR_NONE)){
                $this->fmt->startContain('json', true);
                $this->evaluate($data);
                $this->fmt->endContain();
              }  

              $decodingJson--;            
            }

            // attempt to match a regex            
            if(!$isSerialized && !$isJson && $length < 768){
              try{
                $components = $this->splitRegex($subject);
                if($components){
                  $regex = '';

                  $this->fmt->startContain('regex', true);
                  foreach($components as $component)
                    $this->fmt->text('regex-' . key($component), reset($component));
                  $this->fmt->endContain();
                }  

              }catch(\Exception $e){
                // not a regex
              }

            }
          }  
        }

        return;
    }  
 
    // if we reached this point, $subject must be an object

    // track objects to detect recursion
    static $hashes = array();      

    // hash ID of this object
    $hash = spl_object_hash($subject);
    $recursion = isset($hashes[$hash]);
 
    // sometimes incomplete objects may be created from string unserialization,
    // if the class to which the object belongs wasn't included until the unserialization stage...
    if($subject instanceof \__PHP_Incomplete_Class){
      $this->fmt->text('object');
      $this->fmt->emptyGroup('incomplete');
      return;
    }

    // check cache at this point
    if(!$recursion && $this->fmt->didCache($hash)){
      static::$debug['cacheHits']++;
      return;        
    }
  
    $reflector = new \ReflectionObject($subject);
    $this->fmt->startContain('class');
    $this->fromReflector($reflector);
    $this->fmt->text('object', ' object');
    $this->fmt->endContain();

    // already been here?
    if($recursion)
      return $this->fmt->emptyGroup('recursion');

    $hashes[$hash] = 1;

    $flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED;

    if(static::$config['showPrivateMembers'])
      $flags |= \ReflectionProperty::IS_PRIVATE;

    $props = $magicProps = $methods = array();    

    if($reflector->hasMethod('__debugInfo')){
      $magicProps = $subject->__debugInfo();
    }else{
      $props = $reflector->getProperties($flags);
    }

    if(static::$config['showMethods']){
      $flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED;

      if(static::$config['showPrivateMembers'])
        $flags |= \ReflectionMethod::IS_PRIVATE;          

      $methods = $reflector->getMethods($flags);
    }

    $constants  = $reflector->getConstants();
    $interfaces = $reflector->getInterfaces();
    $traits     = static::$env['is54'] ? $reflector->getTraits() : array();
    $parents    = static::getParentClasses($reflector);        

    // work-around for https://bugs.php.net/bug.php?id=49154
    // @see http://stackoverflow.com/questions/15672287/strange-behavior-of-reflectiongetproperties-with-numeric-keys
    if(!static::$env['is54']){      
      $props = array_values(array_filter($props, function($prop) use($subject){
        return !$prop->isPublic() || property_exists($subject, $prop->name);
      }));
    }
    
    // no data to display?        
    if(!$props && !$methods && !$constants && !$interfaces && !$traits){
      unset($hashes[$hash]);
      return $this->fmt->emptyGroup();
    }

    if(!$this->fmt->startGroup())
      return;

    // show contents for iterators
    if(static::$config['showIteratorContents'] && $reflector->isIterateable()){

      $itContents = iterator_to_array($subject);
      $this->fmt->sectionTitle(sprintf('Contents (%d)', count($itContents)));          

      foreach($itContents as $key => $value){
        $keyInfo = gettype($key);
        if($keyInfo === 'string'){
          $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
          $length   = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key);
          $keyInfo  = sprintf('%s(%s)', $keyInfo, $length);        
        }            

        $this->fmt->startRow();
        $this->fmt->text(array('key', 'iterator'), $key, sprintf('Iterator key: %s', $keyInfo));
        $this->fmt->colDiv();
        $this->fmt->sep('=>');
        $this->fmt->colDiv();
        $this->evaluate($value);
        //$this->evaluate($value instanceof \Traversable ? ((count($value) > 0) ? $value : (string)$value) : $value);
        $this->fmt->endRow();
      }
    }        

    // display the interfaces this objects' class implements
    if($interfaces){
      $items = array();            
      $this->fmt->sectionTitle('Implements');
      $this->fmt->startRow();
      $this->fmt->startContain('interfaces');          

      $i     = 0;
      $count = count($interfaces);

      foreach($interfaces as $name => $interface){
        $this->fromReflector($interface);

        if(++$i < $count)
          $this->fmt->sep(', ');
      }  

      $this->fmt->endContain();          
      $this->fmt->endRow();
    } 

    // traits this objects' class uses
    if($traits){       
      $items = array();      
      $this->fmt->sectionTitle('Uses');
      $this->fmt->startRow();
      $this->fmt->startContain('traits');            

      $i     = 0;
      $count = count($traits);          

      foreach($traits as $name => $trait){
        $this->fromReflector($trait);
        
        if(++$i < $count)  
          $this->fmt->sep(', ');                      
      }  

      $this->fmt->endContain();
      $this->fmt->endRow();             
    }

    // class constants
    if($constants){
      $this->fmt->sectionTitle('Constants');
      $max = max(array_map('static::strLen', array_keys($constants)));
      foreach($constants as $name => $value){
        $meta = null;              
        $type = array('const');
        foreach($parents as $parent){
          if($parent->hasConstant($name)){
            if($parent !== $reflector){
              $type[] = 'inherited';
              $meta = array('sub' => array(array('Prototype defined by', $parent->name)));
            }  
            break;
          }
        }

        $this->fmt->startRow();
        $this->fmt->sep('::');  
        $this->fmt->colDiv();
        $this->fmt->startContain($type);
        $this->fmt->text('name', $name, $meta, $this->linkify($parent, $name));
        $this->fmt->endContain();
        $this->fmt->colDiv($max - static::strLen($name));        
        $this->fmt->sep('=');
        $this->fmt->colDiv();
        $this->evaluate($value);          
        $this->fmt->endRow();
      }    
    }

    // object/class properties
    if($props){
      $this->fmt->sectionTitle('Properties');        

      $max = 0;
      foreach($props as $idx => $prop)
        if(($propNameLen = static::strLen($prop->name)) > $max)
          $max = $propNameLen;

      foreach($props as $idx => $prop){

        if($this->hasInstanceTimedOut())
          break;

        $bubbles     = array();
        $sourceClass = $prop->getDeclaringClass();
        $inherited   = $reflector->getShortName() !== $sourceClass->getShortName();
        $meta        = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment());

        if($meta){
          if($inherited)
            $meta['sub'] = array(array('Declared in', $sourceClass->getShortName()));

          if(isset($meta['tags']['var'][0]))
            $meta['left'] = $meta['tags']['var'][0][0];

          unset($meta['tags']);        
        }

        if($prop->isProtected() || $prop->isPrivate())        
          $prop->setAccessible(true);

        $value = $prop->getValue($subject);

        $this->fmt->startRow();
        $this->fmt->sep($prop->isStatic() ? '::' : '->');
        $this->fmt->colDiv();

        $bubbles  = array();
        if($prop->isProtected())
          $bubbles[] = array('P', 'Protected');

        if($prop->isPrivate())
          $bubbles[] = array('!', 'Private');

        $this->fmt->bubbles($bubbles);

        $type = array('prop');

        if($inherited)
          $type[] = 'inherited';

        if($prop->isPrivate())
          $type[] = 'private';

        $this->fmt->colDiv(2 - count($bubbles));
        $this->fmt->startContain($type);
        $this->fmt->text('name', $prop->name, $meta, $this->linkify($prop));
        $this->fmt->endContain();
        $this->fmt->colDiv($max - static::strLen($prop->name));
        $this->fmt->sep('=');
        $this->fmt->colDiv();
        $this->evaluate($value);
        $this->fmt->endRow();
      }
    }    

    // __debugInfo()
    if($magicProps){
      $this->fmt->sectionTitle('Properties (magic)');

      $max = 0;
      foreach($magicProps as $name => $value)
        if(($propNameLen = static::strLen($name)) > $max)
          $max = $propNameLen;

      foreach($magicProps as $name => $value){

        if($this->hasInstanceTimedOut())
          break;

        // attempt to pull out doc comment from the "regular" property definition
        try{
          $prop = $reflector->getProperty($name);
          $meta = static::parseComment($prop->getDocComment());

        }catch(\Exception $e){
          $meta = null;
        }

        $this->fmt->startRow();
        $this->fmt->sep('->');
        $this->fmt->colDiv();

        $type = array('prop');

        $this->fmt->startContain($type);
        $this->fmt->text('name', $name, $meta);
        $this->fmt->endContain();
        $this->fmt->colDiv($max - static::strLen($name));
        $this->fmt->sep('=');
        $this->fmt->colDiv();
        $this->evaluate($value);
        $this->fmt->endRow();        
      }
    }      

    // class methods
    if($methods && !$this->hasInstanceTimedOut()){

      $this->fmt->sectionTitle('Methods');  
      foreach($methods as $idx => $method){

        $this->fmt->startRow();
        $this->fmt->sep($method->isStatic() ? '::' : '->');
        $this->fmt->colDiv();
        
        $bubbles = array();
        if($method->isAbstract())
          $bubbles[] = array('A', 'Abstract');

        if($method->isFinal())
          $bubbles[] = array('F', 'Final');

        if($method->isProtected())
          $bubbles[] = array('P', 'Protected');

        if($method->isPrivate())
          $bubbles[] = array('!', 'Private');

        $this->fmt->bubbles($bubbles);
         
        $this->fmt->colDiv(4 - count($bubbles));

        // is this method inherited?
        $inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName();

        $type = array('method');

        if($inherited)
          $type[] = 'inherited';

        if($method->isPrivate())
          $type[] = 'private';

        $this->fmt->startContain($type);

        $name = $method->name;
        if($method->returnsReference())
          $name = "&{$name}";    

        $this->fromReflector($method, $name, $reflector); 

        $paramCom   = $method->isInternal() ? array() : static::parseComment($method->getDocComment(), 'tags');
        $paramCom   = empty($paramCom['param']) ? array() : $paramCom['param'];
        $paramCount = $method->getNumberOfParameters();

        $this->fmt->sep('(');

        // process arguments
        foreach($method->getParameters() as $idx => $parameter){
          $meta      = null;
          $paramName = "\${$parameter->name}";
          $optional  = $parameter->isOptional();
          $variadic  = static::$env['is56'] && $parameter->isVariadic();

          if($parameter->isPassedByReference())
            $paramName = "&{$paramName}";

          if($variadic)
            $paramName = "...{$paramName}";

          $type = array('param');

          if($optional)
            $type[] = 'optional';

          $this->fmt->startContain($type);            
          
          // attempt to build meta
          foreach($paramCom as $tag){
            list($pcTypes, $pcName, $pcDescription) = $tag;
            if($pcName !== $paramName)
              continue;

            $meta = array('title' => $pcDescription);

            if($pcTypes)
              $meta['left'] = $pcTypes;

            break;
          }
       
          try{
            $paramClass = $parameter->getClass();
          }catch(\Exception $e){
            // @see https://bugs.php.net/bug.php?id=32177&edit=1
          }

          if(!empty($paramClass)){
            $this->fmt->startContain('hint');
            $this->fromReflector($paramClass, $paramClass->name);
            $this->fmt->endContain();
            $this->fmt->sep(' ');
          
          }elseif($parameter->isArray()){
            $this->fmt->text('hint', 'array');
            $this->fmt->sep(' ');            

          }else{
            $hasType = static::$env['is7'] && $parameter->hasType();
            if($hasType){
              $type = $parameter->getType();
              $this->fmt->text('hint', (string)$type);
              $this->fmt->sep(' ');
            }
          }

          $this->fmt->text('name', $paramName, $meta);

          if($optional){
            $paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;            
            if ($paramValue !== null) {
                $this->fmt->sep(' = ');

                if(static::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant()){
                  $this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant');

                }else{
                  $this->evaluate($paramValue, true);
                }
            }
          }

          $this->fmt->endContain();

          if($idx < $paramCount - 1)
            $this->fmt->sep(', ');
        }
        $this->fmt->sep(')');
        $this->fmt->endContain();

        $hasReturnType = static::$env['is7'] && $method->hasReturnType();
        if($hasReturnType){
          $type = $method->getReturnType();
          $this->fmt->startContain('ret');
          $this->fmt->sep(':');
          $this->fmt->text('hint', (string)$type);
          $this->fmt->endContain();
        }

        $this->fmt->endRow();
      }
    }  

    unset($hashes[$hash]);
    $this->fmt->endGroup();

    $this->fmt->cacheLock($hash);
  }



  /**
   * Scans for known classes and functions inside the provided expression,
   * and linkifies them when possible
   *
   * @param   string $expression   Expression to format
   * @return  string               Formatted output
   */
  protected function evaluateExp($expression = null){

    if($expression === null)
      return;

    if(static::strLen($expression) > 120)
      $expression = substr($expression, 0, 120) . '...';

    $this->fmt->sep('> ');

    if(strpos($expression, '(') === false)
      return $this->fmt->text('expTxt', $expression);

    $keywords = array_map('trim', explode('(', $expression, 2));
    $parts = array();

    // try to find out if this is a function
    try{
      $reflector = new \ReflectionFunction($keywords[0]);
      $parts[] = array($keywords[0], $reflector, '');
    
    }catch(\Exception $e){

      if(stripos($keywords[0], 'new ') === 0){
        $cn = explode(' ' , $keywords[0], 2);

        // linkify 'new keyword' (as constructor)
        try{          
          $reflector = new \ReflectionMethod($cn[1], '__construct');
          $parts[] = array($cn[0], $reflector, '');

        }catch(\Exception $e){
          $reflector = null;
          $parts[] = $cn[0];
        }            

        // class name...
        try{          
          $reflector = new \ReflectionClass($cn[1]);
          $parts[] = array($cn[1], $reflector, ' ');

        }catch(\Exception $e){
          $reflector = null;
          $parts[] = $cn[1];
        }      

      }else{

        // we can only linkify methods called statically
        if(strpos($keywords[0], '::') === false)
          return $this->fmt->text('expTxt', $expression);

        $cn = explode('::', $keywords[0], 2);

        // attempt to linkify class name
        try{
          $reflector = new \ReflectionClass($cn[0]);
          $parts[] = array($cn[0], $reflector, '');

        }catch(\Exception $e){
          $reflector = null;
          $parts[] = $cn[0];
        }        

        // perhaps it's a static class method; try to linkify method
        try{
          $reflector = new \ReflectionMethod($cn[0], $cn[1]);
          $parts[] = array($cn[1], $reflector, '::');

        }catch(\Exception $e){
          $reflector = null;
          $parts[] = $cn[1];
        }              
      }
    }

    $parts[] = "({$keywords[1]}";    

    foreach($parts as $element){
      if(!is_array($element)){
        $this->fmt->text('expTxt', $element);
        continue;
      }

      list($text, $reflector, $prefix) = $element;

      if($prefix !== '')
        $this->fmt->text('expTxt', $prefix);      

      $this->fromReflector($reflector, $text);
    }
  
  }



  /**
   * Calculates real string length
   *
   * @param   string $string
   * @return  int
   */
  protected static function strLen($string){
    $encoding = function_exists('mb_detect_encoding') ? mb_detect_encoding($string) : false;   
    return $encoding ? mb_strlen($string, $encoding) : strlen($string);
  }



  /**
   * Safe str_pad alternative
   *
   * @param   string $string
   * @param   int $padLen
   * @param   string $padStr
   * @param   int $padType
   * @return  string
   */
  protected static function strPad($input, $padLen, $padStr = ' ', $padType = STR_PAD_RIGHT){
    $diff = strlen($input) - static::strLen($input);
    return str_pad($input, $padLen + $diff, $padStr, $padType);
  }

}



/**
 * Formatter abstraction
 */ 
abstract class RFormatter{

  /**
   * Flush output and send contents to the output device
   */ 
  abstract public function flush();

  /**
   * Generate a base entity
   *
   * @param  string|array $type
   * @param  string|null $text
   * @param  string|array|null $meta
   * @param  string|null $uri
   */ 
  abstract public function text($type, $text = null, $meta = null, $uri = null);

  /**
   * Generate container start token
   *
   * @param  string|array $type
   * @param  string|bool $label
   */ 
  public function startContain($type, $label = false){}

  /**
   * Generate container ending token
   */ 
  public function endContain(){}  

  /**
   * Generate empty group token
   *
   * @param  string $prefix
   */ 
  public function emptyGroup($prefix = ''){}

  /**
   * Generate group start token
   *
   * This method must return boolean TRUE on success, false otherwise (eg. max depth reached).
   * The evaluator will skip this group on FALSE
   *
   * @param   string $prefix
   * @return  bool
   */ 
  public function startGroup($prefix = ''){}

  /**
   * Generate group ending token   
   */ 
  public function endGroup(){}

  /**
   * Generate section title
   *
   * @param  string $title
   */ 
  public function sectionTitle($title){}

  /**
   * Generate row start token
   */ 
  public function startRow(){}

  /**
   * Generate row ending token
   */ 
  public function endRow(){}

  /**
   * Column divider (cell delimiter)
   *
   * @param  int $padLen
   */ 
  public function colDiv($padLen = null){}

  /**
   * Generate modifier tokens
   *
   * @param  array $items
   */ 
  public function bubbles(array $items){}

  /**
   * Input expression start
   */ 
  public function startExp(){}

  /**
   * Input expression end
   */ 
  public function endExp(){}

  /**
   * Root starting token
   */ 
  public function startRoot(){}

  /**
   * Root ending token
   */ 
  public function endRoot(){}

  /**
   * Separator token
   *
   * @param  string $label
   */
  public function sep($label = ' '){}  

  /**
   * Resolve cache request
   *
   * If the ID is not present in the cache, then a new cache entry is created
   * for the given ID, and string offsets are captured until cacheLock is called
   *
   * This method must return TRUE if the ID exists in the cache, and append the cached item
   * to the output, FALSE otherwise.
   *
   * @param   string $id
   * @return  bool
   */ 
  public function didCache($id){
    return false;
  }

  /**
   * Ends cache capturing for the given ID
   *
   * @param  string $id
   */ 
  public function cacheLock($id){}

}




/**
 * Generates the output in HTML5 format
 *   
 */
class RHtmlFormatter extends RFormatter{

  protected

    /**
     * Actual output
     *
     * @var  string
     */  
    $out = '',

    /**
     * Tracks current nesting level
     *
     * @var  int
     */  
    $level = 0,
     
    /**
     * Stores tooltip content for all entries
     *
     * To avoid having duplicate tooltip data in the HTML, we generate them once,
     * and use references (the Q index) to pull data when required; 
     * this improves performance significantly 
     *
     * @var  array
     */     
    $tips = array(),

    /**
     * Used to cache output to speed up processing.
     *
     * Contains hashes as keys and string offsets as values.
     * Cached objects will not be processed again in the same query
     *
     * @var  array
     */ 
    $cache = array(),

    /**
     * Map of used HTML tag and attributes
     *
     * @var string
     */
    $def = array();



  protected static

    /**
     * Instance counter
     *
     * @var  int
     */   
    $counter = 0,

    /**
     * Tracks style/jscript inclusion state
     *
     * @var  bool
     */    
    $didAssets = false;


  public function __construct(){

    if(ref::config('validHtml')){

      $this->def = array(
        'base'   => 'span',
        'tip'    => 'div',
        'cell'   => 'data-cell',
        'table'  => 'data-table',
        'row'    => 'data-row',
        'group'  => 'data-group',
        'gLabel' => 'data-gLabel',
        'match'  => 'data-match',
        'tipRef' => 'data-tip',
      );


    }else{

      $this->def = array(
        'base'   => 'r',
        'tip'    => 't',
        'cell'   => 'c',
        'table'  => 't',        
        'row'    => 'r',        
        'group'  => 'g',
        'gLabel' => 'gl',
        'match'  => 'm',
        'tipRef' => 'h',
      );

    }

  }



  public function flush(){
    print $this->out;
    $this->out   = '';
    $this->cache = array();
    $this->tips  = array();
  }


  public function didCache($id){

    if(!isset($this->cache[$id])){
      $this->cache[$id] = array();
      $this->cache[$id][] = strlen($this->out);     
      return false;
    }

    if(!isset($this->cache[$id][1])){
      $this->cache[$id][0] = strlen($this->out);
      return false;
    }

    $this->out .= substr($this->out, $this->cache[$id][0], $this->cache[$id][1]);
    return true;
  }
  
  public function cacheLock($id){
    $this->cache[$id][] = strlen($this->out) - $this->cache[$id][0];
  }


  public function sep($label = ' '){
    $this->out .= $label !== ' ' ? '<i>' . static::escape($label) . '</i>' : $label;
  }  

  public function text($type, $text = null, $meta = null, $uri = null){

    if(!is_array($type))
      $type = (array)$type;

    $tip  = '';
    $text = ($text !== null) ? static::escape($text) : static::escape($type[0]);    

    if(in_array('special', $type)){
      $text = strtr($text, array(
        "\r" => '<i>\r</i>',     // carriage return
        "\t" => '<i>\t</i>',     // horizontal tab
        "\n" => '<i>\n</i>',     // linefeed (new line)
        "\v" => '<i>\v</i>',     // vertical tab
        "\e" => '<i>\e</i>',     // escape
        "\f" => '<i>\f</i>',     // form feed
        "\0" => '<i>\0</i>',
      ));
    }

    // generate tooltip reference (probably the slowest part of the code ;)
    if($meta !== null){       
      $tipIdx = array_search($meta, $this->tips, true);

      if($tipIdx === false)
        $tipIdx = array_push($this->tips, $meta) - 1;

      $tip = " {$this->def['tipRef']}=\"{$tipIdx}\"";      
      //$tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx);
    }

    // wrap text in a link?
    if($uri !== null)
      $text = '<a href="' . $uri . '" target="_blank">' . $text . '</a>';

    $typeStr = '';
    foreach($type as $part)
      $typeStr .= " data-{$part}";

    $this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}</{$this->def['base']}>";    
    //$this->out .= sprintf('<%1$s%2$s %3$s>%4$s</%1$s>', $this->def['base'], $typeStr, $tip, $text);
  }

  public function startContain($type, $label = false){

    if(!is_array($type))
      $type = (array)$type;    

    if($label)
      $this->out .= '<br>';

    $typeStr = '';
    foreach($type as $part)
      $typeStr .= " data-{$part}";

    $this->out .= "<{$this->def['base']}{$typeStr}>";

    if($label)
      $this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}</{$this->def['base']}>";
  }    

  public function endContain(){
    $this->out .= "</{$this->def['base']}>";
  } 

  public function emptyGroup($prefix = ''){

    if($prefix !== '')
      $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "</{$this->def['base']}>";
    
    $this->out .= "<i>(</i>{$prefix}<i>)</i>";
  }    


  public function startGroup($prefix = ''){

    $maxDepth = ref::config('maxDepth');

    if(($maxDepth > 0) && (($this->level + 1) > $maxDepth)){
      $this->emptyGroup('...');   
      return false;
    }   

    $this->level++;

    $expLvl = ref::config('expLvl');
    $exp = ($expLvl < 0) || (($expLvl > 0) && ($this->level <= $expLvl)) ? ' data-exp' : '';
    
    if($prefix !== '')
      $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "</{$this->def['base']}>";
   
    $this->out .= "<i>(</i>{$prefix}<{$this->def['base']} data-toggle{$exp}></{$this->def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>";

    return true;
  }

  public function endGroup(){
    $this->out .= "</{$this->def['base']}></{$this->def['base']}><i>)</i>";
    $this->level--;
  }
 
  public function sectionTitle($title){
    $this->out .= "</{$this->def['base']}><{$this->def['base']} data-tHead>{$title}</{$this->def['base']}><{$this->def['base']} {$this->def['table']}>";
  }

  public function startRow(){
    $this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>";
  } 

  public function endRow(){
    $this->out .= "</{$this->def['base']}></{$this->def['base']}>";
  } 
     
  public function colDiv($padLen = null){
    $this->out .= "</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>";
  }   

  public function bubbles(array $items){

    if(!$items)
      return;

    $this->out .= "<{$this->def['base']} data-mod>";   

    foreach($items as $info)
      $this->out .= $this->text('mod-' . strtolower($info[1]), $info[0], $info[1]);

    $this->out .= "</{$this->def['base']}>";
  }      
      
  public function startExp(){
    $this->out .= "<{$this->def['base']} data-input>";
  }                    

  public function endExp(){
    if(ref::config('showBacktrace') && ($trace = ref::getBacktrace())){
      $docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '';
      $path = strpos($trace['file'], $docRoot) !== 0 ? $trace['file'] : ltrim(str_replace($docRoot, '', $trace['file']), '/');
      $this->out .= "<{$this->def['base']} data-backtrace>{$path}:{$trace['line']}</{$this->def['base']}>";
    }

    $this->out .= "</{$this->def['base']}><{$this->def['base']} data-output>";
  } 

  public function startRoot(){
    $this->out .= '<!-- ref#' . ++static::$counter . ' --><div>' . static::getAssets() . '<div class="ref">';
  }                    

  public function endRoot(){
    $this->out .= "</{$this->def['base']}>";

    // process tooltips
    $tipHtml = '';
    foreach($this->tips as $idx => $meta){

      $tip = '';
      if(!is_array($meta))
        $meta = array('title' => $meta);

      $meta += array(
        'title'       => '',
        'left'        => '',
        'description' => '',
        'tags'        => array(),
        'sub'         => array(),            
      );

      $meta = static::escape($meta);
      $cols = array();

      if($meta['left'])
        $cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}</{$this->def['base']}>";

      $title = $meta['title'] ?       "<{$this->def['base']} data-title>{$meta['title']}</{$this->def['base']}>"       : '';
      $desc  = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}</{$this->def['base']}>"  : '';
      $tags  = '';

      foreach($meta['tags'] as $tag => $values){
        foreach($values as $value){
          if($tag === 'param'){
            $value[0] = "{$value[0]} {$value[1]}";
            unset($value[1]);
          }

          $value  = is_array($value) ? implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $value) : $value;
          $tags  .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}</{$this->def['base']}></{$this->def['base']}>";
        }
      }

      if($tags)
        $tags = "<{$this->def['base']} {$this->def['table']}>{$tags}</{$this->def['base']}>";

      if($title || $desc || $tags)
        $cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}</{$this->def['base']}>";

      if($cols)
        $tip = "<{$this->def['base']} {$this->def['row']}>" . implode('', $cols) . "</{$this->def['base']}>";

      $sub = '';
      foreach($meta['sub'] as $line)
        $sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>" . implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $line) . "</{$this->def['base']}></{$this->def['base']}>";
    
      if($sub)
        $tip .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']} data-sub><{$this->def['base']} {$this->def['table']}>{$sub}</{$this->def['base']}></{$this->def['base']}></{$this->def['base']}>";

      if($tip)
        $this->out .= "<{$this->def['tip']}>{$tip}</{$this->def['tip']}>";
    }

    if(($timeout = ref::getTimeoutPoint()) > 0)
      $this->out .= sprintf("<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fs</{$this->def['base']}>", $timeout);

    $this->out .= '</div></div><!-- /ref#' . static::$counter . ' -->';        
  }



  /**
   * Get styles and javascript (only generated for the 1st call)
   *
   * @return  string
   */
  public static function getAssets(){
    
    // first call? include styles and javascript
    if(static::$didAssets)
      return '';   

    ob_start();

    if(ref::config('stylePath') !== false){
      ?>
      <style>
        <?php readfile(str_replace('{:dir}', __DIR__, ref::config('stylePath'))); ?>
      </style>
      <?php
    }

    if(ref::config('scriptPath') !== false){
      ?>
      <script>
        <?php readfile(str_replace('{:dir}', __DIR__, ref::config('scriptPath'))); ?>
      </script>
      <?php
    }  

    // normalize space and remove comments
    $output = preg_replace('/\s+/', ' ', trim(ob_get_clean()));
    $output = preg_replace('!/\*.*?\*/!s', '', $output);
    $output = preg_replace('/\n\s*\n/', "\n", $output);

    static::$didAssets = true;
    return $output;
  }


  /**
   * Escapes variable for HTML output
   *
   * @param   string|array $var
   * @return  string|array
   */
  protected static function escape($var){
    return is_array($var) ? array_map('static::escape', $var) : htmlspecialchars($var, ENT_QUOTES);
  }

}



/**
 * Generates the output in plain text format
 *   
 */
class RTextFormatter extends RFormatter{

  protected

    /**
     * Actual output
     *
     * @var  string
     */  
    $out        = '',

    /**
     * Tracks current nesting level
     *
     * @var  int
     */  
    $level      = 0,    

    /**
     * Current indenting level
     *
     * @var  int
     */ 
    $indent     = 0,

    $lastIdx    = 0,
    $lastLineSt = 0,
    $levelPad   = array(0);



  public function flush(){
    print $this->out;    
    $this->out   = '';
    $this->cache = array();
  }

  public function sep($label = ' '){
    $this->out .= $label;
  }  

  public function text($type, $text = null, $meta = null, $uri = null){

    if(!is_array($type))
      $type = (array)$type;

    if($text === null)
      $text = $type[0];

    if(in_array('special', $type, true)){
      $text = strtr($text, array(
        "\r" => '\r',     // carriage return
        "\t" => '\t',     // horizontal tab
        "\n" => '\n',     // linefeed (new line)
        "\v" => '\v',     // vertical tab
        "\e" => '\e',     // escape
        "\f" => '\f',     // form feed
        "\0" => '\0',
      ));

      $this->out .= $text;
      return;
    }        

    $formatMap = array(
      'string'   => '%3$s "%2$s"',
      'integer'  => 'int(%2$s)',
      'double'   => 'double(%2$s)',
      'true'     => 'bool(%2$s)',
      'false'    => 'bool(%2$s)',
      'key'      => '[%2$s]',
    );

    if(!is_string($meta))
      $meta = '';

    $this->out .= isset($formatMap[$type[0]]) ? sprintf($formatMap[$type[0]], $type[0], $text, $meta) : $text;
  }

  public function startContain($type, $label = false){

    if(!is_array($type))
      $type = (array)$type;   

    if($label)
      $this->out .= "\n" . str_repeat(' ', $this->indent + $this->levelPad[$this->level]) . "┗ {$type[0]} ~ ";
  }    

  public function emptyGroup($prefix = ''){
    $this->out .= "({$prefix})";
  }    

  public function startGroup($prefix = ''){

    $maxDepth = ref::config('maxDepth');

    if(($maxDepth > 0) && (($this->level + 1) > $maxDepth)){
      $this->emptyGroup('...');              
      return false;
    }

    $this->level++;  
    $this->out .= '(';  

    $this->indent += $this->levelPad[$this->level - 1];
    return true;
  }

  public function endGroup(){
    $this->out .= "\n" . str_repeat(' ', $this->indent) . ')';
    $this->indent -= $this->levelPad[$this->level - 1];
    $this->level--;
  }

  public function sectionTitle($title){
    $pad = str_repeat(' ', $this->indent + 2);
    $this->out .= sprintf("\n\n%s%s\n%s%s", $pad, $title, $pad, str_repeat('-', strlen($title)));
  }

  public function startRow(){
    $this->out .= "\n  " . str_repeat(' ', $this->indent);
    $this->lastLineSt = strlen($this->out);
  } 

  public function endRow(){
  } 
     
  public function colDiv($padLen = null){
    $padLen = ($padLen !== null) ? $padLen + 1 : 1;
    $this->out .= str_repeat(' ', $padLen);

    $this->lastIdx = strlen($this->out);
    $this->levelPad[$this->level] = $this->lastIdx - $this->lastLineSt + 2;     
  }   

  public function bubbles(array $items){    

    if(!$items){
      $this->out .= '  ';
      return;
    }

    $this->out .= '<';

    foreach($items as $item)
      $this->out .= $item[0];

    $this->out .= '>';
  }      

  public function endExp(){

    if(ref::config('showBacktrace') && ($trace = ref::getBacktrace()))
      $this->out .= ' - ' . $trace['file'] . ':' . $trace['line'];

    $this->out .= "\n" . str_repeat('=', strlen($this->out)) . "\n";
  }                    
 
  public function startRoot(){
    $this->out .= "\n\n";

  }                    

  public function endRoot(){
    $this->out .= "\n"; 
    if(($timeout = ref::getTimeoutPoint()) > 0)
      $this->out .= sprintf("\n-- Listing incomplete. Timed-out after %4.2fs -- \n", $timeout);    
  }

}



/**
 * Text formatter with color support for CLI -- unfinished
 *   
 */
class RCliTextFormatter extends RTextFormatter{

  public function sectionTitle($title){
    $pad = str_repeat(' ', $this->indent + 2);
    $this->out .= sprintf("\n\n%s\x1b[4;97m%s\x1b[0m", $pad, $title);    
  }

  public function startExp(){
    $this->out .= "\x1b[1;44;96m ";
  }                    

  public function endExp(){
    if(ref::config('showBacktrace') && ($trace = ref::getBacktrace()))
      $this->out .= "\x1b[0m\x1b[44;36m " . $trace['file'] . ':' . $trace['line'];

    $this->out .=  " \x1b[0m\n";
  }                                 

  public function endRoot(){
    $this->out .= "\n"; 
    if(($timeout = ref::getTimeoutPoint()) > 0)
      $this->out .= sprintf("\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout);    
  }

}
