<?php
// phpcs:disable WordPress.PHP.DevelopmentFunctions -- Logger intentionally writes diagnostic output and caller traces for admin debug mode.
namespace SPAATG\ShortPixelLogger;

if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

  /*** Logger class
  *
  * Class uses the debug data model for keeping log entries.
  * Logger should not be called before init hook!
  */
 class ShortPixelLogger
 {
   static protected $instance = null;
   protected $start_time;
   protected $memoryLimit; // to be used for memory logs only.

   protected $is_active = false;
   protected $is_manual_request = false;
   protected $show_debug_view = false;

   protected $items = array();
   protected $logPath = false;
   protected $logMode = FILE_APPEND;

   protected $logLevel;
   protected $format = "[ %%time%% ] %%color%% %%level%% %%color_end%% \t %%message%%  \t %%caller%% ( %%time_passed%% )";
   protected $format_data = "\t %%data%% ";

   protected $hooks = array();

	 private $logFile; // pointer resource to the logFile.
/*   protected $hooks = array(
      'shortpixel_image_exists' => array('numargs' => 3),
      'shortpixel_webp_image_base' => array('numargs' => 2),
      'shortpixel_image_urls' => array('numargs' => 2),
   ); // @todo monitor hooks, but this should be more dynamic. Do when moving to module via config.
*/

   // utility
   private $namespace;
   private $view;

   protected $template = 'view-debug-box';

   /** Debugger constructor
   *  Two ways to activate the debugger. 1) Define SPAATG_DEBUG in wp-config.php. Either must be true or a number corresponding to required LogLevel
   *  2) Put SPAATG_DEBUG in the request. Either true or number.
   */
   public function __construct()
   {
      $this->start_time = microtime(true);
      $this->logLevel = DebugItem::LEVEL_WARN;

      $ns = __NAMESPACE__;
      $this->namespace = substr($ns, 0, strpos($ns, '\\')); // try to get first part of namespace

			// phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form.
      $debug_value = isset($_REQUEST['SPAATG_DEBUG']) ? sanitize_text_field(wp_unslash($_REQUEST['SPAATG_DEBUG'])) : null;
      if (! is_null($debug_value) && (! function_exists('current_user_can') || ! current_user_can('manage_options')))
      {
        $debug_value = null;
      }
      if (! is_null($debug_value)) // manual takes precedence over constants
      {
        $this->is_manual_request = true;
        $this->is_active = true;

        if ($debug_value === 'true')
        {
          $this->logLevel = DebugItem::LEVEL_INFO;
        }
        else {
          $this->logLevel = intval($debug_value);
        }

      }
      else if ( (defined('SPAATG_DEBUG') && SPAATG_DEBUG > 0) )
      {
            $this->is_active = true;
            if (SPAATG_DEBUG === true)
              $this->logLevel = DebugItem::LEVEL_INFO;
            else {
              $this->logLevel = intval(SPAATG_DEBUG);
            }
      }

      if (defined('SPAATG_DEBUG_TARGET') && SPAATG_DEBUG_TARGET || $this->is_manual_request)
      {
          if ($this->logPath === false)
          {
            $this->setLogPath($this->namespace . '.log');
          }
          if (defined('SPAATG_LOG_OVERWRITE') && $this->logPath) // if overwrite, do this on init once.
            $this->resetLogFile();

      }

      if ($this->is_active)
      {
        /* On Early init, this function might not exist, then queue it when needed */
        if (! function_exists('wp_get_current_user'))
          add_action('init', array($this, 'initView'));
        else
         $this->initView();
      }

      if ($this->is_active && count($this->hooks) > 0)
          $this->monitorHooks();
   }

   /** Init the view when needed. Private function ( public because of WP_HOOK )
   * Never call directly */
   public function initView()
   {
     $user_is_administrator = (current_user_can('manage_options')) ? true : false;

     if ($this->is_active && $this->is_manual_request && $user_is_administrator )
     {

         $logPath = $logLink = $this->logPath; // default
         $uploads = wp_get_upload_dir();


     		  if ( 0 === strpos( $logPath, $uploads['basedir'] ) ) { // Simple as it should, filepath and basedir share.
                     // Replace file location with url location.
                     $logLink = str_replace( $uploads['basedir'], $uploads['baseurl'], $logPath );
     		  }


         $this->view = new \stdClass;
         $this->view->logLink = 'view-source:' . esc_url($logLink);
         add_action('admin_footer', array($this, 'loadView'));
     }
   }

   public static function getInstance()
   {
      if ( self::$instance === null)
      {
          self::$instance = new ShortPixelLogger();
      }
      return self::$instance;
   }

   public function setLogPath($logPath)
   {
      $this->logPath = $this->sanitizeLogPath($logPath);
			$this->getWriteFile(true); // reset the writeFile here.
      return $this->logPath;
   }
   protected function addLog($message, $level, $data = array())
   {
  //   $log = self::getInstance();

     // don't log anything too low or when not active.
     if ($this->logLevel < $level || ! $this->is_active)
     {
       return;
     }

     // Force administrator on manuals.
     if ( $this->is_manual_request )
     {
        if (! function_exists('wp_get_current_user')) // not loaded yet
          return false;

        $user_is_administrator = (current_user_can('manage_options')) ? true : false;
        if (! $user_is_administrator)
          return false;
     }

     // Check where to log to.
     if ($this->logPath === false)
     {
       $this->logPath = $this->setLogPath($this->namespace . '.log');
     }

     $arg = array();
     $args['level'] = $level;
     $args['data'] = $data;

     $newItem = new DebugItem($message, $args);
     $this->items[] = $newItem;

      if ($this->is_active)
      {
          $this->write($newItem);
      }
   }

   /** Writes to log File. */
   protected function write($debugItem, $mode = 'file')
   {
      $items = $debugItem->getForFormat();
      $items['time_passed'] =  round ( ($items['time'] - $this->start_time), 5);
      $items['time'] =  gmdate('Y-m-d H:i:s', (int) $items['time'] );

      if ( ($items['caller']) && is_array($items['caller']) && count($items['caller']) > 0)
      {
          $caller = $items['caller'];
          $items['caller'] = $caller['file'] . ' in ' . $caller['function'] . '(' . $caller['line'] . ')';
      }

      $line = $this->formatLine($items);

			$file = $this->getWriteFile();

      // try to write to file. Don't write if directory doesn't exists (leads to notices)
      if ($file )
      {
					// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Logger keeps an open local file handle for low-overhead debug logging.
					fwrite($file, $line);
      }
      else {
       // error_log($line);
      }
   }

	 protected function getWriteFile($reset = false)
	 {
		  if ($this->logPath === false)
			{
				 $this->logFile = false;
				 return false;
			}

		 	if (! is_null($this->logFile) && $reset === false)
			{
					return $this->logFile;
			}
			elseif(is_object($this->logFile))
			{
					// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing the logger-owned file handle.
					fclose($this->logFile);
			}

			$logDir = dirname($this->logPath);
			  // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Logger needs to check the local log directory before opening a handle.
			  if (! is_dir($logDir) || ! is_writable($logDir))
			{
				error_log('ShortpixelLogger: Log Directory is not writable : ' . $logDir);
				$this->logFile = false;
				return false;
			}

      $file = false;
      if (file_exists($this->logPath))
      {
	         // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Logger needs to check the local log file before opening a handle.
	         if (! is_writable($this->logPath))
         {
            error_log('ShortPixelLogger: File Exists, but not writable: ' . $this->logPath);
            $this->logFile = false;
            return $file;
         }
      }

				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Logger keeps an open local file handle for low-overhead debug logging.
				$file = fopen($this->logPath, 'a');

      if ($file === false)
			{
				 error_log('ShortpixelLogger: File could not be opened / created: ' . $this->logPath);
				 $this->logFile = false;
				 return $file;
			}

			$this->logFile = $file;
			return $file;
	 }

   protected function resetLogFile()
   {
      $logDir = dirname($this->logPath);
      // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Logger checks its uploads-scoped log directory before opening a local handle.
      if (! is_dir($logDir) || ! is_writable($logDir))
      {
        return false;
      }

      // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Logger checks its own uploads-scoped file before resetting it.
      if (file_exists($this->logPath) && ! is_writable($this->logPath))
      {
        return false;
      }

      // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Logger resets its own uploads-scoped file after path sanitization.
      $file = fopen($this->logPath, 'w');
      if (false === $file)
      {
        return false;
      }

      // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Logger resets its own uploads-scoped file after path sanitization.
      fwrite($file, '-- Log Reset -- ' . PHP_EOL);
      // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Handle opened locally above.
      fclose($file);
      $this->getWriteFile(true);
      return true;
   }

   protected function getPluginLogDir()
   {
      $uploads = wp_get_upload_dir();
      $baseDir = isset($uploads['basedir']) ? $uploads['basedir'] : '';
      $baseDir = wp_normalize_path($baseDir);

      if ($baseDir === '')
      {
        return false;
      }

      $logDir = trailingslashit($baseDir) . 'shortpixel-ai-alt-text-generator/logs';
      $logDir = wp_normalize_path($logDir);

      if (! is_dir($logDir))
      {
        wp_mkdir_p($logDir);
      }

      if (! is_dir($logDir))
      {
        return false;
      }

      return $logDir;
   }

   protected function sanitizeLogPath($logPath)
   {
      $logDir = $this->getPluginLogDir();
      if ($logDir === false)
      {
        return false;
      }

      $filename = is_string($logPath) ? basename($logPath) : '';
      $filename = sanitize_file_name($filename);

      if ($filename === '')
      {
        $filename = 'spaatg.log';
      }

      return trailingslashit($logDir) . $filename;
   }

   protected function formatLine($args = array() )
   {
      $line= $this->format;
      foreach($args as $key => $value)
      {
        if (! is_array($value) && ! is_object($value))
          $line = str_replace('%%' . $key . '%%', $value, $line);
      }

      $line .= PHP_EOL;

      if (isset($args['data']))
      {
        $data = array_filter($args['data']);
        if (count($data) > 0)
        {
          // @todo This should probably be a formatter function to handle multiple stuff?
          foreach($data as $item)
          {
              if (is_bool($item))
              {
                 $item = (true === $item) ? 'true' : 'false';
              }
              $line .= $item . PHP_EOL;
          }
        }
      }

      return $line;
   }

   protected function setLogLevel($level)
   {
     $this->logLevel = $level;
   }

   protected function getEnv($name)
   {
     if (isset($this->{$name}))
     {
       return $this->{$name};
     }
     else {
       return false;
     }
   }


   protected function monitorHooks()
   {

      foreach($this->hooks as $hook => $data)
      {
        $numargs = isset($data['numargs']) ? $data['numargs'] : 1;
        $prio = isset($data['priority']) ? $data['priority'] : 10;

        add_filter($hook, function($value) use ($hook) {
              $args = func_get_args();
              return $this->logHook($hook, $value, $args); }, $prio, $numargs);
      }
   }

   public function logHook($hook, $value, $args)
   {
      array_shift($args);
      self::addInfo('[Hook] - ' . $hook . ' with ' . var_export($value,true), $args);
      return $value;
   }

   public function loadView()
   {
       // load either param or class template.
       $template = $this->template;

       $view = $this->view;
       $view->namespace = $this->namespace;
       $controller = $this;

       $template_path = __DIR__ . '/' . $this->template  . '.php';
       if (file_exists($template_path))
       {

         include($template_path);
       }
       else {
         self::addError("View $template for ShortPixelLogger could not be found in " . $template_path,
         array('class' => get_class($this)));
       }
   }

   public function addMemoryLog($message, $args = array())
   {
      if (is_null($this->memoryLimit))
      {
        $this->memoryLimit = $this->unitToInt(ini_get('memory_limit'));
      }

      $usage = memory_get_usage();
      $percentage = round(($usage / $this->memoryLimit) * 100, 2);
      $memmsg = sprintf("( %s / %s - %s %%)",
         $this->formatBytes($usage),
         $this->formatBytes($this->memoryLimit),
         $percentage
      );
      $level = DebugItem::LEVEL_DEBUG;
      $this->addLog($message . ' ' . $memmsg, $level, $args);

   }

   private function unitToInt($s)
   {
     return (int)preg_replace_callback('/(\-?\d+)(.?)/', function ($m) {
         return $m[1] * pow(1024, strpos('BKMG', $m[2]));
     }, strtoupper($s));
   }

   private function formatBytes($size, $precision = 2)
   {
       $base = log($size, 1024);
       $suffixes = array('', 'K', 'M', 'G', 'T');

       if (0 === $size)
       {
         return 0;
       }

      $calculation = pow(1024, $base - floor($base));
      if (is_nan($calculation))
      {
         return 0;
      }

       return round($calculation, $precision) .' '. $suffixes[floor($base)];
   }

   public static function addError($message, $args = array())
   {
      $level = DebugItem::LEVEL_ERROR;
      $log = self::getInstance();
      $log->addLog($message, $level, $args);
   }
   public static function addWarn($message, $args = array())
   {
     $level = DebugItem::LEVEL_WARN;
     $log = self::getInstance();
     $log->addLog($message, $level, $args);
   }
   // Alias, since it goes wrong so often.
   public static function addWarning($message, $args = array())
   {
      self::addWarn($message, $args);
   }
   public static function addInfo($message, $args = array())
   {
     $level = DebugItem::LEVEL_INFO;
     $log = self::getInstance();
     $log->addLog($message, $level, $args);
   }
   public static function addDebug($message, $args = array())
   {
     $level = DebugItem::LEVEL_DEBUG;
     $log = self::getInstance();
     $log->addLog($message, $level, $args);
   }

   /**
    * Adds a trace for debuggins.
    * @param String  $message       Description
    * @param integer  $amount        Amount of lines needed.
    * @param integer $debug_option  Debug backtrace ( default IGNORE_ARGS, see docs )
    */
   public static function addTrace($message, $amount = 10, $debug_option = 2)
   {
      $trace = debug_backtrace($debug_option, $amount);
      $log = self::getInstance();
      $log->addLog($message, DebugItem::LEVEL_DEBUG, $trace);
   }

   public static function addMemory($message, $args = array())
   {
      $log = self::getInstance();
      $log->addMemoryLog($message, $args);
   }

   /** These should be removed every release. They are temporary only for d'bugging the current release */
   public static function addTemp($message, $args = array())
   {
     self::addDebug($message, $args);
   }

   public static function logLevel($level)
   {
      $log = self::getInstance();
      static::addInfo('Changing Log level' . $level);
      $log->setLogLevel($level);
   }

   public static function getLogLevel()
   {
     $log = self::getInstance();
     return $log->getEnv('logLevel');
   }

   public static function isManualDebug()
   {
        $log = self::getInstance();
        return $log->getEnv('is_manual_request');
   }

   public static function getLogPath()
   {
     $log = self::getInstance();
     return $log->getEnv('logPath');
   }

   /** Function to test if the debugger is active
   * @return boolean true when active.
   */
   public static function debugIsActive()
   {
      $log = self::getInstance();
      return $log->getEnv('is_active');
   }
 } // class debugController
