<?php


namespace IMATHUZH\Qfq\Core\Helper;

/**
 * Glossary:
 * - App: directory in which the index.php file is located. All urls should be relative to this.
 * - Ext: directory in which the QFQ extension is located. e.g. the folder Classes is in there.
 * - API: api folder of qfq extension
 *
 * Conventions of Path class:
 * 1) naming conventions of of path constants/functions/variables:
 *   a) name a path by its origin and its destination separated by 'to'. E.g. APP_TO_SYSTEM_LOG, $appToProject.
 *   b) Or only by its destination and prefix "absolute" if the path is absolute e.g. absoluteApp().
 *   c) Or by its destination and prefix "url" if the path returns a fully qualified url using the base path e.g. urlExt().
 * 2) if the destination is a file, append "File". E.g. APP_TO_SYSTEM_QFQ_LOG_FILE.
 * 3) if a path has to be variable, create a setter and getter. E.g. self::setAbsoluteApp(), self::absoluteApp(), private static $absoluteApp.
 * 4) a path getter appends the given arguments to the requested path using self::join(..., func_get_args()). E.g. see absoluteApp().
 * 5) additional path getters may be defined which combine other getters. E.g. see absoluteProject().
 * 6) avoid manually defining new absolute paths. Define paths relative to App then create a getter which joins it with absoluteApp()
 * 7) avoid defining redundant paths in constants. E.g. create appToApi() by combining appToExt() and extToApi() instead of defining APP_TO_API.
 */

use IMATHUZH\Qfq\Core\Exception\Thrower;
use IMATHUZH\Qfq\Core\Store\Config;

class Path {
    // App
    private static $absoluteApp = null;
    private static $urlApp = null;

    // Extension
    const APP_TO_EXT = 'typo3conf/ext/qfq';

    // API
    const EXT_TO_API = 'Classes/Api';

    // Report
    const EXT_TO_REPORT_SYSTEM = 'Resources/Private/Report';
    const EXT_TO_FORM_SYSTEM = 'Resources/Private/Form';

    // Javascript
    const EXT_TO_JAVASCRIPT = 'Resources/Public/JavaScript';
    const JAVASCRIPT_TO_EXT = '../../../';

    // Icons
    const EXT_TO_GFX_INFO_FILE = 'Resources/Public/icons/note.gif';
    const EXT_TO_PATH_ICONS = 'Resources/Public/icons';

    // Annotate
    const EXT_TO_HIGHLIGHT_JSON = 'Resources/Public/Json';

    // Twig
    const EXT_TO_TWIG_TEMPLATES = 'Resources/Public/twig_templates';

    // QFQ Project dir
    private static $appToProject = null;
    private static $projectToForm = null;
    private static $projectToReport = null;
    private const APP_TO_PROJECT_DEFAULT = '../'; // Don't use directly, use appToProject()
    private const APP_TO_FILEADMIN = 'fileadmin';
    private const APP_TO_PROJECT_IN_PROTECTED = 'fileadmin/protected/qfqProject'; // Don't use directly, use appToProject()
    const PROJECT_TO_CONF = 'conf';
    const PROJECT_TO_FORM_DEFAULT = 'form'; // Don't use directly, use projectToForm()
    const PROJECT_TO_FORM_PHPUNIT = 'form_phpunit'; // Don't use directly, use projectToForm()
    const FORM_TO_FORM_BACKUP = '.backup';
    const PROJECT_TO_REPORT_DEFAULT = 'report'; // Don't use directly, use projectToReport()
    const PROJECT_TO_REPORT_PHPUNIT = 'report_phpunit'; // Don't use directly, use projectToReport()
    const REPORT_FILE_TO_BACKUP = '.backup'; // The path from a directory containing a report file to the directory containing backups of that report file
    const APP_TO_FILEADMIN_PROTECTED = 'fileadmin/protected';
    const APP_TO_TRIGGER_DIR = 'fileadmin/protected/qfqProject/trigger'; // Trigger directory

    // Config
    const APP_TO_TYPO3_CONF = 'typo3conf';

    // Log files
    private static $absoluteLog = null;
    private static $overloadAbsoluteQfqLogFile = null;
    private static $overloadAbsoluteMailLogFile = null;
    private static $overloadAbsoluteSqlLogFile = null;
    private static $overloadAbsoluteImapLogFile = null;
    private static $overloadAbsoluteWebsocketLogFile = null;
    private static $overloadAbsoluteMergeLogFile = null;
    private const LOG_TO_QFQ_LOG_FILE_DEFAULT = 'qfq.log'; // Don't use directly, use absoluteQfqLogFile()
    private const LOG_TO_MAIL_LOG_FILE_DEFAULT = 'mail.log'; // Don't use directly, use absoluteMailLogFile()
    private const LOG_TO_SQL_LOG_FILE_DEFAULT = 'sql.log'; // Don't use directly, use absoluteSqlLogFile()
    private const LOG_TO_MERGE_LOG_FILE_DEFAULT = 'merge.log'; // Don't use directly, use absoluteMergeLogFile()
    private const LOG_TO_IMAP_LOG_FILE_DEFAULT = 'imap.log'; // Don't use directly, use absoluteSqlLogFile()
    private const LOG_TO_WEBSOCKET_LOG_FILE_DEFAULT = 'websocket.log';
    private const PROJECT_TO_LOG_DEFAULT = 'log'; // Don't use directly, use absoluteLog()
    private const APP_TO_LOG_IN_PROTECTED = 'fileadmin/protected/log'; // Don't use directly, use absoluteLog()

    // Thumbnail
    const APP_TO_SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT = 'fileadmin/protected/qfqThumbnail';
    const APP_TO_SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT = 'typo3temp/qfqThumbnail';
    const APP_TO_THUMBNAIL_UNKNOWN_TYPE = 'typo3/sysext/frontend/Resources/Public/Icons/FileIcons/';

    // Send Email
    const EXT_TO_SEND_EMAIL_FILE = 'Classes/External/sendEmail';

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteApp(...$pathPartsToAppend): string {
        if (is_null(self::$absoluteApp)) {
            self::findAbsoluteApp();
        }
        if (is_null(self::$absoluteApp)) {
            Thrower::userFormException('Path not set.', 'Absolute app path is null. This should not happen at this point.');
        }
        return self::join(self::$absoluteApp, $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteExt(...$pathPartsToAppend): string {
        return self::absoluteApp(self::appToExt(), $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteTypo3Conf(...$pathPartsToAppend): string {
        return self::absoluteApp(self::appToTypo3Conf(), $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteLog(...$pathPartsToAppend): string {
        if (is_null(self::$absoluteLog)) {
            self::findAbsoluteLog();
        }
        if (is_null(self::$absoluteLog)) {
            Thrower::userFormException('Path not set.', 'Absolute log path is null. This should not happen at this point.');
        }
        return self::join(self::$absoluteLog, $pathPartsToAppend);
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteSqlLogFile(): string {
        if (is_null(self::$overloadAbsoluteSqlLogFile)) {
            return self::absoluteLog(self::LOG_TO_SQL_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteSqlLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteImapLogFile(): string {
        if (is_null(self::$overloadAbsoluteImapLogFile)) {
            return self::absoluteLog(self::LOG_TO_IMAP_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteImapLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteQfqLogFile(): string {
        if (is_null(self::$overloadAbsoluteQfqLogFile)) {
            return self::absoluteLog(self::LOG_TO_QFQ_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteQfqLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteMailLogFile(): string {
        if (is_null(self::$overloadAbsoluteMailLogFile)) {
            return self::absoluteLog(self::LOG_TO_MAIL_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteMailLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteWebsocketLogFile(): string {
        if (is_null(self::$overloadAbsoluteWebsocketLogFile)) {
            return self::absoluteLog(self::LOG_TO_WEBSOCKET_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteWebsocketLogFile;
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function appToProject(...$pathPartsToAppend): string {
        if (is_null(self::$appToProject)) {
            self::findAppToProject();
        }
        if (is_null(self::$appToProject)) {
            Thrower::userFormException('Path not set.', 'Project path is null. This should not happen at this point.');
        }
        return self::join(self::$appToProject, $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteProject(...$pathPartsToAppend): string {
        return self::absoluteApp(self::appToProject($pathPartsToAppend));
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteConf(...$pathPartsToAppend): string {
        return self::absoluteProject(self::PROJECT_TO_CONF, $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function appToExt(...$pathPartsToAppend): string {
        return self::join(self::APP_TO_EXT, $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function appToTypo3Conf(...$pathPartsToAppend): string {
        return self::join(self::APP_TO_TYPO3_CONF, $pathPartsToAppend);
    }

    /**
     * @param mixed ...$pathPartsToAppend
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function urlApp(...$pathPartsToAppend): string {
        if (is_null(self::$urlApp)) {
            self::setUrlApp(Config::get(SYSTEM_BASE_URL));
        }

        // ensure base url is configured
        if (is_null(self::$urlApp) || self::$urlApp === '') {
            Thrower::userFormException('Base url not configured.', 'Go to QFQ extension configuration in the Typo3 backend and fill in a value for config.baseUrl');
        }
        return self::join(self::$urlApp, $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function urlExt(...$pathPartsToAppend): string {
        return self::urlApp(self::appToExt($pathPartsToAppend));
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function appToApi(...$pathPartsToAppend): string {
        return self::join(self::APP_TO_EXT, self::EXT_TO_API, $pathPartsToAppend);
    }

    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
    public static function urlApi(...$pathPartsToAppend): string {
        return self::urlApp(self::appToApi($pathPartsToAppend));
    }

    /**
     * @param mixed ...$pathPartsToAppend
     * @return string
     */
    public static function projectToForm(...$pathPartsToAppend): string {
        $projectToForm = is_null(self::$projectToForm) ? self::PROJECT_TO_FORM_DEFAULT : self::$projectToForm;
        return self::join($projectToForm, $pathPartsToAppend);
    }

    /**
     * @param mixed ...$pathPartsToAppend
     * @return string
     */
    public static function projectToReport(...$pathPartsToAppend): string {
        $projectToReport = is_null(self::$projectToReport) ? self::PROJECT_TO_REPORT_DEFAULT : self::$projectToReport;
        return self::join($projectToReport, $pathPartsToAppend);
    }

    /**
     * @param string $newPath
     */
    public static function setProjectToForm(string $newPath) {
        self::$projectToForm = $newPath;
    }

    /**
     * @param string $newPath
     */
    public static function setProjectToReport(string $newPath) {
        self::$projectToReport = $newPath;
    }

    /**
     * @param string $newPath
     * @throws \UserFormException
     */
    public static function setAbsoluteSqlLogFile(string $newPath, array &$config = null) {
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteSqlLogFile = $newPath;

        if ($config !== null) {
            $config[SYSTEM_SQL_LOG_PATHFILENAME] = $newPath;
        }
    }

    /**
     * @param string $newPath
     * @throws \UserFormException
     */
    public static function setAbsoluteQfqLogFile(string $newPath, array &$config) {
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteQfqLogFile = $newPath;
        $config[SYSTEM_QFQ_LOG_PATHFILENAME] = $newPath;
    }
    /**
     * @param string $newPath
     * @throws \UserFormException
     */
    public static function setAbsoluteImapLogFile(string $newPath, array &$config) {
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteImapLogFile = $newPath;
        $config[SYSTEM_IMAP_LOG_PATHFILENAME] = $newPath;
    }

    /**
     * @param string $newPath
     * @throws \UserFormException
     */
    public static function setAbsoluteMailLogFile(string $newPath, array &$config) {
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteMailLogFile = $newPath;
        $config[SYSTEM_MAIL_LOG_PATHFILENAME] = $newPath;
    }

    /**
     * @param string $newPath
     * @throws \UserFormException
     */
    public static function setAbsoluteMergeLogFile(string $newPath, array &$config) {
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteMergeLogFile = $newPath;
        $config[SYSTEM_MERGE_LOG_PATHFILENAME] = $newPath;
    }

    /**
     * @throws \UserFormException
     * @return string
     */
    public static function absoluteMergeLogFile(): string {
        if (is_null(self::$overloadAbsoluteMergeLogFile)) {
            return self::absoluteProject(self::LOG_TO_MERGE_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteMergeLogFile;
    }

    /**
     * @param $urlApp
     */
    public static function setUrlApp($urlApp) {
        self::$urlApp = $urlApp;
    }

    /**
     * Override the paths of sql.log, qfq.log, mail.log using the values from the config file or Typo3.
     *
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function overrideLogPathsFromConfig(array &$config) {
        // QFQ log
        $absoluteQfqLogFile = Config::get(SYSTEM_QFQ_LOG_PATHFILENAME);
        if (!empty($absoluteQfqLogFile)) {
            self::setAbsoluteQfqLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteQfqLogFile), $config);
        }

        // Mail log
        $absoluteMailLogFile = Config::get(SYSTEM_MAIL_LOG_PATHFILENAME);
        if (!empty($absoluteMailLogFile)) {
            self::setAbsoluteMailLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteMailLogFile), $config);
        }

        // Merge log
        $absoluteMergeLogFile = Config::get(SYSTEM_MERGE_LOG_PATHFILENAME);
        if (!empty($absoluteMergeLogFile)) {
            self::setAbsoluteMergeLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteMergeLogFile), $config);
        }

        // SQL log
        $absoluteSqlLogFile = Config::get(SYSTEM_SQL_LOG_PATHFILENAME);
        if (!empty($absoluteSqlLogFile)) {
            self::setAbsoluteSqlLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteSqlLogFile), $config);
        }

        // IMAP log
        $absoluteImapLogFile = Config::get(SYSTEM_IMAP_LOG_PATHFILENAME);
        if (!empty($absoluteImapLogFile)) {
            self::setAbsoluteImapLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteImapLogFile), $config);
        }
    }

    /**
     * Return the second path if it is absolute. Otherwise concatenate and return the two paths.
     *
     * @param string $path1
     * @param string $path2
     * @return string
     * @throws \UserFormException
     */
    public static function joinIfNotAbsolute(string $path1, string $path2): string {
        if ($path2 !== '' && $path2[0] === '/') {
            return $path2;
        }
        return self::join($path1, $path2);
    }

    /**
     * Join the arguments together as a path. Array arguments are flattened!
     * Trailing path parts may not start with '/'.
     *
     * join(['this', 'is'], 'my/wonderful', [['path/of', 'nice'], 'things']) === 'this/is/my/wonderful/path/of/nice/things'
     *
     * @param array $pathPartsToAppend
     * @return string
     */
    public static function join(...$pathPartsToAppend): string {
        $parts = $pathPartsToAppend;

        // concatenate all parts (arrays are flattened)
        $path = '';
        array_walk_recursive($parts, function ($part) use (&$path) {

            // filter out empty string and null arguments
            if (is_null($part) || $part === '') {
                return;
            }

            // first part added without '/'
            if ($path === '') {
                $path .= $part;
            } else {
                $part = (string)$part;

                $path .= ((($part[0] ?? '') === '/') ? '' : '/') . $part;
            }
        });

        // remove multiple occurrences of '/' (but keep http://)
        if (preg_match('/^\w*:\/\//', $path, $match)) {
            $protocol = $match[0];
            $path = substr($path, strlen($protocol));
        } else {
            $protocol = '';
        }
        $path = preg_replace('/\/{2,}/', '/', $path);

        return $protocol . $path;
    }

    /**
     * @param string $newPath
     */
    public static function setAbsoluteApp(string $newPath) {
        self::$absoluteApp = $newPath;
    }

    /**
     * @param string $newPath
     */
    private static function setAppToProject(string $newPath) {
        self::$appToProject = $newPath;
    }

    /**
     * @param string $newPath
     */
    private static function setAbsoluteLog(string $newPath) {
        self::$absoluteLog = $newPath;
    }

    /**
     * Searches these places for log directory:
     *   1) project-directory/log
     *   2) fileadmin/protected/log
     * If not found create log dir in: project-directory/log
     *
     * @throws \UserFormException
     */
    private static function findAbsoluteLog() {

        // search log dir qfqProject/log
        $absoluteLog = self::absoluteApp(self::appToProject(self::PROJECT_TO_LOG_DEFAULT));
        if (file_exists($absoluteLog)) {
            self::setAbsoluteLog($absoluteLog);

            // search log dir fileadmin/protected/log
        } elseif (file_exists(self::absoluteApp(self::APP_TO_LOG_IN_PROTECTED))) {
            self::setAbsoluteLog(self::absoluteApp(self::APP_TO_LOG_IN_PROTECTED));

            // create default log dir qfqProject/log
        } else {
            HelperFile::createPathRecursive($absoluteLog);
            self::setAbsoluteLog($absoluteLog);
        }
    }

    /**
     * Read the project location from qfq.project.path.php or create the file with default path.
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    private static function findAppToProject() {

        // does qfq.project.path.php exist? => read path
        $absoluteProjectPathFile = self::absoluteApp(PROJECT_PATH_PHP_FILE);
        if (HelperFile::isReadableException($absoluteProjectPathFile)) {
            self::setAppToProject(HelperFile::include($absoluteProjectPathFile));

            // does the deprecated config.qfq.php exist? => fileadmin/protected/qfqProject & migrate to qfq.json
        } elseif (HelperFile::isReadableException(self::absoluteApp(self::APP_TO_TYPO3_CONF, CONFIG_QFQ_PHP))) {
            HelperFile::createPathRecursive(self::absoluteApp(self::APP_TO_PROJECT_IN_PROTECTED));
            self::setAppToProject(self::APP_TO_PROJECT_IN_PROTECTED);
            Config::migrateConfigPhpToJson();
            self::writeProjectPathPhp();

            // does fileadmin exist? => fileadmin/protected/qfqProject
        } elseif (file_exists(self::absoluteApp(self::APP_TO_FILEADMIN))) {
            HelperFile::createPathRecursive(self::absoluteApp(self::APP_TO_PROJECT_IN_PROTECTED));
            self::setAppToProject(self::APP_TO_PROJECT_IN_PROTECTED);
            self::writeProjectPathPhp();

            // else => folder above APP
        } else {
            self::setAppToProject(self::APP_TO_PROJECT_DEFAULT);
            self::writeProjectPathPhp();
        }
    }

    /**
     * Find the absolute path of the App directory using the path of this file.
     * Fails if typo3conf is not found in that path.
     *
     * @throws \UserFormException
     */
    public static function findAbsoluteApp() {
        // look for typo3conf directory
        $absoluteApp = self::realpath(self::join(__DIR__, '../../../../../../'));

        if (!defined('PHPUNIT_QFQ')) {
            if (!file_exists(self::join($absoluteApp, self::APP_TO_TYPO3_CONF))) {
                Thrower::userFormException('App path seems to be wrong: Directory "typo3conf" not found in app path.' .
                    " Current app path: $absoluteApp .",
                    " In unit tests this can be manually set using Path::setAbsoluteApp() before the path is accessed.");
            }
        } else {
            //TODO: CR: Keine Idee ob das hier Sinn macht
            Path::setAbsoluteApp('typo3conf');
        }
        self::setAbsoluteApp($absoluteApp);
    }

    /**
     * Write the project path configuration file to the project directory.
     *
     * @throws \UserFormException
     */
    private static function writeProjectPathPhp() {
        $appToProject = self::appToProject();
        $fileContent = <<<EOF
<?php

/**
QFQ project path configuration
!! ATTENTION !!: The files in the project directory should NOT be served by your http server! 
Only exception: The app directory inside the project directory may be served.
*/

return '$appToProject'; // path relative to app directory (i.e. location of this file).
EOF;
        HelperFile::file_put_contents(self::absoluteApp(PROJECT_PATH_PHP_FILE), $fileContent);
    }

    /**
     * Throw exception if path does not start with '/'
     *
     * @param $path
     * @throws \UserFormException
     */
    private static function enforcePathIsAbsolute(string $path) {
        if ($path !== '' && $path[0] === '/') {
            return;
        }
        Thrower::userFormException('Path is not absolute', "Path does not start with '/' : $path");
    }

    /**
     * PHP System function: realpath() with QFQ exception.
     * The first argument must be a path to something that exists. Otherwise an exception is thrown!
     *
     * @param string $pathToSomethingThatExists This file/dir MUST exist! Otherwise exception!
     * @param array $pathPartsToAppend The appended path doesnt have to exist.
     * @return string
     * @throws \UserFormException
     */
    private static function realpath(string $pathToSomethingThatExists, ...$pathPartsToAppend): string {
        $absolutePath = realpath($pathToSomethingThatExists);
        if ($absolutePath === false) {
            Thrower::userFormException('Path not found.', "Either path does not exist or access not permitted. Make sure the whole path is executable by the current user. Path: $pathToSomethingThatExists.");
        }
        return self::join($absolutePath, $pathPartsToAppend);
    }

    /**
     * Returns true if the given path contains the double dot operator '..'.
     * File/directory names which contain '..' are not counted.
     *
     * @param string $path
     * @return bool
     */
    private static function containsDoubleDot(string $path) {
        return $path === '..' || OnString::strStartsWith($path, '../') || OnString::strEndsWith($path, '/..') || OnString::strContains($path, '/../');
    }

    /**
     * Get full path for trigger directory
     */
    public static function absoluteTriggerDir(): string {
        return self::absoluteApp(self::APP_TO_TRIGGER_DIR);
    }
}