<?php

namespace Analog;

use Stringable;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\InvalidArgumentException;

/**
 * Analog - PHP 5.3+ logging class
 *
 * Copyright (c) 2012 Johnny Broadway
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished
 * to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * Implements the PSR-3 standard as a wrapper to Analog. For more information,
 * see:
 *
 * https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
 *
 * Usage:
 *
 *     <?php
 *     
 *     require_once ('vendor/autoload.php');
 *
 *     $log = new Analog\Logger ();
 *     
 *     $log->notice ('Things are really happening right now.');
 *     
 *     ?>
 *
 * @package Analog
 * @author Johnny Broadway
 */
class Logger implements LoggerInterface {
	private $handler = null;

	/**
	 * Converts from PSR-3 log levels to Analog log levels.
	 */
	public function convert_log_level ($level, $reverse = false) {
		if ($reverse) {
			switch ($level) {
				case Analog::URGENT:
					return LogLevel::EMERGENCY;
				case Analog::ALERT:
					return LogLevel::ALERT;
				case Analog::CRITICAL:
					return LogLevel::CRITICAL;
				case Analog::ERROR:
					return LogLevel::ERROR;
				case Analog::WARNING:
					return LogLevel::WARNING;
				case Analog::NOTICE:
					return LogLevel::NOTICE;
				case Analog::INFO:
					return LogLevel::INFO;
				case Analog::DEBUG:
					return LogLevel::DEBUG;
			}
			throw new InvalidArgumentException ('Level "' . $level . '" is not defined.');
		} else {
			switch ($level) {
				case LogLevel::EMERGENCY:
					return Analog::URGENT;
				case LogLevel::ALERT:
					return Analog::ALERT;
				case LogLevel::CRITICAL:
					return Analog::CRITICAL;
				case LogLevel::ERROR:
					return Analog::ERROR;
				case LogLevel::WARNING:
					return Analog::WARNING;
				case LogLevel::NOTICE:
					return Analog::NOTICE;
				case LogLevel::INFO:
					return Analog::INFO;
				case LogLevel::DEBUG:
					return Analog::DEBUG;
			}
			throw new InvalidArgumentException ('Level "' . $level . '" is not defined.');
		}
	}

	/**
	 * Interpolates context values into the message placeholders.
	 */
	private function interpolate ($message, array $context = array ()) {
		if (is_array ($message)) {
			return $message;
		}

		// build a replacement array with braces around the context keys
		$replace = array ();
		foreach ($context as $key => $val) {
			if (is_object ($val) && get_class ($val) === 'DateTime') {
				$val = $val->format ('Y-m-d H:i:s');
			} elseif (is_object ($val)) {
				$val = json_encode ($val);
			} elseif (is_array ($val)) {
				$val = json_encode ($val);
			} elseif (is_resource ($val)) {
				$val = (string) $val;
			}
			$replace['{' . $key . '}'] = $val;
		}
		
		// interpolate replacement values into the the message and return
		return strtr ($message, $replace);
	}

	/**
	 * Sets the Analog log handler.
	 */
	public function handler ($handler) {
		$this->handler = $handler;
	}

	/**
	 * Sets the log message format.
	 */
	public function format ($format) {
		Analog::$format = $format;
	}

	/**
	 * System is unusable.
	 */
	public function emergency (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::URGENT, $message, $context);
	}

	/**
	 * Action must be taken immediately.
	 */
	public function alert (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::ALERT, $message, $context);
	}

	/**
	 * Critical conditions.
	 */
	public function critical (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::CRITICAL, $message, $context);
	}

	/**
	 * Runtime errors that do not require immediate action but should typically
	 * be logged and monitored.
	 */
	public function error (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::ERROR, $message, $context);
	}

	/**
	 * Exceptional occurrences that are not errors.
	 */
	public function warning (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::WARNING, $message, $context);
	}

	/**
	 * Normal but significant events.
	 */
	public function notice (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::NOTICE, $message, $context);
	}

	/**
	 * Interesting events.
	 */
	public function info (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::INFO, $message, $context);
	}

	/**
	 * Detailed debug information.
	 */
	public function debug (Stringable|string $message, array $context = array ()) : void {
		$this->_log (Analog::DEBUG, $message, $context);
	}

	/**
	 * Logs with an arbitrary level.
	 */
	public function log ($level, Stringable|string $message, array $context = array ()) : void {
		$this->_log (
			$this->convert_log_level ($level),
			$message,
			$context
		);
	}

	/**
	 * Perform the logging to Analog after the log level has been converted.
	 */
	private function _log ($level, $message, $context) {
		if ($this->handler != null) {
			Analog::handler ($this->handler);
		}

		Analog::log (
			$this->interpolate ($message, $context),
			$level
		);
	}
}