<?php
namespace CMSMSExt\dbg
{
  use   \CMSMSExt\core\VarDumper;
  

  /**
   * Returns an instance of dbg for OOP-style calls
   * @note for this to work, arguments for any callable method must be nullable
   *
   * @param $item
   *
   * @return \CMSMSExt\dbg\dbg
   */
  function dbg($item = NULL)
  {
    $dbg = new dbg;
    if(\func_num_args() > 0) { $dbg->_wrapped = $item; }
    
    return $dbg;
  }
  
  class dbg
  {
    // Singleton
    private static                      $_instance;
    private static \CMSMSExt\core\stack $_stack;
    
    // Start the chain
    private bool $_chained = FALSE; // Are we in a chain?
    // Extend the class with your own functions
    private $_mixins = [];
    
    public $_wrapped; // Value passed from one chained method to the next
    
    /**
     * internals
     */
    
    public function __construct()
    {
      self::$_stack = new \CMSMSExt\core\stack();
    }
    
    public static function getInstance()
    {
      if(!isset(self::$_instance))
      {
        $c               = __CLASS__;
        self::$_instance = new $c;
      }
      
      return self::$_instance;
    }
    

    /**
     * Calls a static method or mixin by name and passes the provided arguments.
     * Allows extending methods in static context
     *
     * @note methods prefixed with an underscore are treated as private
     * @param string $name The name of the method or mixin to call.
     * @param array $arguments The arguments to pass to the method or mixin.
     * @throws \BadMethodCallException If the method or mixin does not exist.
     *
     * @return mixed The result of the method or mixin call.
     */
    public static function __callStatic($name, $arguments)
    {
      if( !\startswith($name,  '_') && \method_exists(__CLASS__, $name) )
      {
        $c = self::getInstance();
        return \call_user_func_array([$c, $name], $arguments);
      }
      
      $mixins = &self::getInstance()->_mixins;
      
      if( !isset($mixins[$name]) )
      {
        throw new \BadMethodCallException($name);
      }
      
      return \call_user_func_array($mixins[$name], $arguments);
    }
    
    
    
    /**
     * Executes inaccessible methods or mixins dynamically based on the given name and arguments.
     *
     * @note methods prefixed with an underscore are treated as private
     * @param string $name The name of the method or mixin to execute.
     * @param array $arguments The arguments to pass to the method or mixin.
     * @throws \BadMethodCallException If the method does not exist or is not accessible.
     * @return mixed The result of the method execution.
     */
    public function __call($name, $arguments)
    {
      if( !\startswith($name,  '_') && \method_exists($this, $name) )
      {
        return \call_user_func_array([$this, $name], $arguments);
      }
      
      $mixins    = &self::getInstance()->_mixins;
      
      if( !isset($mixins[$name]) )
      {
        throw new \BadMethodCallException($name);
      }
      
      $arguments = self::_wrapArgs($arguments);
      
      return \call_user_func_array($mixins[$name], $arguments);
    }
    
    /**
     * Wraps a value and returns it.
     *
     * @note  All methods should wrap their returns within _wrap,
     *        because this function understands both OO-style
     *        and functional  calls
     *
     * @param mixed $val The value to be wrapped.
     *
     * @return mixed The wrapped value.
     */
    private function _wrap($val)
    {
      if(isset($this) && isset(self::$_chained) && self::$_chained)
      {
        $this->_wrapped = $val;
        
        return $this;
      }
      
      return $val;
    }
    
    
    /**
     * Wraps the arguments passed to the function.
     *
     * methods should get their arguments from _wrapArgs
     * because this function understands both OO-style and functional calls
     *
     * @param array    $caller_args The arguments passed to the function.
     * @param int|null $num_args The number of arguments to wrap.
     *                           Defaults to the total number of caller arguments minus 1.
     *
     * @return array The wrapped arguments.
     */
    private function _wrapArgs($caller_args, $num_args = NULL)
    {
      $num_args = $num_args ?? (\count($caller_args) - 1);
      
      $filled_args = [];
      
      if( isset($this, $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);
    }
    
    /**
     * Chains a new item to the existing dbg object.
     *
     * @param mixed $item The item to chain.
     * @return dbg The dbg object with the new item chained.
     */
    private function chain($item = NULL) : dbg
    {
      [$item] = self::_wrapArgs(\func_get_args(), 1);
      
      $dbg           = (isset($this, self::$_chained) && self::$_chained) ? $this : dbg($item);
      $dbg->_chained = TRUE;
      
      return $dbg;
    }
  
    
    /**
     * End the chain and return the value.
     *
     * @return null
     */
    private function value()
    {
      return ( isset($this) ) ? $this->_wrapped : NULL;
    }
    
    /**
     * Mixes in the given functions into the class's list of mixins.
     *
     * @param mixed $functions The functions to be mixed in.
     *
     * @return mixed The result of the _wrap method.
     */
    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);
    }
    
    /**
     * Retrieves information about the current stack frame.
     *
     * @return string The formatted string containing debug information.
     */
    private function _info()
    {
      $data = self::$_stack->pop();
      
      $out = 'debug:<br> FILE: ' . $data['file']
             . ' <br> LINE: ' . $data['line']
             # . ' <br> FUNCTION: ' . $data['function'] # <- not what we expected it to be
      ;
      
      if( '::' === $data['function'] )
      {
        $out .= ' (static method call)';
      }
      else if( '->' === $data['type'] )
      {
        $out .= ' (instance method call)';
      }
      else
      {
        $out .= ' (function call or undetermined call)';
      }
      
      return $out;
    }
    
    
    /**
     * now the functions
     */
    
    /**
     * Dump the value and display it in a formatted way.
     *
     * @param mixed $val The value to be dumped.

     * @return dbg the dbg instance
     */
    private function dump($val = NULL, ... $args) : dbg
    {
      self::$_stack->push(\debug_backtrace(2,3)[1]);
      [$val, $args] = self::_wrapArgs(\func_get_args(), 2);
      
      if(isset($args))
      {
        $send = \array_merge([$val], [$args]);
      }
      else
      {
        $send = $val;
      }
      
      $out = '<pre style="word-wrap: break-word;">'
             . $this->_info()
             . '<div style="word-wrap: break-word;">'
             . $this->_dump($send)
             . '</div>'
             . '</pre>';
      
      echo $out;
      
      return self::_wrap($this);
      $send = \array_merge([$val], [$args]);
      $out = '<pre style="word-wrap: break-word;">'
             . $this->_info()
             . '<div style="word-wrap: break-word;">'
             . $this->_dump($send)
             . '</div>'
             . '</pre>';
      
      echo $out;
      
      return self::_wrap($this);
    }
    
    /**
     * Dumps a value and terminates the script execution.
     *
     * @param mixed $val The value to be dumped.
     * @return void This function does not return any value.
     */
    private function die($val = NULL, ... $args)
    {
      self::$_stack->push(\debug_backtrace(2,3)[1]);
      [$val, $args] = self::_wrapArgs(\func_get_args(), 2);
      
      if(isset($args))
      {
        $send = \array_merge([$val], [$args]);
      }
      else
      {
        $send = $val;
      }
      
      $out = '<pre style="word-wrap: break-word;">'
             . $this->_info()
             . '<div style="word-wrap: break-word;">'
             . $this->_dump($send)
             . '</div>'
             . '</pre>';
      
      die($out);
    }
    
    
    /**
     * Dumps the given value along with some additional information.
     *
     * get_defined_vars() can be passed as $val
     *
     * @param mixed $vals       The value to be dumped.
     * @param mixed $exclusions An array of keys to be excluded from the dumped value.
     *
     * @return dbg the dbg instance.
     */
    private function dump_ext($vals = NULL, $exclusions = NULL) : dbg
    {
      self::$_stack->push(\debug_backtrace(2,3)[1]);
      [$vals, $exclusions] = self::_wrapArgs(\func_get_args(), 2);
      $exclusions = (array)$exclusions;
      
      $temp1 = \array_values(\array_diff(\array_keys($vals), $exclusions));
      $temp2 = [];
      
      foreach($temp1 as $value)
      {
        global $$value;
        $temp2[$value] = $$value ?? NULL;
      }
      
      $this->_dump($vals);
      
      $out = '<pre style="word-wrap: break-word;">'
             . $this->_info()
             . '<div style="word-wrap: break-word;">'
             . $this->_dump($temp2)
             . '</div>'
             . '</pre>';
      
      echo $out;
      
      return self::_wrap($this);
    }
    
    
    /**
     * Dumps the given value along with some additional information
     * and terminate script execution.
     *
     * get_defined_vars() can be passed as $val
     *
     * @param mixed $vals       The value to be dumped.
     * @param mixed $exclusions An array of keys to be excluded from the dumped value.
     *
     * @return void
     */
    private function die_ext($vals = NULL, $exclusions = NULL) : void
    {
      self::$_stack->push(\debug_backtrace(2,3)[1]);
      [$val, $exclusions] = self::_wrapArgs(\func_get_args(), 2);
      $exclusions = (array)$exclusions;
      $temp1 = \array_values(\array_diff(\array_keys($val), $exclusions));
      $temp2 = [];
      
      foreach($temp1 as $value)
      {
        global $$value;
        $temp2[$value] = $$value ?? NULL;
      }
      
      $this->_dump($val);
      
      $out = '<pre style="word-wrap: break-word;">'
             . $this->_info()
             . '<div style="word-wrap: break-word;">'
             . $this->_dump($temp2)
             . '</div>'
             . '</pre>';
      
      die($out);
    }
    
    /**
     * Outputs debugging information.
     *
     * @param mixed $val The value to be dumped.
     *
     * @return string
     */
    private function _dump($val)
    {
      return VarDumper::dump($val, 5, TRUE);
    }
    
//    function dump2(bool $populateMissingArgumentsWithDefaults = FALSE) : array
//    {
//      $trace      = \debug_backtrace(0, 2)[1];
//      $reflection = NULL;
//
//      if(isset($trace['class']))
//      {
//        $reflection = new \ReflectionMethod($trace['class'], $trace['function']);
//      }
//      else
//      {
//        $reflection = new \ReflectionFunction($trace['function']);
//      }
//
//      $ret = [];
//      foreach($reflection->getParameters() as $param)
//      {
//        if(\array_key_exists($param->getPosition(), $trace['args']))
//        {
//          $ret[$param->name] = $trace['args'][$param->getPosition()];
//        }
//        else if($populateMissingArgumentsWithDefaults)
//        {
//          // because of the "required arguments declared after an optional argument are implicitly required" rule:
//          \assert(
//            $param->isDefaultValueAvailable(), "i think all params are either in trace[args] or have default values"
//          );
//
//          $ret[$param->name] = $param->getDefaultValue();
//        }
//      }
//
//      \dbg()->die($ret);
//
//      return $ret;
//    }
    
    
  }
  
}

namespace
{
  
  /**
   * imports the lib into the global namespace
   */
  
  /**
   * this allows to call as a function using either
   * dbg()->function($target);
   * or
   * dbg($target)->function();
   */
  if( !\function_exists('dbg') )
  {
    function dbg($item = NULL)
    {
      return \CMSMSExt\dbg\dbg($item);
    }
  }
  
  /**
   * this allows static use
   * dbg::function($target);
   */
  if( !\class_exists('dbg') )
  {
    class dbg extends \CMSMSExt\dbg\dbg {}
  }
}
?>