<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 2/10/18
 * Time: 3:14 PM
 *
 * See: doc/THUMBNAIL.md
 */

namespace IMATHUZH\Qfq\Core\Report;


use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Store;

/**
 * Class Thumbnail
 * @package qfq
 */
class Monitor {

    /**
     * @var Store
     */
    private $store = null;

    /**
     * @var Session
     */
    private $session = null;

    /**
     * @param bool|false $phpUnit
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct($phpUnit = false) {

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
        $this->store = Store::getInstance();
        $this->session = Session::getInstance($phpUnit);
    }

    /**
     * Renders some JS to monitor a file on the server.
     * Report: $str: 'file:typo3conf/sql.log|tail:1000|append:1|interval:1000|htmlId:monitor1'
     * Fetch: download.php?s=<sip> with sip: file=<filename>&tail=<number of lines>
     *
     * @param array $vars
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function process(array $vars) {

        if (empty($vars[TOKEN_L_FILE])) {
            throw new \UserReportException("Monitor: empty or missing parameter 'file'", ERROR_MISSING_PARAMETER_FILE);
        }

        // Set defaults
        Support::setIfNotSet($vars, TOKEN_L_TAIL, MONITOR_TAIL_DEFAULT);
        $isContinuous = Support::setIfNotSet($vars, TOKEN_L_APPEND, MONITOR_APPEND_DEFAULT) == MONITOR_MODE_APPEND_1 ? 'true' : 'false';
        $interval = Support::setIfNotSet($vars, TOKEN_L_INTERVAL, MONITOR_INTERVAL_DEFAULT);
        $htmlId = Support::setIfNotSet($vars, TOKEN_L_HTML_ID, MONITOR_HTML_ID_DEFAULT);

        // Setup query to generate SIP
        $queryString = DOWNLOAD_MODE . '=' . DOWNLOAD_MODE_MONITOR .
            '&' . TOKEN_L_FILE . '=' . $vars[TOKEN_L_FILE] .
            '&' . TOKEN_L_TAIL . '=' . $vars[TOKEN_L_TAIL] .
            '&' . TOKEN_L_APPEND . '=' . $vars[TOKEN_L_APPEND];
//        $url = store::getSipInstance()->queryStringToSip(API_DIR . '/' . API_DOWNLOAD_PHP . '?' . $queryString, RETURN_URL);
        $arr = store::getSipInstance()->queryStringToSip(Path::join(Path::JAVASCRIPT_TO_EXT, Path::EXT_TO_API, API_DOWNLOAD_PHP) . '?' . $queryString, RETURN_ARRAY);
        $url = $arr[SIP_SIP_URL];

        // On page reload, take care to remove optional existing old seek position.
        $key = $this->getSeekSessionKey($arr[CLIENT_SIP]);
        $this->session::unsetItem($key);

        $webworker = Path::urlExt(Path::EXT_TO_JAVASCRIPT, 'GetFileContent.js');

        $code = <<<EOF
<script type="text/javascript">
    $(document).ready(function () {
        var getFile = new QfqNS.DisplayFile({
            webworker: "$webworker",
            filePath: "$url",
            interval: $interval,
            targetId: "$htmlId",
            isContinuous: $isContinuous
        });
        getFile.show();
    });
</script>
EOF;

        return $code;
    }

    /**
     * @param $filepath
     * @param int $lines
     * @param $modeAppend
     * @return string
     * @throws \CodeException
     * @throws \DownloadException
     * @throws \UserFormException
     */
    public function dump($filepath, $lines, $modeAppend) {

        // Retrieve always the last $lines
        if ($modeAppend == MONITOR_MODE_APPEND_0) {
            return $this->tailCustom($filepath, $lines);
        }

        // First call to this file in this session.
        $f = $this->open($filepath);

        // Check if there is already a seek position.
        $key = $this->getSeekSessionKey($this->store->getVar(SIP_SIP, STORE_SIP));
        $seek = $this->session::get($key);
        if ($seek === false) {

            // Get seek position EOF
            fseek($f, 0, SEEK_END);
            $seek = ftell($f);

            // Store the position in session.
            $this->session::set($key, $seek);

            // On initial call, get the specified last $lines.
            return $this->tailCustom($filepath, $lines, true, $f);
        }

        fseek($f, $seek, SEEK_SET);
        $output = fread($f, 8192);
        $this->session::set($key, ftell($f));

        return $output;
    }

    /**
     * @param $sip
     * @return string
     */
    private function getSeekSessionKey($sip) {
        return MONITOR_SESSION_FILE_SEEK . '-' . $sip;
    }

    /**
     * @param string $filepath
     * @param string $mode
     * @return bool|resource
     * @throws \DownloadException
     */
    private function open($filepath, $mode = 'rb') {

        // Open file
        $f = @fopen($filepath, $mode);
        if ($f === false) {
            $err = error_get_last();
            $msg = $err['message'] ?? '';
            throw new \DownloadException ("Error open '" . $filepath . "': " . $msg, ERROR_IO_FILE_NOT_FOUND);
        }

        return $f;
    }


    /**
     * Slightly modified version of http://www.geekality.net/2011/05/28/php-tail-tackling-large-files/
     * @param string $filepath
     * @param int $lines
     * @param bool $adaptive
     * @param null $f
     * @return string
     * @throws \DownloadException
     * @author Torleif Berger, Lorenzo Stanco
     * @link http://stackoverflow.com/a/15025877/995958
     * @license http://creativecommons.org/licenses/by/3.0/
     *
     */
    private function tailCustom($filepath, $lines = 1, $adaptive = true, $f = null) {

        // Open file
        if ($f === null) {
            $f = $this->open($filepath);
        }

        // Sets buffer size, according to the number of lines to retrieve.
        // This gives a performance boost when reading a few lines from the file.
        if (!$adaptive) $buffer = 4096;
        else $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));

        // Jump to last character
        fseek($f, -1, SEEK_END);

        // Read it and adjust line number if necessary
        // (Otherwise the result would be wrong if file doesn't end with a blank line)
        if (fread($f, 1) != "\n") $lines -= 1;

        // Start reading
        $output = '';
        $chunk = '';

        // While we would like more
        while (ftell($f) > 0 && $lines >= 0) {

            // Figure out how far back we should jump
            $seek = min(ftell($f), $buffer);

            // Do the jump (backwards, relative to where we are)
            fseek($f, -$seek, SEEK_CUR);

            // Read a chunk and prepend it to our output
            $output = ($chunk = fread($f, $seek)) . $output;

            // Jump back to where we started reading
            fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);

            // Decrease our line counter
            $lines -= substr_count($chunk, "\n");

        }

        // While we have too many lines
        // (Because of buffer size we might have read too many)
        while ($lines++ < 0) {

            // Find first newline and remove all text before that
            $output = substr($output, strpos($output, "\n") + 1);

        }

        // Close file and return
        fclose($f);
        return trim($output);
    }
}