<?php
define('CHEN_DIR', dirname(__FILE__));
require_once(CHEN_DIR.'/sql_helpers.php');

/**
 * チャットルームの管理機能を提供します。
 */
class ChatEngine extends PDO {
  const CLASS_ROOM = 'ChatRoom';
  const CLASS_USER = 'User';
  const CLASS_ROOM_READONLY = 'ChatRoom';
  const CLASS_ROOM_LOGIN = 'Room';

  /*
   * 自動生成される属性
   * var $rooms;
   * var $users;
   * var $players;
   * var $talk;
   * var $indicator;
   */

  var $currentUser = null;
  var $currentRoom = null;

  /**
   * PDOオブジェクトを指定してChatEngineクラスの新しいインスタンスを初期化します。
   * @param DBConnection $connection 有効なデータベース接続を保持したDBConnectionオブジェクト
   * @param string $host 接続先のホストアドレスを指定します。
   * @param string $database 接続するデータベース名を指定します。
   * @param string $user ログインするユーザー名を指定します。
   * @param string $passwd パスワードを指定します。
   * @param string $conf_dir 接続用のカスタム構成ファイルのパスを指定します。
   */
  function __construct($host, $database, $user, $passwd, $conf_path = null) {
    $conf_path = isset($conf_path) ? $conf_path : CHEN_DIR.'/config/my.cnf';
    $dsn = "mysql:host={$host};dbname={$database}";
    //可能な限りMySQL自体のプリペアドステートメントをサポートする。
    //また、この設定では接続の文字コードを確実にutf8にするため、独自のmy.cnfを読み込ませている。
    parent::__construct($dsn, $user, $passwd, array(
      PDO::ATTR_EMULATE_PREPARES => false,
      PDO::MYSQL_ATTR_READ_DEFAULT_FILE => $conf_path,
      PDO::MYSQL_ATTR_READ_DEFAULT_GROUP => 'client'
    ));
  }

  /**
   * エラー情報を文字列に変換して出力します。
   */
  function errorText() {
    return print_r($this->errorInfo(), true);
  }

  function  __get($name) {
    switch ($name) {
      case 'rooms':
      case 'users':
      case 'players':
      case 'talk':
      case 'indicator':
        /* ゲートウェイの本体をロードする */
        require_once(CHEN_DIR."/{$name}_table.php");
        $class = ucfirst($name).'Gateway';
        return $this->$name = new $class($this);
    }
    return null;
  }

  /**
   * 部屋情報を管理するテーブルを作成します。
   * @return bool テーブルの作成に成功したか既に存在している場合はtrue、失敗した場合false。
   */
  function CreateRoomsTable() {
    $sql = <<<SQL
CREATE TABLE IF NOT EXISTS jinrou_rooms (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  comment VARCHAR(255) NOT NULL,
  is_active TINYINT DEFAULT 1 NOT NULL,
  max_users TINYINT NOT NULL,
  internal_time INT DEFAULT 0 NOT NULL,
  builder_ip VARCHAR(50) NOT NULL,
  game_option TEXT NOT NULL,
  option_role TEXT NOT NULL,
  built_time DATETIME NOT NULL,
  last_updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=INNODB
SQL;
    return $this->exec($sql) !== false;
  }
  /**
   * 会話記録を管理するテーブルを作成します。
   * @return bool テーブルの作成に成功したか既に存在している場合はtrue、失敗した場合false。
   */
  function CreateTalkTable() {
    $sql = <<<SQL
CREATE TABLE IF NOT EXISTS jinrou_talk (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  room INT NOT NULL,
  player INT NOT NULL,
  internal_time INT NOT NULL,
  posted_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  channel CHAR(10) NOT NULL,
  type CHAR(10) NOT NULL,
  volume VARCHAR(50) NOT NULL,
  sentence TEXT NOT NULL,
  INDEX (internal_time)
) ENGINE=INNODB
SQL;
    return $this->exec($sql) !== false;
  }
  /**
   * 通知領域を管理するテーブルを作成します。
   * @return bool テーブルの作成に成功したか既に存在している場合はtrue、失敗した場合false。
   */
  function CreateIndicatorTable() {
    $sql = <<<SQL
CREATE TABLE IF NOT EXISTS jinrou_indicator (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  room INT NOT NULL,
  internal_time INT NOT NULL,
  posted_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  channel CHAR(10) NOT NULL,
  type CHAR(10) NOT NULL,
  sentence TEXT NOT NULL,
  INDEX (internal_time)
) ENGINE=INNODB
SQL;
    return $this->exec($sql) !== false;
  }
  /**
   * ユーザーを管理するテーブルを作成します。
   * @return bool テーブルの作成に成功したか既に存在している場合はtrue、失敗した場合false。
   */
  function CreateUserTable() {
    $sql = <<<SQL
CREATE TABLE IF NOT EXISTS jinrou_users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  room INT NOT NULL,
  uname VARCHAR(50) NOT NULL,
  passwd CHAR(32) NOT NULL,
  enabled TINYINT DEFAULT 1 NOT NULL,
  session_id CHAR(32) NOT NULL,
  ip_address VARCHAR(50) NOT NULL,
  UNIQUE (room, uname),
  UNIQUE (session_id)
) ENGINE=INNODB
SQL;
    return $this->exec($sql) !== false;
  }
  /**
   * プレイヤーを管理するテーブルを作成します。
   * @return bool テーブルの作成に成功したか既に存在している場合はtrue、失敗した場合false。
   */
  function CreatePlayerTable() {
    $sql = <<<SQL
CREATE TABLE IF NOT EXISTS jinrou_players (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  created_time DATETIME NOT NULL,
  user INT NOT NULL,
  user_no TINYINT NOT NULL,
  handle_name VARCHAR(50) NOT NULL,
  profile TEXT NOT NULL,
  last_words TEXT,
  sex ENUM('male', 'female') NOT NULL,
  live ENUM('live', 'dead', 'drop', 'kick') DEFAULT 'live' NOT NULL,
  role TEXT NOT NULL,
  icon_no INT NOT NULL,
  INDEX (user,created_time)
) ENGINE=INNODB
SQL;
    return $this->exec($sql) !== false;
  }


  /**
  * 部屋番号とログイン情報を指定してチャットルームにアクセスします。
  * このメソッドはcurrentUser属性とcurrentRoom属性を更新します。
  * @access    public
  * @param int $id チャットルームの番号
  * @param string $uname ユーザー名
  * @param string $passwd ログインパスワード
  * @return ChatRoom|bool 一連のログイン処理に成功した場合true,それ以外の場合falseを返します。
  * このメソッドはログイン自体の成否を返しません。ログインに成功したかどうかはcurrentUserに適切なオブジェクトが設定されているかどうかで確認することができます。
  */
  function Login($room_id, $uname = null, $passwd = null) {
    //TODO:ここにログインしてユーザー情報を取得するクエリを記述します。
    $bindings = array(':room_id'=>$room_id);
    if (isset($uname)) {
      $cond_user = 'usr.uname = :uname AND usr.passwd = MD5(:passwd)';
      $bindings[':uname'] = $uname;
      $bindings[':passwd'] = $passwd;
    }
    else {
      $cond_user = 'usr.session_id = :session_id';
      $bindings[':session_id'] = session_id();
    }
    $sql = <<<SQL
SELECT
  usr.id,
  usr.room AS room_id,
  pl.id AS player_id,
  pl.user_no,
  usr.uname,
  pl.handle_name,
  pl.sex,
  pl.live,
  pl.profile,
  ico.icon_no,
  ico.icon_filename,
  ico.color
FROM jinrou_users AS usr
  INNER JOIN jinrou_players AS pl ON pl.user = usr.id
  INNER JOIN user_icon AS ico ON pl.icon_no = ico.icon_no
WHERE usr.room = :room_id
  AND {$cond_user}
ORDER BY pl.created_time DESC
LIMIT 1
SQL;
echo $sql;
    if (($stmt = $this->prepare($sql)) !== false) {
      if ($stmt->execute($bindings)) {
        $login_success = 0 < $stmt->rowCount();
        $this->currentUser = $login_success ? $stmt->fetchObject(self::CLASS_USER, array($this)) : null;
        $stmt->closeCursor();
        $sql = <<<SQL
SELECT
  :user_id AS login_user,
  room.*,
  alarm.type AS last_alarm_type,
  alarm.posted_time AS last_alarm_time,
  alarm.internal_time AS last_alarm_itime,
  alarm.sentence AS last_alarm_message
FROM jinrou_rooms AS room
  INNER JOIN jinrou_talk AS alarm ON alarm.room = room.id
WHERE room.id = :room_id
  AND alarm.channel = 'alarm'
GROUP BY room.id HAVING last_alarm_time = MAX(last_alarm_time)
SQL;
        if (($stmt = $this->prepare($sql)) !== false) {
          $stmt->bindValue(':room_id', $room_id);
          $stmt->bindValue(':user_id', $login_success ? $this->currentUser->id : 0);
          if ($stmt->execute()) {
            $this->currentRoom = $login_success
              ? $stmt->fetchObject(self::CLASS_ROOM_LOGIN, array($this))
              : $stmt->fetchObject(self::CLASS_ROOM_READONLY, array($this));
            $this->currentRoom->UpdateSessionInfo();
            return true;
          }
        }
      }
    }
    $this->currentUser = null;
    $this->currentRoom = null;
    return false;
  }
  /**
   * 一つ以上のidを指定して部屋のユーザーを取得します。
   * @param int $room ユーザー情報を取得する部屋のid
   * @param mixed $uid 一つ、または配列に格納されたユーザーid。true、または文字列'*'を指定すると指定した部屋の全てのユーザーを取得しようとします。
   * @param mixed $id 一つ、または配列に格納されたプレイヤーid。true、または文字列'*'を指定すると部屋の全てのプレイヤーを取得しようとします。
   * @return ChatUser[] 選択されたユーザー情報を持つChatUserオブジェクトの配列。
   */
  function GetUsersById($room_id, $uid='*', $pid='*') {
    list($uid_selector, $uid_binding) = generateIdSelector('usr', $uid, 'uid');
    list($pid_selector, $pid_binding) = generateIdSelector('ply', $pid, 'pid');
    $binding = $uid_binding + $pid_binding;
    $binding[':room_id'] = $room_id;
    $sql = <<<SQL
SELECT
  usr.id,
  usr.room AS room_id,
  ply.id AS player_id,
  usr.uname,
  ply.user_no,
  ply.handle_name,
  ply.profile,
  ply.sex,
  ply.live,
  ply.role,
  ico.icon_filename,
  ico.color
FROM jinrou_users AS usr
  INNER JOIN jinrou_players AS ply ON usr.id = ply.user
  INNER JOIN user_icon AS ico ON ply.icon_no = ico.icon_no
WHERE {$uid_selector}
  AND {$pid_selector}
  AND usr.room = :room_id
ORDER BY ply.user_no

SQL;
    if (($stmt = $this->prepare($sql)) !== false) {
      if ($stmt->execute($binding)) {
        return $stmt->fetchAll(PDO::FETCH_CLASS, self::CLASS_USER, array($this));
      }
    }
    return false;
  }
  /**
   * 現在アクティブになっているチャットルームオブジェクトの一覧を取得します。
   * @param bool $is_asc ソート順序が昇順であるかどうかを示す値
   * @param int $limit 取得する部屋数の上限を示す整数値。0を指定すると全ての部屋を取得します。
   * @param int $page 部屋を取得するときのオフセットを示す整数値。オフセットは$limitと$pageの積で決定されます。
   * @return mixed 結果を取得できた場合はチャットルームオブジェクトの配列、それ以外はfalse
   */
  function GetActiveRooms($is_asc=true, $limit=0, $page=0) {
    list($order_clause, $limit_clause) = generatePaginationClauses('id', $is_asc, $limit, $page);
    $sql = <<<SQL
SELECT *
FROM jinrou_rooms
WHERE is_active
{$order_clause}
{$limit_clause}
SQL;
    $stmt = $this->query($sql);
    return $stmt->fetchAll(PDO::FETCH_CLASS, self::CLASS_ROOM, array($this));
  }
  /**
   * 終了したチャットルームオブジェクトの一覧を取得します。
   * @param bool $is_asc ソート順序が昇順であるかどうかを示す値
   * @param int $limit 取得する部屋数の上限を示す整数値。0を指定すると全ての部屋を取得します。
   * @param int $page 部屋を取得するときのオフセットを示す整数値。オフセットは$limitと$pageの積で決定されます。
   * @return mixed 結果を取得できた場合はチャットルームオブジェクトの配列、それ以外はfalse
   */
  function GetClosedRooms($limit=0, $page=0) {
    list($order_clause, $limit_clause) = generatePaginationClauses('id', $is_asc, $limit, $page);
    $sql = <<<SQL
SELECT *
FROM jinrou_rooms
WHERE NOT(is_active)
{$order_clause}
{$limit_clause}
SQL;
    $stmt = $this->query($sql);
    return $stmt->fetchAll(PDO::FETCH_CLASS, self::CLASS_ROOM, array($this));
  }
}
