<?php
  
  /**
   * Data manager class
   *
   * This class provides interface for extracting multiple rows form a specific 
   * table and population of item objects with extracted data
   * 
   * @package angie.library.database
   */
  class DataManager {
    
    /**
     * How do we know which class name to use
     * 
     * - CLASS_NAME_FROM_TABLE - Class name from table name, value is prepared 
     *   by generator
     * - CLASS_NAME_FROM_FIELD - Load class name from row field
     */
    const CLASS_NAME_FROM_TABLE = 0;
    const CLASS_NAME_FROM_FIELD = 1;
    
    /**
     * Do a SELECT query over database with specified arguments
     * 
     * This function can return single instance or array of instances that match 
     * requirements provided in $arguments associative array
     * 
     * $arguments is an associative array with following fields (all optional):
     * 
     *  - one        - select first row
     *  - conditions - additional conditions
     *  - group      - group by string
     *  - having     - having string
     *  - order      - order by string
     *  - offset     - limit offset, valid only if limit is present
     *  - limit      - number of rows that need to be returned
     *
     * @param array $arguments
     * @param string $table_name
     * @param integer $class_name_from
     * @param string $class_name_from_value
     * @param string $class_name_suffix
     * @return mixed
     * @throws DBQueryError
     */
    function find($arguments = null, $table_name = null, $class_name_from = DataManager::CLASS_NAME_FROM_TABLE, $class_name_from_value = null, $class_name_suffix = '') {
      return DataManager::findBySQL(DataManager::prepareSelectFromArguments($arguments, $table_name), null, array_var($arguments, 'one'), $table_name, $class_name_from, $class_name_from_value, $class_name_suffix);
    } // find
    
    /**
     * Return object of a specific class by SQL
     *
     * @param string $sql
     * @param array $arguments
     * @param boolean $one
     * @param string $table_name
     * @param integer $class_name_from
     * @param string $class_name_from_value
     * @param string $class_name_suffix
     * @return array
     */
    function findBySQL($sql, $arguments = null, $one = false, $table_name = null, $class_name_from = DataManager::CLASS_NAME_FROM_TABLE, $class_name_from_value = null, $class_name_suffix = '') {
      if($arguments !== null) {
        $sql = DB::prepare($sql, $arguments);
      } // if
      if($one) {
        if($row = DB::executeFirstRow($sql)) {
          switch($class_name_from) {
            case self::CLASS_NAME_FROM_FIELD:
              $class_name = $row[$class_name_from_value] . $class_name_suffix;
              break;
            case self::CLASS_NAME_FROM_TABLE:
              $class_name = $class_name_from_value . $class_name_suffix;
              break;
            default:
              throw new InvalidParamError('class_name_from', $class_name_from, 'Unexpected value');
          } // switch
          
          $item = new $class_name();
          $item->loadFromRow($row, true);
          return $item;
        } else {
          return null;
        } // if
        
      } else {
        switch($class_name_from) {
          case self::CLASS_NAME_FROM_FIELD:
            return DB::getConnection()->execute($sql, null, DB::LOAD_ALL_ROWS, DB::RETURN_OBJECT_BY_FIELD, $class_name_from_value, $class_name_suffix);
          case self::CLASS_NAME_FROM_TABLE:
            return DB::getConnection()->execute($sql, null, DB::LOAD_ALL_ROWS, DB::RETURN_OBJECT_BY_CLASS, $class_name_from_value, $class_name_suffix);
          default:
            throw new InvalidParamError('class_name_from', $class_name_from, 'Unexpected value');
        } // switch
      } // if
    } // findBySQL
    
    /**
     * Return paginated result
     * 
     * This function will return paginated result as array. First element of 
     * returned array is array of items that match the request. Second parameter 
     * is Pager class instance that holds pagination data (total pages, current 
     * and next page and so on)
     *
     * @param array $arguments
     * @param integer $page
     * @param integer $per_page
     * @param string $table_name
     * @param integer $class_name_from
     * @param string $class_name_from_value
     * @param string $class_name_suffix
     * @return array
     * @throws DBQueryError
     */
    function paginate($arguments = null, $page = 1, $per_page = 10, $table_name = null, $class_name_from = DataManager::CLASS_NAME_FROM_TABLE, $class_name_from_value = null, $class_name_suffix = '') {
      if(empty($arguments)) {
        $arguments = array();
      } // if
      
      $arguments['limit'] = $per_page;
      $arguments['offset'] = ($page - 1) * $per_page;
      
      return array(
        DataManager::find($arguments, $table_name, $class_name_from, $class_name_from_value, $class_name_suffix),
        new Pager(DataManager::count(array_var($arguments, 'conditions'), $table_name), $page, $per_page)
      ); // array
    } // paginate
    
    /**
     * Return object by ID
     *
     * @param mixed $id
     * @param string $table_name
     * @param integer $class_name_from
     * @param string $class_name_from_value
     * @param string $class_name_suffix
     * @return DataObject
     */
    function findById($id, $table_name = null, $class_name_from = DataManager::CLASS_NAME_FROM_TABLE, $class_name_from_value = null, $class_name_suffix = '') {
      if(is_array($id)) {
        ksort($id);
        
        $cache_id = $table_name;
        foreach($id as $k => $v) {
          $cache_id .= '_' . $k . '_' . $v;
        } // if
      } else {
        if(empty($id)) {
          return null;
        } // if
        
        $cache_id = $table_name . '_id_' . $id;
      } // if
      
      $cached = cache_get($cache_id);
      
      if($cached) {
        switch($class_name_from) {
          case self::CLASS_NAME_FROM_FIELD:
            $class_name = $cached[$class_name_from_value] . $class_name_suffix;
            break;
          case self::CLASS_NAME_FROM_TABLE:
            $class_name = $class_name_from_value . $class_name_suffix;
            break;
          default:
            throw new InvalidParamError('class_name_from', $class_name_from, 'Unexpected value');
        } // switch
        
        $item = new $class_name();
        $item->loadFromRow($cached);
        return $item;
      } // if
      
      $conditions = array();
      if(is_array($id)) {
        foreach($id as $pk_field => $pk_field_value) {
          $conditions[] = $pk_field . ' = ' . DB::escape($pk_field_value);
        } // foreach
      } else {
        $conditions[] = 'id = ' . DB::escape($id);
      } // if
      
      $object = DataManager::find(array(
        'conditions' => implode(' AND ', $conditions),
        'one' => true
      ), $table_name, $class_name_from, $class_name_from_value, $class_name_suffix);
      
      return $object;
    } // findById
    
    /**
     * Return number of rows in this table
     *
     * @param string $conditions Query conditions
     * @param string $table_name
     * @return integer
     * @throws DBQueryError
     */
    function count($conditions = null, $table_name = null) {
      if($conditions = trim(DB::prepareConditions($conditions))) {
        return (integer) DB::executeFirstCell("SELECT COUNT(*) AS 'row_count' FROM $table_name WHERE $conditions");
      } else {
        return (integer) DB::executeFirstCell("SELECT COUNT(*) AS 'row_count' FROM $table_name");
      } // if
    } // count
    
    /**
     * Update table
     * 
     * $updates is associative array where key is field name and value is new 
     * value
     *
     * @param array $updates
     * @param string $conditions
     * @param string $table_name
     * @return boolean
     * @throws DBQueryError
     */
    function update($updates, $conditions = null, $table_name = null) {
      $updates_part = array();
      foreach($updates as $field => $value) {
        $updates_part[] = $field . ' = ' . DB::escape($value);
      } // foreach
      $updates_part = implode(',' , $updates_part);
      
      $conditions = DB::prepareConditions($conditions);
      
      $where_string = trim($conditions) == '' ? '' : "WHERE $conditions";
      return DB::execute("UPDATE $table_name SET $updates_part $where_string");
    } // update
    
    /**
     * Delete all rows that match given conditions
     *
     * @param string $conditions Query conditions
     * @param string $table_name
     * @return boolean
     * @throws DBQueryError
     */
    function delete($conditions = null, $table_name = null) {
      if($conditions = trim(DB::prepareConditions($conditions))) {
        return DB::execute("DELETE FROM $table_name WHERE $conditions");
      } else {
        return DB::execute("DELETE FROM $table_name");
      } // if
    } // delete
    
    /**
     * Prepare SELECT query string from arguments and table name
     *
     * @param array $arguments
     * @param string $table_name
     * @return string
     */
    function prepareSelectFromArguments($arguments = null, $table_name = null) {
      $one        = (boolean) array_var($arguments, 'one', false);
      $conditions = array_var($arguments, 'conditions') ? DB::prepareConditions(array_var($arguments, 'conditions')) : '';
      $group_by   = array_var($arguments, 'group', '');
      $having     = array_var($arguments, 'having', '');
      $order_by   = array_var($arguments, 'order', '');
      $offset     = (integer) array_var($arguments, 'offset', 0);
      $limit      = (integer) array_var($arguments, 'limit', 0);
      
      if($one && $offset == 0 && $limit == 0) {
        $limit = 1; // Narrow the query
      } // if
      
      $where_string = trim($conditions) == '' ? '' : "WHERE $conditions";
      $group_by_string = trim($group_by) == '' ? '' : "GROUP BY $group_by";
      $having_string = trim($having) == '' ? '' : "HAVING $having";
      $order_by_string = trim($order_by) == '' ? '' : "ORDER BY $order_by";
      $limit_string = $limit > 0 ? "LIMIT $offset, $limit" : '';
      
      return "SELECT * FROM $table_name $where_string $group_by_string $having_string $order_by_string $limit_string";
    } // prepareSelectFromArguments
    
  }