<?php
// $Revision: 1.1.2.11 $
// ------------------------------------------------------------------------- //
//  XooNIps - Neuroinformatics Base Platform System                          //
//  Copyright (C) 2005-2011 RIKEN, Japan All rights reserved.                //
//  http://xoonips.sourceforge.jp/                                           //
// ------------------------------------------------------------------------- //
//  This program is free software; you can redistribute it and/or modify     //
//  it under the terms of the GNU General Public License as published by     //
//  the Free Software Foundation; either version 2 of the License, or        //
//  (at your option) any later version.                                      //
//                                                                           //
//  You may not change or alter any portion of this comment or credits       //
//  of supporting developers from this source code or any supporting         //
//  source code which is considered copyrighted (c) material of the          //
//  original comment or credit authors.                                      //
//                                                                           //
//  This program is distributed in the hope that it will be useful,          //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of           //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            //
//  GNU General Public License for more details.                             //
//                                                                           //
//  You should have received a copy of the GNU General Public License        //
//  along with this program; if not, write to the Free Software              //
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA //
// ------------------------------------------------------------------------- //
if ( ! defined( 'XOOPS_ROOT_PATH' ) ) {
  exit();
}

require_once dirname( __FILE__ ).'/base/transaction.class.php';

class XooNIpsRankingHandler {

  /**
   * ranking orm object handlers
   * @access protected
   * @var array
   */
  var $handlers = array();

  /**
   * ranking table base names
   * @access protected
   * @var array
   */
  var $basenames;

  /**
   * constractor
   *
   * @access public
   */
  function XooNIpsRankingHandler() {
    // set base name of ranking tables
    $this->basenames = array(
      // php-indent: disable
      'viewed_item',       /* most accessed items */
      'downloaded_item',   /* most downloaded items */
      'contributing_user', /* most active contributors */
      'searched_keyword',  /* most searched keywords */
      'active_group',      /* most active groups */
      'new_item',          /* newly arrived items */
      'new_group',         /* newly created groups */
      // php-indent: enable,
    );
    // load object handlers
    foreach ( $this->basenames as $basename ) {
      $name = $basename;
      $this->handlers[$name] =& xoonips_getormhandler( 'xoonips', 'ranking_'.$name );
      $name = 'sum_'.$basename;
      $this->handlers[$name] =& xoonips_getormhandler( 'xoonips', 'ranking_'.$name );
    }
    $this->handlers['event_log'] =& xoonips_getormhandler( 'xoonips', 'event_log' );
    $this->handlers['config'] =& xoonips_getormhandler( 'xoonips', 'config' );
  }

  /**
   * update rankings
   *
   * @access public
   * @return bool false if failure
   */
  function update() {
    // lock ranking data
    $h = $this->_lock();
    if ( $h === false ) {
      return false;
    }
    // update ranking
    $ret = $this->_recalc( true );
    // unlock ranking data
    $this->_unlock( $h );
    return $ret;
  }

  /**
   * get item viewed count
   *
   * @access public
   * @param int $item_id item id
   * @return int viewed count
   */
  function get_count_viewed_item( $item_id ) {
    $obj =& $this->handlers['viewed_item']->get( $item_id );
    $res = ( is_object( $obj ) ) ? $obj->getVar( 'count', 'n' ) : 0;
    return $res;
  }

  /**
   * recalc rankings
   *
   * @access protected
   * @param bool $is_update flag for update mode
   * @return bool false if failure
   */
  function _recalc( $is_update ) {
    $config_names = array(
      'last_update',
      'days',
      'days_enabled',
      'sum_last_update',
    );

    $config = array();
    foreach ( $config_names as $name ) {
      $config[$name] = $this->_get_config( $name );
    }

    $now = time();
    if ( $now <= $config['last_update'] && $is_update ) {
      return true;
    }

    $add_days_criteria = new CriteriaCompo();
    $sub_days_criteria = new CriteriaCompo();
    // condition 1: on update delta
    //  delta = ( last update <= timestamp < now )
    //  all = delta + current
    // condition 2: on update delta & days limit
    //  delta = ( last update <= timestamp < now )
    //          - ( last update - days <= timestamp < now - days )
    //  all = delta + current
    // condition 3: on rebuild
    //  all = ( timestamp < now )
    // condition 4: on rebuild & days limit
    //  all = ( now - days <= timestamp < now )
    // condition 5: on rebuild & use sum data
    //  delta = ( sum last update <= timestamp < now )
    //  all = sum + delta
    if ( $is_update ) {
      // update rankings
      $add_days_criteria->add( new Criteria( 'timestamp', $config['last_update'], '>=' ) );
      $add_days_criteria->add( new Criteria( 'timestamp', $now, '<' ) );
      if ( $config['days_enabled'] ) {
        $sub_days_criteria->add( new Criteria( 'timestamp', $config['last_update'] - $config['days'] * 86400, '>=' ) );
        $sub_days_criteria->add( new Criteria( 'timestamp', $now - $config['days'] * 86400, '<' ) );
      }
    } else {
      // rebuild rankings
      // delete existing rankings
      foreach ( $this->basenames as $basename ) {
        $this->handlers[$basename]->deleteAll( null, true );
      }
      $add_days_criteria->add( new Criteria( 'timestamp', $now, '<' ) );
      if ( $config['days_enabled'] ) {
        $add_days_criteria->add( new Criteria( 'timestamp', $now - $config['days'] * 86400, '>=' ) );
      } else {
        if ( $config['sum_last_update'] != 0 ) {
          // rankings are ( sum_start ... sum_last_updated of ranking_sum ) +
          //              ( sum_last_update ... now of event_log )
          $add_days_criteria->add( new Criteria( 'timestamp', $config['sum_last_update'], '>=' ) );
          // copy data of ranking_sum_* table into ranking_* table.
          foreach ( $this->basenames as $basename ) {
            if ( ! $this->handlers[$basename]->copy_from_sum_table() ) {
              die( 'fatal error in '.__FILE__.' at '.__LINE__ );
            }
          }
        }
      }
    }
    $this->_recalc_sql( $add_days_criteria, $sub_days_criteria, false );
    $this->_set_config( 'last_update', $now );
    return true;
  }

  /**
   * recalc rankings of sql body part
   *
   * @access protected
   * @param object &$add_days_criteria
   * @param object &$sub_days_criteria
   * @param bool $update_sum flag for update sum tables
   * @return bool false if failure
   */
  function _recalc_sql( &$add_days_criteria, &$sub_days_criteria, $update_sum ) {
    global $xoopsDB;
    $config_names = array(
      'visible',
      'new_visible',
      'num_rows',
      'new_num_rows',
    );
    $config = array();
    foreach ( $config_names as $name ) {
      $config[$name] = $this->_get_config( $name );
    }
    $config['visible'] = explode( ',', $config['visible'] );
    $config['new_visible'] = explode( ',', $config['new_visible'] );

    // days criteria
    $days_criteria = new CriteriaCompo();
    $days_criteria->add( $add_days_criteria );
    if ( $sub_days_criteria->render() != '' ) {
      $days_criteria->add( $sub_days_criteria, 'OR' );
    }
    $add_days_sql = $add_days_criteria->render();
    if ( $add_days_sql == '' ) {
      $add_days_sql = '0';
    }
    $sub_days_sql = $sub_days_criteria->render();
    if ( $sub_days_sql == '' ) {
      $sub_days_sql = '0';
    }

    $etids = array();
    if ( $config['visible'][0] || $update_sum ) {
      $etids[] = ETID_VIEW_ITEM;
    }
    if ( $config['visible'][1] || $update_sum ) {
      $etids[] = ETID_DOWNLOAD_FILE;
    }
    if ( $config['visible'][2] || $update_sum ) {
      $etids[] = ETID_CERTIFY_ITEM;
    }
    if ( $config['visible'][3] || $update_sum ) {
      $etids[] = ETID_QUICK_SEARCH;
      $etids[] = ETID_ADVANCED_SEARCH;
    }
    if ( $config['visible'][4] || $update_sum ) {
      $etids[] = ETID_REQUEST_CERTIFY_ITEM;
    }
    $new_etids = array();
    if ( $config['new_visible'][0] || $update_sum ) {
      $new_etids[] = ETID_CERTIFY_ITEM;
    }
    if ( $config['new_visible'][1] || $update_sum ) {
      $new_etids[] = ETID_INSERT_GROUP;
    }
    $sum = $update_sum ? 'sum_' : '';

    $log_table = $xoopsDB->prefix( 'xoonips_event_log' );
    $new_group_table = $xoopsDB->prefix( 'xoonips_ranking_'.$sum.'new_group' );
    $new_item_table = $xoopsDB->prefix( 'xoonips_ranking_'.$sum.'new_item' );

    if ( count( $etids ) ) {
      $etids_str = '('.implode( ',', $etids ).')';
      $criteria = new CriteriaCompo();
      $criteria->add( new Criteria( 'event_type_id', $etids_str, 'IN' ) );
      $criteria->add( $days_criteria );
      $cnt = $this->handlers['event_log']->getCount( $criteria );
      if ( $cnt ) {
        // log changed. need recalc.
        // ranking viewed item
        if ( $config['visible'][0] || $update_sum ) {
          $etid = ETID_VIEW_ITEM;
          $basename = $sum.'viewed_item';
          $fields = 'tb.item_id, SUM('.$add_days_sql.') - SUM('.$sub_days_sql.') AS count';
          $criteria = new CriteriaCompo();
          $criteria->add( new Criteria( 'event_type_id', $etid ) );
          $criteria->add( $days_criteria );
          $criteria->setGroupby( 'tb.item_id' );
          $join_criteria = new XooNIpsJoinCriteria( 'xoonips_item_basic', 'item_id', 'item_id', 'INNER', 'tb' );
          $res =& $this->handlers['event_log']->open( $criteria, $fields, false, $join_criteria );
          while ( $obj =& $this->handlers['event_log']->getNext( $res ) ) {
            $item_id = $obj->getVar( 'item_id', 'n' );
            $delta = $obj->getExtraVar( 'count' );
            if ( ! $this->handlers[$basename]->increment( $item_id, $delta ) ) {
              die( 'fatal error in '.__FILE__.' at '.__LINE__ );
            }
          }
          $this->handlers['event_log']->close( $res );
        }

        // ranking downloaded item
        if ( $config['visible'][1] || $update_sum ) {
          $etid = ETID_DOWNLOAD_FILE;
          $basename = $sum.'downloaded_item';
          $fields = 'tb.item_id, SUM('.$add_days_sql.') - SUM('.$sub_days_sql.') AS count';
          $criteria = new CriteriaCompo();
          $criteria->add( new Criteria( 'event_type_id', $etid ) );
          $criteria->add( $days_criteria );
          $criteria->setGroupby( 'tb.item_id' );
          $join_criteria = new XooNIpsJoinCriteria( 'xoonips_item_basic', 'item_id', 'item_id', 'INNER', 'tb' );
          $res =& $this->handlers['event_log']->open( $criteria, $fields, false, $join_criteria );
          while ( $obj =& $this->handlers['event_log']->getNext( $res ) ) {
            $item_id = $obj->getVar( 'item_id', 'n' );
            $delta = $obj->getExtraVar( 'count' );
            if ( ! $this->handlers[$basename]->increment( $item_id, $delta ) ) {
              die( 'fatal error in '.__FILE__.' at '.__LINE__ );
            }
          }
          $this->handlers['event_log']->close( $res );
        }

        // ranking contributing user
        if ( $config['visible'][2] || $update_sum ) {
          $etid = ETID_CERTIFY_ITEM;
          $basename = $sum.'contributing_user';
          $fields = 'tb.item_id, tb.uid, timestamp';
          $criteria = new CriteriaCompo();
          $criteria->add( new Criteria( 'event_type_id', $etid ) );
          $criteria->add( $add_days_criteria );
          $join_criteria = new XooNIpsJoinCriteria( 'xoonips_item_basic', 'item_id', 'item_id', 'INNER', 'tb' );
          $res =& $this->handlers['event_log']->open( $criteria, $fields, false, $join_criteria );
          while ( $obj =& $this->handlers['event_log']->getNext( $res ) ) {
            $item_id = $obj->getVar( 'item_id', 'n' );
            $uid = $obj->getVar( 'uid', 'n' );
            $timestamp = $obj->getVar( 'timestamp', 'n' );
            if ( ! $this->handlers[$basename]->replace( $item_id, $uid, $timestamp ) ) {
              die( 'fatal error in '.__FILE__.' at '.__LINE__ );
            }
          }
          $this->handlers['event_log']->close( $res );
          // TODO: slow if $contributing_user_table.timestamp is not indexed.
          //       use event_log.timestamp instead?
          $table = $xoopsDB->prefix( 'xoonips_ranking_'.$basename );
          $xoopsDB->queryF( 'DELETE FROM '.$table.' WHERE '.str_replace( 'timestamp', 'UNIX_TIMESTAMP(timestamp)', $sub_days_sql ) );
        }

        // ranking searched keyword
        if ( $config['visible'][3] || $update_sum ) {
          $etid = '('.ETID_QUICK_SEARCH.','.ETID_ADVANCED_SEARCH.')';
          $basename = $sum.'searched_keyword';
          $fields = 'search_keyword, IF('.$add_days_sql.',1,-1) AS count';
          $criteria = new CriteriaCompo();
          $criteria->add( new Criteria( 'event_type_id', $etid, 'IN' ) );
          $criteria->add( $days_criteria );
          $res =& $this->handlers['event_log']->open( $criteria, $fields );
          while ( $obj =& $this->handlers['event_log']->getNext( $res ) ) {
            $keyword = $obj->getVar( 'search_keyword', 'n' );
            $delta = $obj->getExtraVar( 'count' );
            // extract keywords from log
            $matches = array();
            if ( 0 == preg_match( '/(?:^|&)keyword=([^&]*)(?:&|$)/', $keyword, $matches ) ) {
              continue;
            }
            $keyword = urldecode( $matches[1] );
            preg_match_all( '/([^ "()]+)|(\\()|(\\))|"([^"]+)"/', $keyword, $match, PREG_SET_ORDER );
            $len = count( $match );
            for ( $i = 0; $i < $len; $i++ ) {
              if ( isset( $match[$i][1] ) ) {
                $keyword = $match[$i][1];
              } else if ( isset( $match[$i][4] ) ) {
                $keyword = $match[$i][4];
              } else {
                continue;
              }
              $keyword = strtolower( $keyword );
              if ( $keyword == 'and' || $keyword == 'or' ) {
                continue;
              }
              if ( strlen( $keyword ) > 2 ) {
                if ( ! $this->handlers[$basename]->increment( $keyword, $delta ) ) {
                  die( 'fatal error in '.__FILE__.' at '.__LINE__ );
                }
              }
            }
          }
          $this->handlers['event_log']->close( $res );
        }

        // ranking active group
        if ( $config['visible'][4] || $update_sum ) {
          $etid = ETID_REQUEST_CERTIFY_ITEM;
          $basename = $sum.'active_group';
          $fields = 'tx.gid, IF('.$add_days_sql.',1,-1) AS dir, COUNT( DISTINCT timestamp, item_id, tx.gid ) AS count';
          $criteria = new CriteriaCompo();
          $criteria->add( new Criteria( 'event_type_id', $etid ) );
          $criteria->add( $days_criteria );
          $criteria->add( new Criteria( 'ISNULL(tx.gid)', '0', '=' ) );
          $criteria->setGroupby( 'timestamp, item_id, tx.gid' );
          $join_criteria = new XooNIpsJoinCriteria( 'xoonips_index', 'index_id', 'index_id', 'INNER', 'tx' );
          $res =& $this->handlers['event_log']->open( $criteria, $fields, false, $join_criteria );
          while ( $obj =& $this->handlers['event_log']->getNext( $res ) ) {
            $gid = $obj->getVar( 'gid', 'n' );
            $dir = $obj->getExtraVar( 'dir' );
            $delta = $obj->getExtraVar( 'count' );
            $delta *= $dir;
            if ( ! $this->handlers[$basename]->increment( $gid, $delta ) ) {
              echo $this->handlers[$basename]->getLastSQL();
              die( 'fatal error in '.__FILE__.' at '.__LINE__ );
            }
          }
          $this->handlers['event_log']->close( $res );
        }
      }
    }

    if ( count( $new_etids ) ) {
      $new_etids_str = '('.implode( ',', $new_etids ).')';
      $criteria = new CriteriaCompo();
      $criteria->add( new Criteria( 'event_type_id', $new_etids_str, 'IN' ) );
      $criteria->add( $days_criteria );
      $cnt = $this->handlers['event_log']->getCount( $criteria );
      if ( $cnt ) {
        // log changed. need recalc.
        // ranking new item
        if ( $config['new_visible'][0] || $update_sum ) {
          // TODO: support 'REPLACE INTO ... (...) SELECT ...' query by object
          //       handler
          $sql = 'REPLACE INTO '.$new_item_table.' ( item_id, timestamp ) SELECT t.item_id, FROM_UNIXTIME(timestamp) FROM '.$log_table.' AS t INNER JOIN '.$xoopsDB->prefix( 'xoonips_index_item_link' ).' AS txil ON t.item_id=txil.item_id INNER JOIN '.$xoopsDB->prefix( 'xoonips_index' ).' AS tx ON txil.index_id=tx.index_id WHERE tx.open_level='.OL_PUBLIC.' AND event_type_id='.ETID_CERTIFY_ITEM.' AND ( '.$add_days_sql.' ) GROUP BY t.item_id ORDER BY timestamp DESC LIMIT '.$config['new_num_rows'];
          $result = $xoopsDB->queryF( $sql );
          if ( $result == false ) {
            die( 'fatal error in '.__FILE__.' at '.__LINE__ );
          }
          // remove old entry
          if ( ! $this->handlers[$sum.'new_item']->trim( $config['new_num_rows'] ) ) {
            die( 'fatal error in '.__FILE__.' at '.__LINE__ );
          }
        }

        // ranking new group
        if ( $config['new_visible'][1] || $update_sum ) {
          // TODO: support 'REPLACE INTO ... (...) SELECT ...' query by object
          //       handler
          $sql = 'REPLACE INTO '.$new_group_table.' ( gid, timestamp ) SELECT t.gid, FROM_UNIXTIME(timestamp) FROM '.$log_table.' AS t INNER JOIN '.$xoopsDB->prefix( 'xoonips_groups' ).' AS tg ON t.gid=tg.gid WHERE tg.gid IS NOT NULL AND event_type_id='.ETID_INSERT_GROUP.' AND ( '.$add_days_sql.' ) ORDER BY timestamp DESC LIMIT '.$config['new_num_rows'];
          $result = $xoopsDB->queryF( $sql );
          if ( $result == false ) {
            die( 'fatal error in '.__FILE__.' at '.__LINE__ );
          }
          // remove old entry
          if ( ! $this->handlers[$sum.'new_group']->trim( $config['new_num_rows'] ) ) {
            die( 'fatal error in '.__FILE__.' at '.__LINE__ );
          }
        }
      }
    }
  }

  /**
   * lock rankings table for update
   *
   * @access protected
   * @return int timeout time
   */
  function _lock() {
    global $xoopsDB;
    $table = $xoopsDB->prefix( 'xoonips_config' );
    $now = time();
    $timeout = $now + 180;
    $result = $xoopsDB->queryF( 'UPDATE '.$table.' SET value='.$timeout.' WHERE name=\'ranking_lock_timeout\' AND value < '.$now );
    if ( $result != false && $xoopsDB->getAffectedRows() ) {
      // transaction
      $transaction =& XooNIpsTransaction::getInstance();
      $transaction->start();
      // lock exclusively
      $xoopsDB->queryF( 'SELECT value FROM '.$table.' WHERE name=\'ranking_last_update\' FOR UPDATE' );
      return $timeout;
    }
    return false;
  }

  /**
   * unlock rankings table for update
   *
   * @access protected
   * @param int $timeout timeout time
   * @return bool false if failure
   */
  function _unlock( $timeout ) {
    // transaction
    $transaction =& XooNIpsTransaction::getInstance();
    $transaction->commit();
    $old_timeout = $this->_get_config( 'lock_timeout' );
    if ( $old_timeout != $timeout ) {
      return false;
    }
    $this->_set_config( 'lock_timeout', 0 );
    return true;
  }

  /**
   * get ranking configuration
   *
   * @access protected
   * @param string $key ranking configuration key
   * @return mixed configuration value
   */
  function _get_config( $key ) {
    $val = $this->handlers['config']->getValue( 'ranking_'.$key );
    if ( is_null( $val ) ) {
      die( 'fatal error in '.__FILE__.' at '.__LINE__ );
    }
    return $val;
  }

  /**
   * set ranking configuration
   *
   * @access protected
   * @param string $key ranking configuration key
   * @param mixed $val ranking configuration value
   * @return bool false if failure
   */
  function _set_config( $key, $val ) {
    // force insertion
    return $this->handlers['config']->setValue( 'ranking_'.$key, $val, true );
  }
}

?>
