<?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\HelperFile;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\Token;
use IMATHUZH\Qfq\Core\Store\Store;

//use qfq;

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

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

    private $inkscape = '';
    private $convert = '';
    private $absoluteThumbnailDirSecure = '';
    private $absoluteThumbnailDirPublic = '';

    /**
     * @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->inkscape = $this->store->getVar(SYSTEM_CMD_INKSCAPE, STORE_SYSTEM);
        $this->convert = $this->store->getVar(SYSTEM_CMD_CONVERT, STORE_SYSTEM);
        $this->absoluteThumbnailDirSecure = Path::absoluteApp($this->store->getVar(SYSTEM_THUMBNAIL_DIR_SECURE_REL_TO_APP, STORE_SYSTEM));
        $this->absoluteThumbnailDirPublic = $this->store->getVar(SYSTEM_THUMBNAIL_DIR_PUBLIC_REL_TO_APP, STORE_SYSTEM);
    }

    /**
     * Renders a thumbnail based on $str.
     * $str: "T:<pathfilename source>|W:<dimension>|s:<0|1>|a:<alt-text>"
     *
     * Argument 'T' is mandatory.
     * Argument 'W' is optional. Defaults to 'W:150x'.
     * Argument 's' is optional. Defaults to 's:1'
     *
     * @param string $str
     * @param string $modeRender
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function process($str, $modeRender = THUMBNAIL_PREPARE) {

        $control = Token::explodeTokenString($str);
        Support::setIfNotSet($control, TOKEN_ALT_TEXT, 'thumbnail');
        Support::setIfNotSet($control, TOKEN_SIP);
        $control[TOKEN_SIP] = Token::checkForEmptyValue(TOKEN_SIP, $control[TOKEN_SIP]);

        if (empty($control[TOKEN_THUMBNAIL_DIMENSION])) {
            $control[TOKEN_THUMBNAIL_DIMENSION] = THUMBNAIL_WIDTH_DEFAULT;
        }

        if (file_exists($control[TOKEN_THUMBNAIL]) && !empty($control[TOKEN_THUMBNAIL_DIMENSION])) {
            $pathFilenameSource = $control[TOKEN_THUMBNAIL];
        } else {
            $pathFilenameSource = QFQ_ICON_PATH . QFQ_ICON_DEFAULT_THUMBNAIL;
        }

        $dir = ($control[TOKEN_SIP] == "1") ? $this->absoluteThumbnailDirSecure : $this->absoluteThumbnailDirPublic;
        $pathFilenameThumbnail = Support::joinPath($dir, md5($pathFilenameSource . $control[TOKEN_THUMBNAIL_DIMENSION]) . '.png');

        // Check if the file has to exist now.
        if ($modeRender == THUMBNAIL_VIA_DOWNLOAD || $control[TOKEN_SIP] != "1") {
            $pathFilenameThumbnail = $this->getOrCreateThumbnail($pathFilenameSource, $pathFilenameThumbnail, $control, $modeRender);
        }

        return $this->buildImageTag($str, $modeRender, $control, $pathFilenameThumbnail);
    }

    /**
     * Creates a thumbnail (saved under $pathFilenameThumbnail) based on $pathFilenameSource.
     * Returns the pathFilename of the thumbnail.
     *
     * @param string $pathFilenameSource
     * @param string $pathFilenameThumbnail
     * @param array $control
     * @param string $modeRender DOWNLOAD_RENDER_AUTO | DOWNLOAD_RENDER_NOW
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function getOrCreateThumbnail($pathFilenameSource, $pathFilenameThumbnail, array $control, $modeRender) {
        $debugMode = false;
        $statThumbnail = false;

        // Check if the Thumbnail already exists (#21415)
        if (file_exists($pathFilenameThumbnail)) {
            $statThumbnail = stat($pathFilenameThumbnail);
        }
        $statSource = stat($pathFilenameSource);

        // thumbnail already exist?
        if ($statThumbnail !== false) {

            // Another process already creating this thumbnail? Just wait until it's finished.
            if ($statThumbnail['size'] == 0) {

                if (time() - $statThumbnail['mtime'] > THUMBNAIL_MAX_SECONDS) {
                    unlink($pathFilenameThumbnail); // remove old empty files: this gives a chance to rerender the image on the next call.
                    return 'brokenimage';
                }

                $max = THUMBNAIL_MAX_SECONDS;
                while ($statThumbnail['size'] == 0 && $max-- > 0) {
                    sleep(1);
                    clearstatcache();
                    $statThumbnail = stat($pathFilenameThumbnail);
                }
                sleep(1); // additional time to be sure that the whole file is written.
            }

            // Check if the file is recent.
            if ($statSource['mtime'] < $statThumbnail['mtime']) {
                return $pathFilenameThumbnail;
            }
        }

        // Render thumbnail: either it's a) public thumbnail or b) requested via download.php
        if (($modeRender == THUMBNAIL_PREPARE && $control[TOKEN_SIP] != '1') || ($modeRender == THUMBNAIL_VIA_DOWNLOAD)) {

            $pathFilenameThumbnail = $this->createThumbnail($pathFilenameSource, $pathFilenameThumbnail, $control[TOKEN_THUMBNAIL_DIMENSION], $debugMode);
        }

        return $pathFilenameThumbnail;
    }

    /**
     * Creates a thumbnail from '$pathFilenameSource' under '$pathFilenameThumbnail'.
     * SVG will be rendered via 'inkscape', all others via 'convert'.
     * Creates an empty '$pathFilenameThumbnail' before the rendering starts. Will be overwritten through the rendering.
     *
     * @param string $pathFilenameSource
     * @param string $pathFilenameThumbnail
     * @param string $dimension
     * @param $debugMode
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function createThumbnail($pathFilenameSource, $pathFilenameThumbnail, $dimension, $debugMode) {
        $outputInkscape = '';
        $cmdInkscape = '';

        // Check if file is writeable.
        if (false === @touch($pathFilenameThumbnail)) {
            // Be sure that the target directory exist
            HelperFile::mkDirParent($pathFilenameThumbnail);
            if (false === touch($pathFilenameThumbnail)) {
                throw new \UserReportException('Could not create file: ' . OnString::strrstr($pathFilenameSource, '/'), ERROR_IO_CREATE_FILE);
            }
        }

        // Get extension.
        $ext = strtolower(OnString::strrstr($pathFilenameSource, '.'));

        // SVG files are best to thumbnail via 'inkscape'
//        if ( ($ext == 'svg' || $ext == 'pdf') && $this->inkscape != '') {
        if (($ext == 'svg') && $this->inkscape != '') {
            $inkscapeDimension = Token::explodeDimension($dimension);
            // Automatically cut white border: --export-area-drawing
            // --export-text-to-path
            $cmdInkscape = $this->inkscape . " --without-gui $inkscapeDimension --export-png $pathFilenameThumbnail $pathFilenameSource";
            $outputInkscape = Support::qfqExec($cmdInkscape, $rc);
            if ($rc == 0) {
                return $pathFilenameThumbnail;
            }
            // if process failed, try 'convert' below.
        }

        // On multi page files: just take the first page
        if ($ext == 'pdf') {
            $pathFilenameSource .= '[0]';
        }

        $cmd = $this->convert . " -scale $dimension $pathFilenameSource $pathFilenameThumbnail";
        $output = Support::qfqExec($cmd, $rc);

        if ($rc != 0) {
            // Check if the extension is from a supported filetype
            switch ($ext) {

                // The following files should always be thumbnail'able - if not, throw an exception.
                case 'pdf':
                case 'jpg':
                case 'gif':
                case 'png':
                case 'svg':
                    break;

                default:
                    $placeholder = Support::joinPath(Path::absoluteApp(Path::APP_TO_THUMBNAIL_UNKNOWN_TYPE), $ext . '.gif');
                    if (is_readable($placeholder)) {
                        return $placeholder;
                    }
            }

            $debug = 'Source: ' . $pathFilenameSource . PHP_EOL;
            $debug .= 'Thumbnail: ' . $pathFilenameThumbnail . PHP_EOL;
            if ($outputInkscape) {
                $debug .= $cmdInkscape . PHP_EOL;
                $debug .= $outputInkscape . PHP_EOL;
            }
            $debug .= $cmd . PHP_EOL;
            $debug .= $output . PHP_EOL;
            $this->store->setVar(SYSTEM_MESSAGE_DEBUG, $debug, STORE_SYSTEM);

            throw new \UserReportException("Thumbnail creation failed: " . OnString::strrstr($pathFilenameSource, '/'), ERROR_THUMBNAIL_RENDER);
        }

        return $pathFilenameThumbnail;
    }

    /**
     * Returns the thumbnail URL, either:
     * secure) s:1 >> '<img loading="lazy" src=".../Api/download.php?s=....">'
     * public) s:0 >> '<img loading="lazy" src="fileadmin/<thumb dir>/....png">'
     * pure URL secure) s:1 >> '.../Api/download.php?s=....'
     * pure URL public) s:0 >> 'fileadmin/<thumb dir>/....png'
     *
     * @param string $str
     * @param string $modeRender THUMBNAIL_PREPARE | THUMBNAIL_RENDERED_FILE
     * @param array $control
     * @param string $pathFilenameThumbnail
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @internal param bool $flagSecure
     */
    private function buildImageTag($str, $modeRender, array $control, $pathFilenameThumbnail) {

        if ($modeRender == THUMBNAIL_VIA_DOWNLOAD) {
            return $pathFilenameThumbnail;
        }

        $src = ($control[TOKEN_SIP] == "1") ? $this->buildSecureDownloadLink($pathFilenameThumbnail, $str) : $pathFilenameThumbnail;

        // With RENDER_MODE_7_ONLY_URL return only the URL
        if (isset($control[TOKEN_RENDER]) && $control[TOKEN_RENDER] == RENDER_MODE_7_ONLY_URL) {
            return $src;
        }

        $attribute = Support::doAttribute('src', $src);
        $attribute .= Support::doAttribute('alt', $control[TOKEN_ALT_TEXT] ?? '', false);

        return '<img loading="lazy" ' . $attribute . '>';

    }

    /**
     * Creates a SIP Url to be used as download.
     *
     * @param string $pathFilenameThumbnail
     * @param $str
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function buildSecureDownloadLink($pathFilenameThumbnail, $str) {

        $urlParam = Path::urlApi(API_DOWNLOAD_PHP) . '?' . DOWNLOAD_MODE . '=' . DOWNLOAD_MODE_THUMBNAIL;
        $urlParam .= '&' . SIP_DOWNLOAD_PARAMETER . '=' . base64_encode(TOKEN_FILE . ':' . $pathFilenameThumbnail . '|' . $str);

        $sip = $this->store->getSipInstance();

        return $sip->queryStringToSip($urlParam);
    }
}