<?php
/**
 * a heavily modified version of:
 */
/**
 * Underscore.php v1.3.1
 * Copyright (c) 2011 Brian Haveri
 * Underscore.php is licensed under the MIT license
 * Underscore.php was inspired by and borrowed from Underscore.js
 * For docs, license, tests, and downloads, see: http://brianhaveri.github.com/Underscore.php
 */

namespace CMSMSExt\underscore
{
  // Returns an instance of __ for OO-style calls
  function __($item = NULL)
  {
    $__ = new __;
    if(\func_num_args() > 0) { $__->_wrapped = $item; }
  
    return $__;
  }
  
  // Underscore.php
  class __
  {
    // Start the chain
    private bool $_chained = FALSE; // Are we in a chain?
  
    private function chain($item = NULL) : __
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      $__           = (isset($this) && isset(self::$_chained) && self::$_chained) ? $this : __($item);
      $__->_chained = TRUE;
  
      return $__;
    }
  
  
    // End the chain
    private function value()
    {
      return ( isset($this) ) ? $this->_wrapped : NULL;
    }
  
  
    // Invoke the iterator on each item in the collection
    private function each($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      if(NULL === $collection)
      {
        return self::_wrap(NULL);
      }
  
      $collection = (array)self::_collection($collection);
      if( 0 === \count($collection) )
      {
        return self::_wrap(NULL);
      }
  
      foreach($collection as $k => $v)
      {
        $iterator($v, $k, $collection);
      }
  
      return self::_wrap(NULL);
    }
  
  
    // Return an array of values by mapping each item through the iterator
    // map alias: collect
    private function collect($collection = NULL, $iterator = NULL) { return self::map($collection, $iterator); }
  
    private function map($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      if(NULL === $collection)
      {
        return self::_wrap([]);
      }
  
      $collection = (array)self::_collection($collection);
      if(0 === \count($collection))
      {
        self::_wrap([]);
      }
  
      $return = [];
      foreach($collection as $k => $v)
      {
        $return[] = $iterator($v, $k, $collection);
      }
  
      return self::_wrap($return);
    }
  
  
    // Reduce a collection to a single value
    // reduce aliases: foldl, inject
    private function foldl($collection = NULL, $iterator = NULL, $memo = NULL)
    {
      return self::reduce(
        $collection, $iterator, $memo
      );
    }
  
    private function inject($collection = NULL, $iterator = NULL, $memo = NULL)
    {
      return self::reduce(
        $collection, $iterator, $memo
      );
    }
  
    private function reduce($collection = NULL, $iterator = NULL, $memo = NULL)
    {
      [$collection, $iterator, $memo] = self::_wrapArgs(\func_get_args(), 3);
  
      if(!\is_object($collection) && !\is_array($collection))
      {
        if(NULL === $memo)
        {
          throw new \Exception('Invalid object');
        }
        else
        {
          return self::_wrap($memo);
        }
      }
  
      return self::_wrap(\array_reduce($collection, $iterator, $memo));
    }
  
  
    // Right-associative version of reduce
    // reduceRight alias: foldr
    private function foldr($collection = NULL, $iterator = NULL, $memo = NULL)
    {
      return self::reduceRight(
        $collection, $iterator, $memo
      );
    }
  
    private function reduceRight($collection = NULL, $iterator = NULL, $memo = NULL)
    {
      [$collection, $iterator, $memo] = self::_wrapArgs(\func_get_args(), 3);
  
      if(!\is_object($collection) && !\is_array($collection))
      {
        if(NULL === $memo)
        {
          throw new \Exception('Invalid object');
        }
        else
        {
          return self::_wrap($memo);
        }
      }
  
      \krsort($collection);
  
      $__ = new self;
  
      return self::_wrap($__->reduce($collection, $iterator, $memo));
    }
  
  
    // Extract an array of values for a given property
    private function pluck($collection = NULL, $key = NULL)
    {
      [$collection, $key] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = (array)self::_collection($collection);
  
      $return = [];
      foreach($collection as $item)
      {
        foreach($item as $k => $v)
        {
          if($k === $key)
          {
            $return[] = $v;
          }
        }
      }
  
      return self::_wrap($return);
    }
  
  
    // Does the collection contain this value?
    // includ alias: contains
    private function contains($collection = NULL, $val = NULL) { return self::includ($collection, $val); }
  
    private function includ($collection = NULL, $val = NULL)
    {
      [$collection, $val] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = (array)self::_collection($collection);
  
      return self::_wrap( (\in_array($val, $collection, TRUE) ) );
    }
  
  
    // Invoke the named function over each item in the collection, optionally passing arguments to the function
    private function invoke($collection = NULL, $function_name = NULL, $arguments = NULL)
    {
      $args = self::_wrapArgs(\func_get_args(), 2);
      $__   = new self;
      [$collection, $function_name] = $__->first($args, 2);
      $arguments = $__->rest(\func_get_args(), 2);
  
      // If passed an array or string, return an array
      // If passed an object, return an object
      $is_obj = \is_object($collection);
      $result = (empty($arguments))
        ? \array_map($function_name, (array)$collection)
        : \array_map(
          $function_name, (array)$collection, $arguments
        );
      if($is_obj)
      {
        $result = (object)$result;
      }
  
      return self::_wrap($result);
    }
  
  
    // Does any values in the collection meet the iterator's truth test?
    // any alias: some
    private function some($collection = NULL, $iterator = NULL) { return self::any($collection, $iterator); }
  
    private function any($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      $__ = new self;
      if(NULL !== $iterator)
      {
        $collection = $__->map($collection, $iterator);
      }
      if(0 === \count($collection))
      {
        return self::_wrap(FALSE);
      }
  
      return self::_wrap(\is_int(\array_search(TRUE, $collection, FALSE)));
    }
  
  
    // Do all values in the collection meet the iterator's truth test?
    // all alias: every
    private function every($collection = NULL, $iterator = NULL) { return self::all($collection, $iterator); }
  
    private function all($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      $__ = new self;
      if(NULL !== $iterator)
      {
        $collection = $__->map($collection, $iterator);
      }
      $collection = (array)$collection;
      if(0 === \count($collection))
      {
        return TRUE;
      }
  
      return self::_wrap(\is_bool(\array_search(FALSE, $collection, FALSE)));
    }
  
  
    // Return an array of values that pass the truth iterator test
    // filter alias: select
    private function select($collection = NULL, $iterator = NULL) { return self::filter($collection, $iterator); }
  
    private function filter($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      $return = [];
      foreach($collection as $val)
      {
        if(\call_user_func($iterator, $val))
        {
          $return[] = $val;
        }
      }
  
      return self::_wrap($return);
    }
  
  
    // Return an array where the items failing the truth test are removed
    private function reject($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      $return = [];
      foreach($collection as $val)
      {
        if(!\call_user_func($iterator, $val))
        {
          $return[] = $val;
        }
      }
  
      return self::_wrap($return);
    }
  
  
    // Return the value of the first item passing the truth iterator test
    // find alias: detect
    private function detect($collection = NULL, $iterator = NULL) { return self::find($collection, $iterator); }
  
    private function find($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      foreach($collection as $val)
      {
        if($iterator($val)) { return $val; }
      }
  
      return self::_wrap(FALSE);
    }
  
  
    // How many items are in this collection?
    private function size($collection = NULL)
    {
      [$collection] = self::_wrapArgs(\func_get_args(), 1);
  
      $collection = self::_collection($collection);
  
      return self::_wrap(\count((array)$collection));
    }
  
  
    // Get the first element of an array. Passing n returns the first n elements.
    // first alias: head
    private function head($collection = NULL, $n = NULL) { return self::first($collection, $n); }
  
    private function first($collection = NULL, $n = NULL)
    {
      [$collection, $n] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      if(0 === $n)
      {
        return self::_wrap([]);
      }
      if(NULL === $n)
      {
        return self::_wrap(\current(\array_splice($collection, 0, 1, TRUE)));
      }
  
      return self::_wrap(\array_splice($collection, 0, $n, TRUE));
    }
  
  
    // Get the rest of the array elements. Passing n returns from that index onward.
    private function tail($collection = NULL, $index = NULL) { return self::rest($collection, $index); }
  
    private function rest($collection = NULL, $index = NULL)
    {
      [$collection, $index] = self::_wrapArgs(\func_get_args(), 2);
  
      if(NULL === $index) { $index = 1; }
  
      $collection = self::_collection($collection);
  
      return self::_wrap(\array_splice($collection, $index));
    }
  
  
    // Return everything but the last array element. Passing n excludes the last n elements.
    private function initial($collection = NULL, $n = NULL)
    {
      [$collection, $n] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = (array)self::_collection($collection);
  
      if(NULL === $n) { $n = 1; }
  
      $first_index = \count($collection) - $n;
      $__          = new self;
  
      return self::_wrap($__->first($collection, $first_index));
    }
  
  
    // Get the last element from an array. Passing n returns the last n elements.
    private function last($collection = NULL, $n = NULL)
    {
      [$collection, $n] = self::_wrapArgs(\func_get_args(), 2);
      $collection = self::_collection($collection);
  
      if(0 === $n)
      {
        $result = [];
      }
      else if(1 === $n || NULL === $n)
      {
        $result = \array_pop($collection);
      }
      else
      {
        $__     = new self;
        $result = $__->rest($collection, -$n);
      }
  
      return self::_wrap($result);
    }
  
  
    // Return a copy of the array with falsy values removed
    private function compact($collection = NULL)
    {
      [$collection] = self::_wrapArgs(\func_get_args(), 1);
  
      $collection = self::_collection($collection);
  
      $__ = new self;
  
      return self::_wrap(
        $__->select($collection, function($val) {
          return (bool)$val;
        })
      );
    }
  
  
    // Flattens a multidimensional array
    private function flatten($collection = NULL, $shallow = NULL)
    {
      [$collection, $shallow] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      $return = [];
      if(\count($collection) > 0)
      {
        foreach($collection as $item)
        {
          if(\is_array($item))
          {
            $__     = new self;
            $return = array_merge($return, ($shallow) ? $item : $__->flatten($item));
          }
          else
          {
            $return[] = $item;
          }
        }
      }
  
      return self::_wrap($return);
    }
  
  
    // Returns a copy of the array with all instances of val removed
    private function without($collection = NULL, $val = NULL)
    {
      $args       = self::_wrapArgs(\func_get_args(), 1);
      $collection = $args[0];
      $collection = self::_collection($collection);
  
      $num_args = \count($args);
      if(1 === $num_args) { return self::_wrap($collection); }
  
      if(0 === \count($collection)) { return self::_wrap($collection); }
  
      $__      = new self;
      $removes = $__->rest($args);
      foreach($removes as $remove)
      {
        $remove_keys = \array_keys($collection, $remove, TRUE);
        if(\count($remove_keys) > 0)
        {
          foreach($remove_keys as $key)
          {
            unset($collection[$key]);
          }
        }
      }
  
      return self::_wrap($collection);
    }
  
  
    // Return an array of the unique values
    // uniq alias: unique
    private function unique($collection = NULL, $is_sorted = NULL, $iterator = NULL)
    {
      return self::uniq(
        $collection, $is_sorted, $iterator
      );
    }
  
    private function uniq($collection = NULL, $is_sorted = NULL, $iterator = NULL)
    {
      [$collection, $is_sorted, $iterator] = self::_wrapArgs(\func_get_args(), 3);
  
      $collection = self::_collection($collection);
  
      $return = [];
      if(0 === \count($collection))
      {
        return self::_wrap($return);
      }
  
      $calculated = [];
      foreach($collection as $item)
      {
        $val = (NULL !== $iterator) ? $iterator($item) : $item;
        if(\is_bool(\array_search($val, $calculated, TRUE)))
        {
          $calculated[] = $val;
          $return[]     = $item;
        }
      }
  
      return self::_wrap($return);
    }
  
  
    // Returns an array containing the intersection of all the arrays
    private function intersection($array = NULL)
    {
      $arrays = self::_wrapArgs(\func_get_args(), 1);
  
      if(1 === \count($arrays))
      {
        return self::_wrap($array);
      }
  
      $__     = new self;
      $return = $__->first($arrays);
      foreach($__->rest($arrays) as $next)
      {
        if(!$__->isArray($next))
        {
          $next = str_split((string)$next);
        }
  
        $return = \array_intersect($return, $next);
      }
  
      return self::_wrap(\array_values($return));
    }
  
  
    // Merge together multiple arrays
    private function union($array = NULL)
    {
      $arrays = self::_wrapArgs(\func_get_args(), 1);
  
      if(1 === \count($arrays))
      {
        return self::_wrap($array);
      }
  
      $__ = new self;
  
      return self::_wrap($__->flatten(\array_values(\array_unique(\array_merge(...$arrays)))));
    }
  
  
    // Get the difference between two arrays
    private function difference($array_one = NULL, $array_two = NULL)
    {
      $arrays = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\array_values(\array_diff(...$arrays)));
    }
  
  
    // Get the index of the first match
    private function indexOf($collection = NULL, $item = NULL)
    {
      [$collection, $item] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      $key = \array_search($item, $collection, TRUE);
  
      return self::_wrap((\is_bool($key)) ? -1 : $key);
    }
  
  
    // Get the index of the last match
    private function lastIndexOf($collection = NULL, $item = NULL)
    {
      [$collection, $item] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = self::_collection($collection);
  
      \krsort($collection);
      $__ = new self;
  
      return self::_wrap($__->indexOf($collection, $item));
    }
  
  
    // Returns an array of integers from start to stop (exclusive) by step
    private function range($stop = NULL)
    {
      $args = self::_wrapArgs(\func_get_args(), 1);
  
      $__   = new self;
      $args = $__->reject($args, function($val) {
        return NULL === $val;
      });
  
      $num_args = \count($args);
      switch($num_args)
      {
        case 1:
          [$start, $stop, $step] = [0, $args[0], 1];
        break;
        case 2:
          [$start, $stop, $step] = [$args[0], $args[1], 1];
          if($stop < $start)
          {
            return self::_wrap([]);
          }
        break;
        default:
          [$start, $stop, $step] = [$args[0], $args[1], $args[2]];
          if($step > 0 && $step > $stop)
          {
            return self::_wrap([$start]);
          }
      }
      $results = \range($start, $stop, $step);
  
      // Switch inclusive to exclusive
      if($step > 0 && $__->last($results) >= $stop)
      {
        \array_pop($results);
      }
      else if($step < 0 && $__->last($results) <= $stop)
      {
        \array_pop($results);
      }
  
      return self::_wrap($results);
    }
  
  
    // Merges arrays
    private function zip($array = NULL)
    {
      $arrays     = self::_wrapArgs(\func_get_args());
      $num_arrays = \count($arrays);
  
      if(1 === $num_arrays)
      {
        return self::_wrap($array);
      }
  
      $__                = new self;
      $num_return_arrays = $__->max(
        $__->map($arrays, function($array) {
          return \count($array);
        })
      );
      $return_arrays     = $__->range($num_return_arrays);
  
      foreach($return_arrays as $k => $v)
      {
        if(!\is_array($v))
        {
          $return_arrays[$k] = [];
        }
  
        foreach($arrays as $a => $one)
        {
          $return_arrays[$k][$a] = $one[$k] ?? NULL;
        }
      }
  
      return self::_wrap($return_arrays);
    }
  
  
    // Get the max value in the collection
    private function max($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      if(NULL === $iterator)
      {
        return self::_wrap(\max($collection));
      }
  
      $results = [];
      foreach($collection as $k => $item)
      {
        $results[$k] = $iterator($item);
      }
      \arsort($results);
      $__        = new self;
      $first_key = $__->first(\array_keys($results));
  
      return $collection[$first_key];
    }
  
  
    // Get the min value in the collection
    private function min($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      if(NULL === $iterator)
      {
        return self::_wrap(\min($collection));
      }
  
      $results = [];
      foreach($collection as $k => $item)
      {
        $results[$k] = $iterator($item);
      }
      \asort($results);
      $__        = new self;
      $first_key = $__->first(\array_keys($results));
  
      return self::_wrap($collection[$first_key]);
    }
  
  
    // Sort the collection by return values from the iterator
    private function sortBy($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      $results = [];
      foreach($collection as $k => $item)
      {
        $results[$k] = $iterator($item);
      }
      \asort($results);
      foreach($results as $k => $v)
      {
        $results[$k] = $collection[$k];
      }
  
      return self::_wrap(\array_values($results));
    }
    
    // Group the collection by return values from the iterator
    private function groupBy($collection = NULL, $iterator = NULL)
    {
      [$collection, $iterator] = self::_wrapArgs(\func_get_args(), 2);
  
      $result     = [];
      $collection = (array)$collection;
      foreach($collection as $k => $v)
      {
        $key = (\is_callable($iterator)) ? $iterator($v, $k) : $v[$iterator];
        if(!\array_key_exists($key, $result))
        {
          $result[$key] = [];
        }
        $result[$key][] = $v;
      }
  
      return $result;
    }
  
  
    // Returns the index at which the value should be inserted into the sorted collection
    private function sortedIndex($collection = NULL, $value = NULL, $iterator = NULL)
    {
      [$collection, $value, $iterator] = self::_wrapArgs(\func_get_args(), 3);
  
      $collection = (array)self::_collection($collection);
      $__         = new self;
  
      $calculated_value = (NULL !== $iterator) ? $iterator($value) : $value;
  
      while(\count($collection) > 1)
      {
        $midpoint                  = \floor(\count($collection) / 2);
        $midpoint_values           = \array_slice($collection, $midpoint, 1);
        $midpoint_value            = $midpoint_values[0];
        $midpoint_calculated_value = (NULL !== $iterator) ? $iterator($midpoint_value) : $midpoint_value;
  
        $collection = ($calculated_value < $midpoint_calculated_value) ? \array_slice($collection, 0, $midpoint, TRUE)
          : \array_slice($collection, $midpoint, NULL, TRUE);
      }
      $keys = \array_keys($collection);
  
      return self::_wrap(\current($keys) + 1);
    }
  
    // \shuffle the array
    private function shuffle($collection = NULL)
    {
      [$collection] = self::_wrapArgs(\func_get_args(), 1);
  
      $collection = (array)self::_collection($collection);
      \shuffle($collection);
  
      return self::_wrap($collection);
    }
  
  
    // Return the collection as an array
    private function toArray($collection = NULL)
    {
      return (array)$collection;
    }
  
  
    // Get the collection's keys
    private function keys($collection = NULL)
    {
      [$collection] = self::_wrapArgs(\func_get_args(), 1);
  
      if(!\is_object($collection) && !\is_array($collection))
      {
        throw new \Exception('Invalid object');
      }
  
      return self::_wrap(\array_keys((array)$collection));
    }
  
  
    // Get the collection's values
    private function values($collection = NULL)
    {
      [$collection] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\array_values((array)$collection));
    }
  
  
    // Copy all properties from the source objects into the destination object
    private function extend($object = NULL)
    {
      $args = self::_wrapArgs(\func_get_args(), 1);
  
      $num_args = \func_num_args();
      if(1 === $num_args)
      {
        return $object;
      }
  
      $is_object  = \is_object($object);
      $array      = (array)$object;
      $__         = new self;
      $extensions = $__->rest(\func_get_args());
      foreach($extensions as $extension)
      {
        $extension = (array)$extension;
        $array     = array_merge($array, $extension);
      }
  
      return self::_wrap(($is_object) ? (object)$array : $array);
    }
  
  
    // Returns the object with any missing values filled in using the defaults.
    private function defaults($object = NULL)
    {
      $args = self::_wrapArgs(\func_get_args(), 1);
      [$object] = $args;
  
      $num_args = \count($args);
      if(1 === $num_args)
      {
        return $object;
      }
  
      $is_object  = \is_object($object);
      $array      = (array)$object;
      $__         = new self;
      $extensions = $__->rest($args);
      foreach($extensions as $extension)
      {
        $extension = (array)$extension;
        $array     = array_merge($extension, $array);
      }
  
      return self::_wrap(($is_object) ? (object)$array : $array);
    }
  
  
    // Get the names of functions available to the object
    // functions alias: methods
    private function methods($object = NULL) { return self::functions($object); }
  
    private function functions($object = NULL)
    {
      [$object] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\get_class_methods(\get_class($object)));
    }
  
  
    // Returns a shallow copy of the object
    private function clon(&$object = NULL)
    {
      [$object] = self::_wrapArgs(\func_get_args(), 1);
  
      $clone = NULL;
      if(\is_array($object))
      {
        $object = (object) $object;
        $clone  = (array) clone $object;
      }
      else if(!\is_object($object))
      {
        $clone = $object;
      }
      else if(!$clone)
      {
        $clone = clone $object;
      }
  
      // shallow copy object
      if(\is_object($clone) && \count($clone) > 0)
      {
        foreach($clone as $k => $v)
        {
          if(\is_array($v) || \is_object($v))
          {
            $clone->$k =& $object->$k;
          }
        }
      }
  
      // shallow copy array
      else if(\is_array($clone) && \count($clone) > 0)
      {
        foreach($clone as $k => $v)
        {
          if(\is_array($v) || \is_object($v))
          {
            $clone[$k] =& $object[$k];
          }
        }
      }
      return self::_wrap($clone);
    }
  
  
    // Invokes the interceptor on the object, then returns the object
    private function tap($object = NULL, $interceptor = NULL)
    {
      [$object, $interceptor] = self::_wrapArgs(\func_get_args(), 2);
  
      $interceptor($object);
  
      return self::_wrap($object);
    }
  
  
    // Does the given key exist?
    private function has($collection = NULL, $key = NULL)
    {
      [$collection, $key] = self::_wrapArgs(\func_get_args(), 2);
  
      $collection = (array)self::_collection($collection);
  
      return self::_wrap(\array_key_exists($key, $collection));
    }
  
  
    // Are these items equal?
    private function isEqual($a = NULL, $b = NULL)
    {
      [$a, $b] = self::_wrapArgs(\func_get_args(), 2);
  
      if(isset($this) && isset(self::$_chained) && self::$_chained)
      {
        $a =& $this;
      }
  
      if($a === $b)
      {
        return self::_wrap(TRUE);
      }
      if(\gettype($a) !== \gettype($b))
      {
        return self::_wrap(FALSE);
      }
      if(\is_callable($a) !== \is_callable($b))
      {
        return self::_wrap(FALSE);
      }
  
      if($a == $b)
      {
        return self::_wrap(TRUE);
      }
  
      // Objects and arrays compared by values
      if(\is_object($a) || \is_array($a))
      {
  
        // Do either implement isEqual()?
        if(\is_object($a) && isset($a->isEqual))
        {
          return self::_wrap($a->isEqual($b));
        }
        if(\is_object($b) && isset($b->isEqual))
        {
          return self::_wrap($b->isEqual($a));
        }
        if(\is_array($a) && \array_key_exists('isEqual', $a))
        {
          return self::_wrap($a['isEqual']($b));
        }
        if(\is_array($b) && \array_key_exists('isEqual', $b))
        {
          return self::_wrap($b['isEqual']($a));
        }
  
        if(\count($a) !== \count($b))
        {
          return self::_wrap(FALSE);
        }
  
        $__           = new self;
        $keys_equal   = $__->isEqual($__->keys($a), $__->keys($b));
        $values_equal = $__->isEqual($__->values($a), $__->values($b));
  
        return self::_wrap($keys_equal && $values_equal);
      }
  
      return self::_wrap(FALSE);
    }
  
  
    // Is this item empty?
    private function isEmpty($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_array($item) || \is_object($item)) ? !((bool)\count((array)$item)) : (!(bool)$item);
    }
  
  
    // Is this item an object?
    private function isObject($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_object($item));
    }
  
  
    // Is this item an array?
    private function isArray($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_array($item));
    }
  
  
    // Is this item a string?
    private function isString($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_string($item));
    }
  
  
    // Is this item a number?
    private function isNumber($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap((\is_int($item) || \is_float($item)) && !\is_nan($item) && !\is_infinite($item));
    }
  
  
    // Is this item a bool?
    private function isBoolean($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_bool($item));
    }
  
  
    // Is this item a function (by type, not by name)?
    private function isFunction($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_object($item) && \is_callable($item));
    }
  
  
    // Is this item an instance of DateTime?
    private function isDate($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_object($item) && 'DateTime' === \get_class($item)); # TODO verify if $item is ever NULL here
    }
  
  
    // Is this item a NaN value?
    private function isNaN($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\is_nan($item));
    }
  
  
    // Returns the same value passed as the argument
    private function identity()
    {
      $args = self::_wrapArgs(\func_get_args(), 1);
  
      if(\is_array($args))
      {
        return self::_wrap($args[0]);
      }
  
      return self::_wrap(function($x) {
        return $x;
      });
    }
  
  
    // Generate a globally unique id, optionally prefixed
    private $_uniqueId = -1;
  
    private function uniqueId($prefix = NULL)
    {
      [$prefix] = self::_wrapArgs(\func_get_args(), 1);
  
      $_instance = self::getInstance();
      $_instance->_uniqueId++;
  
      return (NULL === $prefix) ? self::_wrap($_instance->_uniqueId) : self::_wrap($prefix . $_instance->_uniqueId);
    }
  
  
    // Invokes the iterator n times
    private function times($n = NULL, $iterator = NULL)
    {
      [$n, $iterator] = self::_wrapArgs(\func_get_args(), 2);
      if(NULL === $n)
      {
        $n = 0;
      }
  
      for($i = 0; $i < $n; $i++)
      {
        $iterator($i);
      }
  
      return self::_wrap(NULL);
    }
  
  
    // Extend the class with your own functions
    private $_mixins = [];
  
    private function mixin($functions = NULL)
    {
      [$functions] = self::_wrapArgs(\func_get_args(), 1);
  
      $mixins = &self::getInstance()->_mixins;
      foreach($functions as $name => $function)
      {
        $mixins[$name] = $function;
      }
  
      return self::_wrap(NULL);
    }
  
  
    // Allows extending methods in static context
    public static function __callStatic($name, $arguments)
    {
      if( \method_exists(__CLASS__, $name) )
      {
        $c = self::getInstance();
        return \call_user_func_array([$c, $name], $arguments);
      }
  
      $mixins = &self::getInstance()->_mixins;
  
      return \call_user_func_array($mixins[$name], $arguments);
    }
  
    // Allows extending methods in non-static context
    public function __call($name, $arguments)
    {
      if( \method_exists($this, $name) )
      {
        return \call_user_func_array([$this, $name], $arguments);
      }
      
      $mixins    = &self::getInstance()->_mixins;
      $arguments = self::_wrapArgs($arguments);
  
      return \call_user_func_array($mixins[$name], $arguments);
    }
  
  
    // Temporary PHP open and close tags used within templates
    // Allows for normal processing of templates even when
    // the developer uses PHP open or close tags for interpolation or evaluation
    public const TEMPLATE_OPEN_TAG = '760e7dab2836853c63805033e514668301fa9c47';
    public const TEMPLATE_CLOSE_TAG       = 'd228a8fa36bd7db108b01eddfb03a30899987a2b';
  
    private const TEMPLATE_DEFAULT_EVALUATE    = '/<%([\s\S]+?)%>/';
    public const TEMPLATE_DEFAULT_INTERPOLATE = '/<%=([\s\S]+?)%>/';
    public const TEMPLATE_DEFAULT_ESCAPE      = '/<%-([\s\S]+?)%>/';
    public array $_template_settings = [
      'evaluate'    => self::TEMPLATE_DEFAULT_EVALUATE,
      'interpolate' => self::TEMPLATE_DEFAULT_INTERPOLATE,
      'escape'      => self::TEMPLATE_DEFAULT_ESCAPE
    ];
  
    // Set template settings
    private function templateSettings($settings = NULL)
    {
      $_template_settings =& self::getInstance()->_template_settings;
  
      if(NULL === $settings)
      {
        $_template_settings = [
          'evaluate'    => self::TEMPLATE_DEFAULT_EVALUATE,
          'interpolate' => self::TEMPLATE_DEFAULT_INTERPOLATE,
          'escape'      => self::TEMPLATE_DEFAULT_ESCAPE
        ];
  
        return TRUE;
      }
  
      foreach($settings as $k => $v)
      {
        if(!\array_key_exists($k, $_template_settings))
        {
          continue;
        }
  
        $_template_settings[$k] = $v;
      }
  
      return TRUE;
    }
  
  
    // Compile templates into functions that can be evaluated for rendering
    private function template($code = NULL, $context = NULL)
    {
      [$code, $context] = self::_wrapArgs(\func_get_args(), 2);
  
      $class_name = __CLASS__;
  
      $return = self::_wrap(function($context = NULL) use ($code, $class_name) {
        $ts = $class_name::getInstance()->_template_settings;
  
        // Wrap escaped, interpolated, and evaluated blocks inside PHP tags
        \extract((array)$context);
        \preg_match_all($ts['escape'], $code, $vars, \PREG_SET_ORDER);
        if(\count($vars) > 0)
        {
          foreach($vars as $var)
          {
            $echo = $class_name::TEMPLATE_OPEN_TAG . ' echo htmlentities(' . \trim($var[1]) . '); ' .
                    $class_name::TEMPLATE_CLOSE_TAG;
            $code = \str_replace($var[0], $echo, $code);
          }
        }
        \preg_match_all($ts['interpolate'], $code, $vars, \PREG_SET_ORDER);
        if(\count($vars) > 0)
        {
          foreach($vars as $var)
          {
            $echo = $class_name::TEMPLATE_OPEN_TAG . ' echo ' . \trim($var[1]) . '; ' . $class_name::TEMPLATE_CLOSE_TAG;
            $code = \str_replace($var[0], $echo, $code);
          }
        }
        \preg_match_all($ts['evaluate'], $code, $vars, \PREG_SET_ORDER);
        if(\count($vars) > 0)
        {
          foreach($vars as $var)
          {
            $echo = $class_name::TEMPLATE_OPEN_TAG . \trim($var[1]) . $class_name::TEMPLATE_CLOSE_TAG;
            $code = \str_replace($var[0], $echo, $code);
          }
        }
        $code = \str_replace([$class_name::TEMPLATE_OPEN_TAG, $class_name::TEMPLATE_CLOSE_TAG], ['<?php ', '?>'], $code);
  
        // Use the output buffer to grab the return value
        $code = 'ob_start(); extract($context); ?>' . $code . '<?php return ob_get_clean();';
  
        $func = \create_function('$context', $code);
  
        return $func((array)$context);
      });
  
      // Return function or call function depending on context
      return self::_wrap(
        ((isset($this) && isset($this->_wrapped) && $this->_wrapped) || NULL !== $context) ? $return($context) : $return
      );
    }
  
    // Escape
    private function escape($item = NULL)
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(\htmlentities($item));
    }
  
  
    // Memoizes a function by caching the computed result.
    public $_memoized = [];
  
    private function memoize($function = NULL, $hashFunction = NULL)
    {
      [$function, $hashFunction] = self::_wrapArgs(\func_get_args(), 2);
  
      $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
  
      return self::_wrap(function() use ($function, &$_instance, $hashFunction) {
  
        // Generate a key based on hashFunction
        $args = \func_get_args();
        if(NULL === $hashFunction)
        {
          $hashFunction = function($function, $args) {
  
            // Try using var_export to identify the function
            return \md5(
              \implode('_', [
                \var_export($function, TRUE),
                \var_export($args, TRUE)
              ])
            );
          };
        }
        $key = $hashFunction($function, $args);
  
        if(!\array_key_exists($key, $_instance->_memoized))
        {
          $_instance->_memoized[$key] = \call_user_func_array($function, $args);
        }
  
        return $_instance->_memoized[$key];
      });
    }
  
  
    // Throttles a function so that it can only be called once every wait milliseconds
    public $_throttled = [];
  
    private function throttle($function = NULL, $wait = NULL)
    {
      [$function, $wait] = self::_wrapArgs(\func_get_args(), 2);
  
      $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
  
      return self::_wrap(function() use ($function, $wait, &$_instance) {
  
        // Try using \var_export to identify the function
        $key = \md5(
          \implode('', [
            \var_export($function, TRUE),
            $wait
          ])
        );
  
        $microtime     = \microtime(TRUE);
        $ready_to_call = (!\array_key_exists($key, $_instance->_throttled) || $microtime >= $_instance->_throttled[$key]);
        if($ready_to_call)
        {
          $next_callable_time          = $microtime + ($wait / 1000);
          $_instance->_throttled[$key] = $next_callable_time;
  
          return \call_user_func_array($function, \func_get_args());
        }
      });
    }
  
  
    // Creates a version of the function that can only be called once
    public $_onced = [];
  
    private function once($function = NULL)
    {
      [$function] = self::_wrapArgs(\func_get_args(), 1);
  
      $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
  
      return self::_wrap(function() use ($function, &$_instance) {
  
        // Try using \var_export to identify the function
        $key = \md5(\var_export($function, TRUE));
        if(!\array_key_exists($key, $_instance->_onced))
        {
          $_instance->_onced[$key] = \call_user_func_array($function, \func_get_args());
        }
  
        return $_instance->_onced[$key];
      });
    }
  
  
    // Wraps the function inside the wrapper function, passing it as the first argument
    private function wrap($function = NULL, $wrapper = NULL)
    {
      [$function, $wrapper] = self::_wrapArgs(\func_get_args(), 2);
  
      return self::_wrap(function() use ($wrapper, $function) {
        $args = \array_merge([$function], \func_get_args());
  
        return \call_user_func_array($wrapper, $args);
      });
    }
  
  
    // Returns the composition of the functions
    private function compose()
    {
      $functions = self::_wrapArgs(\func_get_args(), 1);
  
      return self::_wrap(function() use ($functions) {
        $args = \func_get_args();
        foreach($functions as $function)
        {
          $args[0] = \call_user_func_array($function, $args);
        }
  
        return $args[0];
      });
    }
  
  
    // Creates a version of the function that will only run after being called \count times
    public $_aftered = [];
  
    private function after($count = NULL, $function = NULL)
    {
      [$count, $function] = self::_wrapArgs(\func_get_args(), 2);
  
      $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
      $key       = \md5(\mt_rand());
  
      $func = static function() use ($function, &$_instance, $count, $key) {
        if(!\array_key_exists($key, $_instance->_aftered))
        {
          $_instance->_aftered[$key] = 0;
        }
  
        ++$_instance->_aftered[$key];
  
        if($_instance->_aftered[$key] >= $count)
        {
          return \call_user_func_array($function, \func_get_args());
        }
      };
  
      return self::_wrap(($count) ? $func : $func());
    }
  
  
    // Singleton
    private static $_instance;
  
    public static function getInstance()
    {
      if(!isset(self::$_instance))
      {
        $c               = __CLASS__;
        self::$_instance = new $c;
      }
  
      return self::$_instance;
    }
  
  
    // All methods should wrap their returns within _wrap
    // because this function understands both OO-style and functional calls
    public $_wrapped; // Value passed from one chained method to the next
  
    private function _wrap($val)
    {
      if(isset($this) && isset(self::$_chained) && self::$_chained)
      {
        $this->_wrapped = $val;
  
        return $this;
      }
  
      return $val;
    }
    
    // All methods should get their arguments from _wrapArgs
    // because this function understands both OO-style and functional calls
    private function _wrapArgs($caller_args, $num_args = NULL)
    {
      $num_args = (NULL === $num_args) ? \count($caller_args) - 1 : $num_args;
  
      $filled_args = [];
      if(isset($this) && isset($this->_wrapped))
      {
        $filled_args[] =& $this->_wrapped;
      }
      if(\count($caller_args) > 0)
      {
        foreach($caller_args as $k => $v)
        {
          $filled_args[] = $v;
        }
      }
  
      return \array_pad($filled_args, $num_args, NULL);
    }
  
  
    // Get a collection in a way that supports strings
    private function _collection($collection)
    {
      return (!\is_array($collection) && !\is_object($collection)) ? str_split((string)$collection) : $collection;
    }
  }
}

namespace
{
  if( !\function_exists('__') )
  {
    function __($item = NULL)
    {
      return \CMSMSExt\underscore\__($item);
    }
  }
  
  if( !\class_exists('__') )
  {
    class __ extends \CMSMSExt\underscore\__ {}
  }
}