<?php
/**
 * @file
 * Contains the Redis\Logger\Reader class
 */

namespace Redis\Logger;


/**
 * This class reads the logger information stored in Redis:
 *
 * - messages templates
 * - channels used in message templates
 * - messages for a given template
 *
 * @package Redis\Logger
 */
class Reader {
  /**
   * @var \Redis
   */
  protected $redis;

  /**
   * @var \Redis\Logger\Settings
   */
  protected $settings;

  /**
   * @var string[]
   */
  protected $templateCache = NULL;

  /**
   * @param \Redis $redis
   * @param \Redis\Logger\Settings $settings
   *   We pass this constant instance in order to allow overriding it, say for
   *   tests.
   */
  public function __construct(\Redis $redis, Settings $settings = NULL) {
    $this->redis = $redis;
    if (!isset($settings)) {
      $settings = new Settings();
    }
    $this->settings = $settings;
  }

  /**
   * Load and cache the list of templates in the instance for this page cycle.
   *
   * Note that the templates are in Redis storage format, including the prefix,
   * to link directly to the results.
   */
  protected function ensureTemplateCache()  {
    if (!isset($this->templateCache)) {
      $this->templateCache = $this->scan($this->settings->getRedisPattern(), 0, $this->settings->getScanBatchSize());
    }
  }

  /**
   * Return the list of channels. Assumed to be constant for a given page cycle.
   *
   * @return string[int]
   */
  public function getChannels() {
    $this->ensureTemplateCache();
    $channels = array();
    foreach ($this->templateCache as $key) {
      preg_match($this->settings->getChannelRegex(), $key, $matches);
      $channel = $matches[1];
      $channels[$channel] = $channel;
    }
    ksort($channels);
    return $channels;
  }

  /**
   * @param $template
   *   The Redis key for a template.
   * @param int $skip
   *   The number of pages to skip.
   * @param int $entries_per_page
   *   The maximum number of entries in a page.
   */
  public function getEntries($template, $skip = 0, $entries_per_page = 0) {
    $entries_per_page = $this->settings->getEntriesPerPage($entries_per_page);

    // Redis expect actual number of entries in the RANGE parameters.
    $start = $skip * $entries_per_page;
    $end = $start + $entries_per_page;
    $entries = $this->redis->lRange($template, $start, $end);
    return $entries;
  }

  /**
   * @param mixed $criterium
   *   Filter will always pass if criterium is not set, but be checked otherwise.
   * @param string $regex
   *   The regex against which to match to pass the filter.
   * @param $string
   *   The string to match against the regex.
   *
   * @return bool
   */
  protected function passesFilter($criterium, $regex, $string) {
    if (!isset($criterium)) {
      $ret = TRUE;
    }
    else {
      $sts = preg_match($regex, $string, $matches);
      $ret = $sts && ($matches[1] == $criterium);
    }

    return $ret;
  }

  /**
   * @param null $channel
   * @param int $severity
   *   WATCHDOG_* : 0 to 7
   * @param int $skip
   *   Number of pages to skip.
   * @param int $entries_per_page
   *   Maximum number of entries per page.
   */
  public function getTemplates($channel = NULL, $severity = NULL, $skip = 0, $entries_per_page = 0) {
    $this->ensureTemplateCache();
    $matches = array();
    $count = 0;
    $channelRegex = $this->settings->getChannelRegex();
    $severityRegex = $this->settings->getSeverityRegex();

    $entries_per_page = $this->settings->getEntriesPerPage($entries_per_page);
    $start = $skip * $entries_per_page;
    $end = $start + $entries_per_page;

    foreach ($this->templateCache as $template) {
      if (!$this->passesFilter($channel, $channelRegex, $template)) {
        continue;
      }

      if (!$this->passesFilter($severity, $severityRegex, $template)) {
        continue;
      }

      $count++;
      if ($count >= $skip) {
        if ($count >= $end) {
          break;
        }
        $matches[] = $template;
      }
    }

    return $matches;
  }

  /**
   * Perform a complete Redis SCAN series.
   *
   * @param string $pattern
   *   The Redis SCAN MATCH optional argument.
   * @param int $max
   *   Maximum number of matches to be returned. Cursor is left dangling if $max
   *   is lower than the maximum number of available results: only the minimum
   *   number of SCAN iterations needed to reach $max results will be performed.
   * @param int $batch_suggestion
   *   The Redis SCAN COUNT optional argument.
   *
   * @return string[]
   */
  public function scan($pattern = '*', $max = 0, $batch_suggestion = 10) {
    $redis = $this->redis;
    $saved_scan_option = $redis->getOption(\Redis::OPT_SCAN);
    $redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);

    $ret = array();
    // Initialize our iterator to NULL.
    $scan_it = NULL;

    $count = 0;
    $request_count = 1;
    // Retry when we get no keys back.
    while ($arr_keys = $redis->scan($scan_it, $pattern, $batch_suggestion)) {
      $request_count++;
      foreach ($arr_keys as $str_key) {
        $ret[] = $str_key;
        if ($max) {
          $count++;
          if ($count >= $max) {
            break 2; // Break out of foreach + while.
          }
        }
      }
    }

    if ($saved_scan_option != \Redis::SCAN_RETRY)  {
      $redis->setOption(\Redis::OPT_SCAN, $saved_scan_option);
    }

    return $ret;
  }
}