<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/28/16
 * Time: 8:05 AM
 */

namespace IMATHUZH\Qfq\Core\Helper;


use IMATHUZH\Qfq\Core\Store\Store;

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

    /**
     * Iterate over array $files. Delete only named files which are stored in '/tmp/' . DOWNLOAD_FILE_PREFIX.
     *
     * @param array $files
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function cleanTempFiles(array $files) {

        foreach ($files as $file) {
            if (self::isQfqTemp($file)) {
                self::unlink($file);
            }

            $dir = dirname($file);
            if (self::isQfqTemp($dir)) {
                self::rmdir($dir);
            }
        }
    }

    /**
     * Returns a uniq (use for temporary) filename, prefixed with QFQ TMP_FILE_PREFIX
     *
     * @return bool|string
     */
    public static function tempnam() {
        return tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX);
    }

    /**
     * Check against standard QFQ Temp location. If it is a Qfq Temp location, return true, else false.
     *
     * @param string $name Absolute filename.
     *
     * @return bool
     */
    public static function isQfqTemp($name) {
        $prefix = sys_get_temp_dir() . '/' . TMP_FILE_PREFIX;
        $len = strlen($prefix);

        return (substr($name, 0, $len) == $prefix);
    }

    /**
     * Creates a temporary directory.
     * Be aware: '/tmp' is under systemd/apache2 (Ubuntu 18...) remapped to something like: '/tmp/systemd-private-...-apache2.service-.../tmp'
     *
     * @return bool|string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function mktempdir() {
        $name = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX);
        self::unlink($name);
        mkdir($name);

        return $name;
    }

    /**
     * Return the mimetype of $pathFilename
     *
     * @param string $pathFilename
     * @param bool $flagIgnoreError
     * @return string
     * @throws \UserFormException
     */
    public static function getMimeType($pathFilename, $flagIgnoreError = false) {

        // E.g.: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary'
        $fileMimeType = exec('file --brief --mime ' . $pathFilename, $output, $return_var);
        if ($return_var != 0) {

            if ($flagIgnoreError) {
                return '';
            }

            throw new \UserFormException('Error get mime type of upload.', ERROR_UPLOAD_GET_MIME_TYPE);
        }

        self::fixMimeTypeHeif($pathFilename, $fileMimeType);

        return $fileMimeType;
    }

    /**
     * Checks if the already found MimeType is 'application/octet-stream;'
     * If yes: Check if the real file is of type HEIC or HEIF by using `heif-info`.
     * If yes: Set $fileMimeType = 'image/heic; charset=binary';
     *
     * @param $pathFilename
     * @param $fileMimeType
     */
    public static function fixMimeTypeHeif($pathFilename, &$fileMimeType) {

        // Workaround for HEIF/HEIC: At least on Ubuntu 18.04 the mime type for HEIC/HEIF is not recognized.
        // If the false delivered mimetype is detected, check explicitly for HEIC/HEIF
        if (strpos($fileMimeType, 'application/octet-stream') !== false) {
            // 'heif-info' is part of the package 'libheif-examples'. If it does not exist: heic/heif is not detected.
            exec('heif-info ' . $pathFilename, $output, $return_var);
            if ($return_var == 0) {
                $fileMimeType = 'image/heic; charset=binary';
            }
        }
    }

    /**
     * Returns an array with filestat information to $pathFileName
     * - mimeType
     * - fileSize
     *
     * @param $pathFileNameRelToApp
     * @return array
     * @throws \UserFormException
     */
    public static function getFileStat($pathFileNameRelToApp) {

        $vars = [VAR_FILE_MIME_TYPE => '-', VAR_FILE_SIZE => '-'];

        if (empty($pathFileNameRelToApp)) {
            return $vars;
        }

        $absolutePathFileName = Path::absoluteApp($pathFileNameRelToApp);

        if (!file_exists($absolutePathFileName)) {
            return $vars;
        }

        $vars[VAR_FILE_MIME_TYPE] = self::getMimeType($absolutePathFileName);
        $vars[VAR_FILE_SIZE] = filesize($absolutePathFileName);

        if ($vars[VAR_FILE_SIZE] === false) {
            $vars[VAR_FILE_SIZE] = '-';
        }

        return $vars;
    }

    /**
     * Split $pathFileName into it's components and fill an array, with array keys like used in STORE_VAR.
     *
     * @param string $pathFileName
     * @return array
     */
    public static function pathInfo($pathFileName) {
        $vars = array();

        $pathParts = pathinfo($pathFileName);
        $vars[VAR_FILENAME] = $pathFileName;

        if (isset($pathParts['basename'])) {
            $vars[VAR_FILENAME_ONLY] = $pathParts['basename'];
        }

        if (isset($pathParts['filename'])) {
            $vars[VAR_FILENAME_BASE] = $pathParts['filename'];
        }

        if (isset($pathParts['extension'])) {
            $vars[VAR_FILENAME_EXT] = $pathParts['extension'];
        }

        return $vars;
    }

    /**
     * Checks the file filetype against the allowed mimeType definition. Return true as soon as one match is found.
     * Types recognized:
     *   * 'mime type' as delivered by `file` which matches a definition on
     *   http://www.iana.org/assignments/media-types/media-types.xhtml
     *   * Joker based: audio/*, video/*, image/*
     *   * Filename extension based: .pdf,.doc,..
     *
     * @param string $pathFileName
     * @param string $origFileName
     * @param string $mimeTypeListAccept
     *
     * @return bool
     * @throws \UserFormException
     */
    public static function checkFileType($pathFileName, $origFileName, $mimeTypeListAccept) {

        // E.g.: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary'
        $fileMimeType = self::getMimeType($pathFileName);

        // Strip optional '; charset=binary'
        $arr = explode(';', $fileMimeType, 2);
        $fileMimeType = $arr[0];

        // Split between 'Media Type' and 'Media Subtype'
        $fileMimeTypeSplitted = explode('/', $arr[0], 2);

        $path_parts = pathinfo($origFileName); // to extract the filename extension of the uploaded file.

        // Process all listed mimeTypes (incl. filename extension and joker)
        // $accept e.g.: 'image/*,application/pdf,.pdf'
        $arr = explode(',', $mimeTypeListAccept); // Split multiple defined mimetypes/extensions in single chunks.
        foreach ($arr as $listElementMimeType) {
            $listElementMimeType = trim(strtolower($listElementMimeType));
            if ($listElementMimeType == '') {
                continue; // will be skipped
            } elseif ($listElementMimeType[0] == '.') { // Check for definition 'filename extension'
                if ('.' . strtolower($path_parts['extension']) == $listElementMimeType) {
                    return true;
                }
            } else {
                // Check for Joker, e.g.: 'image/*'
                $splitted = explode('/', $listElementMimeType, 2);

                if ($splitted[1] == '*') {
                    if ($splitted[0] == $fileMimeTypeSplitted[0]) {
                        return true;
                    }
                } elseif ($fileMimeType == $listElementMimeType) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @param $pathFileName
     * @param int|bool $mode
     * @throws \UserFormException
     */
    public static function chmod($pathFileName, $mode = false) {

        if ($mode !== false) {
            if (false === @chmod($pathFileName, $mode)) {
                throw new \UserFormException(
                    json_encode([ERROR_MESSAGE_TO_USER => 'Failed: chmod', ERROR_MESSAGE_TO_DEVELOPER => self::errorGetLastAsString()]),
                    ERROR_IO_CHMOD);
            }
        }
    }

    /**
     * @return string
     */
    public static function errorGetLastAsString() {

        if (NULL === ($errors = error_get_last())) {
            return 'error_get_last(): no error';
        }

        return $errors['type'] . ' - ' . $errors['message'];

    }

    /**
     * PHP System function: chdir() with QFQ exception.
     *
     * @param $newCwd
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function chdir($newCwd) {

        if (false === @chdir($newCwd)) {
            $msg = self::errorGetLastAsString() . " - chdir($newCwd)";
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'chdir failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_CHDIR);
        }

        return true;
    }

    /**
     * PHP System function: unlink() with QFQ exception
     *
     * @param $filename
     * @param string $logFilename
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function unlink($filename, $logFilename = '') {

        if ($logFilename != '') {
            Logger::logMessageWithPrefix("Unlink: $filename", $logFilename);
        }

        if (false === @unlink($filename)) {
            $msg = self::errorGetLastAsString() . " - unlink($filename)";
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'unlink failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_UNLINK);
        }

        // If hidden orig file exist, remove it too
        $origFilename = self::getQfqOrigHiddenFilename($filename);
        if (file_exists($origFilename) && false === @unlink($origFilename)) {
            $msg = self::errorGetLastAsString() . " - unlink($origFilename)";
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'unlink failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_UNLINK);
        }
        return true;
    }

    /**
     * If $filename exist and $origFilename not: move $filename to $origFlename
     *
     * @param $filename
     * @return void
     * @throws \UserFormException
     */
    public static function createOrigFileIfNotExist($filename): void {

        if (!file_exists($filename)) {
            return;
        }

        $origFilename = self::getQfqOrigHiddenFilename($filename);
        if (file_exists($origFilename)) {
            // If $origFilename already exist: do nothing
            return;
        }

        // Rename to $origFilename: source will dissappear. Currently opnly used for FE.type=imageCut
        self::rename($filename, $origFilename);
    }

    /**
     * Based on $filename, create the QFQ Orignal hidden filename.
     * Example /tmp/hello.jpg >> /tmp/.qfq.orig.hello.jpg
     * @param $filename
     * @return string
     */
    public static function getQfqOrigHiddenFilename($filename = '') {
        if ($filename == '') {
            return '';
        }

        $arr = pathinfo($filename);
        return self::joinPathFilename($arr['dirname'], QFQ_PREPEND_HIDDEN_ORIG . '.' . $arr['basename']);

    }

    /**
     * PHP System function: rmdir() with QFQ exception
     *
     * @param $tempDir
     * @return string
     * @throws \UserFormException
     */
    public static function rmdir($tempDir) {

        if (false === @rmdir($tempDir)) {
            $msg = self::errorGetLastAsString() . " - rmdir($tempDir)";
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'rmdir failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_RMDIR);
        }

        return true;
    }

    /**
     * Recursively remove a directory and all its contents.
     *
     * @param string $dir Directory path to remove
     * @return bool True on success
     * @throws \UserFormException
     */
    public static function rmdirRecursive(string $dir): bool {
        if (!is_dir($dir)) {
            return true;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $file) {
            $file->isDir() ? self::rmdir($file->getPathname()) : self::unlink($file->getPathname());
        }

        return self::rmdir($dir);
    }

    /**
     * PHP System function: rename() with QFQ exception
     *
     * @param $oldname
     * @param $newname
     * @return string
     * @throws \UserFormException
     */
    public static function rename($oldname, $newname) {

        if (false === @rename($oldname, $newname)) {
            $msg = self::errorGetLastAsString() . " - rename($oldname, $newname)";
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'unlink failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_RENAME);
        }

        return true;
    }

    /**
     * PHP System function: copy() with QFQ exception
     *
     * @param $source
     * @param $dest
     * @return string
     * @throws \UserFormException
     */
    public static function copy($source, $dest) {

        self::mkDirParent($dest);
        if (!is_file($dest)) {
            touch($dest);
        }

        if (false === @copy($source, $dest)) {

            if (!is_readable($source)) {
                throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => "Can't read file '$source'"]), ERROR_IO_READ_FILE);
            }

            if (!is_writeable($dest)) {
                throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$dest'"]), ERROR_IO_WRITE_FILE);
            }

            $msg = self::errorGetLastAsString(); // Often, there is no specific error string.
            $msg .= " - copy($source, $dest)";
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_COPY);
        }

        return true;
    }

    /**
     * Creates all necessary directories in $pathFileName, but not the last part, the filename. A filename has to be
     * specified.
     *
     * @param string $pathFileName Path with Filename
     * @param bool|int $chmodDir false if not explicit set
     * @throws \UserFormException
     * @throws \CodeException
     */
    public static function mkDirParent($pathFileName, $chmodDir = false) {
        $path = "";
        $cwd = '';

        // Leading '/' will be removed - chdir to / to still use correct path
        if ($pathFileName[0] == '/') {
            $cwd = getcwd();
            self::chdir('/');
        }

        // Teile "Directory/File.Extension" auf
        $pathParts = pathinfo($pathFileName);


        // Zerlege Pfad in einzelne Directories
        $arr = explode("/", $pathParts["dirname"]);

        // Durchlaufe die einzelnen Dirs und überprüfe ob sie angelegt sind.
        // Wenn nicht, lege sie an.
        foreach ($arr as $part) {

            if ($part == '') {// Happens with '/...'
                continue;
            }

            $path .= $part;

            // Check ob der Pfad ein Link ist
            if ("link" == @filetype($path)) {
                if ("0" == ($path1 = readlink($path))) {
                    throw new \UserFormException("Can't create '$pathFileName': '$path' contains an invalid link.", ERROR_IO_INVALID_LINK);
                }
            } else {
                if (file_exists($path)) {
                    if ("dir" != filetype($path)) {
                        throw new \UserFormException("Can't create '$pathFileName': There is already a file with the same name as '$path'", ERROR_IO_DIR_EXIST_AS_FILE);
                    }
                } else {
                    mkdir($path);
                    HelperFile::chmod($path, $chmodDir);
                }
            }
            $path .= "/";
        }

        if ($cwd != '') {
            self::chdir($cwd);
        }
    }

    /**
     * Determine highlight name, based on the given $highlight or provided filename  (the extension will be used)
     *
     * @param $highlight
     * @param $fileName
     * @return string
     * @throws \UserFormException
     */
    public static function getFileTypeHighlight($highlight, $fileName) {

        $extToFileType = [
            'js' => 'javascript.json'
            , 'javascript' => 'javascript.json'
            , 'qfq' => 'highlight.qfq.json'
            , 'php' => 'highlight.php.json'
            , 'py' => 'highlight.py.json'
            , 'python' => 'highlight.py.json'
            , 'matlab' => 'highlight.m.json'
            , 'm' => 'highlight.m.json'
        ];

        switch ($highlight) {
            case FE_HIGHLIGHT_AUTO:
                $arr = explode('.', $fileName);
                $ext = strtolower(end($arr));
                $highlight = (isset($extToFileType[$ext])) ? $extToFileType[$ext] : '';
                break;
            case FE_HIGHLIGHT_JAVASCRIPT:
            case FE_HIGHLIGHT_QFQ:
            case FE_HIGHLIGHT_PYTHON:
            case FE_HIGHLIGHT_MATLAB:
                $ext = $highlight;
                break;
            case FE_HIGHLIGHT_OFF:
            case '':
                $ext = '';
                break;
            default:
                throw new \UserFormException("Unknown highlight type: " . $highlight, ERROR_UNKNOWN_MODE);
                break;
        }

        if (isset($extToFileType[$ext])) {
            return Path::urlExt(Path::EXT_TO_HIGHLIGHT_JSON) . '/' . $extToFileType[$ext];
        }

        return '';
    }

    /**
     * Get array of split file names. Remove '.', '..', QFQ_TEMP_SOURCE.
     * @param $dir
     * @return array
     * @throws \UserFormException
     */
    public static function getSplitFileNames($dir) {

        // Array of created file names.
        if (false === ($files = scandir($dir))) {
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Splitted files not found', ERROR_MESSAGE_TO_DEVELOPER => "[cwd=$dir] scandir(.)" . HelperFile::errorGetLastAsString()]),
                ERROR_PDF2JPEG);
        }

        $new = array();
        foreach ($files as $key => $value) {

            if ($value == '.' || $value == '..' || $value == QFQ_TEMP_SOURCE) {
                continue;
            }

            // IM 'convert' use a strange auto numbering: if there is only one page: split.jpg, if there are multiple pages, first: split-0.jpg, ....
            if ($value == 'split.jpg') {
                $value = 'split-0.jpg';
                self::rename($dir . '/' . $files[$key], $dir . '/' . $value);
            }

            $new[] = $value;
        }

        return $new;
    }

    /**
     * Joins $pre and $post.
     * If $post is absolute, returns only $post.
     * If $pre is empty, return $post
     * If $post is empty: return $pre
     * If $pre ends without '/', merge with '/'
     *
     * @param $pre
     * @param $post
     *
     * @return string
     */
    public static function joinPathFilename($pre, $post) {
        $separator = '';

        if ($pre == '' || $post == '') {
            return $pre . $post;
        }

        if ($post[0] == '/') {
            return $post;
        }

        if ((substr($pre, -1) != '/')) {
            $separator = '/';
        }

        return $pre . $separator . $post;
    }

    /**
     * Checks if the given file name only contains alphanumeric characters and ".", "-", "_"
     * @param string $fileName
     * @return bool
     */
    public static function isValidFileName(string $fileName): bool {
        return preg_match('/^([-\.\w]+)$/', $fileName) > 0;
    }

    /**
     * Creates the given path if it does not exist.
     *
     * @param $path
     * @throws \UserFormException
     */
    public static function createPathRecursive($path) // : void
    {
        if (!is_dir($path)) {

            $errorMessage = '';
            try {
                $success = mkdir($path, 0777, true);
            } catch (\Error|\Exception $e) {
                $errorMessage = 'Reason: ' . $e->getMessage();
                $success = false;
            }

            if ($success === false) {
                throw new \UserFormException(json_encode([
                    ERROR_MESSAGE_TO_USER => "Can't create file path.",
                    ERROR_MESSAGE_TO_DEVELOPER => "Can't create path: '$path' $errorMessage"]),
                    ERROR_IO_WRITE_FILE);
            }
        }
    }

    /**
     * Wrapper for file_put_contents which throws exception on failure.
     * Clear the stat cache to make sure that stat(), file_exists(),... etc. return current data.
     *
     * @param $pathFileName
     * @param $content
     * @throws \UserFormException
     */
    public static function file_put_contents($pathFileName, $content, $flag = 0) // : void
    {
        $success = file_put_contents($pathFileName, $content, $flag);
        if ($success === false) {
            throw new \UserFormException(json_encode([
                ERROR_MESSAGE_TO_USER => "Writing file failed.",
                ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$pathFileName'"]),
                ERROR_IO_WRITE_FILE);
        }
        clearstatcache(true, $pathFileName);
    }

    /**
     * Wrapper for file_get_contents which throws exception on failure.
     *
     * @param string $pathFileName
     * @return string
     * @throws \UserFormException
     */
    public static function file_get_contents(string $pathFileName): string {
        $fileContents = file_get_contents($pathFileName);
        if ($fileContents === false) {
            throw new \UserFormException(json_encode([
                ERROR_MESSAGE_TO_USER => 'File not readable.',
                ERROR_MESSAGE_TO_DEVELOPER => "Can't read file '$pathFileName'"]), ERROR_IO_READ_FILE);
        }
        return $fileContents;
    }

    /**
     * Wrapper fo json_decode which throws exception on failure.
     *
     * @param string $jsonString
     * @param bool $associativeArray
     * @return array
     * @throws \UserFormException
     */
    public static function json_decode(string $jsonString, bool $associativeArray = true) {
        $result = json_decode($jsonString, $associativeArray);
        if (is_null($result)) {
            throw new \UserFormException(json_encode([
                ERROR_MESSAGE_TO_USER => 'Json decode failed.',
                ERROR_MESSAGE_TO_DEVELOPER => "JSON: $jsonString"]), ERROR_IO_READ_FILE);
        }
        return $result;
    }

    /**
     * Wrapper for is_writeable which throws exception on failure.
     * Throws exception if file does not exist!
     *
     * @param $path
     * @throws \UserFormException
     */
    public static function enforce_writable($path) {
        if (!is_writeable($path)) {
            throw new \UserFormException(json_encode([
                ERROR_MESSAGE_TO_USER => 'File/directory not found or not writable.',
                ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file/directory '$path'"]), ERROR_IO_WRITE_FILE);
        }
    }

    /**
     * Like enforce_writable but does not throw exception if file does not exist and the containing directory is writable.
     *
     * @param $pathFileName
     * @throws \UserFormException
     */
    public static function enforce_writable_or_creatable($pathFileName) {
        if (file_exists($pathFileName)) {
            self::enforce_writable($pathFileName);
        } else {
            self::enforce_writable(dirname($pathFileName));
        }
    }

    /**
     * Wrapper for is_readable() but throws exception if file/directory exists but is not readable.
     * Returns false if file does not exist.
     *
     * @param $pathFileName
     * @return bool
     * @throws \UserFormException
     */
    public static function isReadableException($pathFileName): bool {
        if (!file_exists($pathFileName)) {
            return false;
        }
        if (!is_readable($pathFileName)) {
            throw new \UserFormException(json_encode([
                ERROR_MESSAGE_TO_USER => 'File/directory found but not readable.',
                ERROR_MESSAGE_TO_DEVELOPER => "Can't read existing file/directory '$pathFileName'"]), ERROR_IO_READ_FILE);
        }
        return true;
    }

    /**
     * Wrapper for include() but throws exception on failure.
     *
     * @param string $pathFileName
     * @return mixed
     * @throws \UserFormException
     */
    public static function include(string $pathFileName) {
        $result = include($pathFileName);
        if (empty($result) || $result === true) {
            throw new \UserFormException(json_encode([
                ERROR_MESSAGE_TO_USER => 'Error read file.',
                ERROR_MESSAGE_TO_DEVELOPER => "Can't include file '$pathFileName'"]), ERROR_IO_READ_FILE);
        }
        return $result;
    }

    /**
     * Translates ZIP error codes to text.
     *
     * @param $errno
     * @return string
     */
    public static function zipFileErrMsg($errno) {

        // using constant name as a string to make this function PHP4 compatible
        $zipFileFunctionsErrors = array(
            'ZIPARCHIVE::ER_MULTIDISK' => 'Multi-disk zip archives not supported.',
            'ZIPARCHIVE::ER_RENAME' => 'Renaming temporary file failed.',
            'ZIPARCHIVE::ER_CLOSE' => 'Closing zip archive failed',
            'ZIPARCHIVE::ER_SEEK' => 'Seek error',
            'ZIPARCHIVE::ER_READ' => 'Read error',
            'ZIPARCHIVE::ER_WRITE' => 'Write error',
            'ZIPARCHIVE::ER_CRC' => 'CRC error',
            'ZIPARCHIVE::ER_ZIPCLOSED' => 'Containing zip archive was closed',
            'ZIPARCHIVE::ER_NOENT' => 'No such file.',
            'ZIPARCHIVE::ER_EXISTS' => 'File already exists',
            'ZIPARCHIVE::ER_OPEN' => 'Can\'t open file',
            'ZIPARCHIVE::ER_TMPOPEN' => 'Failure to create temporary file.',
            'ZIPARCHIVE::ER_ZLIB' => 'Zlib error',
            'ZIPARCHIVE::ER_MEMORY' => 'Memory allocation failure',
            'ZIPARCHIVE::ER_CHANGED' => 'Entry has been changed',
            'ZIPARCHIVE::ER_COMPNOTSUPP' => 'Compression method not supported.',
            'ZIPARCHIVE::ER_EOF' => 'Premature EOF',
            'ZIPARCHIVE::ER_INVAL' => 'Invalid argument',
            'ZIPARCHIVE::ER_NOZIP' => 'Not a zip archive',
            'ZIPARCHIVE::ER_INTERNAL' => 'Internal error',
            'ZIPARCHIVE::ER_INCONS' => 'Zip archive inconsistent',
            'ZIPARCHIVE::ER_REMOVE' => 'Can\'t remove file',
            'ZIPARCHIVE::ER_DELETED' => 'Entry has been deleted',
        );

        return $zipFileFunctionsErrors[$errno] ?? 'unknown';
    }

    /**
     * Check if file exists with given destination path and filename. If exists filename will be changed till its unique.
     *
     * @param $destinationFolderPath
     * @param $fileName
     * @return array|string|string[]
     */
    public static function getUniqueFileName($destinationFolderPath, $fileName) {
        $originalFileName = pathinfo($fileName, PATHINFO_FILENAME);
        $extension = pathinfo($fileName, PATHINFO_EXTENSION);
        $changedFileName = $originalFileName;
        $changedFileNameFullPath = $destinationFolderPath . '/' . $changedFileName . '.' . $extension;
        $count = 0;

        while (file_exists($changedFileNameFullPath)) {
            $count++;
            $changedFileName = $originalFileName . '-' . $count;
            $changedFileNameFullPath = $destinationFolderPath . '/' . $changedFileName . '.' . $extension;
        }

        if ($extension != '') {
            $changedFileName .= '.' . $extension;
        }
        return $changedFileName;
    }

    /**
     * Search in files 'Classes/Sql/*.sql' for 'CREATE TABLE IF NOT EXISTS ...'.
     * If $tableName is found, return an array with all  SQL files and the created tables.
     * If $tableName not found, return false.
     *
     * @param $tableName
     * @return array|false
     */
    public static function isQfqTable($tableName, &$rcIndexMatch): bool|array {
        $isQfq = false;
        $files = array();

        $directory = __DIR__ . '/../../Sql';
        // Open the directory
        $dir = opendir($directory);

        // Loop through each file/directory in the directory
        while (($file = readdir($dir)) !== false) {
            // Skip . and .. directories
            if ($file == '.' || $file == '..' || strpos($file, QFQ_TABLE_PREFIX) !== 0 || substr($file, -4) != '.sql') {
                continue;
            }

            // Get the full path of the file/directory
            $filePath = $directory . '/' . $file;

            if (!is_dir($filePath)) {
                $foundTableNames = '';
                $lines = file($filePath);
                foreach ($lines as $lineNumber => $lineContent) {
                    // Search for 'CREATE TABLE ...'
                    if (strpos($lineContent, "CREATE TABLE IF NOT EXISTS ") !== false) {
                        // Get and collect tablename
                        $arr = explode('`', $lineContent);
                        $found = $arr[1] ?? '';
                        $foundTableNames .= ',' . $found;
                        if ($tableName == $found) {
                            $isQfq = true;
                            $rcIndexMatch = $file;
                        }
                    }
                }
                if ($foundTableNames != '') {
                    // Skip komma.
                    $files[$file] = substr($foundTableNames, 1);
                }
            }
        }

        // Close the directory
        closedir($dir);

        return $isQfq ? $files : false;
    }

    public static function isQfqFunction($functionName, &$rcIndexMatch): bool|array {
        $isQfq = false;
        $files = array();

        $directory = __DIR__ . '/../../Sql';
        // Open the directory
        $dir = opendir($directory);

        // Loop through each file/directory in the directory
        while (($file = readdir($dir)) !== false) {
            // Skip . and .. directories
            if ($file == '.' || $file == '..' || strpos($file, QFQ_FUNCTION_PREFIX) !== 0 || substr($file, -4) != '.sql') {
                continue;
            }

            // Get the full path of the file/directory
            $filePath = $directory . '/' . $file;

            if (!is_dir($filePath)) {
                $foundNames = '';
                $lines = file($filePath);
                foreach ($lines as $lineNumber => $lineContent) {
                    // Search for 'CREATE FUNCTION QMARK()'
                    if (strpos($lineContent, "CREATE FUNCTION ") !== false) {
                        // Get and collect tablename
                        $arr = explode(' ', $lineContent);
                        $arr = explode('(', $arr[2] ?? '');
                        $found = OnString::trimQuote($arr[0] ?? '');
                        $foundNames .= ',' . $found;
                        if ($functionName == $found) {
                            $isQfq = true;
                            $rcIndexMatch = $file;
                        }
                    }
                }
                if ($foundNames != '') {
                    // Skip komma.
                    $files[$file] = substr($foundNames, 1);
                }
            }
        }

        // Close the directory
        closedir($dir);

        return $isQfq ? $files : false;
    }

    /**
     * Encodes the given original file name into a unique file name based on a file path.
     *
     * The template should contain the placeholder defined by FILE_NAME_UNIQUE_VAR (e.g. '{{filenameUnique:V}}').
     * This function extracts the base name of the original file (excluding its extension), encodes it in Base64,
     * and then constructs a new file name by inserting a "_B64" marker with an optional numeric suffix if a file with
     * the constructed name already exists.
     *
     * If no prefix is provided in the template, a default "_" is used to ensure the file name starts with "_B64_".
     *
     * @param string $filePath The file path containing the placeholder.
     *                                 Examples:
     *                                  - '{{filenameUnique:V}}'
     *                                  - 'test_{{filenameUnique:V}}'
     *                                  - '/test/test_{{filenameUnique:V}}'
     * @param string $originalFileName The original file name (e.g. 'test.pdf').
     * @param string $returnType Determines the return format:
     *                                 - FILE_NAME_UNIQUE_RETURN_NAME: return only the encoded file name without the prefix.
     *                                 - FILE_NAME_UNIQUE_RETURN_PATH: return the full path.
     * @return array                   Returns an associative array with key FILE_NAME_UNIQUE_KEY and value as the
     *                                 unique encoded file name or full path.
     */
    public static function base64EncodeUniqueFileName($filePath, string $originalFileName, string $returnType = FILE_NAME_UNIQUE_RETURN_NAME): array {

        if (!str_contains($filePath, FILE_NAME_UNIQUE_VAR)) {
            return array(FILE_NAME_UNIQUE_KEY => '');
        }

        // Extract the base name and extension from the original file name.
        $pathInfo = pathinfo($originalFileName);
        $baseName = $pathInfo['filename'];
        $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';

        // Separate directory from file.
        $dir = dirname($filePath);

        // Base64 encode only the base name and replace '/' with '-'
        $encoded = base64_encode($baseName);
        $encoded = str_replace('/', '-', $encoded);
        $encoded = str_replace('=', '_', $encoded);

        // Regex example:
        // file = start{{filenameUnique:V}}end
        // Matches a filename that contains the exact placeholder {{filenameUnique:V}}
        // - Captures any text before the placeholder: $matches[1] = 'start'
        // - Captures the placeholder itself: $matches[2] = '{{filenameUnique:V}}'
        // The pattern ensures that the placeholder is not part of a directory path (uses basename).
        if (preg_match('/^([^\/]*)(\{\{filenameUnique:V\}\})([^\/]*)$/', basename($filePath), $matches)) {
            $filePrefix = $matches[1];
            $length = strlen($filePrefix);
            if ($length < 1) {
                $prefix = '_';
            } else {
                $prefix = $filePrefix[$length - 1] == '_' ? '' : '_';
            }
        } else {
            $filePrefix = "";
            $prefix = "_";
        }


        // Initialize the marker and construct the file name.
        $b64Marker = FILE_NAME_UNIQUE_BASE_64_MARKER . "_";
        $newFileName = $prefix . $b64Marker . $encoded . $extension;


        // Check if the file exists and, if so, iterate with an incremented marker.
        $counter = 1;
        while (file_exists($dir . '/' . $filePrefix . $newFileName)) {
            $b64Marker = FILE_NAME_UNIQUE_BASE_64_MARKER . $counter . "_";
            $newFileName = $prefix . $b64Marker . $encoded . $extension;
            $counter++;
        }

        // Return either the file name only or the full path.
        if ($returnType === FILE_NAME_UNIQUE_RETURN_PATH && $dir !== '.' && $dir !== '') {
            // Add $filePrefix only if the full file path is returned
            return array(FILE_NAME_UNIQUE_KEY => $dir . '/' . $filePrefix . $newFileName);
        } else {
            // User given file prefix will be added when the file path is parsed
            return array(FILE_NAME_UNIQUE_KEY => $newFileName);
        }
    }

    /**
     * Decodes a unique encoded file name back to its original file name.
     *
     * The encoded file name is expected to have been generated by baseEncodeUniqueFileName and should be in the format:
     *     [optionalPrefix]_B64[optionalNumbers]_EncodedPart[.extension]
     *
     * This function separates any directory path, then locates the "_B64" marker, extracts the encoded portion,
     * reverses the character replacement (changing '-' back to '/'), decodes the Base64 string, and reassembles
     * the original file name. If a prefix was present (and is not just the default "_"), it is reattached.
     *
     * @param string $uniqueFileName The unique encoded file name or full path.
     * @param string $returnType Determines the return format:
     *                                - FILE_NAME_UNIQUE_RETURN_NAME: return only the file name.
     *                                - FILE_NAME_UNIQUE_RETURN_PATH: return the full path.
     * @return string                 The decoded original file name or full path.
     */
    public static function base64DecodeUniqueFileName(string $uniqueFileName, string $returnType = FILE_NAME_UNIQUE_RETURN_NAME): string {
        // Separate the directory from the file name.
        $dir = dirname($uniqueFileName);
        $file = basename($uniqueFileName);

        // If the file name does not contain the "_B64[NUMBER]_" marker, return as-is.
        if (!preg_match('/_B64[0-9]*_/', $file)) {
            return ($returnType === FILE_NAME_UNIQUE_RETURN_PATH && $dir !== '.' && $dir !== '') ? $dir . '/' . $file : $file;
        }

        // Regex example:
        // file = Pre_B64_bXlwaWN0dXJl.jpg
        // Matches the pattern: _B64[any digits or none]_
        // - Captures the prefix (if present): $matches[1] = 'Pre'
        // - Captures the Base64-encoded part of the filename: $matches[2] = 'bXlwaWN0dXJl'
        // - Captures the file extension (if present): $matches[3] = '.jpg'
        // The regex enforces that the _B64 marker is always preceded by an underscore.
        if (preg_match('/^(.*?)_B64\d*_(.+?)(\.[^.]+)?$/', $file, $matches)) {
            $prefix = $matches[1];
            $encodedPart = $matches[2];
            $extension = $matches[3] ?? '';

            // Reverse replacement: change '-' back to '/'
            $encodedPart = str_replace('-', '/', $encodedPart);
            $encodedPart = str_replace('_', '=', $encodedPart);

            // Decode the Base64 encoded portion.
            $decodedBaseName = base64_decode($encodedPart);

            // Reassemble the original file name.
            if (!empty($prefix) && $prefix !== "_") {
                $decodedFileName = $prefix . '_' . $decodedBaseName . $extension;
            } else {
                $decodedFileName = $decodedBaseName . $extension;
            }
        } else {
            // If the pattern wasn't matched, return the file name unmodified.
            $decodedFileName = $file;
        }

        // Return either just the file name or the full path.
        if ($returnType === FILE_NAME_UNIQUE_RETURN_PATH && $dir !== '.' && $dir !== '') {
            return $dir . '/' . $decodedFileName;
        }
        return $decodedFileName;
    }

    /**
     * Check if a PDF is encrypted using pdfinfo.
     *
     * @param string $filePath
     * @return bool
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function isPdfProtected(string $filePath): bool {
        if (!file_exists($filePath)) {
            throw new \UserFormException("PDF file not found: $filePath");
        }


        $cmd = Store::getVar(SYSTEM_CMD_PDF_INFO, STORE_SYSTEM) . " " . escapeshellarg($filePath) . " 2>&1";
        exec($cmd, $outputLines, $returnCode);
        $output = implode("\n", $outputLines);

        if ($returnCode !== 0) {
            Logger::logMessageWithPrefix("pdfinfo failed (code $returnCode): $output", Path::absoluteQfqLogFile());

            if (str_contains($output, 'Incorrect password')) {
                throw new \UserFormException("PDF is password-protected and cannot be analyzed.");
            }
            if (str_contains($output, 'syntax error') || str_contains($output, 'not a PDF')) {
                throw new \UserFormException("Invalid PDF file format.");
            }
            throw new \RuntimeException("Unexpected error while checking PDF encryption status.");
        }

        foreach ($outputLines as $line) {
            $line = trim($line);
            if (stripos($line, 'Encrypted:') === 0) {
                return stripos($line, 'yes') !== false;
            }
        }

        return false;
    }

    /**
     * Attempts to remove PDF restrictions using Ghostscript.
     *
     * @param string $path
     * @param int $timeoutUnprotect
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function removePdfProtection(string $path, string $timeoutUnprotect = '20'): void {
        if (!file_exists($path)) {
            throw new \UserFormException("PDF file not found: $path");
        }

        $tempFile = tempnam(sys_get_temp_dir(), 'unprotected_') . '.pdf';
        $gs = Store::getVar(SYSTEM_CMD_GS, STORE_SYSTEM);
        $cmd = sprintf('timeout ' . $timeoutUnprotect . 's ' .
            $gs . ' -dNOSAFER -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dPDFSETTINGS=/prepress ' .
            '-sOutputFile=%s -f %s 2>&1',
            escapeshellarg($tempFile),
            escapeshellarg($path)
        );

        exec($cmd, $outputLines, $returnCode);
        $output = implode("\n", $outputLines);

        // Timeout?
        if($returnCode===124){
            // In case timeout reach: skip unprotect.
            return;
        }

        if ($returnCode !== 0 || !file_exists($tempFile) || filesize($tempFile) === 0) {
            Logger::logMessageWithPrefix("Ghostscript failed to remove protection: $output", Path::absoluteQfqLogFile());
            throw new \RuntimeException("Failed to remove PDF protection.");
        }

        if (!rename($tempFile, $path)) {
            Logger::logMessageWithPrefix("Could not overwrite protected file: $path with $tempFile", Path::absoluteQfqLogFile());
            @unlink($tempFile);
            throw new \RuntimeException("Failed to remove PDF protection.");
        }
    }

    /**
     * Detects whether a PDF is an Adobe-only PDF
     *
     * @param string $filePath
     * @return bool
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function checkPdfAcrobat(string $filePath): bool {
        if (!file_exists($filePath) || !is_readable($filePath)) {
            throw new \UserFormException("File is missing or unreadable: $filePath");
        }

        // Check if the PDF is tagged using pdfinfo
        $infoCmd = Store::getVar(SYSTEM_CMD_PDF_INFO, STORE_SYSTEM) . " " . escapeshellarg($filePath) . " 2>&1";
        exec($infoCmd, $infoOutput, $infoCode);

        if ($infoCode !== 0) {
            Logger::logMessageWithPrefix("pdfinfo failed (code $infoCode): " . implode("\n", $infoOutput), Path::absoluteQfqLogFile());
            throw new \RuntimeException("Failed to check if PDF is Acrobat.");
        }

        $isTagged = false;
        foreach ($infoOutput as $line) {
            if (stripos($line, 'Tagged:') === 0 && stripos($line, 'yes') !== false) {
                $isTagged = true;
                break;
            }
        }

        // If not tagged, it's unlikely to be an Adobe
        if (!$isTagged) {
            return false;
        }

        // Extract visible text and search for Adobe-specific content
        $textCmd = Store::getVar(SYSTEM_CMD_PDF_TO_TEXT, STORE_SYSTEM) . " " . escapeshellarg($filePath) . " - 2>&1";
        $textOutput = shell_exec($textCmd);

        if ($textOutput === null) {
            Logger::logMessageWithPrefix("pdftotext failed or returned empty output for: $filePath", Path::absoluteQfqLogFile());
            throw new \RuntimeException("Failed to check if PDF is Acrobat.");
        }

        // Common Adobe-only message pattern
        $needle = 'Adobe Reader jetzt herunterladen';
        return stripos($textOutput, $needle) !== false;
    }

    /**
     * Reads the contents of multiple files and returns them in an associative array.
     *
     * The keys in the returned array are formatted as 'file-<filename><Extension>',
     * where <filename> is the base name of the file (without extension) and <Extension>
     * is the file extension with the first letter capitalized.
     *
     * If a file cannot be read, its corresponding value in the array will be null.
     *
     *
     * @param array $filePaths An array of file paths to read.
     * @return array An associative array with file contents or null for unreadable files.
     * @throws \UserFormException
     */
    public static function getFileContents(array $filePaths, array $blacklistWords = [], array &$t3DbConfig = []): array {
        $result = [];

        foreach ($filePaths as $path) {
            $filename = pathinfo($path, PATHINFO_FILENAME);
            $extension = pathinfo($path, PATHINFO_EXTENSION);

            // Create key: file-<filename><Extension>
            $key = 'file-' . $filename . ucfirst($extension);

            // Read file content
            if (is_file($path) && is_readable($path)) {

                // Extract T3 DB config from LocalConfiguration.php
                if ($filename === 'LocalConfiguration' && $extension === 'php') {
                    $config = include $path;
                    if (isset($config['DB']['Connections']['Default'])) {
                        $t3DbConfig = $config['DB']['Connections']['Default'];
                    }
                    // Also extract systemMaintainers
                    if (isset($config['SYS']['systemMaintainers'])) {
                        $t3DbConfig['systemMaintainers'] = $config['SYS']['systemMaintainers'];
                    }
                }

                $content = self::file_get_contents($path);

                if (!empty($blacklistWords)) {
                    $content = self::maskSensitiveValues($content, $blacklistWords, $extension);
                }

                $result[$key] = $content;
            } else {
                $result[$key] = null;
            }
        }

        return $result;
    }

    /**
     * Masks sensitive values in the given content based on the provided blacklist words.
     *
     * @param string $content The content to be processed.
     * @param array $blacklistWords An array of words whose values should be masked.
     * @param string $extension The file extension to determine the format (e.g., 'json', 'php').
     * @return string The content with sensitive values masked.
     */
    private static function maskSensitiveValues(string $content, array $blacklistWords, string $extension): string {
        foreach ($blacklistWords as $word) {
            $quotedWord = preg_quote($word, '/');

            if (strtolower($extension) === 'json') {
                // JSON format: "key": "value" or "key": "value",
                $pattern = '/("' . $quotedWord . '"\s*:\s*)"[^"]*"/i';
                $replacement = '$1"***"';
            } else {
                // PHP format: 'key' => 'value' or 'key' => "value"
                $pattern = '/([\'"]' . $quotedWord . '[\'"]\s*=>\s*)[\'"][^\'"]*[\'"]/i';
                $replacement = '$1\'***\'';
            }

            $content = preg_replace($pattern, $replacement, $content);
        }

        return $content;
    }

    /**
     * Make HTTP POST request with JSON body
     *
     * @param string $url Target URL
     * @param array $data Data to send as JSON
     * @param int $timeout Timeout in seconds
     * @return array ['httpCode' => int, 'body' => string]
     * @throws \UserFormException On connection failure
     */
    public static function httpPostJson(string $url, array $data, int $timeout = 10): array {
        $postData = json_encode($data);

        $context = stream_context_create([
            'http' => [
                'method' => 'POST',
                'header' => [
                    'Content-Type: application/json',
                    'Accept: application/json',
                    'Content-Length: ' . strlen($postData)
                ],
                'content' => $postData,
                'timeout' => $timeout,
                'ignore_errors' => true
            ],
            'ssl' => [
                'verify_peer' => true,
                'verify_peer_name' => true
            ]
        ]);

        $response = @file_get_contents($url, false, $context);

        // Extract HTTP code from response headers
        $httpCode = 0;
        if (isset($http_response_header) && is_array($http_response_header)) {
            foreach ($http_response_header as $header) {
                if (preg_match('/^HTTP\/\d+\.?\d*\s+(\d+)/', $header, $matches)) {
                    $httpCode = (int)$matches[1];
                }
            }
        }

        if ($response === false) {
            throw new \UserFormException(json_encode([
                ERROR_MESSAGE_TO_USER => 'HTTP request failed.',
                ERROR_MESSAGE_TO_DEVELOPER => "Can't connect to '$url'"
            ]), ERROR_IO_READ_FILE);
        }

        return [
            'httpCode' => $httpCode,
            'body' => $response
        ];
    }


}

