<?php
// Copyright 2011 Toby Zerner, Simon Zerner
// This file is part of esoTalk. Please see the included license file for usage information.

if (!defined("IN_ESOTALK")) exit;

/**
 * The channel model provides functions for retrieving and managing channel data.
 *
 * @package esoTalk
 */
class ETChannelModel extends ETModel {

const CACHE_KEY = "channels";


/**
 * A local cache of all channels and their details.
 * @var array
 */
protected $channels;


/**
 * Class constructor; sets up the base model functions to use the channel table.
 *
 * @return void
 */
public function __construct()
{
	parent::__construct("channel");
}


/**
 * Get a full list of channels with all permission details and cache it.
 *
 * @return array An array of channel information indexed by the channel IDs.
 */
public function getAll()
{
	if (!$this->channels) {

		// If we don't have a local cache of channels, attempt to retrieve the data from the global cache.
		$channels = ET::$cache->get(self::CACHE_KEY);

		if (!$channels) {

			// Still no luck? Let's get all of the channels and their details + permissions from the db.
			// We'll construct the query with a comma-separated column of group IDs, and comma-separated
			// columns for the corresponding permissions.
			$sql = ET::SQL()
				->select("c.*")
				->select("GROUP_CONCAT(g.groupId)", "groupId")
				->from("channel c")
				->from("channel_group g", "c.channelId=g.channelId", "left")
				->groupBy("c.channelId")
				->orderBy("c.lft ASC");

			// Define the permission columns that we need to get.
			$permissionColumns = array("view", "reply", "start", "moderate");
			foreach ($permissionColumns as $column)
				$sql->select("GROUP_CONCAT(g.$column)", $column);

			// Get the channels, indexed by channel ID.
			$channels = $sql->exec()->allRows("channelId");

			// Loop through the channels and expand that comma-separated permission columns into nice arrays
			// of groups that do have the specified permission. eg. "view" => array(1, 2, 3)
			foreach ($channels as &$channel) {

				// Expand the channel's attributes.
				$channel["attributes"] = unserialize($channel["attributes"]);

				// Expand the group IDs.
				$groupIds = explode(",", $channel["groupId"]);
				unset($channel["groupId"]);

				// For each permission type, expand the comma-separated bool values.
				$permissions = array();
				$channel["permissions"] = array();
				foreach ($permissionColumns as $column) {
					$channel["permissions"][$column] = array();
					$permissions[$column] = explode(",", $channel[$column]);
					unset($channel[$column]);
				}

				// Now, for each group ID, and then for each permission type, add the group ID to that
				// permission type's array if it does have permission.
				foreach ($groupIds as $i => $id) {
					foreach ($permissionColumns as $column) {
						if ($permissions[$column][$i]) $channel["permissions"][$column][] = $id;
					}
				}

			}

			// Store the result in the global cache.
			ET::$cache->store(self::CACHE_KEY, $channels);
		}

		// Store the result in the local cache.
		$this->channels = $channels;
	}

	return $this->channels;
}


/**
 * Get a list of channels which the user has the specified permission for.
 *
 * @param string $permission The name of the permission to filter channels by.
 * @return array An array of channel information indexed by the channel IDs.
 */
public function get($permission = "view")
{
	$channels = $this->getAll();

	// Go through each of the channels and remove ones that the user doesn't have this permission for.
	$groupModel = ET::groupModel();
	$groupIds = ET::$session->getGroupIds();
	foreach ($channels as $k => $channel) {
		if (!$groupModel->groupIdsAllowedInGroupIds($groupIds, $channel["permissions"][$permission], true))
			unset($channels[$k]);
	}

	if (empty($channels))
		return false;

	// Add user data (eg. unsubscribed) into the channel array.
	$this->joinUserData($channels);

	return $channels;
}


/**
 * Add user-channel-specific data (from the member_channel table) into an array of channel data.
 *
 * @param array $channels The array of channels to add the user data onto.
 * @return void
 */
public function joinUserData(&$channels)
{
	// If there's no user logged in, we don't need to add anything.
	if (!ET::$session->userId) {
		foreach ($channels as &$channel) {
			if ($channel["attributes"]["defaultUnsubscribed"])
				$channel["unsubscribed"] = true;
		}
		return;
	}

	if (!count($channels)) return;

	// Get the user data from the database for all channel IDs in the array.
	$result = ET::SQL()
		->select("*")
		->from("member_channel")
		->where("memberId=:memberId")
		->where("channelId IN (:channelIds)")
		->bind(":memberId", ET::$session->userId)
		->bind(":channelIds", array_keys($channels))
		->exec();

	// For each row, merge the row into the respective row in the channels array.
	foreach ($result->allRows() as $row) {
		unset($row["memberId"]);
		$channels[$row["channelId"]] = array_merge($channels[$row["channelId"]], $row);
	}
}


/**
 * Returns whether or not the current user has the specified permission for $channelId.
 *
 * @param int $channelId The channel ID.
 * @param string $permission The name of the permission to check.
 * @return bool
 */
public function hasPermission($channelId, $permission = "view")
{
	$sql = ET::SQL()
		->select("COUNT(1)")
		->from("channel c")
		->where("channelId=:channelId")
		->bind(":channelId", (int)$channelId);

	$this->addPermissionPredicate($sql, $permission);

	return (bool)$sql->exec()->result();
}


/**
 * Add a WHERE predicate to an SQL query which makes sure only rows for which the user has the specified
 * permission are returned.
 *
 * @param ETSQLQuery $sql The SQL query to add the predicate to.
 * @param string $field The name of the permission to check for.
 * @param array $member The member to filter out channels for. If not specified, the currently
 * 		logged-in user will be used.
 * @param string $table The channel table alias used in the SQL query.
 * @return void
 */
public function addPermissionPredicate(&$sql, $field = "view", $member = false, $table = "c")
{
	// If no member was specified, use the current user.
	if (!$member) $member = ET::$session->user;

	// Get an array of group IDs for this member.
	$groups = ET::groupModel()->getGroupIds($member["account"], array_keys((array)$member["groups"]));

	// If the user is an administrator, don't add any SQL, as admins can do anything!
	if (in_array(GROUP_ID_ADMINISTRATOR, $groups)) return;

	// Construct a query that will fetch all channelIds for which this member has the specified permission.
	$query = ET::SQL()
		->select("channelId")
		->from("channel_group")
		->where("groupId IN (:groups)")
		->where("$field=1")
		->get();

	// Add this as a where clause to the SQL query.
	$sql->where("$table.channelId IN ($query)")
		->bind(":groups", $groups, PDO::PARAM_INT);
}


/**
 * Create a channel.
 *
 * @param array $values An array of fields and their values to insert.
 * @return bool|int The new channel ID, or false if there are errors.
 */
public function create($values)
{
	// Check that a channel title has been entered.
	if (!isset($values["title"])) $values["title"] = "";
	$this->validate("title", $values["title"], array($this, "validateTitle"));

	// Check that a channel slug has been entered and isn't already in use.
	if (!isset($values["slug"])) $values["slug"] = "";
	$this->validate("slug", $values["slug"], array($this, "validateSlug"));
	$values["slug"] = slug($values["slug"]);

	// Add the channel at the end at the root level.
	$right = ET::SQL()->select("MAX(rgt)")->from("channel")->exec()->result();
	$values["lft"] = ++$right;
	$values["rgt"] = ++$right;

	// Collapse the attributes.
	if (isset($values["attributes"])) $values["attributes"] = serialize($values["attributes"]);

	if ($this->errorCount()) return false;

	$channelId = parent::create($values);

	// Reset channels in the global cache.
	ET::$cache->remove(self::CACHE_KEY);

	return $channelId;
}


/**
 * Update a channel's details.
 *
 * @param array $values An array of fields to update and their values.
 * @param array $wheres An array of WHERE conditions.
 * @return bool|ETSQLResult
 */
public function update($values, $wheres = array())
{
	if (isset($values["title"]))
		$this->validate("title", $values["title"], array($this, "validateTitle"));

	if (isset($values["slug"])) {
		$this->validate("slug", $values["slug"], array($this, "validateSlug"));
		$values["slug"] = slug($values["slug"]);
	}

	// Collapse the attributes.
	if (isset($values["attributes"])) $values["attributes"] = serialize($values["attributes"]);

	if ($this->errorCount()) return false;

	// Reset channels in the global cache.
	ET::$cache->remove(self::CACHE_KEY);

	return parent::update($values, $wheres);
}


/**
 * Set permissions for a channel.
 *
 * @param int $channelId The ID of the channel to set permissions for.
 * @param array $permissions An array of permissions to set.
 */
public function setPermissions($channelId, $permissions)
{
	// Delete already-existing permissions for this channel.
	ET::SQL()
		->delete()
		->from("channel_group")
		->where("channelId=:channelId")
		->bind(":channelId", $channelId, PDO::PARAM_INT)
		->exec();

	// Go through each group ID and set its permission types.
	foreach ($permissions as $groupId => $types) {
		$set = array();
		foreach ($types as $type => $v) {
			if ($v) $set[$type] = 1;
		}
		ET::SQL()
			->insert("channel_group")
			->set("channelId", $channelId)
			->set("groupId", $groupId)
			->set($set)
			->exec();
	}

	// Reset channels in the global cache.
	ET::$cache->remove(self::CACHE_KEY);
}


/**
 * Set a member's status entry for a channel (their record in the member_channel table.)
 *
 * @param int $channelId The ID of the channel to set the member's status for.
 * @param int $memberId The ID of the member to set the status for.
 * @param array $data An array of key => value data to save to the database.
 * @return void
 */
public function setStatus($channelIds, $memberIds, $data)
{
	$channelIds = (array)$channelIds;
	$memberIds = (array)$memberIds;

	$keys = array_merge(array("memberId", "channelId"), array_keys($data));
	$inserts = array();
	foreach ($memberIds as $memberId) {
		foreach ($channelIds as $channelId) {
			$inserts[] = array_merge(array($memberId, $channelId), array_values($data));
		}
	}

	if (empty($inserts)) return;

	ET::SQL()
		->insert("member_channel")
		->setMultiple($keys, $inserts)
		->setOnDuplicateKey($data)
		->exec();
}


/**
 * Delete a channel and its conversations (or optionally move its conversations to another channel.)
 *
 * @param int $channelId The ID of the channel to delete.
 * @param bool|int $moveToChannelId The ID of the channel to move conversations to, or false to delete them.
 * @return bool true on success, false on error.
 */
public function deleteById($channelId, $moveToChannelId = false)
{
	$channelId = (int)$channelId;

	// Do we want to move the conversations to another channel?
	if ($moveToChannelId !== false) {

		// If the channel does exist, move all the conversation over to it.
		if (array_key_exists((int)$moveToChannelId, $this->getAll())) {
			ET::SQL()
				->update("conversation")
				->set("channelId", (int)$moveToChannelId)
				->where("channelId=:channelId")
				->bind(":channelId", $channelId)
				->exec();
		}

		// But if it doesn't, set an error.
		else $this->error("moveToChannelId", "invalidChannel");

	}

	// Or do we want to simply delete the conversations?
	else ET::conversationModel()->delete(array("channelId" => $channelId));

	if ($this->errorCount()) return false;

	$result = parent::deleteById($channelId);

	// Reset channels in the global cache.
	ET::$cache->remove(self::CACHE_KEY);

	return $result;
}


/**
 * Validate a channel title.
 *
 * @param string $title The channel title.
 * @return string|null An error code, or null if there were no errors.
 */
public function validateTitle($title)
{
	if (!strlen($title)) return "empty";
}


/**
 * Validate a channel slug.
 *
 * @param string $slug The channel slug.
 * @return string|null An error code, or null if there were no errors.
 */
public function validateSlug($slug)
{
	if (!strlen($slug)) return "empty";
	if (ET::SQL()
		->select("COUNT(channelId)")
		->from("channel")
		->where("slug=:slug")
		->bind(":slug", $slug)
		->exec()
		->result() > 0)
		return "channelSlugTaken";
}

}
