<?php
/**
 * -------------------------------------------------------------------------
 *
 * System routines.
 *
 * -------------------------------------------------------------------------
 *
 * This file is always loaded from the root "index.php" file and is used to
 * implement a few necessary routines. They are as follows:
 *
 *     mimimiSafePath    ( $dir,          $asFile         )  -->  to get a safe path of the specified directory
 *     mimimiBasePath    ( $dir,          $asFile         )  -->  to get a full path of the specified directory
 *     mimimiCreate      ( $name, $owner, $test           )  -->  to load a named module into owner module of application
 *     mimimiInclude     ( $file, $from, $once, $params   )  -->  to run a PHP file with some input parameters
 *     mimimiFolders     ( $dir, $except,   $filesOnly    )  -->  to get a list of directories or files
 *     mimimiScan        ( $dir, $callback, $filesOnly    )  -->  to scan a directory
 *     mimimiClean       ( $dir,            $filesOnly    )  -->  to clean a directory
 *     mimimiServer      ( $param, $def                   )  -->  to get a server parameter
 *     mimimiEnvironment ( $param, $def                   )  -->  to get an environment parameter
 *     mimimiStop        ( $message, $code                )  -->  to stop application using HTTP status code and display a message
 *     mimimiRandomId    ( $length, $alpha                )  -->  to generate random identifier
 *
 * Below are also a few necessary constants:
 *
 *     MIMIMI_CLASS_PREFIX  -->  to define a prefix for your module classes
 *
 * Please note that you can override this file if needed by using a file
 * with the same name in your application folder. In addition, each of these
 * system routines is written with an existence check, allowing you to
 * override only a single routine if necessary.
 *
 * -------------------------------------------------------------------------
 *
 * Look at this diagram to understand how this file loads:
 *
 *     ├─> .htaccess
 *     │
 *     │   ┌─<─ constant MIMIMI_CORE_VERSION
 *     │   ├─<─ constant MIMIMI_CORE_FOLDER
 *     │   ├─<─ constant MIMIMI_MODULES_FOLDER
 *     │   ├─<─ constant MIMIMI_APP_FOLDER
 *     │   ├─<─ constant MIMIMI_INSTALL_FOLDER
 *     │   │
 *     └─> index.php
 *           │
 *           ├─> mimimi.core/Config.php
 *           │               │
 *           │               └─<─ constant ...
 *           │
 *           └─> mimimi.core/RoutinesSystem.php
 *                           │
 *                           ├─<─ constant MIMIMI_CLASS_PREFIX
 *                           │
 *                           ├─<─ routine mimimiSafePath()
 *                           ├─<─ routine mimimiBasePath()
 *                           ├─<─ routine mimimiCreate()
 *                           ├─<─ routine mimimiInclude()
 *                           ├─<─ routine mimimiFolders()
 *                           ├─<─ routine mimimiClean()
 *                           ├─<─ routine mimimiServer()
 *                           ├─<─ routine mimimiEnvironment()
 *                           ├─<─ routine mimimiStop()
 *                           └─<─ routine mimimiRandomId()
 *
 * The right arrows show the order in which application files are loaded
 * when processing a request to your app. The left arrows show some public
 * routines or constants that the corresponding file exposes to other app
 * modules.
 *
 * -------------------------------------------------------------------------
 *
 * @package    MimimiFramework
 * @subpackage Core
 * @copyright  2022 MiMiMi Community
 *             https://mimimi.software/
 * @license    GPL-2.0
 *             https://opensource.org/license/gpl-2-0/
 *
 * -------------------------------------------------------------------------
 */

    define ( 'MIMIMI_CLASS_PREFIX', 'Mimimi' );

    /**
     * ---------------------------------------------------------------------
     *
     * Converts the directory path to the safe version.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $dir     Relative path to the required directory.
     * @param   bool    $asFile  (optional) TRUE  if the $dir parameter should be interpreted as a file name.
     *                                      FALSE if the $dir parameter is a directory name.
     * @return  string
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiSafePath' ) ) {

        function mimimiSafePath ( $dir, $asFile = FALSE ) {
            if ( $dir != '' ) {
                $dir = preg_replace ( '~[/\\\\]+~u',    '/', $dir );
                $dir = preg_replace ( '~[.\s]*/+\s*~u', '/', $dir );
                $dir = preg_replace ( '~/+~u',          '/', $dir );
                $dir = preg_replace ( '~[.\s/]+$~u',    '',  $dir );
                $dir = preg_replace ( '~^[\s/]+~u',     '',  $dir );
                if ( ! $asFile ) {
                    if ( $dir != '' ) $dir .=  '/';
                }
            }
            return $dir;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Returns the full path to the root or nested directory.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $dir     (optional) Relative path to the nested entity whose full path will be returned.
     * @param   bool    $asFile  (optional) TRUE  if the $dir parameter should be interpreted as a file name.
     *                                      FALSE if the $dir parameter is a directory name.
     * @return  string
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiBasePath' ) ) {

        function mimimiBasePath ( $dir = '', $asFile = FALSE ) {
            $dir = mimimiSafePath ( $dir, $asFile );
            return dirname ( __FILE__, 2 ) . '/' . $dir;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Loads a named module as the same property of the parent module.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string       $name   A name of object property that becomes the name of module being loaded.
     * @param   object       $owner  (optional) Reference to the parent module.
     * @param   bool         $test   (optional) TRUE  if this is just a test.
     *                                          FALSE if this is a real loading.
     * @return  object|bool          The loaded module if this call was not in test mode.
     *                               TRUE  if the module is tested successfully.
     *                               FALSE if the module is not found.
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiCreate' ) ) {

        function mimimiCreate ( $name, $owner = NULL, $test = FALSE ) {
            $path      = mimimiBasePath ( );
            $extension = '.php';

            /**
             * -------------------------------------------------------------
             *
             * Compute a parent path by cutting off the first segment of
             * the node path.
             *
             * -------------------------------------------------------------
             */

            $isOwner   = ! empty ( $owner );
            $ownerPath = $isOwner
                            ? preg_replace ( '~^[^/]*/+~u', '', $owner->getNodePath ( ) )
                            : '';

            /**
             * -------------------------------------------------------------
             *
             * Convert a module name to CamelCase.
             *
             * -------------------------------------------------------------
             */

            $module = preg_replace    ( '/[\W_]+/u',      ' ', $name    );
            $module = preg_replace    ( '/(^\s+|\s+$)/u', '',  $module  );
            $module = mb_convert_case ( $module, MB_CASE_TITLE, 'UTF-8' );
            $module = preg_replace    ( '/\s/u',          '',  $module  );

            /**
             * -------------------------------------------------------------
             *
             * Build a plan for loading.
             *
             * -------------------------------------------------------------
             */

            $plan = [
                MIMIMI_INSTALL_FOLDER => 'My',
                MIMIMI_APP_FOLDER     => 'My',
                MIMIMI_MODULES_FOLDER => '',
                MIMIMI_CORE_FOLDER    => ''
            ];

            /**
             * -------------------------------------------------------------
             *
             * Try to load the requested module by plan.
             *
             * -------------------------------------------------------------
             */

            foreach ( $plan as $folder => $class ) {

                $folder .= $isOwner
                              ? ( $ownerPath . $module . '/' )
                              : '';
                $file = $path . $folder . $module . $extension;
                if ( file_exists ( $file ) ) {

                    /**
                     * -----------------------------------------------------
                     *
                     * Try to test (if that mode is requested).
                     *
                     * -----------------------------------------------------
                     */

                    $class .= MIMIMI_CLASS_PREFIX . preg_replace ( '~/~u', '', $ownerPath ) . $module;
                    if ( $test ) {
                        if ( ! class_exists ( $class ) ) {
                            $file = @ file_get_contents ( $file );
                            return is_string  ( $file )
                                && preg_match ( '/[\s;]class\s+' . $class . '[\s\{]/ui', ' ' . $file );
                        }
                        return TRUE;
                    }

                    /**
                     * -----------------------------------------------------
                     *
                     * Try to create its object.
                     *
                     * -----------------------------------------------------
                     */

                    try {
                        require_once ( $file );
                    } catch ( Exception $e ) { }

                    if ( class_exists ( $class ) ) {
                        if ( $isOwner ) {
                            $owner->$name = new $class ( $owner );
                            return $owner->$name;
                        }
                        return new $class ( );
                    }

                    /**
                     * -----------------------------------------------------
                     *
                     * Show the current user an error message.
                     *
                     * -----------------------------------------------------
                     */

                    mimimiStop (
                       'ERROR: The "' . $folder . $module . $extension . '" file does not have the required class ' . $class . '!'
                    );
                }
            }

            /**
             * -------------------------------------------------------------
             *
             * Show the current user an error message.
             *
             * -------------------------------------------------------------
             */

            if ( ! $test ) {
                mimimiStop (
                    'ERROR: The "' . $folder . $module . $extension . '" file not found!'
                );
            }
            return FALSE;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Loads a PHP file if it exists.
     *
     * ---------------------------------------------------------------------
     *
     * Please note that these varaibles will be available in the loaded
     * file: $params, $folder, $filename, $file, $app.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $filename  Relative file name, for example Hello/Hello.php.
     * @param   bool    $fromCore  (optional) TRUE  if you want to find that file only in MODULES or CORE folder.
     *                                        FALSE if you want to find that file only in INSTALLER or APP folder.
     *                                        NULL  if you need to find in all folders.
     * @param   bool    $once      (optional) TRUE  if you need to load that file only on the first call.
     *                                        FALSE if you need loading on every call.
     * @param   mixed   $params    (optional) Some parameters from the calling file.
     * @return  void
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiInclude' ) ) {

        function mimimiInclude ( $filename, $fromCore = TRUE, $once = TRUE, $params = FALSE ) {
            $path = mimimiBasePath ( );

            /**
             * -------------------------------------------------------------
             *
             * Build a plan for loading.
             *
             * -------------------------------------------------------------
             */

            $plan = [
                MIMIMI_INSTALL_FOLDER => $fromCore !== TRUE,
                MIMIMI_APP_FOLDER     => $fromCore !== TRUE,
                MIMIMI_MODULES_FOLDER => $fromCore !== FALSE,
                MIMIMI_CORE_FOLDER    => $fromCore !== FALSE
            ];

            /**
             * -------------------------------------------------------------
             *
             * Go through the plan items.
             *
             * -------------------------------------------------------------
             */

            foreach ( $plan as $folder => $enabled ) {
                if ( $enabled ) {
                    $file = $path . $folder . $filename;
                    if ( file_exists ( $file ) ) {

                        /**
                         * -------------------------------------------------
                         *
                         * Remove unnecessary variables from the scope.
                         *
                         * -------------------------------------------------
                         */

                        unset ( $path     );
                        unset ( $enabled  );
                        unset ( $fromCore );
                        unset ( $plan     );

                        /**
                         * -------------------------------------------------
                         *
                         * Put a reference to this app to the scope.
                         *
                         * -------------------------------------------------
                         */

                        global $app;

                        /**
                         * -------------------------------------------------
                         *
                         * Try to load the requested file.
                         *
                         * -------------------------------------------------
                         */

                        try {
                            if ( $once ) {
                                unset ( $once );
                                include_once ( $file );
                            } else {
                                unset ( $once );
                                include ( $file );
                            }
                        } catch ( Exception $e ) { }
                        break;
                    }
                }
            }
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Retrieves a list of directories (or files) in the specified directory.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $dir        Relative path to the directory being scanned.
     * @param   array   $except     (optional) List of ignored directory names (or file names).
     * @param   bool    $filesOnly  (optional) TRUE if you want to retrieve file names.
     *                                         FALSE if you want to retrieve directory names.
     * @return  array
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiFolders' ) ) {

        function mimimiFolders ( $dir, $except = [ ], $filesOnly = FALSE ) {
            $result = [ ];
            $path   = mimimiBasePath ( $dir );
            $handle = @ opendir ( $path );
            if ( $handle !== FALSE ) {

                /**
                 * ---------------------------------------------------------
                 *
                 * Remove trailing slashes from excepts.
                 *
                 * ---------------------------------------------------------
                 */

                $except = ( array ) $except;
                foreach ( $except as & $dir ) {
                    $dir = is_string ( $dir )
                                     ? preg_replace ( '/[.\s\/\\\\]+$/u', '', $dir )
                                     : '';
                }

                /**
                 * ---------------------------------------------------------
                 *
                 * Scan the directory.
                 *
                 * ---------------------------------------------------------
                 */

                while ( ( $file = readdir ( $handle ) ) !== FALSE ) {
                    if ( trim ( $file, '.' ) != '' ) {
                        if ( $filesOnly ) {
                            if ( is_file ( $path . $file ) ) {
                                if ( ! in_array ( $file, $except ) ) {
                                    $index = mb_strtolower ( $file, 'UTF-8' );
                                    $result[ $index ] = $file;
                                }
                            }
                        } else {
                            if ( is_dir ( $path . $file ) ) {
                                if ( ! in_array ( $file, $except ) ) {
                                    $index = mb_strtolower ( $file, 'UTF-8' );
                                    $result[ $index ] = $file;
                                }
                            }
                        }
                    }
                }
                closedir ( $handle );

                /**
                 * ---------------------------------------------------------
                 *
                 * Sort names alphabetically.
                 *
                 * ---------------------------------------------------------
                 */

                asort ( $result, SORT_STRING | SORT_FLAG_CASE );
            }
            return $result;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Scans the specified directory.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string    $dir        Relative path to the directory being scanned.
     * @param   callable  $callback   Your external function that will be called for the current scanned entity.
     * @param   bool      $filesOnly  (optional) TRUE  if you want to scan files only.
     *                                           FALSE if you want to scan any entities.
     * @return  bool                  TRUE  if all expected entities are successfully scanned.
     *                                FALSE if at least one entity could not be scanned.
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiScan' ) ) {

        function mimimiScan ( $dir, $callback, $filesOnly = FALSE ) {
            $result = 1;
            $path   = mimimiBasePath ( $dir );
            $handle = @ opendir ( $path );
            if ( $handle !== FALSE ) {
                while ( ( $name = readdir ( $handle ) ) !== FALSE ) {
                    if ( trim ( $name, '.' ) != '' ) {
                        $absoluteFile = $path . $name;
                        $relativeFile = $dir . '/' . $name;
                        if ( $isDirectory = is_dir ( $absoluteFile ) ) {
                            $result &= mimimiScan ( $relativeFile, $callback, $filesOnly );
                            if ( $filesOnly ) continue;
                        }
                        $result &= $callback ( $absoluteFile, $relativeFile, $isDirectory );
                    }
                }
                closedir ( $handle );
            }
            return $result;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Cleans the specified directory.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $dir        Relative path to the directory being cleaned.
     * @param   bool    $filesOnly  (optional) TRUE  if you want to remove files only.
     *                                         FALSE if you want to remove any entities.
     * @return  bool                TRUE  if all expected entities are successfully removed.
     *                              FALSE if at least one entity could not be removed.
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiClean' ) ) {

        function mimimiClean ( $dir, $filesOnly = FALSE ) {
            $callback = function ( $absoluteFile, $relativeFile, $isDirectory ) {
                            return $isDirectory ? @ rmdir  ( $absoluteFile )
                                                : @ unlink ( $absoluteFile );
                        };
            return mimimiScan ( $dir, $callback, $filesOnly );
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Retrieves a named SERVER parameter.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $param  The name of the parameter you are looking for.
     * @param   mixed   $def    (optional) Default value if that parameter does not exist.
     * @return  mixed
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiServer' ) ) {

        function mimimiServer ( $param, $def = '' ) {
            return isset ( $_SERVER[ $param ] )
                   ? $_SERVER[ $param ]
                   : $def;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Retrieves a named ENV parameter.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $param  The name of the parameter you are looking for.
     * @param   mixed   $def    (optional) Default value if that parameter does not exist.
     * @return  mixed
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiEnvironment' ) ) {

        function mimimiEnvironment ( $param, $def = '' ) {
            return isset ( $_ENV[ $param ] )
                   ? $_ENV[ $param ]
                   : $def;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Stops execution and send a message to the user's browser.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   string  $message  STRING  Your message to the user.
     *                            ARRAY   List of URLs for the "Location:" values of status code "300 Multiple Choices".
     *                            URI     The "Location:" value for 301, 302, 307, 308 statuses.
     *                            RULES   The values of "WWW-Authenticate:" and "Proxy-Authenticate:" for 401 and 407 statuses, respectively.
     *                            INTEGER The "Retry-After:" value for status code "429 Too Many Requests".
     * @param   int     $code     (optional) The HTTP status code that will be sent to the browser.
     * @return  void
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiStop' ) ) {

        function mimimiStop ( $message, $code = 404 ) {
            $headers = [ ];
            switch ( $code ) {
                case 0: break;

                /**
                 * ---------------------------------------------------------
                 *
                 * If it's a success code.
                 *
                 * ---------------------------------------------------------
                 */

                case 200: $headers = [ '200 OK' => $code ]; break;

                /**
                 * ---------------------------------------------------------
                 *
                 * If it's a redirect code.
                 *
                 * ---------------------------------------------------------
                 */

                case 300: $message = ( array ) $message;
                          $headers = [ '300 Multiple Choices'   => $code                          ]; foreach ( $message as $uri ) $headers[ ] = 'Location: ' . $uri; $message = ''; break;
                case 301: $headers = [ '301 Moved Permanently'  => $code, 'Location: ' . $message ]; $message = ''; break;
                case 302: $headers = [ '302 Found'              => $code, 'Location: ' . $message ]; $message = ''; break;
                case 307: $headers = [ '307 Temporary Redirect' => $code, 'Location: ' . $message ]; $message = ''; break;
                case 308: $headers = [ '308 Permanent Redirect' => $code, 'Location: ' . $message ]; $message = ''; break;

                /**
                 * ---------------------------------------------------------
                 *
                 * If it's a content error code.
                 *
                 * ---------------------------------------------------------
                 */

                case 400: $headers = [ '400 Bad Request'                   => $code                                    ]; break;
                case 401: $headers = [ '401 Unauthorized'                  => $code, 'WWW-Authenticate: ' . $message   ]; $message = ''; break;
                case 403: $headers = [ '403 Forbidden'                     => $code                                    ]; break;
                case 404: $headers = [ '404 Not Found'                     => $code                                    ]; break;
                case 405: $headers = [ '405 Method Not Allowed'            => $code                                    ]; break;
                case 406: $headers = [ '406 Not Acceptable'                => $code                                    ]; break;
                case 407: $headers = [ '407 Proxy Authentication Required' => $code, 'Proxy-Authenticate: ' . $message ]; $message = ''; break;
                case 408: $headers = [ '408 Request Timeout'               => $code                                    ]; break;
                case 409: $headers = [ '409 Conflict'                      => $code                                    ]; break;
                case 410: $headers = [ '410 Gone'                          => $code                                    ]; break;
                case 429: $headers = [ '429 Too Many Requests'             => $code, 'Retry-After: ' . $message        ]; $message = ''; break;
                case 451: $headers = [ '451 Unavailable For Legal Reasons' => $code                                    ]; break;

                /**
                 * ---------------------------------------------------------
                 *
                 * If it's a server error code.
                 *
                 * ---------------------------------------------------------
                 */

                case 500: $headers = [ '500 Internal Server Error' => $code ]; break;
                case 501: $headers = [ '501 Not Implemented'       => $code ]; break;
                case 503: $headers = [ '503 Service Unavailable'   => $code ]; break;

                /**
                 * ---------------------------------------------------------
                 *
                 * Other codes.
                 *
                 * ---------------------------------------------------------
                 */

                default:
                    $headers = [ $code . ' See Specification For Details' => $code ];
            }

            /**
             * -------------------------------------------------------------
             *
             * Send headers if they exist.
             *
             * -------------------------------------------------------------
             */

            if ( $headers ) {
                $scheme = mimimiServer ( 'SERVER_PROTOCOL', 'HTTP/1.1' );
                foreach ( $headers as $index => $value ) {
                    if ( is_string ( $index ) ) {
                        @ header ( $scheme . ' ' . $index, TRUE, $value );
                    } else {
                        @ header ( $value );
                    }
                }
            }

            /**
             * -------------------------------------------------------------
             *
             * Send the message and stop.
             *
             * -------------------------------------------------------------
             */

            echo $message;
            exit;
        }
    }

    /**
     * ---------------------------------------------------------------------
     *
     * Generates random identifier.
     *
     * ---------------------------------------------------------------------
     *
     * @public
     * @param   int   $length  Required length of ID string.
     * @param   bool  $alpha   (optional) TRUE  if it must be an alpha-numeric identifier.
     *                                    FALSE if it must be only a numeric identifier.
     * @return  string
     *
     * ---------------------------------------------------------------------
     */

    if ( ! function_exists ( 'mimimiRandomId' ) ) {

        function mimimiRandomId ( $length, $alpha = TRUE ) {
            $id = FALSE;
            $func = 'openssl_random_pseudo_bytes';
            if ( $alpha && function_exists ( $func ) ) {
                $id = $func ( $length );
            }
            if ( $id !== FALSE ) {
                $id = bin2hex    ( $id             );
                $id = substr     ( $id, 0, $length );
                $id = strtolower ( $id             );
            } else {
                $id = '';
                while ( $length > 0 ) {
                    $id .= mt_rand ( 0, 9 );
                    $length--;
                }
            }
            return $id;
        }
    }
