<?php

/**
 * @copyright (C) 2013 iJoomla, Inc. - All rights reserved.
 * @license GNU General Public License, version 2 (http://www.gnu.org/licenses/gpl-2.0.html)
 * @author iJoomla.com <webmaster@ijoomla.com>
 * @url https://www.jomsocial.com/license-agreement
 * The PHP code portions are distributed under the GPL license. If not otherwise stated, all images, manuals, cascading style sheets, and included JavaScript *are NOT GPL, and are released under the IJOOMLA Proprietary Use License v1.0
 * More info at https://www.jomsocial.com/license-agreement
 */
// no direct access
defined('_JEXEC') or die('Restricted access');

/**
 * Class exists checking
 */
if (!class_exists('CLogger')) {

    /**
     * Simple logging class
     */
    class CLogger extends cobject {

        /**
         * Error severity, from low to high. From BSD syslog RFC, secion 4.1.1
         * @link http://www.faqs.org/rfcs/rfc3164.html
         */
        const EMERG = 0;  // Emergency: system is unusable
        const ALERT = 1;  // Alert: action must be taken immediately
        const CRIT = 2;  // Critical: critical conditions
        const ERR = 3;  // Error: error conditions
        const WARN = 4;  // Warning: warning conditions
        const NOTICE = 5;  // Notice: normal but significant condition
        const INFO = 6;  // Informational: informational messages
        const DEBUG = 7;  // Debug: debug messages

        /**
         * Log nothing at all
         */
        const OFF = 8;

        /**
         * Alias for CRIT
         * @deprecated
         */
        const FATAL = 2;

        /**
         * Internal status codes
         */
        const STATUS_LOG_OPEN = 1;
        const STATUS_OPEN_FAILED = 2;
        const STATUS_LOG_CLOSED = 3;

        /**
         * We need a default argument value in order to add the ability to easily
         * print out objects etc. But we can't use NULL, 0, FALSE, etc, because those
         * are often the values the developers will test for. So we'll make one up.
         */
        const NO_ARGUMENTS = 'CLogger:NO_ARGUMENTS';

        /**
         * Current status of the log file
         * @var integer
         */
        private $_logStatus = self::STATUS_LOG_CLOSED;

        /**
         * Holds messages generated by the class
         * @var array
         */
        private $_messageQueue = array();

        /**
         * Path to the log file
         * @var string
         */
        private $_logFilePath = null;

        /**
         * Current minimum logging threshold
         * @var integer
         */
        private $_severityThreshold = self::DEBUG;

        /**
         * This holds the file handle for this instance's log file
         * @var resource
         */
        private $_fileHandle = null;

        /**
         * Standard messages produced by the class. Can be modified for il8n
         * @var array
         */
        private $_messages = array(
            'writefail' => 'The file could not be written to. Check that appropriate permissions have been set.',
            'opensuccess' => 'The log file was opened successfully.',
            'openfail' => 'The file could not be opened. Check permissions.',
        );

        /**
         * Valid PHP date() format string for log timestamps
         * @var string
         */
        private static $_dateFormat = 'Y-m-d G:i:s';

        /**
         * Octal notation for default permissions of the log file
         * @var integer
         */
        private static $_defaultPermissions = 0644;

        /**
         * Array of CLogger instances, part of Singleton pattern
         * @var array
         */
        private static $_instances = array();

        /**
         * Class constructor
         *
         * @param string  $logDirectory File path to the logging directory
         * @param integer $severity     One of the pre-defined severity constants
         * @return void
         */
        public function __construct($logDirectory, $severity) {
            $logDirectory = rtrim($logDirectory, '\\/');

            if ($severity === self::OFF) {
                return;
            }

            $this->_severityThreshold = $severity;

            /* Prepare log dir */
            $this->_logFilePath = $logDirectory . DIRECTORY_SEPARATOR . 'log_' . date('Y-m-d') . '.txt';
            if (!file_exists($logDirectory)) {
                mkdir($logDirectory, self::$_defaultPermissions, true);
            }
            /* Prepare log file */
            if (file_exists($this->_logFilePath) && !is_writable($this->_logFilePath)) {
                $this->_logStatus = self::STATUS_OPEN_FAILED;
                $this->_messageQueue[] = $this->_messages['writefail'];
                return;
            }
            /* Open file */
            if (($this->_fileHandle = fopen($this->_logFilePath, 'a'))) {
                $this->_logStatus = self::STATUS_LOG_OPEN;
                $this->_messageQueue[] = $this->_messages['opensuccess'];
            } else {
                $this->_logStatus = self::STATUS_OPEN_FAILED;
                $this->_messageQueue[] = $this->_messages['openfail'];
            }
        }

        /**
         * Partially implements the Singleton pattern. Each $logDirectory gets one instance.
         *
         * @param string  $logDirectory File path to the logging directory
         * @param integer $severity     One of the pre-defined severity constants
         * @return CLogger
         */
        public static function getInstance($logDirectory = null, $severity = CLogger::DEBUG) {

            if ($logDirectory === null) {
                /* Return current instance if we already have instances */
                if (count(self::$_instances) > 0) {
                    return current(self::$_instances);
                } else {
                    /* Or create new instance with current directory */
                    $logDirectory = dirname(__FILE__);
                }
            }

            if (in_array($logDirectory, self::$_instances)) {
                return self::$_instances[$logDirectory];
            }

            self::$_instances[$logDirectory] = new self($logDirectory, $severity);

            return self::$_instances[$logDirectory];
        }

        /**
         * Class destructor
         */
        public function __destruct() {
            if ($this->_fileHandle) {
                fclose($this->_fileHandle);
            }
        }

        /**
         * Returns (and removes) the last message from the queue.
         * @return string
         */
        public function getMessage() {
            return array_pop($this->_messageQueue);
        }

        /**
         * Returns the entire message queue (leaving it intact)
         * @return array
         */
        public function getMessages() {
            return $this->_messageQueue;
        }

        /**
         * Empties the message queue
         * @return void
         */
        public function clearMessages() {
            $this->_messageQueue = array();
        }

        /**
         * Sets the date format used by all instances of CLogger
         * 
         * @param string $dateFormat Valid format string for date()
         */
        public static function setDateFormat($dateFormat) {
            self::$_dateFormat = $dateFormat;
        }

        /**
         * Writes a $line to the log with a severity level of DEBUG
         *
         * @param string $line Information to log
         * @return void
         */
        public function debug($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::DEBUG, $args);
        }

        /**
         * Writes a $line to the log with a severity level of INFO. Any information
         * can be used here, or it could be used with E_STRICT errors
         *
         * @param string $line Information to log
         * @return void
         */
        public function info($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::INFO, $args);
        }

        /**
         * Writes a $line to the log with a severity level of NOTICE. Generally
         * corresponds to E_STRICT, E_NOTICE, or E_USER_NOTICE errors
         *
         * @param string $line Information to log
         * @return void
         */
        public function notice($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::NOTICE, $args);
        }

        /**
         * Writes a $line to the log with a severity level of WARN. Generally
         * corresponds to E_WARNING, E_USER_WARNING, E_CORE_WARNING, or 
         * E_COMPILE_WARNING
         *
         * @param string $line Information to log
         * @return void
         */
        public function warn($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::WARN, $args);
        }

        /**
         * Writes a $line to the log with a severity level of ERR. Most likely used
         * with E_RECOVERABLE_ERROR
         *
         * @param string $line Information to log
         * @return void
         */
        public function error($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::ERR, $args);
        }

        /**
         * Writes a $line to the log with a severity level of FATAL. Generally
         * corresponds to E_ERROR, E_USER_ERROR, E_CORE_ERROR, or E_COMPILE_ERROR
         *
         * @param string $line Information to log
         * @return void
         * @deprecated Use logCrit
         */
        public function fatal($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::FATAL, $args);
        }

        /**
         * Writes a $line to the log with a severity level of ALERT.
         *
         * @param string $line Information to log
         * @return void
         */
        public function alert($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::ALERT, $args);
        }

        /**
         * Writes a $line to the log with a severity level of CRIT.
         *
         * @param string $line Information to log
         * @return void
         */
        public function crit($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::CRIT, $args);
        }

        /**
         * Writes a $line to the log with a severity level of EMERG.
         *
         * @param string $line Information to log
         * @return void
         */
        public function emerg($line, $args = self::NO_ARGUMENTS) {
            $this->log($line, self::EMERG, $args);
        }

        /**
         * Writes a $line to the log with the given severity
         *
         * @param string  $line     Text to add to the log
         * @param integer $severity Severity level of log message (use constants)
         */
        public function log($line, $severity, $args = self::NO_ARGUMENTS) {
            if ($this->_severityThreshold >= $severity) {
                $status = $this->_getTimeLine($severity);

                $line = "$status $line";

                if ($args !== self::NO_ARGUMENTS) {
                    /* Print the passed object value */
                    $line = $line . '; ' . var_export($args, true);
                }

                $this->writeFreeFormLine($line . PHP_EOL);
            }
        }

        /**
         * Writes a line to the log without prepending a status or timestamp
         *
         * @param string $line Line to write to the log
         * @return void
         */
        public function writeFreeFormLine($line) {
            if ($this->_logStatus == self::STATUS_LOG_OPEN && $this->_severityThreshold != self::OFF) {
                if (fwrite($this->_fileHandle, $line) === false) {
                    $this->_messageQueue[] = $this->_messages['writefail'];
                }
            }
        }

        /**
         * 
         * @param type $level
         * @return type
         */
        private function _getTimeLine($level) {
            $time = date(self::$_dateFormat);

            switch ($level) {
                case self::EMERG:
                    return "$time - EMERG -->";
                case self::ALERT:
                    return "$time - ALERT -->";
                case self::CRIT:
                    return "$time - CRIT -->";
                case self::FATAL: # FATAL is an alias of CRIT
                    return "$time - FATAL -->";
                case self::NOTICE:
                    return "$time - NOTICE -->";
                case self::INFO:
                    return "$time - INFO -->";
                case self::WARN:
                    return "$time - WARN -->";
                case self::DEBUG:
                    return "$time - DEBUG -->";
                case self::ERR:
                    return "$time - ERROR -->";
                default:
                    return "$time - LOG -->";
            }
        }

    }

}