<?php

/**
 * Class used to log information
 *
 * Standard: PSR-2
 *
 * @link http://www.php-fig.org/psr/psr-2 Full Documentation
 */

namespace Duplicator\Installer\Utils\Log;

use Duplicator\Installer\Core\Security;
use Duplicator\Installer\Core\Params\PrmMng;
use Duplicator\Installer\ViewHelpers\Resources;
use Duplicator\Libs\Snap\SnapIO;
use Error;
use Exception;

/**
 * Log
 * Class used to log information
 */
class Log
{
    /**
     * Maximum length of the log on the log.
     * Prevents uncontrolled increase in log size. This dimension should never be reached
     */
    const MAX_LENGTH_FWRITE = 50000;
    const LV_DEFAULT        = 1;
    const LV_DETAILED       = 2;
    const LV_DEBUG          = 3;
    const LV_HARD_DEBUG     = 4;

    /**
     * if true throw exception on error else die on error
     *
     * @var bool
     */
    private static $thowExceptionOnError = false;

    /**
     * log level
     *
     * @var int
     */
    private static $logLevel = self::LV_DEFAULT;

    /**
     * num of \t before log string.
     *
     * @var int
     */
    private static $indentation = 0;

    /**
     *
     * @var float
     */
    private static $microtimeStart = 0;

    /**
     *
     * @var callable
     */
    private static $postprocessCallback;

    /**
     * @var callable
     */
    private static $afterFatalErrorCallback;

    /**
     *
     * @var null|resource
     */
    private static $logHandle;

    /**
     * set log level from param manager
     *
     * @return void
     */
    public static function setLogLevel(): void
    {
        self::$logLevel = PrmMng::getInstance()->getValue(PrmMng::PARAM_LOGGING);
    }

    /**
     * Used to write debug info to the text log file
     *
     * @param string $msg     Any text data
     * @param int    $logging Log level
     * @param bool   $flush   if true flush file log
     *
     * @return void
     */
    public static function info($msg, $logging = self::LV_DEFAULT, $flush = false): void
    {
        if ($logging > self::$logLevel) {
            return;
        }

        $preLog = '';
        if (self::$indentation) {
            $preLog .= str_repeat("\t", self::$indentation);
        }
        if (self::$logLevel >= self::LV_DETAILED) {
            $preLog .= sprintf('[DELTA:%10.5f] ', microtime(true) - self::$microtimeStart);
        }
        if (is_callable(self::$postprocessCallback)) {
            $msg = call_user_func(self::$postprocessCallback, $msg);
        }

        @fwrite(self::getFileHandle(), $preLog . $msg . "\n", self::MAX_LENGTH_FWRITE);

        if ($flush) {
            self::flush();
        }
    }

    /**
     *
     * @return bool Returns true on success or false on failure.
     */
    public static function clearLog()
    {
        self::close();
        if (file_exists(self::getLogFilePath())) {
            return unlink(self::getLogFilePath());
        } else {
            return true;
        }
    }

    /**
     *
     * @return string
     */
    protected static function getLogFileName(): string
    {
        return 'dup-installer-log__' . Security::getInstance()->getSecondaryPackageHash() . '.txt';
    }

    /**
     *
     * @return string
     */
    public static function getLogFilePath(): string
    {
        return DUPX_INIT . '/' . self::getLogFileName();
    }

    /**
     *
     * @return string
     */
    public static function getLogFileUrl(): string
    {
        return Resources::getAssetsBaseUrl() . '/' . self::getLogFileName() . '?now=' . $GLOBALS['NOW_TIME'];
    }

    /**
     * Get trace string
     *
     * @param array<int, mixed> $callers   result of debug_backtrace
     * @param int               $fromLevel level to start
     * @param int<0,max>        $tabs      num tabs on each line
     *
     * @return string
     */
    public static function traceToString($callers, $fromLevel = 0, $tabs = 0): string
    {
        $result = '';
        for ($i = $fromLevel; $i < count($callers); $i++) {
            $trace   = $callers[$i];
            $result .= str_repeat("\t", $tabs);
            if (!empty($trace['class'])) {
                $result .= str_pad('TRACE[' . $i . '] CLASS___: ' . $trace['class'] . $trace['type'] . $trace['function'], 45, ' ');
            } else {
                $result .= str_pad('TRACE[' . $i . '] FUNCTION: ' . $trace['function'], 45, ' ');
            }
            if (isset($trace['file'])) {
                $result .= ' FILE: ' . $trace['file'] . '[' . $trace['line'] . ']';
            } else {
                $result .= ' NO FILE';
            }
            $result .= "\n";
        }
        return $result;
    }

    /**
     * Set post process callback
     *
     * @param callable $callback callback function
     *
     * @return void
     */
    public static function setPostProcessCallback($callback): void
    {
        self::$postprocessCallback = is_callable($callback) ? $callback : null;
    }

    /**
     * Set after fatal error callback
     *
     * @param callable $callback callback function
     *
     * @return void
     */
    public static function setAfterFatalErrorCallback($callback): void
    {
        self::$afterFatalErrorCallback = is_callable($callback) ? $callback : null;
    }

    /**
     * Reset time counter
     *
     * @param int     $logging  log level
     * @param boolean $fileInfo Log file info (file, line)
     *
     * @return void
     */
    public static function resetTime($logging = self::LV_DEFAULT, $fileInfo = true): void
    {
        self::$microtimeStart = microtime(true);
        if ($logging > self::$logLevel) {
            return;
        }
        // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
        $callers = debug_backtrace();
        $file    = $callers[0]['file'];
        $line    = $callers[0]['line'];
        Log::info('LOG-TIME' . ($fileInfo ? '[' . $file . ':' . $line . ']' : '') . ' RESET TIME', $logging);
    }

    /**
     * Log time delta from last resetTime call
     *
     * @param string $msg      message
     * @param int    $logging  log level
     * @param bool   $fileInfo if strue write file info (file, line)
     *
     * @return void
     */
    public static function logTime($msg = '', $logging = self::LV_DEFAULT, $fileInfo = true): void
    {
        if ($logging > self::$logLevel) {
            return;
        }
        // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
        $callers = debug_backtrace();
        $file    = $callers[0]['file'];
        $line    = $callers[0]['line'];

        if ($fileInfo) {
            Log::info(
                sprintf('LOG-TIME[%s:%s][DELTA:%10.5f] ', $file, $line, microtime(true) - self::$microtimeStart) . (empty($msg) ? '' : ' MESSAGE:' . $msg),
                $logging
            );
        } else {
            Log::info(sprintf('LOG-TIME[DELTA:%10.5f] ', microtime(true) - self::$microtimeStart) . (empty($msg) ? '' : ' MESSAGE:' . $msg), $logging);
        }
    }

    /**
     * Increment indentation
     *
     * @return void
     */
    public static function incIndent(): void
    {
        self::$indentation++;
    }

    /**
     * Decrease indentation
     *
     * @return void
     */
    public static function decIndent(): void
    {
        if (self::$indentation > 0) {
            self::$indentation--;
        }
    }

    /**
     * Reset indentation
     *
     * @return void
     */
    public static function resetIndent(): void
    {
        self::$indentation = 0;
    }

    /**
     * Return true if log level is >= of loggin level
     *
     * @param int $logging log level
     *
     * @return boolean
     */
    public static function isLevel($logging): bool
    {
        return $logging <= self::$logLevel;
    }

    /**
     * Log passed object
     *
     * @param string $msg     log message
     * @param mixed  $object  object to log
     * @param int    $logging log level
     *
     * @return void
     */
    public static function infoObject($msg, $object, $logging = self::LV_DEFAULT): void
    {
        $msg = $msg . "\n" . print_r($object, true);
        self::info($msg, $logging);
    }

    /**
     * Flush log file
     *
     * @return void
     */
    public static function flush(): void
    {
        if (is_resource(self::$logHandle)) {
            fflush(self::$logHandle);
        }
    }

    /**
     * Close log file
     *
     * @return bool
     */
    public static function close(): bool
    {
        if (is_resource(self::$logHandle)) {
            fclose(self::$logHandle);
        }
        self::$logHandle = null;
        return true;
    }

    /**
     * Get log file stream
     *
     * @return resource
     */
    public static function getFileHandle()
    {
        if (is_resource(self::$logHandle)) {
            return self::$logHandle;
        }

        if (!is_writable(dirname(self::getLogFilePath()))) {
            throw new \Exception('Can\'t write in dup-installer folder, please check the dup-installer permission folder');
        }

        if (file_exists(self::getLogFilePath())) {
            SnapIO::chmod(self::getLogFilePath(), 'u+rw');
        }

        if ((self::$logHandle = fopen(self::getLogFilePath(), "a+")) === false) {
            self::$logHandle = null;
            throw new \Exception('Can\'t open the log file, please check the dup-installer permission folder');
        }

        SnapIO::chmod(self::getLogFilePath(), 'u+rw');

        return self::$logHandle;
    }

    /**
     * Log error and die or thore exception if self::$thowExceptionOnError is true
     *
     * @param string $errorMessage error message
     *
     * @return void
     */
    public static function error($errorMessage): void
    {
        $breaks  = [
            "<br />",
            "<br>",
            "<br/>",
        ];
        $spaces  = ["&nbsp;"];
        $log_msg = str_ireplace($breaks, "\r\n", $errorMessage);
        $log_msg = str_ireplace($spaces, " ", $log_msg);
        $log_msg = strip_tags($log_msg);

        self::info("\nINSTALLER ERROR:\n{$log_msg}\n");

        if (is_callable(self::$afterFatalErrorCallback)) {
            call_user_func(self::$afterFatalErrorCallback);
        }

        if (self::$thowExceptionOnError) {
            throw new \Exception($errorMessage);
        } else {
            self::close();
            die("<div class='dupx-ui-error'><hr size='1' /><b style='color:#B80000;'>INSTALL ERROR!</b><br/><pre>{$errorMessage}</pre></div>");
        }
    }

    /**
     * Get log exception string
     *
     * @param Exception $e     exception object
     * @param string    $title log message
     *
     * @return string
     */
    public static function getLogException($e, $title = 'EXCEPTION ERROR: '): string
    {
        return $title . ' ' . $e->getMessage() . "[CODE:" . $e->getCode() . "]\n" .
            "\tFILE:" . $e->getFile() . '[' . $e->getLIne() . "]\n" .
            "\tTRACE:\n" . $e->getTraceAsString();
    }

    /**
     * Log exception
     *
     * @param Error|Exception $e       exception object
     * @param int             $logging log level
     * @param string          $title   log message
     *
     * @return void
     */
    public static function logException($e, $logging = self::LV_DEFAULT, $title = 'EXCEPTION ERROR: '): void
    {
        if ($logging <= self::$logLevel) {
            Log::info("\n" . self::getLogException($e, $title) . "\n");
        }
    }

    /**
     * If set true error function thorw exception instead die
     *
     * @param boolean $set enable/disable thorw exception
     *
     * @return void
     */
    public static function setThrowExceptionOnError($set): void
    {
        self::$thowExceptionOnError = (bool) $set;
    }

    /**
     * Get string from generic value
     *
     * @param mixed $var           value
     * @param bool  $checkCallable if true check if var is callable and display it
     *
     * @return string
     */
    public static function v2str($var, $checkCallable = false): string
    {
        if ($checkCallable && is_callable($var)) {
            return '(callable) ' . print_r($var, true);
        }
        switch (gettype($var)) {
            case "boolean":
                return $var ? 'true' : 'false';
            case "integer":
            case "double":
                return (string) $var;
            case "string":
                return '"' . $var . '"';
            case "array":
            case "object":
                return print_r($var, true);
            case "resource":
            case "resource (closed)":
            case "NULL":
            case "unknown type":
            default:
                return gettype($var);
        }
    }
}
