<?php
/**
 * -------------------------------------------------------------------------
 *
 * The module for connecting to the database and working with it.
 *
 * -------------------------------------------------------------------------
 *
 * This file defines the base class "MimimiDb" where the following methods
 * are implemented:
 *
 *     run          ( $params         )  -->  to launch this module
 *     lastInsertId (                 )  -->  to return an ID of the last inserted row
 *     query        ( $query, $data   )  -->  to execute an SQL statement
 *     connect      ( $params, $quiet )  -->  to connect your app to the database
 *
 * The "MimimiDb" class also contains the following properties:
 *
 *     $error         -->  to indicate the last error code
 *     $errorInfo     -->  to provide details of the last error (SQLSTATE code, Driver-specific code and message)
 *     $errorQuery    -->  to provide the query that resulted in the last error
 *     $errorData     -->  to provide the query data that was passed during the last error
 *     PROTECTED $db  -->  to reference a database object
 *
 * Please note that you can override this module if needed by using a file
 * with the same directory and file name in your application folder. However,
 * the class name of that module must be "MyMimimiDb". Also note that you
 * can extend this module by preloading its class with directive
 * "mimimiInclude('Db/Db.php')" and then simply extending that class to
 * "MyMimimiDb".
 *
 * -------------------------------------------------------------------------
 *
 * @package    MimimiFramework
 * @subpackage Modules
 * @copyright  2022 MiMiMi Community
 *             https://mimimi.software/
 * @license    GPL-2.0
 *             https://opensource.org/license/gpl-2-0/
 *
 * -------------------------------------------------------------------------
 */

    mimimiInclude ( 'Module.php' );

    class MimimiDb extends MimimiModule {

        /**
         * -----------------------------------------------------------------
         *
         * Information about the last query if an error occurred.
         *
         * -----------------------------------------------------------------
         */

        public $error      = 0;
        public $errorInfo  = [ ];
        public $errorQuery = '';
        public $errorData  = [ ];

        /**
         * -----------------------------------------------------------------
         *
         * Reference to the database object.
         *
         * -----------------------------------------------------------------
         *
         * @var    object|bool
         * @access protected
         *
         * -----------------------------------------------------------------
         */

        protected $db = FALSE;

        /**
         * -----------------------------------------------------------------
         *
         * Launches this module.
         *
         * -----------------------------------------------------------------
         *
         * This method just connects your application to the database if it
         * is not already connected.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   mixed  $params  (optional) Some parameters from the calling process.
         * @return  void
         *
         * -----------------------------------------------------------------
         */

        public function run ( $params = '' ) {
            if ( empty ( $this->db ) ) {
                $this->db = $this->connect ( [
                                'driver'    => MIMIMI_DATABASE_DRIVER,
                                'host'      => MIMIMI_DATABASE_HOST,
                                'port'      => MIMIMI_DATABASE_PORT,
                                'user'      => MIMIMI_DATABASE_USER,
                                'password'  => MIMIMI_DATABASE_PASSWORD,
                                'name'      => MIMIMI_DATABASE_NAME,
                                'charset'   => MIMIMI_DATABASE_CHARSET,
                                'collation' => MIMIMI_DATABASE_COLLATION
                            ], FALSE );
            }
        }

        /**
         * -----------------------------------------------------------------
         *
         * Returns an identifier of the last inserted entry.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @return  int|bool  FALSE if there is no connection to the database.
         *
         * -----------------------------------------------------------------
         */

        public function lastInsertId ( ) {
            $this->run ( );
            return empty ( $this->db )
                         ? FALSE
                         : $this->db->lastInsertId ( );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Executes an SQL statement.
         *
         * -----------------------------------------------------------------
         *
         * You can use wildcard "?" in your query to specify locations that
         * should be replaced with data values. You can also use a double
         * underscore before a table name to replace it with a table prefix.
         * For example:
         *
         *     SELECT month, year, sum
         *     FROM   __payments
         *     WHERE  month = ? AND year = ? AND sum > ?
         *
         * In this example, it was assumed that the input parameter $data
         * would look something like this: [ 'September', 2024, 50.00 ].
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string       $query  The SQL statement being executed.
         * @param   array        $data   (optional) Values to insert instead of "?" signs in the SQL statement.
         * @return  object|bool          OBJECT associated with that statement.
         *                               FALSE  in case of failure or lack of connection to the database.
         *
         * -----------------------------------------------------------------
         */

        public function query ( $query, $data = [ ] ) {
            $this->run ( );
            if ( ! empty ( $this->db ) ) {
                $this->error      = 0;
                $this->errorInfo  = [ 0, NULL, NULL ];
                $this->errorQuery = '';
                $this->errorData  = [ ];

                /**
                 * ---------------------------------------------------------
                 *
                 * Substitute table prefixes.
                 *
                 * ---------------------------------------------------------
                 */

                $quote1  = '[\s`,=\(\+-]';
                $table   = '[a-z][a-z0-9_]*';
                $quote2  = '[\s`,.\);]';
                $pattern = '~(' . $quote1 . ')__(' . $table . $quote2 . ')~ui';
                $prefix  = '$1' . MIMIMI_DATABASE_TABLE_PREFIX . '$2';

                $query   = preg_replace ( $pattern, $prefix, $query );

                /**
                 * ---------------------------------------------------------
                 *
                 * Execute a query.
                 *
                 * ---------------------------------------------------------
                 */

                $object = $this->db->prepare ( $query );
                if ( $object ) {
                    try {
                        $result = $object->execute ( $data );
                    } catch ( Exception $e ) {
                        $result = FALSE;
                    }

                    /**
                     * -----------------------------------------------------
                     *
                     * Return the statement's object.
                     *
                     * -----------------------------------------------------
                     */

                    if ( $result ) {
                        return $object;

                    /**
                     * -----------------------------------------------------
                     *
                     * Otherwise, get an error information.
                     *
                     * -----------------------------------------------------
                     */

                    } else {
                        $this->error     = $object->errorCode ( );
                        $this->errorInfo = $object->errorInfo ( );
                    }
                } else {
                    $this->error     = $this->db->errorCode ( );
                    $this->errorInfo = $this->db->errorInfo ( );
                }
                $this->errorQuery = $query;
                $this->errorData  = $data;
            }
            return FALSE;
        }

        /**
         * -----------------------------------------------------------------
         *
         * Connects to the specified database.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   array               $params  A list of parameters for configuring your database. It is an array like this
         *                                       [
         *                                           'driver',
         *                                           'host',
         *                                           'port',
         *                                           'user',
         *                                           'password',
         *                                           'name',
         *                                           'charset',
         *                                           'collation'
         *                                       ]
         * @param   bool                $quiet   (optional) TRUE  if you want to return an error message when it occurred. 
         *                                                  FALSE if you need to stop execution on any error.
         * @return  object|bool|string           OBJECT associated with your database.
         *                                       TRUE   if the 'driver' parameter is empty (i.e. no connection required).
         *                                       FALSE  if some input parameter is missing.
         *                                       STRING of error message if the quiet mode is requested.
         *
         * -----------------------------------------------------------------
         */

        public function connect ( $params, $quiet = TRUE ) {
            $result = FALSE;
            if ( isset ( $params[ 'driver'    ] )
            &&   isset ( $params[ 'host'      ] )
            &&   isset ( $params[ 'port'      ] )
            &&   isset ( $params[ 'user'      ] )
            &&   isset ( $params[ 'password'  ] )
            &&   isset ( $params[ 'name'      ] )
            &&   isset ( $params[ 'charset'   ] )
            &&   isset ( $params[ 'collation' ] ) ) {

                /**
                 * ---------------------------------------------------------
                 *
                 * Test database configuration.
                 *
                 * ---------------------------------------------------------
                 */

                $driver = $params[ 'driver' ];
                switch ( $driver ) {
                    case '':
                         return TRUE;
                    case 'cubrid':
                    case 'dblib':
                    case 'firebird':
                    case 'ibm':
                    case     'odbc-ibm':
                    case 'informix':
                    case 'mssql':
                    case 'mysql':
                    case 'odbc':
                    case 'oci':
                    case 'oracle':
                    case 'pgsql':
                    case 'sqlsrv':
                    case 'sybase':
                         if ( $params[ 'host' ] == '' ) {
                             $msg = 'ERROR: Database host is an empty string for the "' . $driver . '" driver!';
                             if ( $quiet ) return       $msg;
                                           mimimiStop ( $msg );
                         }
                    case 'odbc':
                    case     'odbc-access':
                    case 'sqlite':
                         if ( $params[ 'name' ] == '' ) {
                             $msg = 'ERROR: Database name is an empty string for the "' . $driver . '" driver!';
                             if ( $quiet ) return       $msg;
                                           mimimiStop ( $msg );
                         }
                         break;
                    default:
                         $msg = 'ERROR: The "' . $driver . '" driver is no support!';
                         if ( $quiet ) return       $msg;
                                       mimimiStop ( $msg );
                }

                /**
                 * ---------------------------------------------------------
                 *
                 * Try to collect parameters.
                 *
                 * ---------------------------------------------------------
                 */

                $name = '';
                switch ( $driver ) {
                    case 'cubrid':
                    case 'dblib':
                    case 'mssql':
                    case 'mysql':
                    case 'pgsql':
                    case 'sybase':
                         $port = $params[ 'port' ] !== ''
                                                     ? 'port=' . $params[ 'port' ] . ';'
                                                     : '';
                         $name = $driver . ':host='   . $params[ 'host' ] . ';' . $port .
                                            'dbname=' . $params[ 'name' ] . ';';
                         break;
                    case 'firebird':
                         $port = $params[ 'port' ] !== ''
                                                     ? '/' . $params[ 'port' ]
                                                     : '';
                         $name = $driver . ':dbname=' . $params[ 'host' ] . $port .
                                           ':' .        $params[ 'name' ];
                         break;
                    case 'odbc-ibm':
                         $driver = 'odbc';
                    case 'ibm':
                         $port = $params[ 'port' ] !== ''
                                                     ? 'PORT=' . $params[ 'port' ] . ';'
                                                     : '';
                         $name = $driver . ':DRIVER={IBM DB2 ODBC DRIVER};' .
                                            'HOSTNAME=' . $params[ 'host' ] . ';' . $port .
                                            'DATABASE=' . $params[ 'name' ] . ';' .
                                            'PROTOCOL=TCPIP;';
                         break;
                    case 'informix':
                         $port = $params[ 'port' ] !== ''
                                                     ? ' service=' . $params[ 'port' ] . ';'
                                                     : '';
                         $name = $driver . ':host='     . $params[ 'host' ] . ';' . $port .
                                           ' database=' . $params[ 'name' ] . ';' .
                                           ' server=ids_server;' .
                                           ' protocol=onsoctcp;';
                         break;
                    case 'oracle':
                         $driver = 'oci';
                    case 'oci':
                         $port = $params[ 'port' ] !== ''
                                                     ? ':' . $params[ 'port' ]
                                                     : '';
                         $name = $driver . ':dbname=//' . $params[ 'host' ] . $port .
                                                    '/' . $params[ 'name' ];
                         break;
                    case 'odbc':
                    case 'sqlite':
                         $name = $driver . ':' . $params[ 'name' ];
                         break;
                    case 'odbc-access':
                         $name = 'odbc:Driver={Microsoft Access Driver (*.mdb)};' .
                                      'Dbq=' . $params[ 'name' ]            . ';';
                         break;
                    case 'sqlsrv':
                         $port = $params[ 'port' ] !== ''
                                                     ? ',port=' . $params[ 'port' ]
                                                     : '';
                         $name = $driver . ':Server='   . $params[ 'host' ] . $port . ';' .
                                            'Database=' . $params[ 'name' ]         . ';';
                         break;
                }

                /**
                 * ---------------------------------------------------------
                 *
                 * Try to connect.
                 *
                 * ---------------------------------------------------------
                 */

                try {
                    $result = new PDO (
                        $name,
                        $params [ 'user'     ],
                        $params [ 'password' ]
                    );

                    if ( $params[ 'charset' ] != '' ) {
                        $result->exec ( 'SET CHARACTER SET "' . $params[ 'charset' ] . '"' );
                        if ( $params[ 'collation' ] != '' ) {
                            $result->exec ( 'SET NAMES   "' . $params[ 'charset'   ] . '"' .
                                                'COLLATE "' . $params[ 'collation' ] . '"' );
                        }
                    }

                    $result->setAttribute (
                        PDO::ATTR_DEFAULT_FETCH_MODE,
                        PDO::FETCH_OBJ
                    );

                /**
                 * ---------------------------------------------------------
                 *
                 * Otherwise, show an error message to the user.
                 *
                 * ---------------------------------------------------------
                 */

                } catch ( PDOException $e ) {
                    $spaces = '~[\s\x00-\x20\xA0]+~u';
                    $error  = $e->getMessage ( );
                    $error  = preg_replace ( $spaces, ' ', $e );

                    $msg = 'ERROR: No connection to the database.';
                    if ( $quiet ) return       $msg . ' ' . $error;
                                  mimimiStop ( $msg );
                }
            }
            return $result;
        }
    };
