<?php

/*
+---------------------------------------------------------------------------+
| Revive Adserver                                                           |
| http://www.revive-adserver.com                                            |
|                                                                           |
| Copyright: See the COPYRIGHT.txt file.                                    |
| License: GPLv2 or later, see the LICENSE.txt file.                        |
+---------------------------------------------------------------------------+
*/

require_once 'DB/DataObject/Generator.php';
require_once MAX_PATH . '/lib/OA/DB/Table/Core.php';


/**
 * Extending standard PEAR DB_DataObject Generator tool
 *
 * @package    OpenXDB
 */
class OA_DB_DataObject_Generator extends DB_DataObject_Generator
{
    /**
     * @var string|mixed
     */
    public $classname;
    /**
     * New() methods work-in-progress
     * refactoring to use mdb2 schema files
     */
    public function start($schema = '')
    {
        global $_DB_DATAOBJECT;
        $_DB_DATAOBJECT['CONFIG']['debug'] = $GLOBALS['_MAX']['CONF']['debug']['priority'];

        $this->debug("START\n");

        $this->_createTableList($schema);

        /**
         * generate the db_schema.ini
         */
        $this->_newConfig = '';
        foreach (array_keys($this->tables) as $this->table) {
            $this->_generateDefinitionsTable();
        }
        $this->_writeSchemaIni();

        $this->generateClasses();

        $this->debug("DONE\n\n");
    }

    /**
     * sets the list of tables and definitions
     * for use in generating the dataobjects
     *
     * table definitions are provided by OA_DB_Table
     * which uses mdb2schema an xml schema file
     *
     * this method unsets any excluded tables
     * and stores the db_dataobject datatype
     *
     */
    public function _createTableList($schema = '')
    {
        $options = &PEAR::getStaticProperty('DB_DataObject', 'options');

        $oTable = new OA_DB_Table();
        $oTable->init($schema, false);
        $aDefinition = $oTable->aDefinition['tables'];

        if (isset($options['generator_exclude_regex'])) {
            foreach ($aDefinition as $table => $aTable) {
                if (preg_match($options['generator_exclude_regex'], $table)) {
                    unset($oTable->aDefinition['tables'][$table]);
                } else {
                    foreach ($aTable['fields'] as $field => $aField) {
                        $openxType = $aField['type'];
                        $mdb2Type = $oTable->oDbh->datatype->mapPrepareDatatype($openxType);
                        $aField['type'] = $mdb2Type;
                        $dboType = $this->deriveDataType($aField, $oTable->oDbh->phptype);

                        $oTable->aDefinition['tables'][$table]['fields'][$field]['oxtype'] = $openxType;
                        $oTable->aDefinition['tables'][$table]['fields'][$field]['type'] = $mdb2Type;
                        $oTable->aDefinition['tables'][$table]['fields'][$field]['dbotype'] = $dboType;
                        //$this->debug("{$table}.{$field} type map :: {$openxType} => {$mdb2Type} => {$dboType} \n");
                    }
                }
            }
        }
        $this->_definitions = $oTable->aDefinition['tables'];
        $this->tables = $this->_definitions;
    }

    /**
     * dump the db_schema.ini contents to file
     *
     */
    public function _writeSchemaIni()
    {
        $options = &PEAR::getStaticProperty('DB_DataObject', 'options');

        $base = @$options['schema_location'];
        $file = "{$base}/db_schema.ini";

        if (!file_exists(dirname($file))) {
            require_once 'System.php';
            System::mkdir(['-p', '-m', 0755, dirname($file)]);
        }
        $this->debug("Writing ini as {$file}\n");
        touch($file);
        $fh = fopen($file, 'w');
        fwrite($fh, $this->_newConfig);
        fclose($fh);
    }

    /**
     * straight from the parent unit
     * determine the DB_DATAOBJECT datatype value
     *
     * @param array $t
     * @param string $dbtype
     * @return integer
     */
    public function deriveDataType(&$t, $dbtype)
    {
        switch (strtoupper($t['type'])) {
            case 'INT':
            case 'INT2':    // postgres
            case 'INT4':    // postgres
            case 'INT8':    // postgres
            case 'SERIAL4': // postgres
            case 'SERIAL8': // postgres
            case 'INTEGER':
            case 'TINYINT':
            case 'SMALLINT':
            case 'MEDIUMINT':
            case 'BIGINT':
                $type = DB_DATAOBJECT_INT;
                if ($t['length'] == 1) {
                    $type += DB_DATAOBJECT_BOOL;
                }
                break;

            case 'REAL':
            case 'DOUBLE':
            case 'DOUBLE PRECISION': // double precision (firebird)
            case 'FLOAT':
            case 'FLOAT4': // real (postgres)
            case 'FLOAT8': // double precision (postgres)
            case 'DECIMAL':
            case 'MONEY':  // mssql and maybe others
            case 'NUMERIC':
            case 'NUMBER': // oci8
                $type = DB_DATAOBJECT_INT; // should really by FLOAT!!! / MONEY...
                break;

            case 'YEAR':
                $type = DB_DATAOBJECT_INT;
                break;

            case 'BIT':
            case 'BOOL':
            case 'BOOLEAN':

                $type = DB_DATAOBJECT_BOOL;
                // postgres needs to quote '0'
                if ($dbtype == 'pgsql') {
                    $type += DB_DATAOBJECT_STR;
                }
                break;

            case 'STRING':
            case 'CHAR':
            case 'VARCHAR':
            case 'VARCHAR2':
            case 'TINYTEXT':

            case 'ENUM':
            case 'SET':         // not really but oh well
            case 'TIMESTAMPTZ': // postgres
            case 'BPCHAR':      // postgres
            case 'INTERVAL':    // postgres (eg. '12 days')

            case 'CIDR':        // postgres IP net spec
            case 'INET':        // postgres IP
            case 'MACADDR':     // postgress network Mac address.

            case 'INTEGER[]':   // postgres type
            case 'BOOLEAN[]':   // postgres type

                $type = DB_DATAOBJECT_STR;
                break;

            case 'TEXT':
            case 'MEDIUMTEXT':
            case 'LONGTEXT':

                $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TXT;
                break;


            case 'DATE':
                $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE;
                break;

            case 'TIME':
                $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TIME;
                break;


            case 'DATETIME':
                $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME;
                break;

            case 'TIMESTAMP': // do other databases use this???
                $type = ($dbtype == 'mysql' || $dbtype == 'mysqli') ?
                    DB_DATAOBJECT_MYSQLTIMESTAMP :
                    DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME;
                break;


            case 'TINYBLOB':
            case 'BLOB':       /// these should really be ignored!!!???
            case 'MEDIUMBLOB':
            case 'LONGBLOB':
            case 'BYTEA':   // postgres blob support..
                $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_BLOB;
                break;
            default:
                echo "*****************************************************************\n" .
                     "**               WARNING UNKNOWN TYPE                          **\n" .
                     "** Found column '{$t['name']}', of type  '{$t['type']}'            **\n" .
                     "** Please submit a bug, describe what type you expect this     **\n" .
                     "** column  to be                                               **\n" .
                     "*****************************************************************\n";
                $type = null;
                break;
        }
        if ($type && $t['notnull']) {
            $type += DB_DATAOBJECT_NOTNULL;
        }
        return $type;
    }

    /**
     * to use, you must set option: generate_links=true
     *
     */
    public function generateForeignKeys()
    {
        // the db_schema.link.ini file is managed manually
    }

    /*
     * building the class files
     * for each of the tables output a file!
     */
    public function generateClasses()
    {
        //echo "Generating Class files:        \n";
        $options = &PEAR::getStaticProperty('DB_DataObject', 'options');


        if ($extends = @$options['extends']) {
            $this->_extends = $extends;
            $this->_extendsFile = $options['extends_location'];
        }

        foreach (array_keys($this->tables) as $this->table) {
            $this->table = trim($this->table);
            $this->classname = $this->getClassNameFromTableName($this->table);
            $i = '';
            $outfilename = $this->getFileNameFromTableName($this->table);

            $oldcontents = '';
            if (file_exists($outfilename)) {
                // file_get_contents???
                $oldcontents = implode('', file($outfilename));
            }
            $this->debug("generating $this->classname");
            $out = $this->_generateClassTable($oldcontents);
            $this->debug("writing $this->classname\n");
            $fh = fopen($outfilename, "w");
            fwrite($fh, $out);
            fclose($fh);
        }
        //echo $out;
    }

    /**
     * The table class generation part - single file.
     *
     * @access  private
     * @return  none
     */
    public function _generateClassTable($input = '')
    {
        $n = DIRECTORY_SEPARATOR == '\\' ? "\r\n" : "\n";

        // title = expand me!
        $foot = "";
        $head = "<?php{$n}{$n}/**{$n} * Table Definition for {$this->table}{$n} */{$n}";
        // requires
        $head .= "require_once MAX_PATH.'{$this->_extendsFile}';{$n}{$n}";
        // add dummy class header in...
        // class
        $head .= "class {$this->classname} extends {$this->_extends} {$n}{";

        $body = "{$n}    ###START_AUTOCODE{$n}";
        $body .= "    /* the code below is auto generated do not remove the above tag */{$n}{$n}";
        // table
        $padding = (30 - strlen($this->table));
        $padding = max(2, $padding);

        $p = str_repeat(' ', $padding);

        $options = &PEAR::getStaticProperty('DB_DataObject', 'options');


        $var = empty($options['generator_var_keyword']) ? 'public' : $options['generator_var_keyword'];


        $body .= "    {$var} \$__table = '{$this->table}';  {$p}// table name{$n}";

        if (!empty($options['generator_novars'])) {
            $var = '//' . $var;
        }

        $defs = $this->_definitions[$this->table]['fields'];

        // show nice information!
        $sets = [];
        foreach ($defs as $name => $t) {
            $t['name'] = $name;
            if (!strlen(trim($t['name']))) {
                continue;
            }
            $padding = (30 - strlen($t['name']));
            if ($padding < 2) {
                $padding = 2;
            }
            $p = str_repeat(' ', $padding);

            $fielddec = "    {$var} \${$t['name']};  {$p}// {$t['type']}({$t['length']}) => {$t['oxtype']} => {$t['dbotype']} {$n}";

            $this->debug($fielddec);
            $body .= $fielddec;
        }

        // simple creation tools ! (static stuff!)
        $body .= "{$n}";
        $body .= "    /* Static get */{$n}";
        $body .= "    public static function staticGet(\$k, \$v = null){$n}";
        $body .= "    {{$n}";
        $body .= "        return DB_DataObject::staticGetFromClassName('{$this->classname}', \$k, \$v);{$n}";
        $body .= "    }{$n}";

        // generate getter and setter methods
        $body .= $this->_generateGetters($input);
        $body .= $this->_generateSetters($input);

        $body .= $this->_generateDefaultsArray($this->table);

        $body .= "{$n}    /* the code above is auto generated do not remove the tag below */";
        $body .= "{$n}    ###END_AUTOCODE{$n}";


        // stubs..

        if (!empty($options['generator_add_validate_stubs'])) {
            foreach ($defs as $t) {
                if (!strlen(trim($t->name))) {
                    continue;
                }
                $validate_fname = 'validate' . $this->getMethodNameFromColumnName($t->name);
                // dont re-add it..
                if (preg_match('/\s+function\s+' . $validate_fname . '\s*\(/i', $input)) {
                    continue;
                }
                $body .= "{$n}    function {$validate_fname}(){$n}    {{$n}        return false;{$n}    }{$n}";
            }
        }

        $foot .= "}{$n}?>";
        $full = $head . $body . $foot;

        if (!$input) {
            return $full;
        }
        if (!preg_match('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n)/s', $input)) {
            return $full;
        }
        if (!preg_match('/(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', $input)) {
            return $full;
        }


        /* this will only replace extends DB_DataObject by default,
            unless use set generator_class_rewrite to ANY or a name*/

        $class_rewrite = 'DB_DataObject';
        $options = &PEAR::getStaticProperty('DB_DataObject', 'options');
        if (!($class_rewrite = @$options['generator_class_rewrite'])) {
            $class_rewrite = 'DB_DataObject';
        }
        if ($class_rewrite == 'ANY') {
            $class_rewrite = '[a-z_]+';
        }

        $input = preg_replace(
            '/(\n|\r\n)class\s*[a-z0-9_]+\s*extends\s*' . $class_rewrite . '\s*\{(\n|\r\n)/si',
            "{$n}class {$this->classname} extends {$this->_extends} {$n}{{$n}",
            $input,
        );

        $full = preg_replace(
            '/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s',
            $body,
            $input,
        );

        // Remove trailing whitespace
        return preg_replace('#[ \t]+$#m', '', $full);
    }

    /**
     * generates contents of db_schema.ini
     *
     * @return string
     */
    public function _generateDefinitionsTable()
    {
        $aTable = $this->_definitions[$this->table];
        $this->_newConfig .= "\n[{$this->table}]\n";
        $keys_out = "\n[{$this->table}__keys]\n";
        $keys_out_primary = '';
        $keys_out_secondary = '';
        $aKeys_primary = [];
        $aKeys_secondary = [];

        foreach ($aTable['fields'] as $field => $aField) {
            $this->_newConfig .= "{$field} = {$aField['dbotype']}\n";
            if ($aField['autoincrement']) {
                $aKeys_primary[$field] = 'N';
            }
        }
        foreach ($aTable['indexes'] as $index => $aIndex) {
            if (isset($aIndex['primary']) && ($aIndex['primary']) && (count($aIndex['fields']) == 1)) {
                foreach ($aIndex['fields'] as $field => $aSort) {
                    if (!array_key_exists($field, $aKeys_primary)) {
                        $aKeys_primary[$field] = 'N';
                    }
                    if ((array_key_exists($field, $aKeys_secondary))) {
                        unset($aKeys_secondary[$field]);
                    }
                }
                continue;
            } elseif (isset($aIndex['unique']) && ($aIndex['unique']) && (count($aIndex['fields']) == 1)) {
                $key_type = 'U';
            } else {
                $key_type = 'K';
            }
            foreach ($aIndex['fields'] as $field => $aSort) {
                if ((!array_key_exists($field, $aKeys_primary)) && ((!array_key_exists($field, $aKeys_secondary)))) {
                    $aKeys_secondary[$field] = $key_type;
                }
            }
        }
        foreach ($aKeys_primary as $field => $key_type) {
            $keys_out_primary .= "{$field} = {$key_type}\n";
        }
        foreach ($aKeys_secondary as $field => $key_type) {
            $keys_out_secondary .= "{$field} = {$key_type}\n";
        }
        $this->_newConfig .= $keys_out . (empty($keys_out_primary) ? $keys_out_secondary : $keys_out_primary);
        //$this->_newConfig .= $keys_out . $keys_out_primary . $keys_out_secondary;

        return;
    }


    /**
     * Generate array of defaults values
     *
     * @param    string  table name.
     * @return   string
     */
    public function _generateDefaultsArray($table)
    {
        $aFields = $this->_definitions[$table]['fields'];

        $aNulls = [
            'sso_user_id',
            'date_last_login',
            'email_updated',
            'advertiser_account_id',
            'website_account_id',
        ];

        $aDefaults = [];

        foreach ($aFields as $field => $aField) {
            $type = $aField['dbotype'];
            $value = $aField['default'];

            switch (true) {
                case (in_array($field, $aNulls)):
                    $aDefaults[$field] = "OX_DATAOBJECT_NULL";
                    break;
                case ((!$aField['notnull']) && ($value === null)):
                case ($aField['autoincrement']):
                case (is_null($type)):
                    break;
                case ($type & DB_DATAOBJECT_BOOL):
                    $aDefaults[$field] = (int) (bool) $value;
                    break;

                    // Check DATE/TIME type first instead of STR (many date/time fields has multiple types including DB_DATAOBJECT_STR)
                case ($type & DB_DATAOBJECT_MYSQLTIMESTAMP): // not supported yet..
                case ($type & DB_DATAOBJECT_DATE):
                case ($type & DB_DATAOBJECT_TIME):
                    if ($field == 'updated') {
                        $aDefaults[$field] = "'%DATE_TIME%'";
                    } elseif ($field == 'total_basket_value') { //recognized as DB_DATAOBJECT_MYSQLTIMESTAMP & DB_DATAOBJECT_INT
                        $aDefaults[$field] = $value;
                    } else {
                        $aDefaults[$field] = "'%NO_DATE_TIME%'";
                    }
                    break;

                case ($type & DB_DATAOBJECT_STR):
                    $aDefaults[$field] = "'" . addslashes($value) . "'";
                    break;

                case (($type & DB_DATAOBJECT_INT) && $value !== ''):
                    $aDefaults[$field] = $value;
                    break;

                default:
                    //$aDefaults[$field] = $value;
                    break;
            }
        }
        $result = '';
        if (!empty($aDefaults)) {
            $result = "\n" . '    public $defaultValues = [' . "\n";
            foreach ($aDefaults as $k => $v) {
                $result .= '        \'' . addslashes($k) . '\' => ' . $v . ",\n";
            }
            $result .= "    ];\n";
        }
        return $result;
    }
}
