<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Contains the Translation2_Admin_Container_mdb2 class
 *
 * PHP versions 4 and 5
 *
 * LICENSE: Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category  Internationalization
 * @package   Translation2
 * @author    Lorenzo Alberton <l.alberton@quipo.it>
 * @copyright 2004-2007 Lorenzo Alberton
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @version   CVS: $Id$
 * @link      http://pear.php.net/package/Translation2
 */

/**
 * require Translation2_Container_mdb2 class
 */
require_once 'Translation2/Container/mdb2.php';

/**
 * Storage driver for storing/fetching data to/from a database
 *
 * This storage driver can use all databases which are supported
 * by the PEAR::MDB2 abstraction layer to store and fetch data.
 *
 * @category  Internationalization
 * @package   Translation2
 * @author    Lorenzo Alberton <l.alberton@quipo.it>
 * @copyright 2004-2007 Lorenzo Alberton
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @link      http://pear.php.net/package/Translation2
 */
class Translation2_Admin_Container_mdb2 extends Translation2_Container_mdb2
{
    // {{{

    /**
     * Fetch the table names from the db
     *
     * @access private
     * @return array|PEAR_Error
     */
    function _fetchTableNames()
    {
        $this->db->loadModule('Manager');
        return $this->db->manager->listTables();
    }

    // }}}
    // {{{ addLang()

    /**
     * Creates a new table to store the strings in this language.
     * If the table is shared with other langs, it is ALTERed to
     * hold strings in this lang too.
     *
     * @param array $langData array('lang_id'    => 'en',
     *                              'table_name' => 'i18n',
     *                              'name'       => 'english',
     *                              'meta'       => 'some meta info',
     *                              'error_text' => 'not available');
     * @param array $options  array('charset'   => 'utf8',
     *                              'collation' => 'utf8_general_ci');
     *
     * @return true|PEAR_Error
     */
    function addLang($langData, $options = array())
    {
        $tables = $this->_fetchTableNames();
        if (PEAR::isError($tables)) {
            return $tables;
        }

        $lang_col  = $this->_getLangCol($langData['lang_id']);
        $charset   = empty($options['charset'])   ? null : $options['charset'];
        $collation = empty($options['collation']) ? null : $options['collation'];
        $this->db->loadModule('Manager');

        if (in_array($langData['table_name'], $tables)) {
            //table exists
            $table_changes = array(
                'add' => array(
                    $lang_col => array(
                        'type' => 'text',
                        'charset'   => $charset,
                        'collation' => $collation,
                    )
                )
            );
            ++$this->_queries;
            return $this->db->manager->alterTable($langData['table_name'], $table_changes, false);
        }

        //table does not exist
        $table_definition = array(
            $this->options['string_page_id_col'] => array(
                'type'      => 'text',
                'length'    => $this->options['string_page_id_col_length'],
                'default'   => null,
                'charset'   => $charset,
                'collation' => $collation,
            ),
            $this->options['string_id_col'] => array(
                'type'      => 'text',
                'notnull'   => 1,
                'charset'   => $charset,
                'collation' => $collation,
            ),
            $lang_col => array(
                'type'      => 'text',
                'charset'   => $charset,
                'collation' => $collation,
            ),
        );
        ++$this->_queries;
        $res = $this->db->manager->createTable($langData['table_name'], $table_definition);
        if (PEAR::isError($res)) {
            return $res;
        }
        $mysqlClause = ($this->db->phptype == 'mysql') ? '(255)' : '';
        
        $constraint_name = $langData['table_name']
            .'_'. $this->options['string_page_id_col']
            .'_'. $this->options['string_id_col'];
        $constraint_definition = array(
            'fields' => array(
                $this->options['string_page_id_col'] => array(),
                $this->options['string_id_col'].$mysqlClause => array(),
            ),
            'unique' => true,
        );
        ++$this->_queries;
        $res = $this->db->manager->createConstraint($langData['table_name'], $constraint_name, $constraint_definition);
        if (PEAR::isError($res)) {
            return $res;
        }

        $index_name = $langData['table_name'] .'_'. $this->options['string_page_id_col'];
        $index_definition = array(
            'fields' => array($this->options['string_page_id_col'] => array())
        );
        ++$this->_queries;
        $res = $this->db->manager->createIndex($langData['table_name'], $index_name, $index_definition);
        if (PEAR::isError($res)) {
            return $res;
        }
        
        $index_name = $langData['table_name'] .'_'. $this->options['string_id_col'];
        $index_definition = array(
            'fields' => array($this->options['string_id_col'] => array('length' => 255))
        );
        ++$this->_queries;
        $res = $this->db->manager->createIndex($langData['table_name'], $index_name, $index_definition);
        if (PEAR::isError($res)) {
            return $res;
        }
        
        return true;
    }

    // }}}
    // {{{ addLangToList()

    /**
     * Creates a new entry in the langsAvail table.
     * If the table doesn't exist yet, it is created.
     *
     * @param array $langData array('lang_id'    => 'en',
     *                              'table_name' => 'i18n',
     *                              'name'       => 'english',
     *                              'meta'       => 'some meta info',
     *                              'error_text' => 'not available',
     *                              'encoding'   => 'iso-8859-1');
     *
     * @return true|PEAR_Error
     */
    function addLangToList($langData)
    {
        $tables = $this->_fetchTableNames();
        if (PEAR::isError($tables)) {
            return $tables;
        }

        if (!in_array($this->options['langs_avail_table'], $tables)) {
            $queries   = array();
            $queries[] = sprintf('CREATE TABLE %s ('
                                .'%s VARCHAR(16), '
                                .'%s VARCHAR(200), '
                                .'%s TEXT, '
                                .'%s VARCHAR(250), '
                                .'%s VARCHAR(16) )',
                $this->db->quoteIdentifier($this->options['langs_avail_table'], true),
                $this->db->quoteIdentifier($this->options['lang_id_col'], true),
                $this->db->quoteIdentifier($this->options['lang_name_col'], true),
                $this->db->quoteIdentifier($this->options['lang_meta_col'], true),
                $this->db->quoteIdentifier($this->options['lang_errmsg_col'], true),
                $this->db->quoteIdentifier($this->options['lang_encoding_col'], true)
            );
            $queries[] = sprintf('CREATE UNIQUE INDEX %s_%s_index ON %s (%s)',
                $this->db->quoteIdentifier($this->options['langs_avail_table'], true),
                $this->db->quoteIdentifier($this->options['lang_id_col'], true),
                $this->db->quoteIdentifier($this->options['langs_avail_table'], true),
                $this->db->quoteIdentifier($this->options['lang_id_col'], true)
            );

            foreach ($queries as $query) {
                ++$this->_queries;
                $res = $this->db->exec($query);
                if (PEAR::isError($res)) {
                    return $res;
                }
            }
        }

        $query = sprintf('INSERT INTO %s (%s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s)',
            $this->db->quoteIdentifier($this->options['langs_avail_table'], true),
            $this->db->quoteIdentifier($this->options['lang_id_col'], true),
            $this->db->quoteIdentifier($this->options['lang_name_col'], true),
            $this->db->quoteIdentifier($this->options['lang_meta_col'], true),
            $this->db->quoteIdentifier($this->options['lang_errmsg_col'], true),
            $this->db->quoteIdentifier($this->options['lang_encoding_col'], true),
            $this->db->quote($langData['lang_id']),
            $this->db->quote($langData['name']),
            $this->db->quote($langData['meta']),
            $this->db->quote($langData['error_text']),
            $this->db->quote($langData['encoding'])
        );

        ++$this->_queries;
        $res = $this->db->exec($query);
        $this->options['strings_tables'][$langData['lang_id']] = $langData['table_name'];
        if (PEAR::isError($res)) {
            return $res;
        }
        return true;
    }

    // }}}
    // {{{ removeLang()

    /**
     * Remove the lang from the langsAvail table and drop the strings table.
     * If the strings table holds other langs and $force==false, then
     * only the lang column is dropped. If $force==true the whole
     * table is dropped without any check
     *
     * @param string  $langID language ID
     * @param boolean $force  if true, the whole table is dropped without checks
     *
     * @return true|PEAR_Error
     */
    function removeLang($langID, $force)
    {
        //remove from langsAvail
        $query = sprintf('DELETE FROM %s WHERE %s = %s',
            $this->db->quoteIdentifier($this->options['langs_avail_table'], true),
            $this->db->quoteIdentifier($this->options['lang_id_col'], true),
            $this->db->quote($langID, 'text')
        );
        ++$this->_queries;
        $res = $this->db->exec($query);
        if (PEAR::isError($res)) {
            return $res;
        }

        $this->db->loadModule('Manager');
        $lang_table = $this->_getLangTable($langID);
        if ($force) {
            //remove the whole table
            ++$this->_queries;
            return $this->db->manager->dropTable($lang_table);
        }

        //drop only the column for this lang
        $table_changes = array(
            'remove' => array($this->_getLangCol($langID) => array())
        );
        ++$this->_queries;
        return $this->db->manager->alterTable($lang_table, $table_changes, false);
    }

    // }}}
    // {{{ updateLang()

    /**
     * Update the lang info in the langsAvail table
     *
     * @param array $langData language data
     *
     * @return true|PEAR_Error
     */
    function updateLang($langData)
    {
        $allFields = array(
            //'lang_id'    => 'lang_id_col',
            'name'       => 'lang_name_col',
            'meta'       => 'lang_meta_col',
            'error_text' => 'lang_errmsg_col',
            'encoding'   => 'lang_encoding_col',
        );
        $updateFields = array_keys($langData);
        $langSet  = array();
        foreach ($allFields as $field => $col) {
            if (in_array($field, $updateFields)) {
                $langSet[] = $this->db->quoteIdentifier($this->options[$col], true) . ' = ' .
                             $this->db->quote($langData[$field]);
            }
        }
        $query = sprintf('UPDATE %s SET %s WHERE %s=%s',
            $this->db->quoteIdentifier($this->options['langs_avail_table'], true),
            implode(', ', $langSet),
            $this->db->quoteIdentifier($this->options['lang_id_col'], true),
            $this->db->quote($langData['lang_id'])
        );

        ++$this->_queries;
        $res = $this->db->exec($query);
        $this->fetchLangs();  //update memory cache
        if (PEAR::isError($res)) {
            return $res;
        }
        return true;
    }

    // }}}
    // {{{ add()

    /**
     * Add a new entry in the strings table.
     *
     * @param string $stringID    string ID
     * @param string $pageID      page/group ID
     * @param array  $stringArray Associative array with string translations.
     *               Sample format: array('en' => 'sample', 'it' => 'esempio')
     *
     * @return true|PEAR_Error
     */
    function add($stringID, $pageID, $stringArray)
    {
        $langs = array_intersect(
            array_keys($stringArray),
            $this->getLangs('ids')
        );

        if (!count($langs)) {
            //return error: no valid lang provided
            return true;
        }

        // Langs may be in different tables - we need to split up queries along
        // table lines, so we can keep DB traffic to a minimum.

        $unquoted_stringID = $stringID;
        $unquoted_pageID   = $pageID;
        $stringID          = $this->db->quote($stringID, 'text');
        $pageID            = is_null($pageID) ? 'NULL' : $this->db->quote($pageID, 'text');
        // Loop over the tables we need to insert into.
        foreach ($this->_tableLangs($langs) as $table => $tableLangs) {
            $exists = $this->_recordExists($unquoted_stringID, $unquoted_pageID, $table);
            if (PEAR::isError($exists)) {
                return $exists;
            }
            $func  = $exists ? '_getUpdateQuery' : '_getInsertQuery';
            $query = $this->$func($table, $tableLangs, $stringID, $pageID, $stringArray);
            
            ++$this->_queries;
            $res = $this->db->exec($query);
            if (PEAR::isError($res)) {
                return $res;
            }
        }

        return true;
    }

    // }}}
    // {{{ update()

    /**
     * Update an existing entry in the strings table.
     *
     * @param string $stringID    string ID
     * @param string $pageID      page/group ID
     * @param array  $stringArray Associative array with string translations.
     *               Sample format: array('en' => 'sample', 'it' => 'esempio')
     *
     * @return true|PEAR_Error
     */
    function update($stringID, $pageID, $stringArray)
    {
        return $this->add($stringID, $pageID, $stringArray);
    }

    // }}}
    // {{{ _getInsertQuery()

    /**
     * Build a SQL query to INSERT a record
     *
     * @param string $table        table name
     * @param array  &$tableLangs  tables containing the languages
     * @param string $stringID     string ID
     * @param string $pageID       page/group ID
     * @param array  &$stringArray array of strings
     *
     * @return string INSERT query
     * @access private
     */
    function _getInsertQuery($table, &$tableLangs, $stringID, $pageID, &$stringArray)
    {
        $tableCols = $this->_getLangCols($tableLangs);
        $langData = array();
        foreach ($tableLangs as $lang) {
            $langData[$lang] = $this->db->quote($stringArray[$lang], 'text');
        }
        foreach (array_keys($tableCols) as $k) {
            $tableCols[$k] = $this->db->quoteIdentifier($tableCols[$k], true);
        }

        return sprintf('INSERT INTO %s (%s, %s, %s) VALUES (%s, %s, %s)',
            $this->db->quoteIdentifier($table, true),
            $this->db->quoteIdentifier($this->options['string_id_col'], true),
            $this->db->quoteIdentifier($this->options['string_page_id_col'], true),
            implode(', ', $tableCols),
            $stringID,
            $pageID,
            implode(', ', $langData)
        );
    }

    // }}}
    // {{{ _getUpdateQuery()

    /**
     * Build a SQL query to UPDATE a record
     *
     * @param string $table        table name
     * @param array  &$tableLangs  tables containing the languages
     * @param string $stringID     string ID
     * @param string $pageID       page/group ID
     * @param array  &$stringArray array of strings
     *
     * @return string UPDATE query
     * @access private
     */
    function _getUpdateQuery($table, &$tableLangs, $stringID, $pageID, &$stringArray)
    {
        $tableCols = $this->_getLangCols($tableLangs);
        $langSet = array();
        foreach ($tableLangs as $lang) {
            $langSet[] = $this->db->quoteIdentifier($tableCols[$lang], true) . ' = ' .
                         $this->db->quote($stringArray[$lang], 'text');
        }

        return sprintf('UPDATE %s SET %s WHERE %s = %s AND %s = %s',
            $this->db->quoteIdentifier($table, true),
            implode(', ', $langSet),
            $this->db->quoteIdentifier($this->options['string_id_col'], true),
            $stringID,
            $this->db->quoteIdentifier($this->options['string_page_id_col'], true),
            $pageID
        );
    }

    // }}}
    // {{{ remove()

    /**
     * Remove an entry from the strings table.
     *
     * @param string $stringID string ID
     * @param string $pageID   page/group ID
     *
     * @return mixed true on success, PEAR_Error on failure
     */
    function remove($stringID, $pageID)
    {
        $tables = array_unique($this->_getLangTables());

        $stringID = $this->db->quote($stringID, 'text');
        // get the tables and skip the non existent ones
        $dbTables = $this->_fetchTableNames();
        foreach ($tables as $table) {
            if (!in_array($table, $dbTables)) {
                continue;
            }
            $query = sprintf('DELETE FROM %s WHERE %s = %s AND %s',
                 $this->db->quoteIdentifier($table, true),
                 $this->db->quoteIdentifier($this->options['string_id_col'], true),
                 $stringID,
                 $this->db->quoteIdentifier($this->options['string_page_id_col'], true)
            );
            if (is_null($pageID)) {
                $query .= ' IS NULL';
            } else {
                $query .= ' = ' . $this->db->quote($pageID, 'text');
            }

            ++$this->_queries;
            $res = $this->db->exec($query);
            if (PEAR::isError($res)) {
                return $res;
            }
        }

        return true;
    }

    // }}}
    // {{{ removePage

    /**
     * Remove all the strings in the given page/group
     *
     * @param string $pageID page/group ID
     *
     * @return mixed true on success, PEAR_Error on failure
     */
    function removePage($pageID = null)
    {
        $tables = array_unique($this->_getLangTables());

        // get the tables and skip the non existent ones
        $dbTables = $this->_fetchTableNames();
        foreach ($tables as $table) {
            if (!in_array($table, $dbTables)) {
                continue;
            }
            $query = sprintf('DELETE FROM %s WHERE %s',
                 $this->db->quoteIdentifier($table, true),
                 $this->db->quoteIdentifier($this->options['string_page_id_col'], true)
            );
            if (is_null($pageID)) {
                $query .= ' IS NULL';
            } else {
                $query .= ' = ' . $this->db->quote($pageID, 'text');
            }

            ++$this->_queries;
            $res = $this->db->exec($query);
            if (PEAR::isError($res)) {
                return $res;
            }
        }

        return true;
    }

    // }}}
    // {{{ getPageNames()
    
    /**
     * Get a list of all the pageIDs in any table.
     *
     * @return array
     */
    function getPageNames()
    {
        $pages = array();
        foreach ($this->_getLangTables() as $table) {
            $query = sprintf('SELECT DISTINCT %s FROM %s',
                 $this->db->quoteIdentifier($this->options['string_page_id_col'], true),
                 $this->db->quoteIdentifier($table, true)
            );
            ++$this->_queries;
            $res = $this->db->queryCol($query);
            if (PEAR::isError($res)) {
                return $res;
            }
            $pages = array_merge($pages, $res);
        }
        return array_unique($pages);
    }
    
    // }}}
    // {{{ _tableLangs()

    /**
     * Get table -> language mapping
     *
     * The key of the array is the table that a language is stored in;
     * the value is an /array/ of languages stored in that table.
     *
     * @param array $langs Languages to get mapping for
     *
     * @return array Table -> language mapping
     * @access private
     * @see    Translation2_Container_MDB2::_getLangTable()
     */
    function &_tableLangs($langs)
    {
        $tables = array();
        foreach ($langs as $lang) {
            $table = $this->_getLangTable($lang);
            $tables[$table][] = $lang;
        }
        return $tables;
    }

    // }}}
    // {{{ _getLangTables()

    /**
     * Get tables for languages
     *
     * This is like _getLangTable(), but it returns an array of the tables for
     * multiple languages.
     *
     * @param array $langs Languages to get tables for
     *
     * @return array
     * @access private
     */
    function &_getLangTables($langs = null)
    {
        $tables = array();
        $langs  = !is_array($langs) ? $this->getLangs('ids') : $langs;
        foreach ($langs as $lang) {
            $tables[] = $this->_getLangTable($lang);
        }
        $tables = array_unique($tables);
        return $tables;
    }

    // }}}
    // {{{ _getLangCols()

    /**
     * Get table columns strings are stored in
     *
     * This is like _getLangCol(), except it returns an array which contains
     * the mapping for multiple languages.
     *
     * @param array $langs Languages to get mapping for
     *
     * @return array  Language -> column mapping
     * @access private
     * @see    Translation2_Container_DB::_getLangCol()
     */
    function &_getLangCols($langs)
    {
        $cols = array();
        foreach ($langs as $lang) {
            $cols[$lang] = $this->_getLangCol($lang);
        }
        return $cols;
    }

    // }}}
    // {{{ _recordExists()

    /**
     * Check if there's already a record in the table with the
     * given (pageID, stringID) pair.
     *
     * @param string $stringID string ID
     * @param string $pageID   page/group ID
     * @param string $table    table name
     *
     * @return boolean|PEAR_Error
     * @access private
     */
    function _recordExists($stringID, $pageID, $table)
    {
        $stringID = $this->db->quote($stringID, 'text');
        $pageID   = is_null($pageID) ? ' IS NULL' : ' = ' . $this->db->quote($pageID, 'text');
        $query    = sprintf('SELECT COUNT(*) FROM %s WHERE %s=%s AND %s%s',
            $this->db->quoteIdentifier($table, true),
            $this->db->quoteIdentifier($this->options['string_id_col'], true),
            $stringID,
            $this->db->quoteIdentifier($this->options['string_page_id_col'], true),
            $pageID
        );
        ++$this->_queries;
        $res = $this->db->queryOne($query);
        if (PEAR::isError($res)) {
            return $res;
        }
        return ($res > 0);
    }

    // }}}
    // {{{ _filterStringsByTable()

    /**
     * Get only the strings for the langs in the given table
     *
     * @param array  $stringArray Associative array with string translations.
     *               Sample format: array('en' => 'sample', 'it' => 'esempio')
     * @param string $table       table name
     *
     * @return array strings
     * @access private
     */
    function &_filterStringsByTable($stringArray, $table)
    {
        $strings = array();
        foreach ($stringArray as $lang => $string) {
            if ($table == $this->_getLangTable($lang)) {
                $strings[$lang] = $string;
            }
        }
        return $strings;
    }

    // }}}
    // {{{ _getLangsInTable()
    
    /**
     * Get the languages sharing the given table
     *
     * @param string $table table name
     *
     * @return array
     */
    function &_getLangsInTable($table)
    {
        $this->fetchLangs(); // force cache refresh
        $langsInTable = array();
        foreach (array_keys($this->langs) as $lang) {
            if ($table == $this->_getLangTable($lang)) {
                $langsInTable[] = $lang;
            }
        }
        return $langsInTable;
    }
    
    // }}}
}
?>