<?php
class TalkGateway {
  /** @property ChatEngine $db */
  var $db;

  function  __construct($db) {
    $this->db = $db;
  }

  function Add($player_id, $channel, $type, $volume, $sentence) {
    $insert = <<<SQL
INSERT INTO jinrou_talk (room, player, internal_time, channel, type, volume, sentence)
VALUES (:room_id, :player_id, :internal_time, :channel, :type, :volume, :sentence)

SQL;
    if (($stmt = $this->db->prepare($insert)) !== false) {
      $stmt->bindValue(':room_id', $this->id, PDO::PARAM_INT);
      $stmt->bindValue(':player_id', $player_id, PDO::PARAM_INT);
      $stmt->bindValue(':internal_time', $this->internal_time, PDO::PARAM_INT);
      $stmt->bindValue(':channel', $channel, PDO::PARAM_STR);
      $stmt->bindValue(':type', $type, PDO::PARAM_STR);
      $stmt->bindValue(':volume', $volume, PDO::PARAM_STR);
      $stmt->bindValue(':sentence', $sentence, PDO::PARAM_STR);
      return $stmt->execute();
    }
    return false;
  }

  /**
   * 会話ログの取得に使用するフィルタをSQLステートメントで使用する表現に変換します。
   * @return array 条件式とバインディング値のセット。
   * 値はWHEN条件式、条件式で使用するバインディング値、フィルタに使用するチャンネルの順に並んでいます。
   */
  function _CompileFilter($filter) {
    $methods = '';
    $channels = array();
    $bind_methods = array();
    if (!empty($filters)) {
      foreach ($filters as $index => $entry) {
        extract($entry);
        $key = ":channel{$index}";
        $cond = 'talk.channel = '.$key;
        $bind_methods[$key] = $channel;
        if (isset($type) && $type != '*') {
          $key = ":type{$index}";
          $cond .= ' AND talk.type = '.$key;
          $bind_methods[$key] = $type;
        }
        if (!in_array($channel, $channels)) {
          $channels[] = $channel;
        }
        $methods .= "    WHEN {$cond} THEN '{$method}'\n";
      }
    }
    return compact('methods', 'bind_methods', 'channels');
  }

  /**
   * 会話ログを読み込むためにデータアクセスステートメントを最適化します。
   * @return boolean 成功した場合true,それ以外の場合false
   */
  function Prepare($filters) {
    extract($this->_CompileFilter($filters));
    if (count($channels)) {
      $bind_channels = array();
      foreach ($channels as $index => $channel) {
        $bind_channels[":chin{$index}"] = $channel;
      }
      $cond_channels = 'talk.channel IN ('.implode(',', array_keys($bind_channels)).')';
    }
    else {
      $cond_channels = 1;
      $methods = "    WHEN 1 THEN 'Through'";
    }
    $sql = <<<SQL
SELECT
  talk.player,
  talk.volume,
  talk.channel,
  talk.type,
  CASE
{$methods}
    ELSE NULL
  END AS method,
  talk.sentence
FROM jinrou_talk AS talk
WHERE {$cond_channels}
  AND talk.room = :room
  AND talk.internal_time BETWEEN :time_from AND :time_to
HAVING method IS NOT NULL
SQL;
    if (($stmt = $this->db->prepare($sql)) !== false) {
      foreach($bind_methods as $key => $param) {
        $stmt->bindValue($key, $param, PDO::PARAM_STR);
      }
      if ($cond_channels !== 1) {
        foreach($bind_channels as $key => $channel) {
          $stmt->bindValue($key, $channel, PDO::PARAM_STR);
        }
      }
      return $stmt;
    }
    return false;
  }

  /**
   * ゲーム内時間で期間を指定して、部屋の会話ログを取得します。
   * このメソッドで取得される期間の厳密な範囲は $time_from <= [発言時刻] <= $time_to です。
   * @param PDOStatement $stmt Prepareメソッドが返したPDOStatementオブジェクト
   * @param integer $time_from 取得期間の開始位置を示す内部時間表現
   * @param integer $time_to 取得期間の終了位置を示す内部時間表現
   * @return boolean 成功した場合true,それ以外の場合false
   */
  function GetAllPrepared($stmt, $room_id, $time_from = null, $time_to = null) {
    $result = array();
    $stmt->bindValue(':room', $room_id, PDO::PARAM_INT);
    if (isset($stmt)) {
      $stmt->bindValue(':time_from', isset($time_from) ? $time_from : 0, PDO::PARAM_INT);
      $stmt->bindValue(':time_to', isset($time_to) ? $time_to : 999999999, PDO::PARAM_INT);
      $stmt->bindColumn(1, $player, PDO::PARAM_INT);
      $stmt->bindColumn(2, $volume, PDO::PARAM_STR);
      $stmt->bindColumn(3, $channel, PDO::PARAM_STR);
      $stmt->bindColumn(4, $type, PDO::PARAM_STR);
      $stmt->bindColumn(5, $method, PDO::PARAM_STR);
      $stmt->bindColumn(6, $sentence, PDO::PARAM_STR);
      if ($stmt->execute()) {
        $converter = new TalkConverter();
        while ($stmt->fetch(PDO::FETCH_BOUND)) {
          if ($method == 'Through') {
            $result[] = array(null, $player, $volume, $sentence);
          }
          else {
            $log = $converter->$method($this, $player, $volume, $sentence, $channel, $type);
            if ($log !== false) {
              $result[] = $log;
            }
          }
        }
        $stmt->closeCursor();
        return $result;
      }
    }
    return false;
  }

  /**
   * ゲートウェイと協調して発言テーブルの読み込みをサポートするTalkReaderオブジェクトを生成します。
   * @param int|ChatRoom $room アクセスする村を表すChatRoomオブジェクト、または村のIDを指定します。
   * @return TalkReader
   */
  function CreateReader($room) {
    if ($room instanceof ChatRoom) {
      return new TalkReader($this, $room->id);
    }
    else {
      return new TalkReader($this, intval($room));
    }
  }
}


class TalkReader {
  var $gateway;
  var $room_id;
  var $filters = array();
  var $stmt;

  /**
   *
   * @param TalkGateway $gateway 
   */
  function  __construct($gateway, $room_id) {
    $this->gateway = $gateway;
    $this->room_id = $room_id;
  }

  /**
   * 会話チャンネルとログのタイプを指定して会話ログのフィルタを追加します。
   * @param string $channel 会話チャンネルの名前
   * @param string $type ログのタイプ
   * @param string $method 変換に使用するメソッド名。Throughを指定すると実際には変換せず、本文を直接取得します。
   */
  function AddFilter($channel, $type, $method = 'Through') {
    $this->stmt = null; /* ステートメントの中身が変化する */
    $this->filters[] = compact('channel', 'type', 'method');
  }

  function GetAll($time_from = null, $time_to = null) {
    if (!isset($this->stmt)) {
      $this->stmt = $this->gateway->Prepare($this->filters);
    }
    return $this->gateway->GetAllPrepared($this->stmt, $this->room_id, $time_from, $time_to);
  }
}
