text:
<?php defined('BX_DOL') or die('hack attempt');

/**
 * Database queries
 */
class BxMessengerDb extends BxBaseModTextDb
{
   private $CNF;

   function __construct(&$oConfig)
   {
		parent::__construct($oConfig);
		$this->CNF = &$oConfig -> CNF;
	}

	/**
	* Get all lot by class name
	*@param string $sClass
	*@return array lot info
	*/
	public function getLotByClass($sClass){
		$sQuery = $this -> prepare("SELECT * FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `{$this->CNF['FIELD_CLASS']}` = ? LIMIT 1", $sClass);
		return $this -> getRow($sQuery);
	}

	/**
	* Get all lot by page url
	*@param string $sUrl or the page
	*@return array lot info
	*/
	public function getLotByUrl($sUrl)
	{
		$sQuery = $this -> prepare("SELECT * FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `{$this->CNF['FIELD_URL']}` = ? LIMIT 1", $sUrl);
		return $this -> getRow($sQuery);
	}

	/**
	* Get all lot by id
	*@param int $iId lot id
	*@return array lot info
	*/
	public function getLotInfoById($iId)
	{
		$sQuery = $this -> prepare("SELECT * FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `{$this->CNF['FIELD_ID']}` = ? LIMIT 1", (int)$iId);
		return $this -> getRow($sQuery);
	}

	/**
	* Get lot by id or url
	*@param int $iId lot id
	*@param string $sUrl url or url of the page with talk
	*@param int $iAuthor profile id
	*@return array lot info
	*/
	public function getLotByIdOrUrl($iId, $sUrl, $iAuthorId = 0){
		if ($iId)
			return $this -> getLotInfoById($iId);

		if ($sUrl)
			return $this -> getLotByUrl($sUrl);

		return array();
	}

	/**
	* Check if is the member author of the lot or jot
	*@param int $iId lot id
	*@param int $iAuthor profile id
	*@param boolean $bLotAuthor if true checks Lot, otherwise jot
	*@return boolean
	*/
	public function isAuthor($iId, $iAuthorId, $bLotAuthor = true){
		if (!$iAuthorId)
			return false;

		if ($bLotAuthor)
			$sQuery = $this -> prepare("SELECT COUNT(*) FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `{$this->CNF['FIELD_ID']}` = ? AND `{$this->CNF['FIELD_AUTHOR']}` = ? LIMIT 1", (int)$iId, (int)$iAuthorId);
		else
			$sQuery = $this -> prepare("SELECT COUNT(*) FROM `{$this->CNF['TABLE_MESSAGES']}` WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = ? AND `{$this->CNF['FIELD_MESSAGE_AUTHOR']}` = ? LIMIT 1", (int)$iId, (int)$iAuthorId);

		return $this -> getOne($sQuery) == 1;
	}

	/**
	* Deletes lot by lot Id
	*@param int $iLotId lot id
	*@return int affected rows
	*/
	public function deleteLot($iLotId){
		$aJots = $this -> getJotsByLotId($iLotId);
		foreach($aJots as $iKey => $aJot)
			$this -> removeFilesByJotId($aJot[$this->CNF['FIELD_MESSAGE_ID']]);

        $iResult = $this -> query("DELETE FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `{$this->CNF['FIELD_ID']}` = :id", array('id' => $iLotId));
		$iResult += $this -> query("DELETE FROM `{$this->CNF['TABLE_MESSAGES']}` WHERE `{$this->CNF['FIELD_MESSAGE_FK']}` = :id", array('id' => $iLotId));
		return $iResult;
	}

	/**
	* Deletes jot
	*@param int iJotId jot id
	*@param int iProfileId member who deleted jot
	*@param boolean $bCompletely if true totally remove from the site
	*@return int affected rows
	*/
	public function deleteJot($iJotId, $iProfileId, $bCompletely = false){
		$iJotId = (int)$iJotId;
		if ($bCompletely)
		{
			$this -> removeFilesByJotId($iJotId);
			return $this -> query("DELETE FROM `{$this->CNF['TABLE_MESSAGES']}` WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = :id", array('id' => $iJotId));
		}

		return $this -> query("UPDATE `{$this->CNF['TABLE_MESSAGES']}`
								SET
									`{$this->CNF['FIELD_MESSAGE_TRASH']}` = 1,
									`{$this->CNF['FIELD_MESSAGE_LAST_EDIT']}` = UNIX_TIMESTAMP(),
									`{$this->CNF['FIELD_MESSAGE_EDIT_BY']}` = :profile
								WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = :id", array('id' => $iJotId, 'profile' => $iProfileId));
	}

	/**
	* Removes participant from the participants list
	*@param int $iLotId lot id
	*@param int $iParticipant profile id
	*@return int/false
	*/
	public function leaveLot($iLotId, $iParticipant){
		return $this -> removeParticipant($iLotId, $iParticipant);
	}

	/**
	* Get Lot(Talk) settings for a member
	*@param int $iLotId lot id
	*@param int $iParticipant profile id
	*@param string $sName name the option
	*@param boolean $bParams get from params array, otherwise from table's field
	*@return mixed option value
	*/
	private function getParams($iLotId, $iParticipant, $sName = 0, $bParams = false){
		$sQuery = $this -> prepare("SELECT * FROM `{$this->CNF['TABLE_USERS_INFO']}`
									WHERE `{$this->CNF['FIELD_INFO_LOT_ID']}` = ? AND `{$this->CNF['FIELD_INFO_USER_ID']}` = ?
									LIMIT 1", (int)$iLotId, (int)$iParticipant);
		$aInfo = $this -> getRow($sQuery);
		if (empty($aInfo))
			return false;

		if (!$sName)
			return $aInfo;

		if (!$bParams){
			$aParams = unserialize($aInfo[$this->CNF['FIELD_INFO_PARAMS']]);
			return isset($aParams[$sName]) ? $aParams[$sName] : 0;
		}

		return $aInfo[$sName];
	}

	/**
	* Save Lot(Talk) settings for a member
	*@param int $iLotId lot id
	*@param int $iParticipant profile id
	*@param array $aParams options list
	*@param boolean $bParams get from  params array otherwise as table field
	*@return int affected rows
	*/
	private function setParams($iLotId, $iParticipant, $sName, $sValue, $bParams = false){
		$aParams = $this -> getParams($iLotId, $iParticipant);

		if (!$bParams)
		{
			if (empty($aParams))
				return $this -> query("INSERT INTO `{$this->CNF['TABLE_USERS_INFO']}` SET `{$this->CNF['FIELD_INFO_PARAMS']}` = :values, `{$this->CNF['FIELD_INFO_LOT_ID']}` = :id, `{$this->CNF['FIELD_INFO_USER_ID']}` = :user", array('user' => $iParticipant, 'id' => $iLotId, 'values' => serialize(array($sName => $sValue))));

			return $this -> query("UPDATE `{$this->CNF['TABLE_USERS_INFO']}` SET `{$this->CNF['FIELD_INFO_PARAMS']}` = :values WHERE `{$this->CNF['FIELD_INFO_LOT_ID']}` = :id AND `{$this->CNF['FIELD_INFO_USER_ID']}` = :user", array('user' => $iParticipant, 'id' => $iLotId, 'values' => serialize(array($sName => $sValue))));
		}

		if (empty($aParams))
			return $this -> query("INSERT INTO `{$this->CNF['TABLE_USERS_INFO']}` SET `{$sName}` = :value, `{$this->CNF['FIELD_INFO_LOT_ID']}` = :id, `{$this->CNF['FIELD_INFO_USER_ID']}` = :user", array('user' => $iParticipant, 'id' => $iLotId, 'value' => $sValue));

		return $this -> query("UPDATE `{$this->CNF['TABLE_USERS_INFO']}` SET `{$sName}` = :value WHERE `{$this->CNF['FIELD_INFO_LOT_ID']}` = :id AND `{$this->CNF['FIELD_INFO_USER_ID']}` = :user", array('user' => $iParticipant, 'id' => $iLotId, 'value' => $sValue));
	}

	/**
	* Make the lot mute for a member
	*@param int $iLotId lot id
	*@param int $iParticipant profile id
	*@return int set value
	*/
	public function muteLot($iLotId, $iParticipant){
		$iCurrentNotification = (int)$this -> getParams($iLotId, $iParticipant, 'notification');
		$iNotification = (int)!$iCurrentNotification;

		$this -> setParams($iLotId, $iParticipant, 'notification', $iNotification);
		return $iNotification;
	}

	/**
	* Mark the lot with star
	*@param int $iLotId lot id
	*@param int $iParticipant profile id
	*@return int set
	*/
	public function starLot($iLotId, $iParticipant){
		$iCurrent = (int)$this -> getParams($iLotId, $iParticipant, 'star', true);
		$iNew = (int)!$iCurrent;

		$this -> setParams($iLotId, $iParticipant, 'star', $iNew, true);
		return $iNew;
	}

	public function isStarred($iLotId, $iParticipant){
		return $this -> getParams($iLotId, $iParticipant, 'star', true);
	}
	/**
	* Check if the lot is mute for a member
	*@param int $iLotId lot id
	*@param int $iParticipant profile id
	*@return boolean
	*/
	public function isMuted($iLotId, $iParticipant){
		$iMute = $this -> getParams($iLotId, $iParticipant, 'notification');
		return $iMute;
	}

	/**
	* Save participants list for the lot
	*@param int $iLotId lot id
	*@param array $aParticipants
	*@return int affected rows
	*/
	public function savePariticipantsList($iLotId, $aParticipants){
		$sParticipants = '';
		if (!empty($aParticipants))
		{
			$aParticipants = array_map('intval', $aParticipants);
			$sParticipants = implode(',', $aParticipants);
		}

		return $this -> query("UPDATE `{$this->CNF['TABLE_ENTRIES']}` SET `{$this->CNF['FIELD_PARTICIPANTS']}` = :parts WHERE `{$this->CNF['FIELD_ID']}` = :id", array('parts' => $sParticipants, 'id' => $iLotId));
	}

	/**
	* Save message for participants to database
	*@param array $aData lot settings
	*@param array $aParticipants participants list, if it empty then used default for lot
	*@return int affected rows
	*/
	public function saveMessage($aData, $aParticipants = array()) {
		$aLot = array();
		if ((int)$aData['lot'])
			$aLot = $this -> getLotInfoById($aData['lot']);

		if (($aData['type'] != BX_IM_TYPE_PRIVATE || $this -> isAuthor($aData['lot'], $aData['member_id'])) && !$this -> isParticipant($aData['lot'], $aData['member_id'], true))
			$this -> addMemberToParticipantsList($aData['lot'], $aData['member_id']);

		if (empty($aParticipants) && (int)$aData['lot'])
			$aParticipants = $this -> getParticipantsList($aData['lot']);

		$aLot = !empty($aLot) ? $aLot : $this -> getLotByUrlAndPariticipantsList($aData['url'], $aParticipants, $aData['type']);
		if (empty($aLot)){
			$iLotID = $this -> createNewLot($aData['member_id'], $aData['title'], $aData['type'], $aData['url'], $aParticipants);
		} else
			$iLotID = $aLot[$this->CNF['FIELD_ID']];

		return $this -> addNewJot($iLotID, $aData['message'], $aData['member_id']);
	}

	/**
	* Save message for participants to database
	*@param string $sUrl of the page with lot
	*@param array $aParticipants participants list
	*@param int $iType lot type
	*@return array Lot info
	*/
	public function getLotByUrlAndPariticipantsList($sUrl = '', $aParicipants = array(), $iType = BX_IM_TYPE_PRIVATE){
		if ($iType != BX_IM_TYPE_PRIVATE && $sUrl && $aLot = $this -> getLotByUrl($sUrl))
			return $aLot;

		$aResult = array();
		if (!empty($aParicipants)){
			$sWhere = " AND `{$this->CNF['FIELD_AUTHOR']}` IN (" . $this -> implode_escape($aParicipants) . ")";

			$aLots = $this -> getAll("SELECT * FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `type` = :type {$sWhere}", array('type' => $iType));

			if (!empty($aLots))
			{
				foreach($aLots as $iKey => $aValue)
				{
					 $aPerticipantsList = $this -> getParticipantsList($aValue[$this->CNF['FIELD_ID']]);
					 if (empty($aPerticipantsList) || count($aPerticipantsList) != count($aParicipants)) continue;

					 sort($aPerticipantsList);
					 sort($aParicipants);

					 if (array_values($aPerticipantsList) == array_values($aParicipants)){
						$aResult = $aValue;
					 }
				}
			}
		}

		return $aResult;
	}

	/**
	* Mark message as read in lot history
	*@param int $iJotId message id
	*@param int iProfileId
	*/
	public function readMessage($iJotId, $iProfileId){
		$aNotViewed = $this -> getRow("SELECT `{$this->CNF['FIELD_MESSAGE_NEW_FOR']}`, `{$this->CNF['FIELD_MESSAGE_FK']}`, `{$this->CNF['FIELD_MESSAGE_AUTHOR']}`
										FROM `{$this->CNF['TABLE_MESSAGES']}`
										WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = :id", array('id' => $iJotId));
		if ($aNotViewed[$this->CNF['FIELD_MESSAGE_NEW_FOR']])
		{
			$aParticipants = explode(',', $aNotViewed[$this->CNF['FIELD_MESSAGE_NEW_FOR']]);
			$iKey = array_search($iProfileId, $aParticipants);
			if ($iKey !== FALSE)
			{
				unset($aParticipants[$iKey]);
				$sNewList = count($aParticipants) > 0 ? implode(',', $aParticipants) : '';
				$this -> query("UPDATE `{$this->CNF['TABLE_MESSAGES']}` SET `{$this->CNF['FIELD_MESSAGE_NEW_FOR']}` = :part WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = :id", array('part' => $sNewList, 'id' => $iJotId));
                bx_alert($this->_oConfig->getObject('alert'), 'read_jot', $iJotId, $iProfileId, array('author_id' => $aNotViewed[$this->CNF['FIELD_MESSAGE_AUTHOR']], 'lot_id' => $aNotViewed[$this->CNF['FIELD_MESSAGE_FK']]));
                bx_alert($this->_oConfig->getObject('alert'), 'delete_jot_ntfs', $aNotViewed[$this->CNF['FIELD_MESSAGE_FK']],  $iProfileId, array('subobject_id' => $iJotId));
			}
		}
	}

	/**
	* Mark all message as read in lot history for member
	*@param int $iLot lot id
	*@param int iProfileId
	*/
	public function readAllMessages($iLot, $iProfileId){
		$aAll = $this-> getAll("SELECT * FROM `{$this->CNF['TABLE_MESSAGES']}` WHERE `{$this->CNF['FIELD_MESSAGE_FK']}` = :id AND FIND_IN_SET(:user, `{$this->CNF['FIELD_MESSAGE_NEW_FOR']}`)", array('id' => $iLot, 'user' => $iProfileId));
		foreach($aAll as $iKey => $aValue){
			$aParticipants = explode(',', $aValue[$this->CNF['FIELD_MESSAGE_NEW_FOR']]);
			$iPos = array_search($iProfileId, $aParticipants);
			if ($iPos !== FALSE){
				unset($aParticipants[$iPos]);
				$sNewList = count($aParticipants) > 0 ? implode(',', $aParticipants) : '';
				$this -> query("UPDATE `{$this->CNF['TABLE_MESSAGES']}` SET `{$this->CNF['FIELD_MESSAGE_NEW_FOR']}` = :part WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = :id", array('part' => $sNewList, 'id' => $aValue[$this->CNF['FIELD_MESSAGE_ID']]));
                bx_alert($this->_oConfig->getObject('alert'), 'read_jot', $aValue[$this->CNF['FIELD_MESSAGE_ID']], $iProfileId, array('author_id' => $aValue[$this->CNF['FIELD_MESSAGE_AUTHOR']], 'lot_id' => $iLot));
                bx_alert($this->_oConfig->getObject('alert'), 'delete_jot_ntfs', $aValue[$this->CNF['FIELD_MESSAGE_FK']], $iProfileId, array('subobject_id' => $aValue[$this->CNF['FIELD_MESSAGE_ID']]));
			}
		}
	}

	/**
	*Add new new jot to database
	*@param int $iLotID  lot id
	*@param string $sMessage posted message
	*@param int iProfile Id	owner of the message
	*@return  int affected rows
	*/
	public function addNewJot($iLotID, $sMessage, $iProfileId)
	{
		$sParticipants = $this -> getParticipantsList($iLotID, false /* as string list*/, $iProfileId);
		$sQuery = $this->prepare("INSERT INTO `{$this->CNF['TABLE_MESSAGES']}`
												SET  `{$this->CNF['FIELD_MESSAGE']}` = ?,
													 `{$this->CNF['FIELD_MESSAGE_FK']}` = ?,
													 `{$this->CNF['FIELD_MESSAGE_AUTHOR']}` = ?,
													 `{$this->CNF['FIELD_MESSAGE_NEW_FOR']}` = ?,
													 `{$this->CNF['FIELD_MESSAGE_ADDED']}` = UNIX_TIMESTAMP()", $sMessage, $iLotID, $iProfileId, $sParticipants);

		return $this->query($sQuery) ? $this -> lastId() : false;
	}

	/**
	*Create new chat/lot with list of participants
	*@param int iProfileId owner of the lot
	*@param string $sTitle lot title
	*@param int $iType type of the lot
	*@param string $sUrl url of the page
	*@param array $aParticipants list of participants
	*@return  int affected rows
	*/
	public function createNewLot($iProfileId, $sTitle, $iType, $sUrl = '', &$aParticipants = array())
	{
		$mixedParticipants = !empty($aParticipants) ? implode(',', $aParticipants) : $iProfileId;
		$sQuery = $this->prepare("INSERT INTO `{$this->CNF['TABLE_ENTRIES']}`
												SET  `{$this->CNF['FIELD_TITLE']}` = ?,
													 `{$this->CNF['FIELD_TYPE']}` = ?,
													 `{$this->CNF['FIELD_AUTHOR']}` = ?,
													 `{$this->CNF['FIELD_ADDED']}` = UNIX_TIMESTAMP(),
													 `{$this->CNF['FIELD_PARTICIPANTS']}` = ?,
													 `{$this->CNF['FIELD_URL']}` = ?", $sTitle, $iType, $iProfileId, $mixedParticipants, $sUrl);

		return $this->query($sQuery) ? $this -> lastId() : false;
	}

	/**
	*Get list of types of member lots
	*@param int iProfileId
	*@param string $sParam keyword to filter lots
	*@return  array found lots types with lots count per each type
	*/
	public function getMemberLotsTypes($iProfileId, $sParam = ''){
		$sWhere = '';
		$aWhere = array();

		if ($iProfileId){
			$aSWhere[] = "FIND_IN_SET(:profile, `{$this->CNF['FIELD_PARTICIPANTS']}`)";
			$aWhere = array('profile' => $iProfileId);
		}

		if ($sParam){
			$sParam = "%{$sParam}%";
			$aSWhere[] = " (`{$this->CNF['FIELD_TITLE']}` LIKE :title OR `{$this->CNF['FIELD_URL']}` LIKE :url OR `{$this->CNF['FIELD_TYPE']}` LIKE :type)";
			$aWhere = array_merge($aWhere, array('title' => $sParam, 'url' => $sParam, 'type' => $sParam));
		}

		if (!empty($aWhere))
			$sWhere = "WHERE (" . implode(' AND ', $aSWhere) . ')';

		return $this-> getPairs("SELECT `{$this->CNF['FIELD_TYPE']}`, COUNT(*) as `count`
			FROM `{$this->CNF['TABLE_ENTRIES']}`
			{$sWhere}
			GROUP BY `{$this->CNF['FIELD_TYPE']}`", $this->CNF['FIELD_TYPE'], 'count', $aWhere);
	}

	/**
	*Get list of all existed types
	*@return  array types
	*/
	public function getAllLotsTypes(){
		return $this -> getAll("SELECT * FROM `{$this->CNF['TABLE_TYPES']}` ORDER BY `{$this->CNF['FIELD_TYPE_ID']}` ASC");
	}

	/**
	*Get list of all existed types as pairs: Id - Value
	*@return  array types
	*/
	public function getLotsTypesPairs(){
		return $this -> getPairs("SELECT * FROM `{$this->CNF['TABLE_TYPES']}` ORDER BY `{$this->CNF['FIELD_TYPE_ID']}`", $this->CNF['FIELD_TYPE_ID'], $this->CNF['FIELD_TYPE_NAME']);
	}

	/**
	* Check if this ype of lot exists
	*@param int $iType type of the lot
	*@return boolean
	*/
	public function isLotType($iType){
		return $this -> getOne("SELECT COUNT(*) FROM `{$this->CNF['TABLE_TYPES']}` WHERE `{$this->CNF['FIELD_TYPE_ID']}` = :type LIMIT 1", array('type' => $iType)) == 1;
	}

	/**
	* Get lot's participant list
	*@param int $iLotId lot id
	*@param boolean $bArray if true, returns result as array, otherwise string with ids separated commas
	*@param int $bExcludeProfile allows to exclude profile from the list, usually it is owner
	*@return string/array
	*/
	public function getParticipantsList($iLotId, $bArray = true, $bExcludeProfile = 0){
		$sParticipants = $this -> getOne("SELECT `{$this->CNF['FIELD_PARTICIPANTS']}` FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `{$this->CNF['FIELD_ID']}` = :value", array('value' => $iLotId));

		if (!$sParticipants)
				return array();

		$aParticipants = explode(',', $sParticipants);

		if ($bExcludeProfile && ($iId = array_search($bExcludeProfile, $aParticipants)) !== FALSE)
			unset($aParticipants[$iId]);

		return !$bArray ? implode(',', $aParticipants) : $aParticipants;
	}

	/**
	* Check if it is participant of the lot
	*@param int $iLotId lot id
	*@param int $iParticipantId profile id
	*@param boolean $bAsParticipant if true to check only participants list without lot owner
	*@return boolean
	*/
	public function isParticipant($iLotId, $iParticipantId, $bAsParticipant = false)
	{
		$aParticipants = $this -> getParticipantsList($iLotId);
		if (array_search($iParticipantId, $aParticipants) !== FALSE)
			return true;

		return !$bAsParticipant ? $this -> getOne("SELECT COUNT(*) FROM `{$this->CNF['TABLE_ENTRIES']}` WHERE `{$this->CNF['FIELD_ID']}` = :lot AND `{$this->CNF['FIELD_AUTHOR']}` = :author LIMIT 1", array('lot' => (int)$iLotId, 'author' => (int)$iParticipantId)) == 1 : false;
	}

	/**
	* Add profile to lot's participants list
	*@param int $iLotId lot id
	*@param int $iParticipantId profile id
	*@return int affected rows
	*/
	private function addMemberToParticipantsList($iLotId, $iParticipantId){
		$sParticipants = $this -> getParticipantsList($iLotId, false /* as string list */);
		$sParticipants = $sParticipants ? "{$sParticipants},{$iParticipantId}" : $iParticipantId;
		return $this -> query("UPDATE `{$this->CNF['TABLE_ENTRIES']}` SET `{$this->CNF['FIELD_PARTICIPANTS']}` = :parts WHERE `{$this->CNF['FIELD_ID']}` = :id", array('parts' => $sParticipants, 'id' => $iLotId));
	}

	/**
	* Remove profile from participants list
	*@param int $iLotId lot id
	*@param int $iParticipantId profile id
	*@return mixed false or affected rows
	*/
	private function removeParticipant($iLotId, $iParticipantId){
		$aParticipants = $this -> getParticipantsList($iLotId);
		$iKey = array_search($iParticipantId, $aParticipants);

		if ($iKey !== FALSE){
			unset($aParticipants[$iKey]);
			return $this -> savePariticipantsList($iLotId, $aParticipants);
		}

		return false;
	}

	/**
	* Get jots list for specified lot
	*@param int $iLotId lot id
	*@param int $iStart jot id from which start to get jots
	*@param string $sMode just posted or old jots from history
	*@param int $iLimit number of jots to get
	*@return array of the jots
	*/
	public function getJotsByLotId($iLotId, $iStart = BX_IM_EMPTY, $sMode = 'new', $iLimit = BX_IM_EMPTY, $bInclude = false){
		$sLimit = '';
		$aSWhere[] = "`{$this->CNF['FIELD_MESSAGE_FK']}` = :lot_id ";
		$aBindings['lot_id'] = (int)$iLotId;
		$sInsideOrder = 'DESC';

		if ($iStart)
		{
			$sEqual = '';
			if ($bInclude)
				$sEqual='=';

			$aSWhere[] = "`{$this->CNF['FIELD_MESSAGE_ID']}` " . ($sMode == 'new' ? '>' . $sEqual : '<' . $sEqual) . " :start ";
			$aBindings['start'] = (int)$iStart;

			if ($sMode == 'new')
				$sInsideOrder = 'ASC';
		}

		if ($iLimit)
		{
			$sLimit = "LIMIT :limit";
			$aBindings['limit'] = (int)$iLimit;
		}

		if (!empty($aBindings))
			$sWhere = 'WHERE ' . implode(' AND ', $aSWhere);

		$sQuery = "SELECT * FROM `{$this->CNF['TABLE_MESSAGES']}`
									{$sWhere}
									ORDER BY `{$this->CNF['FIELD_MESSAGE_ID']}` {$sInsideOrder}
									$sLimit";

		return $this -> getAll( $iStart && $sMode == 'new' ? $sQuery : "({$sQuery}) ORDER BY `{$this->CNF['FIELD_MESSAGE_ID']}`", $aBindings);
	}

	/**
	* Get lot id by jot id
	*@param int $iJotId jot id
	*@param boolean bIdOnly return id or array with info
	*@return mixed lot id or lot info in array
	*/
	public function getLotByJotId($iJotId, $bIdOnly = true){
		$iLotId = $this -> getOne("SELECT `{$this->CNF['FIELD_MESSAGE_FK']}`
						FROM `{$this->CNF['TABLE_MESSAGES']}`
						WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = :jot LIMIT 1", array('jot' => $iJotId));

		if (!$iLotId)
			return array();

		return $bIdOnly ? $iLotId : $this -> getRow("SELECT * FROM `{$this->CNF['TABLE_ENTRIES']}`
						WHERE `{$this->CNF['FIELD_ID']}` = :lot", array('lot' => $iLotId));
	}

	/**
	* Get the latest posted jot(message)
	*@param int $iLotId lot id
	*@param int $iProfileId if not specified the just latest jot of any member
	*@param boolean $bNotTrash don't show trash message
	*@return array with jot info
	*/
	public function getLatestJot($iLotId, $iProfileId = 0, $bNotTrash = true){
		$sWhere = '';
		$aWhere['lot'] = $iLotId;

		if ($iProfileId)
		{
			$sWhere = " AND `{$this->CNF['FIELD_MESSAGE_AUTHOR']}` = :profile";
			$aWhere['profile'] = $iProfileId;
		}

		if ($bNotTrash)
			$sWhere .= " AND `{$this->CNF['FIELD_MESSAGE_TRASH']}` = 0";

		return $this -> getRow("SELECT *
			FROM `{$this->CNF['TABLE_MESSAGES']}`
			WHERE  `{$this->CNF['FIELD_MESSAGE_FK']}` = :lot {$sWhere}
			ORDER BY `{$this->CNF['FIELD_MESSAGE_ADDED']}` DESC
			LIMIT 1", $aWhere);
	}

	/**
	* Get jot(message) by id
	*@param int $iJotId id
	*@return array with jot info
	*/
	public function getJotById($iJotId){
		$sQuery = $this -> prepare("SELECT * FROM `{$this->CNF['TABLE_MESSAGES']}` WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = ?", $iJotId);
		return $this -> getRow($sQuery);
	}

	/**
	* Get all pages with comments block
	*@return array list with pages
	*/
	public function getPagesWithComments(){
		return $this -> getPairs("SELECT * FROM `sys_objects_cmts`", 'Name', 'BaseUrl');
	}

	/**
	* Check if the page already contains messenger block
	*@param $sPage page name
	*@return boolean
	*/
	public function isBlockAdded($sPage){
		$sPage = bx_process_input($sPage);
		$sQuery = $this -> prepare("SELECT COUNT(*) FROM `sys_pages_blocks` WHERE `object` = ? AND `title` = '_bx_messenger_page_block_title_messenger'", $sPage);

		return $this -> getOne($sQuery) == 1;
	}

	/**
	* Add messenger block to the page
	*@param $sPage page name
	*@return affected rows
	*/
	public function addMessengerBlock($sPage){
		$sPage = bx_process_input($sPage);
		$aInfo = $this -> findCommentsBlock($sPage);

		if (!empty($aInfo))
				return $this -> query("INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`, `active`) VALUES
							  (:page, :cell, 'bx_messenger', '_bx_messenger_page_block_title_messenger', 0, 2147483647, 'service', 'a:3:{s:6:\"module\";s:12:\"bx_messenger\";s:6:\"method\";s:19:\"get_block_messenger\";s:6:\"params\";a:1:{i:0;s:6:\"{type}\";}}', 0, 0, :order, 0)",
							  array('page' => $sPage, 'cell' => $aInfo['cell_id'], 'order' => $aInfo['order'] + 1));

		return $this -> query("INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`, `active`) VALUES
							  (:page, 1, 'bx_messenger', '_bx_messenger_page_block_title_messenger', 0, 2147483647, 'service', 'a:3:{s:6:\"module\";s:12:\"bx_messenger\";s:6:\"method\";s:19:\"get_block_messenger\";s:6:\"params\";a:1:{i:0;s:6:\"{type}\";}}', 0, 0, 0, 0)",
							   array('page' => $sPage));
	}

	/**
	* Find comments block on the page
	*@param $sPage page name
	*@return array block info
	*/
	private function findCommentsBlock($sPage){
		$sPage = bx_process_input($sPage);

		$sQuery = $this -> prepare("SELECT * FROM `sys_pages_blocks` WHERE `object` = ? AND `title` LIKE '%comments%'", $sPage);
		return $this -> getRow($sQuery);
	}

	public function getUnreadJotsMessagesCount($iProfileId, $iLotId){
		if (!(int)$iLotId) return false;

		$aJots = $this -> getMyJots($iProfileId, true, $iLotId);
		return count($aJots);
	}

	public function getLeftJots($iLotId, $iJotId = 0){
		if (!(int)$iLotId) return false;

		$sWhere = '';
		$aWhere['lot'] = $iLotId;
		if ($iJotId)
		{
			$sWhere = ' AND `id` > :jot';
			$aWhere['jot'] = $iJotId;
		}

		return $this->getOne("SELECT COUNT(*) FROM `{$this->CNF['TABLE_MESSAGES']}`
								WHERE `{$this->CNF['FIELD_MESSAGE_FK']}` = :lot {$sWhere}
								ORDER BY `{$this->CNF['FIELD_MESSAGE_ID']}`", $aWhere);
	}

	/**
	* Get all jots of the member
	*@param int $iProfileId
	*@param boolean $bUnread return only unread member
	*@return array list of jots
	*/
	public function getMyJots($iProfileId, $bUnread = false, $iLotId = 0){
		$sWhere = '';
		$aWhere['profile'] = $iProfileId;

		if ($bUnread)
		{
			$sWhere = " AND FIND_IN_SET(:parts, `j`.`{$this->CNF['FIELD_MESSAGE_NEW_FOR']}`)";
			$aWhere['parts'] = $iProfileId;
		}

		if ($iLotId){
			$sWhere .= " AND `l`.`{$this->CNF['FIELD_ID']}` = :lot";
			$aWhere['lot'] = $iLotId;
		}

		return $this-> getAll("SELECT `j`.*
			FROM `{$this->CNF['TABLE_ENTRIES']}` as `l`
			LEFT JOIN `{$this->CNF['TABLE_MESSAGES']}` as `j` ON `l`.`{$this->CNF['FIELD_ID']}` = `j`.`{$this->CNF['FIELD_MESSAGE_FK']}`
			WHERE FIND_IN_SET(:profile, `l`.`{$this->CNF['FIELD_PARTICIPANTS']}`) {$sWhere}
			ORDER BY `j`.`{$this->CNF['FIELD_MESSAGE_ADDED']}` ASC", $aWhere);
	}

	/**
	* Get all member's lots
	*@param int $iProfileId
	*@param int $iType
	*@param string $sParam search keyword
	*@param boolean $bUnread get lots with unread jots only
	*@param int $iLotId lot id
	*@return array list of lots
	*/
	public function getMyLots($iProfileId, $iType = 0, $sParam = '', $bUnread = false, $iLotId = 0, $iStar = 0)
	{
		$sJOIN = $sHaving = $sWhere = '';
		$aSWhere = array();
		$aWhere['parts'] = $aWhere['profile'] = $iProfileId;

		if ($sParam)
		{
			$sParamWhere = "`j`.`{$this->CNF['FIELD_MESSAGE']}` LIKE :message OR `l`.`{$this->CNF['FIELD_TITLE']}` LIKE :title";
			$aWhere['title'] = "%{$sParam}%";
			$aWhere['message'] = "%{$sParam}%";
			$aProfiles = BxDolService::call('system', 'profiles_search', array($sParam), 'TemplServiceProfiles');
			if (!empty($aProfiles))
            {
                $aRegexp = array();
                foreach($aProfiles as &$aProfile)
                    $aRegexp[] = "(^|,){$aProfile['value']}(,|$)";

                if (!empty($aRegexp)) {
                    $aWhere['part_search'] = implode('|', $aRegexp);
                    $sParamWhere .= " OR `participants` REGEXP :part_search";
                }
            }

            $aSWhere[] = "({$sParamWhere})";
		}

		if ($iType)
		{
			$aSWhere[] = " `l`.`{$this->CNF['FIELD_TYPE']}` = :type ";
			$aWhere['type'] = $iType;
		}

		if ($bUnread)
		{
			$sHaving = "HAVING `unread_num` != 0";
		}

		if ($iLotId)
		{
			$aSWhere[] = " `l`.`{$this->CNF['FIELD_ID']}` = :id ";
			$aWhere['id'] = $iLotId;
		}

		if ($iStar)
		{
			$sJOIN = "INNER JOIN `{$this->CNF['TABLE_USERS_INFO']}` as `u` ON `u`.`{$this->CNF['FIELD_INFO_LOT_ID']}` = `l`.`{$this->CNF['FIELD_ID']}`";
			$aSWhere[] = "`u`.`{$this->CNF['FIELD_INFO_STAR']}` = 1";
		}

		if (!empty($aSWhere))
				$sWhere = ' AND ' . implode(' AND ', $aSWhere);

		return $this-> getAll("SELECT
			`l`.*,
			`p`.`count` as `unread_num`,
			`p`.`{$this->CNF['FIELD_MESSAGE_ADDED']}` as `last_created`,
			MAX(`j`.`{$this->CNF['FIELD_MESSAGE_ADDED']}`) as `last_jot_created`
			FROM `{$this->CNF['TABLE_ENTRIES']}` as `l`
			{$sJOIN}
			LEFT JOIN `{$this->CNF['TABLE_MESSAGES']}` as `j` ON `l`.`{$this->CNF['FIELD_ID']}` = `j`.`{$this->CNF['FIELD_MESSAGE_FK']}`
			LEFT JOIN (
						SELECT
							`{$this->CNF['FIELD_MESSAGE_FK']}`,
							COUNT(*) as `count`,
							MAX(`{$this->CNF['FIELD_MESSAGE_ADDED']}`) as `{$this->CNF['FIELD_MESSAGE_ADDED']}`
						FROM `{$this->CNF['TABLE_MESSAGES']}`
						WHERE FIND_IN_SET(:parts, `{$this->CNF['FIELD_MESSAGE_NEW_FOR']}`)
						GROUP BY `{$this->CNF['FIELD_MESSAGE_FK']}`
					  ) as `p` ON `p`.`{$this->CNF['FIELD_MESSAGE_FK']}` = `l`.`{$this->CNF['FIELD_ID']}`
			WHERE (FIND_IN_SET(:profile, `l`.`{$this->CNF['FIELD_PARTICIPANTS']}`) OR `l`.`{$this->CNF['FIELD_AUTHOR']}`=:profile) {$sWhere}
			GROUP BY `l`.`{$this->CNF['FIELD_ID']}`
			{$sHaving}
			ORDER BY `last_created` DESC, `last_jot_created` DESC", $aWhere);
	}

	/**
	* Check if the title of the lot type must contain link
	*@param int $iType
	*@return boolean
	*/
	public function isLinkedTitle($iType){
		$sQuery = $this -> prepare("SELECT `{$this->CNF['FIELD_TYPE_LINKED']}` FROM `{$this->CNF['TABLE_TYPES']}` WHERE `{$this->CNF['FIELD_TYPE_ID']}` = ? LIMIT 1", $iType);
		return (int)$this -> getOne($sQuery) == 1;
	}

	/**
	* Get time when member was online
	*@param int  $iProfileId
	*@return boolean
	*/
	public function lastOnline($iProfileId)
	{
	   $sSql = $this -> prepare("SELECT
				IF (`ts`.`date` != '', `ts`.`date`, `ta`.`logged`) as `logged`
			FROM `sys_profiles` AS `tp`
			INNER JOIN `sys_accounts` AS `ta` ON `tp`.`account_id`=`ta`.`id`
			LEFT JOIN `sys_sessions` AS `ts` ON `tp`.`account_id`=`ts`.`user_id`
			WHERE
				`tp`.`id` = ? AND
				`ta`.`profile_id`=`tp`.`id`
			LIMIT 1", $iProfileId);

		return $this -> getOne($sSql);
	}

	/**
	* Delete all profiles info from lots and jots
	*@param int  $iProfileId
	*@return int affected rows
	*/
	public function deleteProfileInfo($iProfileId){
		$bResult = true;

		$aWhere['profile'] = (int)$iProfileId;

		$bResult &= $this-> query("DELETE
			FROM `{$this->CNF['TABLE_ENTRIES']}`
			WHERE `{$this->CNF['FIELD_AUTHOR']}`=:profile", $aWhere);

		$aJots = $this-> getAll("SELECT *
			FROM `{$this->CNF['TABLE_MESSAGES']}`
			WHERE `{$this->CNF['FIELD_MESSAGE_AUTHOR']}`=:profile", $aWhere);

		foreach($aJots as $iKey => $aJot)
			$this -> removeFilesByJotId($aJot[$this->CNF['FIELD_MESSAGE_ID']]);

		$bResult &= $this-> query("DELETE
			FROM `{$this->CNF['TABLE_MESSAGES']}`
			WHERE `{$this->CNF['FIELD_MESSAGE_AUTHOR']}`=:profile", $aWhere);

		$aJots = $this-> getAll("SELECT *
			FROM `{$this->CNF['TABLE_ENTRIES']}`
			WHERE FIND_IN_SET(:profile, `{$this->CNF['FIELD_PARTICIPANTS']}`)", $aWhere);

		if (empty($aJots))
				return $bResult;

		foreach($aJots as $iKey => $aJot){
			$bResult &= $this -> removeParticipant($aJot[$this->CNF['FIELD_ID']], $iProfileId);
		}

		return $bResult;
	}

	/**
	* Add attachment to the jot
	*@param int $iJotId jot id
	*@param text mixedContent attachment content
	*@param int sType attachment type
	*@return int affected rows
	*/
	public function addAttachment($iJotId, $mixedContent, $sType = BX_ATT_TYPE_REPOST){
		$iJotId = (int)$iJotId;
		$aJotInfo = array();

		if (!$iJotId || !($aJotInfo = $this -> getJotById($iJotId)) || !$mixedContent)
			return false;

		if ($aJotInfo[$this->CNF['FIELD_MESSAGE_AT_TYPE']] && $aJotInfo[$this->CNF['FIELD_MESSAGE_AT_TYPE']] != BX_ATT_TYPE_REPOST) /* don't update attachment if it is already exists and it is not a repost */
			return false;

		$sQuery = $this->prepare("UPDATE `{$this->CNF['TABLE_MESSAGES']}`
												SET  `{$this->CNF['FIELD_MESSAGE_AT_TYPE']}` = ?,
													 `{$this->CNF['FIELD_MESSAGE_AT']}` = ?
												WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = ?", $sType, $mixedContent, $iJotId);

		return $this -> query($sQuery);
	}

	/**
	* Add attachment to the jot
	*@param int  $iJotId jot id
	*@param int  $iProfileId profile id
	*@param string $sMessage
	*@return int affected rows
	*/
	public function editJot($iJotId, $iProfileId, $sMessage){
		$iJotId = (int)$iJotId;
		$aJotInfo = array();

		if (!$iJotId || !($aJotInfo = $this -> getJotById($iJotId)) || !$sMessage)
			return false;

		$sWhere = '';
		if ($aJotInfo[$this->CNF['FIELD_MESSAGE_AT_TYPE']] == BX_ATT_TYPE_REPOST)
			$sWhere = ",`{$this->CNF['FIELD_MESSAGE_AT_TYPE']}` = '', `{$this->CNF['FIELD_MESSAGE_AT']}` = ''";

		$sQuery = $this->prepare("UPDATE `{$this->CNF['TABLE_MESSAGES']}`
												SET  `{$this->CNF['FIELD_MESSAGE']}` = ?,
													 `{$this->CNF['FIELD_MESSAGE_LAST_EDIT']}` = UNIX_TIMESTAMP(),
													 `{$this->CNF['FIELD_MESSAGE_EDIT_BY']}` = ?
													 {$sWhere}
												WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = ?", $sMessage, $iProfileId, $iJotId);

		return $this -> query($sQuery);
	}

	/**
	* Check if the Jot already has attachment
	*@param int  $iJotId jot id
	*@param string $sType attachment type
	*@return int original attachment Id
	*/
	public function hasAttachment($iJotId, $sType='repost'){
		$iResult = $this->getOne("SELECT
									`{$this->CNF['FIELD_MESSAGE_AT']}`
									FROM `{$this->CNF['TABLE_MESSAGES']}`
									WHERE `{$this->CNF['FIELD_MESSAGE_ID']}` = :id AND `{$this->CNF['FIELD_MESSAGE_AT_TYPE']}`= :type", array('id' => $iJotId, 'type' => $sType));

		return $iResult ? $iResult : $iJotId;
	}

	public function updateFiles($iJotId, $sField = 'jot_id', $sValue){
		$sQuery = $this->prepare("UPDATE `{$this->CNF['OBJECT_STORAGE']}`
									SET  `{$sField}` = ?
									WHERE `{$this->CNF['FIELD_ST_ID']}` = ?", $sValue, $iJotId);

		return $this -> query($sQuery);
	}

	public function getJotFiles($iJot, $bCount = false){
		return !$bCount ? $this->getAll("SELECT * FROM `{$this->CNF['OBJECT_STORAGE']}` WHERE `{$this->CNF['FIELD_ST_JOT']}` = :id", array('id' => $iJot)) : $this->getOne("SELECT COUNT(*) FROM `{$this->CNF['OBJECT_STORAGE']}` WHERE `{$this->CNF['FIELD_ST_JOT']}` = :id", array('id' => $iJot));
	}

	private function removeFilesByJotId($iJotId){
		 $aFiles = $this -> getJotFiles($iJotId);
		 if (empty($aFiles))
			 return false;

		 $oStorage = BxDolStorage::getObjectInstance($this->CNF['OBJECT_STORAGE']);
		 $bResult = true;
		 foreach($aFiles as $iKey => $aFile)
			$bResult &= $oStorage -> deleteFile($aFile[$this->CNF['FIELD_ST_ID']], $aFile[$this->CNF['FIELD_ST_AUTHOR']]);

		  return $bResult;
	}

	public function isFileVendor($iFileId, $iProfileId){
		$oStorage = BxDolStorage::getObjectInstance($this->CNF['OBJECT_STORAGE']);
		$aFile = $oStorage -> getFile($iFileId);
		return $aFile[$this->CNF['FIELD_ST_AUTHOR']] == $iProfileId;
	}

	public function isEmptyJotMessage($iJotId){
		$aJotInfo = $this -> getJotById($iJotId);

		return empty($aJotInfo) || !$aJotInfo['FIELD_MESSAGE'];
	}

	public function getFirstUnreadJot($iProfileId, $iLotId){
		$iJotId = 0;

		if (!$iLotId)
			return $iJotId;

		$aUnreadJot = $this -> getMyJots($iProfileId, true, $iLotId);
		if (!empty($aUnreadJot))
			$iJotId = $aUnreadJot[0][$this->CNF['FIELD_MESSAGE_ID']];

		return $iJotId;
	}

    /**
     * @param int $iObjectId item's object ID
     * @param string $sType module's name from sys_modules
     * @param int $iProfileId comments' author
     * @param int $iStart get comments from position
     * @param int $iPerPage number of the comments to get at once
     * @return array list of the comments
     *
     */
    public function getLiveComments($iObjectId, $sType, $iProfileId, $iStart = 0, $iPerPage = 0){
        $aWhere = array('object' => $iObjectId, 'type' => $sType);
        $sLimit = $sAuthor = '';
        if ((int)$iProfileId) {
            $aWhere['author'] = (int)$iProfileId;
            $sAuthor = "AND `{$this->CNF['FIELD_LCMTS_AUTHOR']}`=:author";
        }

        if ($iPerPage) {
            $aWhere['start'] = (int)$iStart;
            $aWhere['per_page'] = (int)$iPerPage;
            $sLimit = "LIMIT :start, :per_page";
        }

        $aComments = $this -> getAll("SELECT
                                                  SQL_CALC_FOUND_ROWS
                                                  `{$this->CNF['FIELD_LCMTS_ID']}`,
                                                  `{$this->CNF['FIELD_LCMTS_TEXT']}`,
                                                  `{$this->CNF['FIELD_LCMTS_AUTHOR']}`,
                                                  `{$this->CNF['FIELD_LCMTS_DATE']}`,
                                                  `{$this->CNF['FIELD_LCMTS_OBJECT_ID']}`
                                                 FROM `{$this->CNF['TABLE_LIVE_COMMENTS']}` as `c`
                                                 LEFT JOIN `{$this->CNF['TABLE_CMTS_OBJECTS']}` as `s` ON `s`.`{$this->CNF['FIELD_COBJ_ID']}` = `c`.`{$this->CNF['FIELD_LCMTS_SYS_ID']}`
                                                 WHERE `s`.`{$this->CNF['FIELD_COBJ_MODULE']}` = :type AND `{$this->CNF['FIELD_LCMTS_OBJECT_ID']}` = :object {$sAuthor}
                                                 ORDER BY `{$this->CNF['FIELD_LCMTS_ID']}` ASC
                                                 {$sLimit}",
            $aWhere);
        return array('result' => $aComments, 'total' => (int)$this->getOne("SELECT FOUND_ROWS()"));
    }

    /**
     * @param string $sText message body
     * @param int $iObjectId item's object ID
     * @param string $sName module's name from sys_modules
     * @param int $iProfileId comments' author
     * @param int $iDate comments' time
     * @return mixed last added comments id or false
     *
     */

    public function addLiveComment($sText, $iObjectId, $sName, $iProfileId, $iDate = 0){
        $iSystem = $this -> getOne("SELECT `{$this->CNF['FIELD_COBJ_ID']}`
                                            FROM `{$this->CNF['TABLE_CMTS_OBJECTS']}`
                                            WHERE `{$this->CNF['FIELD_COBJ_MODULE']}`=:name LIMIT 1", array('name' => $sName));
        if (!$iSystem)
            return false;

        $sQuery = $this -> prepare("REPLACE INTO `{$this->CNF['TABLE_LIVE_COMMENTS']}`
                                   SET
                                         `{$this->CNF['FIELD_LCMTS_TEXT']}`=?,
                                         `{$this->CNF['FIELD_LCMTS_AUTHOR']}`=?,
                                         `{$this->CNF['FIELD_LCMTS_DATE']}`=?,
                                         `{$this->CNF['FIELD_LCMTS_OBJECT_ID']}`=?,
                                         `{$this->CNF['FIELD_LCMTS_SYS_ID']}`=?
                                   ", $sText, $iProfileId, $iDate ? $iDate : time(), $iObjectId, $iSystem);

        return $this -> query($sQuery) ? $this -> lastId() : false;
    }

    public function removeLiveComment($iObjectId, $iProfileId){
        return $this -> query("DELETE FROM `{$this->CNF['TABLE_LIVE_COMMENTS']}`
                              WHERE `{$this->CNF['FIELD_LCMTS_AUTHOR']}`=:author AND `{$this->CNF['FIELD_LCMTS_ID']}`=:object_id", array('author' => $iProfileId, 'object_id' => $iObjectId));
    }

    public function getLotFiles($iLotId){
        return $this->getAll("SELECT `s`.*
                                         FROM `{$this->CNF['OBJECT_STORAGE']}` as `s`
                                         LEFT JOIN `{$this->CNF['TABLE_MESSAGES']}` as `j` ON `s`.`{$this->CNF['FIELD_ST_JOT']}` = `j`.`{$this->CNF['FIELD_MESSAGE_ID']}`
                                         LEFT JOIN `{$this->CNF['TABLE_ENTRIES']}` as `l` ON `l`.`{$this->CNF['FIELD_ID']}` = `j`.`{$this->CNF['FIELD_MESSAGE_FK']}`
                                         WHERE `l`.`{$this->CNF['FIELD_ID']}` = :id", array('id' => $iLotId));
    }

    public function isPushNotificationsEnabled(){
        if (!$this->isModuleByName('bx_notifications'))
            return false;

        return $this -> getOne("SELECT `s`.`active`
                                        FROM `bx_notifications_handlers` as `h`
                                        LEFT JOIN `bx_notifications_settings` as `s` ON `h`.`id` = `s`.`handler_id`
                                        WHERE `h`.`group`=:module AND `h`.`type`='insert' AND `s`.`delivery` = 'push'", array('module' => $this->_oConfig->getName())) == 1;
    }

    public function isAllowedToSendNtfs($iProfileId, $iLotId){
        if (!$this->isModuleByName('bx_notifications'))
            return true;

        $iNumber = (int)$this->CNF['MAX_NTFS_NUMBER'];
        $iInterval = (int)$this->CNF['PARAM_NTFS_INTERVAL'];

        return $this -> getOne("SELECT COUNT(*)
                                        FROM `bx_notifications_events`
                                        WHERE `action`='got_jot_ntfs' AND `type`=:type AND `owner_id` = :profile AND `object_id`=:lot_id
                                              AND `date` > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL :interval HOUR))",
                                array(
                                        'type' => $this->_oConfig->getName(),
                                        'profile' => $iProfileId,
                                        'lot_id' => $iLotId,
                                        'interval' => $iInterval
                                    )) < $iNumber;
    }

    /**
     * @param int $iJotId id of the message
     * @param int $iProfileId viewer id
     * @param int $iJotAuthor message's author
     * @param int $iLotAuthorId author id
     * @return bool
     */
    public function isAllowedToDeleteJot($iJotId, $iProfileId=0, $iJotAuthor=0, $iLotAuthorId=0){
        $oCNF = &$this -> _oConfig -> CNF;
        if (!$iJotId)
            return true;

        if (!$iProfileId)
            $iProfileId = bx_get_logged_profile_id();

        if (!$iJotAuthor){
            $aJot = $this->getJotById($iJotId);
            $iJotAuthor = $aJot[$oCNF['FIELD_MESSAGE_AUTHOR']];
        }

        if (!$iLotAuthorId) {
            $iLotId = $this->getLotByJotId($iJotId);
            $bIsLotAuthor = $this -> isAuthor($iLotId, $iProfileId);
        }
        else
            $bIsLotAuthor = $iLotAuthorId == $iProfileId;

        return ($oCNF['ALLOW_TO_REMOVE_MESSAGE'] && $iJotAuthor == $iProfileId) || $bIsLotAuthor || isAdmin();
    }

    /*********************** REACT JOT Integration part *****************/

    /**
     * Returns installed list of modules with comments ability
     * @param string $sModule
     * @return array
     */
    function getAllCmtsModule($sModule = ''){
        return $this -> getAllWithKey("SELECT
                    `c`.`Module` as `module`,
                    `TriggerTable` as `table`,
                    `TriggerFieldId` as `id`,
                    `TriggerFieldAuthor` as `owner`,
                    `TriggerFieldTitle` as `title`,
                    `TriggerFieldComments` as `cmts`,
                    `sm`.*,
                    `m`.`icon`
                FROM `sys_objects_cmts` as `c`
                LEFT JOIN `sys_modules` as `sm` ON `sm`.`name` = `c`.`module`
                LEFT JOIN `sys_menu_items` as `m` ON `c`.`module` = `m`.`module` AND `m`.`set_name` = 'sys_site'
                WHERE `c`.`module` != 'bx_timeline'
                GROUP BY `c`.`module`
                ", 'module');
    }
}

/** @} */


text:
package sql

import (
	"database/sql"
	"fmt"
	"github.com/cayleygraph/cayley/clog"
	"github.com/cayleygraph/cayley/graph"
	"github.com/cayleygraph/cayley/quad"
	_ "github.com/go-sql-driver/mysql"
	"strings"
)

const flavorMysql = "mysql"

func init() {
	hs := fmt.Sprint(quad.HashSize)
	RegisterFlavor(Flavor{
		Name: flavorMysql,
		NodesTable: `CREATE TABLE nodes (
	hash BINARY(` + hs + `) PRIMARY KEY,
	value BLOB,
	value_string TEXT,
	datatype TEXT,
	language TEXT,
	iri BOOLEAN,
	bnode BOOLEAN,
	value_int BIGINT,
	value_bool BOOLEAN,
	value_float double precision,
	value_time DATETIME(6)
);`,
		QuadsTable: `CREATE TABLE quads (
	horizon SERIAL PRIMARY KEY,
	subject_hash BINARY(` + hs + `) NOT NULL,
	predicate_hash BINARY(` + hs + `) NOT NULL,
	object_hash BINARY(` + hs + `) NOT NULL,
	label_hash BINARY(` + hs + `),
	id BIGINT,
	ts timestamp
);`,
		FieldQuote:  '`',
		Placeholder: func(n int) string { return "?" },
		Indexes: func(options graph.Options) []string {
			return []string{
				`CREATE UNIQUE INDEX spo_unique ON quads (subject_hash, predicate_hash, object_hash);`,
				`CREATE UNIQUE INDEX spol_unique ON quads (subject_hash, predicate_hash, object_hash, label_hash);`,
				`CREATE INDEX spo_index ON quads (subject_hash);`,
				`CREATE INDEX pos_index ON quads (predicate_hash);`,
				`CREATE INDEX osp_index ON quads (object_hash);`,
				`ALTER TABLE quads ADD CONSTRAINT subject_hash_fk FOREIGN KEY (subject_hash) REFERENCES nodes (hash);`,
				`ALTER TABLE quads ADD CONSTRAINT predicate_hash_fk FOREIGN KEY (predicate_hash) REFERENCES nodes (hash);`,
				`ALTER TABLE quads ADD CONSTRAINT object_hash_fk FOREIGN KEY (object_hash) REFERENCES nodes (hash);`,
				`ALTER TABLE quads ADD CONSTRAINT label_hash_fk FOREIGN KEY (label_hash) REFERENCES nodes (hash);`,
			}
		},
		Error: func(err error) error {
			return err
		},
		Estimated: nil,
		RunTx:     runTxMysql,
	})
}

func runTxMysql(tx *sql.Tx, in []graph.Delta, opts graph.IgnoreOpts) error {
	ignore := ""
	if opts.IgnoreDup {
		ignore = " IGNORE"
	}

	var (
		insertQuad  *sql.Stmt
		insertValue map[int]*sql.Stmt     // prepared statements for each value type
		inserted    map[NodeHash]struct{} // tracks already inserted values

		deleteQuad   *sql.Stmt
		deleteTriple *sql.Stmt
	)

	var err error
	for _, d := range in {
		switch d.Action {
		case graph.Add:
			if insertQuad == nil {
				insertQuad, err = tx.Prepare(`INSERT` + ignore + ` INTO quads(subject_hash, predicate_hash, object_hash, label_hash, id, ts) VALUES (?, ?, ?, ?, ?, ?);`)
				if err != nil {
					return err
				}
				insertValue = make(map[int]*sql.Stmt)
				inserted = make(map[NodeHash]struct{}, len(in))
			}
			var hs, hp, ho, hl NodeHash
			for _, dir := range quad.Directions {
				v := d.Quad.Get(dir)
				if v == nil {
					continue
				}
				h := hashOf(v)
				switch dir {
				case quad.Subject:
					hs = h
				case quad.Predicate:
					hp = h
				case quad.Object:
					ho = h
				case quad.Label:
					hl = h
				}
				if !h.Valid() {
					continue
				} else if _, ok := inserted[h]; ok {
					continue
				}
				nodeKey, values, err := nodeValues(h, v)
				if err != nil {
					return err
				}
				stmt, ok := insertValue[nodeKey]
				if !ok {
					var ph = make([]string, len(values)-1)
					for i := range ph {
						ph[i] = "?"
					}
					stmt, err = tx.Prepare(`INSERT IGNORE INTO nodes(hash, ` +
						strings.Join(nodeInsertColumns[nodeKey], ", ") +
						`) VALUES (?, ` +
						strings.Join(ph, ", ") +
						`);`)
					if err != nil {
						return err
					}
					insertValue[nodeKey] = stmt
				}
				_, err = stmt.Exec(values...)
				err = convInsertErrorMySql(err)
				if err != nil {
					clog.Errorf("couldn't exec INSERT statement: %v", err)
					return err
				}
				inserted[h] = struct{}{}
			}
			_, err := insertQuad.Exec(
				hs.toSQL(), hp.toSQL(), ho.toSQL(), hl.toSQL(),
				d.ID.Int(),
				d.Timestamp,
			)
			err = convInsertErrorMySql(err)
			if err != nil {
				clog.Errorf("couldn't exec INSERT statement: %v", err)
				return err
			}
		case graph.Delete:
			if deleteQuad == nil {
				deleteQuad, err = tx.Prepare(`DELETE FROM quads WHERE subject_hash=? and predicate_hash=? and object_hash=? and label_hash=?;`)
				if err != nil {
					return err
				}
				deleteTriple, err = tx.Prepare(`DELETE FROM quads WHERE subject_hash=? and predicate_hash=? and object_hash=? and label_hash is null;`)
				if err != nil {
					return err
				}
			}
			var result sql.Result
			if d.Quad.Label == nil {
				result, err = deleteTriple.Exec(hashOf(d.Quad.Subject).toSQL(), hashOf(d.Quad.Predicate).toSQL(), hashOf(d.Quad.Object).toSQL())
			} else {
				result, err = deleteQuad.Exec(hashOf(d.Quad.Subject).toSQL(), hashOf(d.Quad.Predicate).toSQL(), hashOf(d.Quad.Object).toSQL(), hashOf(d.Quad.Label).toSQL())
			}
			if err != nil {
				clog.Errorf("couldn't exec DELETE statement: %v", err)
				return err
			}
			affected, err := result.RowsAffected()
			if err != nil {
				clog.Errorf("couldn't get DELETE RowsAffected: %v", err)
				return err
			}
			if affected != 1 && !opts.IgnoreMissing {
				return graph.ErrQuadNotExist
			}
		default:
			panic("unknown action")
		}
	}
	return nil
}

func convInsertErrorMySql(err error) error {
	return err // TODO
}

redpajama_set_name: RedPajamaGithub


text:
$artifacts = gci src/ -r -include obj, bin
foreach($a in $artifacts) {
    remove-item -force -Recurse -path $a
}


text:
import { RestEndpoint } from '../types';
export declare const get: RestEndpoint<'ReleaseAction', 'get'>;
export declare const queryForRelease: RestEndpoint<'ReleaseAction', 'queryForRelease'>;


text:
static void LoadMD5Materials(
    Scene* scene,
    const char* assetFolder, const char* modelFolder,
    aiMaterial** materials, int numMaterials,
    int* materialIDMapping)
{
    // Only the texture types we care about
    std::array<aiTextureType,3> textureTypes = {
        aiTextureType_DIFFUSE,
        aiTextureType_SPECULAR,
        aiTextureType_NORMALS
    };
    static const int numTextureTypes = (int)textureTypes.size();

    // find all textures that need to be loaded
    std::vector<std::vector<std::string>> texturesToLoad(numTextureTypes);
    for (int materialIdx = 0; materialIdx < numMaterials; materialIdx++)
    {
        aiMaterial* material = materials[materialIdx];

        for (int textureTypeIdx = 0; textureTypeIdx < (int)textureTypes.size(); textureTypeIdx++)
        {
            int textureCount = (int)aiGetMaterialTextureCount(material, textureTypes[textureTypeIdx]);

            for (int textureIdxInStack = 0; textureIdxInStack < (int)textureCount; textureIdxInStack++)
            {
                aiString path;
                aiReturn result = aiGetMaterialTexture(material, textureTypes[textureTypeIdx], textureIdxInStack, &path, NULL, NULL, NULL, NULL, NULL, NULL);
                if (result != AI_SUCCESS)
                {
                    fprintf(stderr, "aiGetMaterialTexture failed: %s\n", aiGetErrorString());
                    exit(1);
                }

                texturesToLoad[textureTypeIdx].push_back(std::string(modelFolder) + path.C_Str());
            }
        }
    }

    std::vector<std::unordered_map<std::string, int>*> textureNameToIDs
    {
        &scene->DiffuseTextureNameToID,
        &scene->SpecularTextureNameToID,
        &scene->NormalTextureNameToID,
    };
    assert(textureNameToIDs.size() == numTextureTypes);

    // keep only the unique textures
    for (int textureTypeIdx = 0; textureTypeIdx < numTextureTypes; textureTypeIdx++)
    {
        // remove textures that appear more than once in this list
        std::sort(begin(texturesToLoad[textureTypeIdx]), end(texturesToLoad[textureTypeIdx]));
        texturesToLoad[textureTypeIdx].erase(
            std::unique(begin(texturesToLoad[textureTypeIdx]), end(texturesToLoad[textureTypeIdx])),
            end(texturesToLoad[textureTypeIdx]));

        // remove textures that were loaded by previous meshes
        texturesToLoad[textureTypeIdx].erase(
            std::remove_if(begin(texturesToLoad[textureTypeIdx]), end(texturesToLoad[textureTypeIdx]),
                [&textureNameToIDs, textureTypeIdx](const std::string& s) {
            return textureNameToIDs[textureTypeIdx]->find(s) != textureNameToIDs[textureTypeIdx]->end();
        }), end(texturesToLoad[textureTypeIdx]));
    }

    // load all the unique textures
    for (int textureTypeIdx = 0; textureTypeIdx < numTextureTypes; textureTypeIdx++)
    {
        for (int textureToLoadIdx = 0; textureToLoadIdx < (int)texturesToLoad[textureTypeIdx].size(); textureToLoadIdx++)
        {
            const std::string& fullpath = assetFolder + texturesToLoad[textureTypeIdx][textureToLoadIdx];

            int width, height, comp;
            int req_comp = 4;
            stbi_set_flip_vertically_on_load(1); // because GL
            stbi_uc* img = stbi_load(fullpath.c_str(), &width, &height, &comp, req_comp);
            if (!img)
            {
                fprintf(stderr, "stbi_load (%s): %s\n", fullpath.c_str(), stbi_failure_reason());
            }
            else
            {
                bool hasTransparency = false;
                for (int i = 0; i < width * height; i++)
                {
                    if (img[i * 4 + 3] != 255)
                    {
                        hasTransparency = true;
                        break;
                    }
                }

                bool isSRGB = false;
                if (textureTypes[textureTypeIdx] == aiTextureType_DIFFUSE)
                {
                    isSRGB = true;
                }
                else if (textureTypes[textureTypeIdx] == aiTextureType_SPECULAR)
                {
                    isSRGB = false;
                }
                else if (textureTypes[textureTypeIdx] == aiTextureType_NORMALS)
                {
                    isSRGB = false;
                }
                else
                {
                    fprintf(stderr, "%s: Unhandled texture type %d\n", fullpath.c_str(), textureTypes[textureTypeIdx]);
                    exit(1);
                }

                // premultiply teh alphas
                if (hasTransparency)
                {
                    for (int i = 0; i < width * height; i++)
                    {
                        float alpha = glm::clamp(img[i * 4 + 3] / 255.0f, 0.0f, 1.0f);
                        if (isSRGB)
                        {
                            alpha = glm::clamp(std::pow(alpha, 1.0f / 2.2f), 0.0f, 1.0f);
                        }
                        img[i * 4 + 0] = stbi_uc(img[i * 4 + 0] * alpha);
                        img[i * 4 + 1] = stbi_uc(img[i * 4 + 1] * alpha);
                        img[i * 4 + 2] = stbi_uc(img[i * 4 + 2] * alpha);
                    }
                }

                if (req_comp != 0)
                {
                    comp = req_comp;
                }

                GLenum srcDataFormat[4] = {
                    GL_RED, GL_RG, GL_RGB, GL_RGBA
                };

                GLuint texture;
                glGenTextures(1, &texture);
                glBindTexture(GL_TEXTURE_2D, texture);

                if (textureTypes[textureTypeIdx] == aiTextureType_DIFFUSE)
                {
                    float anisotropy;
                    glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisotropy);
                    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisotropy);
                    glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, width, height, 0, srcDataFormat[comp - 1], GL_UNSIGNED_BYTE, img);
                }
                else if (textureTypes[textureTypeIdx] == aiTextureType_SPECULAR)
                {
                    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, srcDataFormat[comp - 1], GL_UNSIGNED_BYTE, img);
                }
                else if (textureTypes[textureTypeIdx] == aiTextureType_NORMALS)
                {
                    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8_SNORM, width, height, 0, srcDataFormat[comp - 1], GL_UNSIGNED_BYTE, img);
                }
                else
                {
                    fprintf(stderr, "%s: Unhandled texture type %d\n", fullpath.c_str(), textureTypes[textureTypeIdx]);
                    exit(1);
                }

                glGenerateMipmap(GL_TEXTURE_2D);
                glBindTexture(GL_TEXTURE_2D, 0);

                int id = -1;

                if (textureTypes[textureTypeIdx] == aiTextureType_DIFFUSE)
                {
                    DiffuseTexture d;
                    d.HasTransparency = hasTransparency;
                    d.TO = texture;
                    scene->DiffuseTextures.push_back(std::move(d));
                    id = (int)scene->DiffuseTextures.size() - 1;
                }
                else if (textureTypes[textureTypeIdx] == aiTextureType_SPECULAR)
                {
                    SpecularTexture s;
                    s.TO = texture;
                    scene->SpecularTextures.push_back(std::move(s));
                    id = (int)scene->SpecularTextures.size() - 1;
                }
                else if (textureTypes[textureTypeIdx] == aiTextureType_NORMALS)
                {
                    NormalTexture n;
                    n.TO = texture;
                    scene->NormalTextures.push_back(std::move(n));
                    id = (int)scene->NormalTextures.size() - 1;
                }
                else
                {
                    fprintf(stderr, "%s: Unhandled texture type %d\n", fullpath.c_str(), textureTypes[textureTypeIdx]);
                    exit(1);
                }

                textureNameToIDs[textureTypeIdx]->emplace(texturesToLoad[textureTypeIdx][textureToLoadIdx], id);

                stbi_image_free(img);
            }

            stbi_set_flip_vertically_on_load(0);
        }
    }

    // hook up list of materials
    for (int materialIdx = 0; materialIdx < numMaterials; materialIdx++)
    {
        aiMaterial* material = materials[materialIdx];

        // Potential improvement:
        // Look for an existing material with the same properties,
        // instead of creating a new one.
        Material newMat;

        for (int textureTypeIdx = 0; textureTypeIdx < (int)textureTypes.size(); textureTypeIdx++)
        {
            int textureCount = (int)aiGetMaterialTextureCount(material, textureTypes[textureTypeIdx]);

            for (int textureIdxInStack = 0; textureIdxInStack < (int)textureCount; textureIdxInStack++)
            {
                aiString path;
                aiReturn result = aiGetMaterialTexture(material, textureTypes[textureTypeIdx], textureIdxInStack, &path, NULL, NULL, NULL, NULL, NULL, NULL);
                if (result != AI_SUCCESS)
                {
                    fprintf(stderr, "aiGetMaterialTexture failed: %s\n", aiGetErrorString());
                    exit(1);
                }

                std::string modelpath = std::string(modelFolder) + path.C_Str();
                auto foundNameToID = textureNameToIDs[textureTypeIdx]->find(modelpath);
                if (foundNameToID != textureNameToIDs[textureTypeIdx]->end())
                {
                    int textureID = foundNameToID->second;

                    if (textureTypes[textureTypeIdx] == aiTextureType_DIFFUSE)
                    {
                        newMat.DiffuseTextureIDs.push_back(textureID);
                    }
                    else if (textureTypes[textureTypeIdx] == aiTextureType_SPECULAR)
                    {
                        newMat.SpecularTextureIDs.push_back(textureID);
                    }
                    else if (textureTypes[textureTypeIdx] == aiTextureType_NORMALS)
                    {
                        newMat.NormalTextureIDs.push_back(textureID);
                    }
                    else
                    {
                        fprintf(stderr, "%s: Unhandled texture type %d\n", modelpath.c_str(), textureTypes[textureTypeIdx]);
                        exit(1);
                    }
                }
            }
        }

        if (materialIDMapping)
        {
            materialIDMapping[materialIdx] = (int)scene->Materials.size();
        }

        scene->Materials.push_back(std::move(newMat));
    }
}

static int LoadMD5SkeletonNode(
    Scene* scene,
    const aiNode* ainode,
    const std::unordered_map<std::string, glm::mat4>& invBindPoseTransforms)
{
    if (strcmp(ainode->mName.C_Str(), "<MD5_Hierarchy>") != 0)
    {
        fprintf(stderr, "Expected <MD5_Hierarchy>, got %s\n", ainode->mName.C_Str());
        exit(1);
    }

    // traverse skeleton and flatten it
    std::vector<aiNode*> boneNodes;
    std::vector<int> boneParentIDs;
    std::vector<std::pair<aiNode*, int>> skeletonDFSStack;

    // Initialize stack with parentless bones
    for (int childIdx = (int)ainode->mNumChildren - 1; childIdx >= 0; childIdx--)
    {
        skeletonDFSStack.emplace_back(ainode->mChildren[childIdx], -1);
    }

    while (!skeletonDFSStack.empty())
    {
        aiNode* node = skeletonDFSStack.back().first;
        int parentID = skeletonDFSStack.back().second;
        skeletonDFSStack.pop_back();

        int myBoneID = (int)boneNodes.size();
        boneNodes.push_back(node);
        boneParentIDs.push_back(parentID);

        for (int childIdx = (int)node->mNumChildren - 1; childIdx >= 0; childIdx--)
        {
            skeletonDFSStack.emplace_back(node->mChildren[childIdx], myBoneID);
        }
    }

    int boneCount = (int)boneNodes.size();

    // Generate bone indices for rendering
    std::vector<glm::uvec2> boneIndices(boneCount - 1);
    for (int boneIdx = 1, indexIdx = 0; boneIdx < boneCount; boneIdx++, indexIdx++)
    {
        boneIndices[indexIdx] = glm::uvec2(boneParentIDs[boneIdx], boneIdx);
    }

    Skeleton skeleton;
    skeleton.BoneNames.resize(boneCount);
    skeleton.BoneInverseBindPoseTransforms.resize(boneCount);
    skeleton.BoneLengths.resize(boneCount);
    skeleton.BoneParents = std::move(boneParentIDs);
    skeleton.NumBones = boneCount;
    skeleton.NumBoneIndices = 2 * (int)boneIndices.size();

    for (int boneID = 0; boneID < boneCount; boneID++)
    {
        skeleton.BoneNames[boneID] = boneNodes[boneID]->mName.C_Str();
        skeleton.BoneNameToID.emplace(skeleton.BoneNames[boneID], boneID);

        int parentBoneID = skeleton.BoneParents[boneID];

        // Unused bones won't have an inverse bind pose transform to use
        auto it = invBindPoseTransforms.find(skeleton.BoneNames[boneID]);
        if (it != invBindPoseTransforms.end())
        {
            skeleton.BoneInverseBindPoseTransforms[boneID] = it->second;
        }
        else
        {
            // Missing inverse bind pose implies no local transformation
            printf("Bone %s has no inverse bind pose transform, assigning from ", skeleton.BoneNames[boneID].c_str());
            if (parentBoneID >= 0)
            {
                // Same absolute transform as parent
                printf("%s\n", skeleton.BoneNames[parentBoneID].c_str());
                skeleton.BoneInverseBindPoseTransforms[boneID] = skeleton.BoneInverseBindPoseTransforms[parentBoneID];
            }
            else
            {
                // No absolute transform
                printf("identity\n");
                skeleton.BoneInverseBindPoseTransforms[boneID] = glm::mat4(1.0);
            }
        }

        if (parentBoneID != -1)
        {
            glm::mat4 childInvBindPose = skeleton.BoneInverseBindPoseTransforms[boneID];
            glm::mat4 childBindPose = inverse(childInvBindPose);
            glm::vec3 childPosition = glm::vec3(childBindPose[3]);

            glm::mat4 parentInvBindPose = skeleton.BoneInverseBindPoseTransforms[parentBoneID];
            glm::mat4 parentBindPose = inverse(parentInvBindPose);
            glm::vec3 parentPosition = glm::vec3(parentBindPose[3]);

            skeleton.BoneLengths[boneID] = length(childPosition - parentPosition);
        }
    }

    // Upload bone indices
    glGenBuffers(1, &skeleton.BoneEBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, skeleton.BoneEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, boneIndices.size() * sizeof(boneIndices[0]), boneIndices.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    scene->Skeletons.push_back(std::move(skeleton));
    return (int)scene->Skeletons.size() - 1;
}

static int LoadMD5Skeleton(
    Scene* scene,
    const aiScene* aiscene)
{
    aiNode* root = aiscene->mRootNode;

    if (strcmp(root->mName.C_Str(), "<MD5_Root>") != 0)
    {
        fprintf(stderr, "Expected <MD5_Root>, got %s\n", root->mName.C_Str());
        exit(1);
    }

    // Global skeleton transformation
    glm::mat4 skeletonTransform = glm::transpose(glm::make_mat4(&root->mTransformation.a1));

    // Retrieve inverse bind pose transformation for each mesh's bones
    std::unordered_map<std::string, glm::mat4> invBindPoseTransforms;
    for (int meshIdx = 0; meshIdx < (int)aiscene->mNumMeshes; meshIdx++)
    {
        for (int boneIdx = 0; boneIdx < (int)aiscene->mMeshes[meshIdx]->mNumBones; boneIdx++)
        {
            const aiBone* bone = aiscene->mMeshes[meshIdx]->mBones[boneIdx];
            std::string boneName = bone->mName.C_Str();
            invBindPoseTransforms[boneName] = glm::transpose(glm::make_mat4(&bone->mOffsetMatrix.a1));
        }
    }

    // traverse all children
    for (int childIdx = 0; childIdx < (int)root->mNumChildren; childIdx++)
    {
        aiNode* child = root->mChildren[childIdx];

        if (strcmp(child->mName.C_Str(), "<MD5_Hierarchy>") == 0)
        {
            // Found skeleton
            int skeletonID = LoadMD5SkeletonNode(scene, child, invBindPoseTransforms);
            scene->Skeletons[skeletonID].Transform = skeletonTransform;
            return skeletonID;
        }
    }

    fprintf(stderr, "Failed to find skeleton in scene\n");
    exit(1);
    return -1;
}

static void LoadMD5Meshes(
    Scene* scene,
    int skeletonID,
    const char* modelFolder, const char* meshFile,
    aiMesh** meshes, int numMeshes,
    const int* materialIDMapping, // 1-1 mapping with assimp scene materials
    int* bindPoseMeshIDMapping)
{
    for (int meshIdx = 0; meshIdx < numMeshes; meshIdx++)
    {
        aiMesh* mesh = meshes[meshIdx];

        if (mesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE)
        {
            fprintf(stderr, "Mesh %s was not made out of triangles\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mTextureCoords[0])
        {
            fprintf(stderr, "Mesh %s didn't have TexCoord\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mNormals)
        {
            fprintf(stderr, "Mesh %s didn't have normals\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mTangents)
        {
            fprintf(stderr, "Mesh %s didn't have tangents\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mBitangents)
        {
            fprintf(stderr, "Mesh %s didn't have bitangents\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mBones)
        {
            fprintf(stderr, "Mesh %s didn't have bones\n", mesh->mName.C_Str());
            exit(1);
        }

        int vertexCount = (int)mesh->mNumVertices;
        std::vector<PositionVertex> positions(vertexCount);
        std::vector<TexCoordVertex> texCoords(vertexCount);
        std::vector<DifferentialVertex> differentials(vertexCount);
        for (int vertexIdx = 0; vertexIdx < vertexCount; vertexIdx++)
        {
            positions[vertexIdx].Position = glm::make_vec3(&mesh->mVertices[vertexIdx][0]);
            texCoords[vertexIdx].TexCoord = glm::make_vec2(&mesh->mTextureCoords[0][vertexIdx][0]);
            differentials[vertexIdx].Normal = glm::make_vec3(&mesh->mNormals[vertexIdx][0]);
            differentials[vertexIdx].Tangent = glm::make_vec3(&mesh->mTangents[vertexIdx][0]);
            differentials[vertexIdx].Bitangent = glm::make_vec3(&mesh->mBitangents[vertexIdx][0]);
        }

        const Skeleton& skeleton = scene->Skeletons[skeletonID];

        int boneCount = (int)mesh->mNumBones;
        std::vector<BoneWeightVertex> boneWeights(vertexCount);
        std::vector<int> vertexNumBones(vertexCount);
        for (int boneIdx = 0; boneIdx < boneCount; boneIdx++)
        {
            aiBone* bone = mesh->mBones[boneIdx];
            int boneWeightCount = (int)bone->mNumWeights;

            auto foundBone = skeleton.BoneNameToID.find(bone->mName.C_Str());
            if (foundBone == end(skeleton.BoneNameToID))
            {
                fprintf(stderr, "Couldn't find bone %s in skeleton\n", bone->mName.C_Str());
                exit(1);
            }

            int boneID = foundBone->second;

            for (int weightIdx = 0; weightIdx < boneWeightCount; weightIdx++)
            {
                aiVertexWeight vertexWeight = bone->mWeights[weightIdx];
                int vertexID = vertexWeight.mVertexId;
                float weight = vertexWeight.mWeight;

                if (vertexNumBones[vertexID] < 4)
                {
                    boneWeights[vertexID].BoneIDs[vertexNumBones[vertexID]] = boneID;
                    boneWeights[vertexID].Weights[vertexNumBones[vertexID]] = weight;
                    vertexNumBones[vertexID]++;
                }
                else if (boneWeights[vertexID].Weights[3] < weight)
                {
                    // Keep the top 4 influencing weights in sorted order. bubble down.
                    boneWeights[vertexID].Weights[3] = weight;
                    for (int nextWeight = 2; nextWeight >= 0; nextWeight--)
                    {
                        if (boneWeights[vertexID].Weights[nextWeight] >= boneWeights[vertexID].Weights[nextWeight + 1])
                        {
                            break;
                        }

                        std::swap(boneWeights[vertexID].Weights[nextWeight], boneWeights[vertexID].Weights[nextWeight + 1]);
                    }
                }
            }
        }

        int faceCount = (int)mesh->mNumFaces;
        std::vector<glm::uvec3> indices(faceCount);
        for (int faceIdx = 0; faceIdx < faceCount; faceIdx++)
        {
            indices[faceIdx] = glm::make_vec3(&mesh->mFaces[faceIdx].mIndices[0]);
        }

        BindPoseMesh bindPoseMesh;
        bindPoseMesh.NumVertices = vertexCount;
        bindPoseMesh.NumIndices = faceCount * 3;
        bindPoseMesh.SkeletonID = skeletonID;
        bindPoseMesh.MaterialID = materialIDMapping[mesh->mMaterialIndex];

        glGenBuffers(1, &bindPoseMesh.PositionVBO);
        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.PositionVBO);
        glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(positions[0]), positions.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glGenBuffers(1, &bindPoseMesh.TexCoordVBO);
        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.TexCoordVBO);
        glBufferData(GL_ARRAY_BUFFER, texCoords.size() * sizeof(texCoords[0]), texCoords.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glGenBuffers(1, &bindPoseMesh.DifferentialVBO);
        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.DifferentialVBO);
        glBufferData(GL_ARRAY_BUFFER, differentials.size() * sizeof(differentials[0]), differentials.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glGenBuffers(1, &bindPoseMesh.BoneVBO);
        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.BoneVBO);
        glBufferData(GL_ARRAY_BUFFER, boneWeights.size() * sizeof(boneWeights[0]), boneWeights.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glGenBuffers(1, &bindPoseMesh.EBO);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bindPoseMesh.EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(indices[0]), indices.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

        glGenVertexArrays(1, &bindPoseMesh.SkinningVAO);
        glBindVertexArray(bindPoseMesh.SkinningVAO);

        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.PositionVBO);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(PositionVertex), (GLvoid*)offsetof(PositionVertex, Position));
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(0);

        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.TexCoordVBO);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(TexCoordVertex), (GLvoid*)offsetof(TexCoordVertex, TexCoord));
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(1);

        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.DifferentialVBO);
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(DifferentialVertex), (GLvoid*)offsetof(DifferentialVertex, Normal));
        glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(DifferentialVertex), (GLvoid*)offsetof(DifferentialVertex, Tangent));
        glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(DifferentialVertex), (GLvoid*)offsetof(DifferentialVertex, Bitangent));
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(2);
        glEnableVertexAttribArray(3);
        glEnableVertexAttribArray(4);

        glBindBuffer(GL_ARRAY_BUFFER, bindPoseMesh.BoneVBO);
        glVertexAttribIPointer(5, 4, GL_UNSIGNED_BYTE, sizeof(BoneWeightVertex), (GLvoid*)offsetof(BoneWeightVertex, BoneIDs));
        glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(BoneWeightVertex), (GLvoid*)offsetof(BoneWeightVertex, Weights));
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(5);
        glEnableVertexAttribArray(6);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bindPoseMesh.EBO);

        glBindVertexArray(0);

        if (bindPoseMeshIDMapping)
        {
            bindPoseMeshIDMapping[meshIdx] = (int)scene->BindPoseMeshes.size();
        }
        scene->BindPoseMeshes.push_back(std::move(bindPoseMesh));
    }
}

void LoadMD5Mesh(
    Scene* scene,
    const char* assetFolder, const char* modelFolder,
    const char* meshFile,
    std::vector<int>* loadedMaterialIDs,
    int* loadedSkeletonID,
    std::vector<int>* loadedBindPoseMeshIDs)
{
    std::string meshpath = std::string(assetFolder) + modelFolder + meshFile;
    const aiScene* aiscene = aiImportFile(meshpath.c_str(), aiProcessPreset_TargetRealtime_MaxQuality);
    if (!aiscene)
    {
        fprintf(stderr, "aiImportFile: %s\n", aiGetErrorString());
        exit(1);
    }

    std::vector<int> materialIDMapping(aiscene->mNumMaterials);
    LoadMD5Materials(
        scene,
        assetFolder, modelFolder,
        aiscene->mMaterials, (int)aiscene->mNumMaterials,
        materialIDMapping.data());

    int skeletonID = LoadMD5Skeleton(scene, aiscene);
    if (loadedSkeletonID) *loadedSkeletonID = skeletonID;

    if (loadedBindPoseMeshIDs)
    {
        loadedBindPoseMeshIDs->resize(aiscene->mNumMeshes);
    }

    LoadMD5Meshes(
        scene,
        skeletonID,
        modelFolder, meshFile,
        &aiscene->mMeshes[0], (int)aiscene->mNumMeshes,
        materialIDMapping.data(),
        loadedBindPoseMeshIDs ? loadedBindPoseMeshIDs->data() : NULL);

    if (loadedMaterialIDs)
    {
        *loadedMaterialIDs = std::move(materialIDMapping);
    }

    aiReleaseImport(aiscene);
}

void LoadMD5Anim(
    Scene* scene,
    int skeletonID,
    const char* assetFolder, const char* modelFolder,
    const char* animFile,
    int* loadedAnimSequenceID)
{
    std::string fullpath = std::string(assetFolder) + modelFolder + animFile;
    const aiScene* animScene = aiImportFile(fullpath.c_str(), aiProcessPreset_TargetRealtime_MaxQuality);

    // Check if file exists and was successfully parsed
    if (!animScene)
    {
        fprintf(stderr, "aiImportFile: %s\n", aiGetErrorString());
        return;
    }

    // Check if file contains an animation
    if (animScene->mNumAnimations != 1)
    {
        fprintf(stderr, "Expected 1 animation in %s, got %d\n", fullpath.c_str(), (int)animScene->mNumAnimations);
        return;
    }

    // TODO: Check if animation is valid for the skeleton

    // One animation per file
    const aiAnimation* animation = animScene->mAnimations[0];

    AnimSequence animSequence;
    animSequence.Name = std::string(modelFolder) + animFile;
    animSequence.SkeletonID = skeletonID;
    animSequence.FramesPerSecond = (int)animation->mTicksPerSecond;
    animSequence.NumFrames = (int)animation->mDuration;

    // Allocate storage for each bone
    animSequence.BoneBaseFrame.resize(animation->mNumChannels);
    animSequence.BoneChannelBits.resize(animation->mNumChannels);
    animSequence.BoneFrameDataOffsets.resize(animation->mNumChannels);

    int numFrameComponents = 0;

    // For each bone
    for (int bone = 0; bone < (int)animation->mNumChannels; bone++)
    {
        aiNodeAnim* boneAnim = animation->mChannels[bone];

        // Base frame bone position
        aiVector3D baseT = boneAnim->mPositionKeys[0].mValue;
        animSequence.BoneBaseFrame[bone].T = glm::vec3(baseT.x, baseT.y, baseT.z);

        // Base frame bone orientation
        aiQuaternion baseQ = boneAnim->mRotationKeys[0].mValue;
        animSequence.BoneBaseFrame[bone].Q = glm::quat(baseQ.w, baseQ.x, baseQ.y, baseQ.z);

        // Find which position components of this bone are animated
        glm::bvec3 isAnimatedT(false);
        for (int i = 1; i < (int)boneAnim->mNumPositionKeys; i++)
        {
            aiVector3D v0 = boneAnim->mPositionKeys[i - 1].mValue;
            aiVector3D v1 = boneAnim->mPositionKeys[i].mValue;

            if (v0.x != v1.x) { isAnimatedT.x = true; }
            if (v0.y != v1.y) { isAnimatedT.y = true; }
            if (v0.z != v1.z) { isAnimatedT.z = true; }

            if (all(isAnimatedT))
            {
                break;
            }
        }

        // Find which orientation components of this bone are animated
        glm::bvec3 isAnimatedQ(false);
        for (int i = 1; i < (int)boneAnim->mNumRotationKeys; i++)
        {
            aiQuaternion q0 = boneAnim->mRotationKeys[i - 1].mValue;
            aiQuaternion q1 = boneAnim->mRotationKeys[i].mValue;

            if (q0.x != q1.x) { isAnimatedQ.x = true; }
            if (q0.y != q1.y) { isAnimatedQ.y = true; }
            if (q0.z != q1.z) { isAnimatedQ.z = true; }

            if (all(isAnimatedQ))
            {
                break;
            }
        }

        // Encode which position and orientation components are animated
        animSequence.BoneChannelBits[bone] |= isAnimatedT.x ? ANIMCHANNEL_TX_BIT : 0;
        animSequence.BoneChannelBits[bone] |= isAnimatedT.y ? ANIMCHANNEL_TY_BIT : 0;
        animSequence.BoneChannelBits[bone] |= isAnimatedT.z ? ANIMCHANNEL_TZ_BIT : 0;
        animSequence.BoneChannelBits[bone] |= isAnimatedQ.x ? ANIMCHANNEL_QX_BIT : 0;
        animSequence.BoneChannelBits[bone] |= isAnimatedQ.y ? ANIMCHANNEL_QY_BIT : 0;
        animSequence.BoneChannelBits[bone] |= isAnimatedQ.z ? ANIMCHANNEL_QZ_BIT : 0;

        animSequence.BoneFrameDataOffsets[bone] = numFrameComponents;

        // Update offset for the next bone
        for (uint8_t bits = animSequence.BoneChannelBits[bone]; bits != 0; bits &= (bits - 1))
        {
            numFrameComponents++;
        }
    }

    // Create storage for frame data
    animSequence.BoneFrameData.resize(animSequence.NumFrames * numFrameComponents);
    animSequence.NumFrameComponents = numFrameComponents;

    // Generate encoded frame data
    for (int bone = 0; bone < (int)animation->mNumChannels; bone++)
    {
        for (int frame = 0; frame < animSequence.NumFrames; frame++)
        {
            int off = animSequence.BoneFrameDataOffsets[bone];
            uint8_t bits = animSequence.BoneChannelBits[bone];

            if (bits & ANIMCHANNEL_TX_BIT)
            {
                int index = frame * numFrameComponents + off++;
                animSequence.BoneFrameData[index] = animation->mChannels[bone]->mPositionKeys[frame].mValue.x;
            }
            if (bits & ANIMCHANNEL_TY_BIT)
            {
                int index = frame * numFrameComponents + off++;
                animSequence.BoneFrameData[index] = animation->mChannels[bone]->mPositionKeys[frame].mValue.y;
            }
            if (bits & ANIMCHANNEL_TZ_BIT)
            {
                int index = frame * numFrameComponents + off++;
                animSequence.BoneFrameData[index] = animation->mChannels[bone]->mPositionKeys[frame].mValue.z;
            }
            if (bits & ANIMCHANNEL_QX_BIT)
            {
                int index = frame * numFrameComponents + off++;
                animSequence.BoneFrameData[index] = animation->mChannels[bone]->mRotationKeys[frame].mValue.x;
            }
            if (bits & ANIMCHANNEL_QY_BIT)
            {
                int index = frame * numFrameComponents + off++;
                animSequence.BoneFrameData[index] = animation->mChannels[bone]->mRotationKeys[frame].mValue.y;
            }
            if (bits & ANIMCHANNEL_QZ_BIT)
            {
                int index = frame * numFrameComponents + off++;
                animSequence.BoneFrameData[index] = animation->mChannels[bone]->mRotationKeys[frame].mValue.z;
            }
        }
    }

    scene->AnimSequences.push_back(std::move(animSequence));

    int animSequenceID = (int)scene->AnimSequences.size() - 1;
    if (loadedAnimSequenceID)
    {
        *loadedAnimSequenceID = animSequenceID;
    }

    aiReleaseImport(animScene);
}

static void LoadOBJMeshes(
    Scene* scene,
    const char* modelFolder, const char* meshFile,
    aiMesh** meshes, int numMeshes,
    const int* materialIDMapping, // 1-1 mapping with assimp scene materials
    int* staticMeshIDMapping)
{
    for (int meshIdx = 0; meshIdx < numMeshes; meshIdx++)
    {
        aiMesh* mesh = meshes[meshIdx];

        if (mesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE)
        {
            fprintf(stderr, "Mesh %s was not made out of triangles\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mTextureCoords[0])
        {
            fprintf(stderr, "Mesh %s didn't have TexCoord\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mNormals)
        {
            fprintf(stderr, "Mesh %s didn't have normals\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mTangents)
        {
            fprintf(stderr, "Mesh %s didn't have tangents\n", mesh->mName.C_Str());
            exit(1);
        }

        if (!mesh->mBitangents)
        {
            fprintf(stderr, "Mesh %s didn't have bitangents\n", mesh->mName.C_Str());
            exit(1);
        }

        int vertexCount = (int)mesh->mNumVertices;
        std::vector<PositionVertex> positions(vertexCount);
        std::vector<TexCoordVertex> texCoords(vertexCount);
        std::vector<DifferentialVertex> differentials(vertexCount);
        for (int vertexIdx = 0; vertexIdx < vertexCount; vertexIdx++)
        {
            positions[vertexIdx].Position = glm::make_vec3(&mesh->mVertices[vertexIdx][0]);
            texCoords[vertexIdx].TexCoord = glm::make_vec2(&mesh->mTextureCoords[0][vertexIdx][0]);
            differentials[vertexIdx].Normal = glm::make_vec3(&mesh->mNormals[vertexIdx][0]);
            differentials[vertexIdx].Tangent = glm::make_vec3(&mesh->mTangents[vertexIdx][0]);
            differentials[vertexIdx].Bitangent = glm::make_vec3(&mesh->mBitangents[vertexIdx][0]);
        }

        int faceCount = (int)mesh->mNumFaces;
        std::vector<glm::uvec3> indices(faceCount);
        for (int faceIdx = 0; faceIdx < faceCount; faceIdx++)
        {
            indices[faceIdx] = glm::make_vec3(&mesh->mFaces[faceIdx].mIndices[0]);
        }

        StaticMesh staticMesh;
        staticMesh.NumVertices = vertexCount;
        staticMesh.NumIndices = faceCount * 3;
        staticMesh.MaterialID = materialIDMapping[mesh->mMaterialIndex];

        glGenBuffers(1, &staticMesh.PositionVBO);
        glBindBuffer(GL_ARRAY_BUFFER, staticMesh.PositionVBO);
        glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(positions[0]), positions.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glGenBuffers(1, &staticMesh.TexCoordVBO);
        glBindBuffer(GL_ARRAY_BUFFER, staticMesh.TexCoordVBO);
        glBufferData(GL_ARRAY_BUFFER, texCoords.size() * sizeof(texCoords[0]), texCoords.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glGenBuffers(1, &staticMesh.DifferentialVBO);
        glBindBuffer(GL_ARRAY_BUFFER, staticMesh.DifferentialVBO);
        glBufferData(GL_ARRAY_BUFFER, differentials.size() * sizeof(differentials[0]), differentials.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glGenBuffers(1, &staticMesh.MeshEBO);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, staticMesh.MeshEBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(indices[0]), indices.data(), GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

        glGenVertexArrays(1, &staticMesh.MeshVAO);
        glBindVertexArray(staticMesh.MeshVAO);

        glBindBuffer(GL_ARRAY_BUFFER, staticMesh.PositionVBO);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(PositionVertex), (GLvoid*)offsetof(PositionVertex, Position));
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(0);

        glBindBuffer(GL_ARRAY_BUFFER, staticMesh.TexCoordVBO);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(TexCoordVertex), (GLvoid*)offsetof(TexCoordVertex, TexCoord));
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(1);

        glBindBuffer(GL_ARRAY_BUFFER, staticMesh.DifferentialVBO);
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(DifferentialVertex), (GLvoid*)offsetof(DifferentialVertex, Normal));
        glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(DifferentialVertex), (GLvoid*)offsetof(DifferentialVertex, Tangent));
        glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(DifferentialVertex), (GLvoid*)offsetof(DifferentialVertex, Bitangent));
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(2);
        glEnableVertexAttribArray(3);
        glEnableVertexAttribArray(4);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, staticMesh.MeshEBO);

        glBindVertexArray(0);

        if (staticMeshIDMapping)
        {
            staticMeshIDMapping[meshIdx] = (int)scene->StaticMeshes.size();
        }
        scene->StaticMeshes.push_back(std::move(staticMesh));
    }
}

void LoadOBJMesh(
    Scene* scene,
    const char* assetFolder, const char* modelFolder,
    const char* objFile,
    std::vector<int>* loadedMaterialIDs,
    std::vector<int>* loadedStaticMeshIDs)
{
    std::string meshpath = std::string(assetFolder) + modelFolder + objFile;
    const aiScene* aiscene = aiImportFile(meshpath.c_str(), aiProcessPreset_TargetRealtime_MaxQuality);
    if (!aiscene)
    {
        fprintf(stderr, "aiImportFile: %s\n", aiGetErrorString());
        exit(1);
    }

    std::vector<int> materialIDMapping(aiscene->mNumMaterials);
    LoadMD5Materials(
        scene,
        assetFolder, modelFolder,
        aiscene->mMaterials, (int)aiscene->mNumMaterials,
        materialIDMapping.data());

    std::vector<int> staticMeshIDMapping(aiscene->mNumMeshes);
    LoadOBJMeshes(
        scene,
        modelFolder, objFile,
        aiscene->mMeshes, (int)aiscene->mNumMeshes,
        materialIDMapping.data(),
        staticMeshIDMapping.data());

    if (loadedMaterialIDs)
    {
        *loadedMaterialIDs = std::move(materialIDMapping);
    }

    if (loadedStaticMeshIDs)
    {
        *loadedStaticMeshIDs = std::move(staticMeshIDMapping);
    }

    aiReleaseImport(aiscene);
}


text:
package io.github.droidkaigi.confsched2017.viewmodel;

import android.content.Context;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.view.View;

import java.util.Date;

import io.github.droidkaigi.confsched2017.BR;
import io.github.droidkaigi.confsched2017.R;
import io.github.droidkaigi.confsched2017.model.Session;
import io.github.droidkaigi.confsched2017.repository.sessions.MySessionsRepository;
import io.github.droidkaigi.confsched2017.util.DateUtil;
import timber.log.Timber;

public class SessionViewModel extends BaseObservable implements ViewModel {

    private static final String TAG = SessionViewModel.class.getSimpleName();

    private Session session;

    private String shortStime = "";

    private String formattedDate = "";

    private String title = "";

    private String speakerName = "";

    private String roomName = "";

    private String languageId = "";

    private String minutes = "";

    private int rowSpan = 1;

    private int colSpan = 1;

    private int titleMaxLines = 3;

    private int speakerNameMaxLines = 1;

    @DrawableRes
    private int backgroundResId;

    @ColorRes
    private int topicColorResId;

    private boolean isClickable;

    private int checkVisibility;

    private int normalSessionItemVisibility;

    private int languageVisibility;

    private MySessionsRepository mySessionsRepository;

    private Callback callback;

    SessionViewModel(@NonNull Session session, Context context, int roomCount, boolean isMySession,
            MySessionsRepository mySessionsRepository) {
        this.session = session;
        this.shortStime = DateUtil.getHourMinute(session.stime);
        this.formattedDate = DateUtil.getMonthDate(session.stime, context);
        this.title = session.title;

        if (session.speaker != null) {
            this.speakerName = session.speaker.name;
        }
        if (session.room != null) {
            this.roomName = session.room.name;
        }
        if (session.lang != null) {
            this.languageId = session.lang.toUpperCase();
        }
        this.languageVisibility = session.lang != null ? View.VISIBLE : View.GONE;

        this.minutes = context.getString(R.string.session_minutes, session.durationMin);

        decideRowSpan(session);
        this.colSpan = decideColSpan(session, roomCount);

        this.checkVisibility = isMySession ? View.VISIBLE : View.GONE;

        if (session.isBreak()) {
            this.isClickable = false;
            this.backgroundResId = R.drawable.bg_empty_session;
            this.topicColorResId = android.R.color.transparent;
            this.normalSessionItemVisibility = View.GONE;
        } else {
            this.isClickable = true;
            this.backgroundResId = session.isLiveAt(new Date())? R.drawable.clickable_purple : R.drawable.clickable_white;
            this.topicColorResId = TopicColor.from(session.topic).middleColorResId;
            this.normalSessionItemVisibility = View.VISIBLE;
        }

        this.mySessionsRepository = mySessionsRepository;
    }

    private SessionViewModel(int rowSpan, int colSpan) {
        this.rowSpan = rowSpan;
        this.colSpan = colSpan;
        this.isClickable = false;
        this.backgroundResId = R.drawable.bg_empty_session;
        this.topicColorResId = android.R.color.transparent;
        this.checkVisibility = View.GONE;
        this.normalSessionItemVisibility = View.GONE;
    }

    static SessionViewModel createEmpty(int rowSpan) {
        return createEmpty(rowSpan, 1);
    }

    static SessionViewModel createEmpty(int rowSpan, int colSpan) {
        return new SessionViewModel(rowSpan, colSpan);
    }

    private int decideColSpan(@NonNull Session session, int roomCount) {
        if (session.isCeremony()) {
            return 3;
        } else if (session.isBreak() || session.isDinner()) {
            return roomCount;
        } else {
            return 1;
        }
    }

    private void decideRowSpan(@NonNull Session session) {
        // Break time is over 30 min, but one row is good
        if (session.durationMin > 30 && !session.isBreak()) {
            this.rowSpan = this.rowSpan * 2;
            this.titleMaxLines = this.titleMaxLines * 2;
            this.speakerNameMaxLines = this.speakerNameMaxLines * 3;
        }
    }

    Date getStime() {
        return session.stime;
    }

    public void showSessionDetail(@SuppressWarnings("unused") View view) {
        if (callback != null && session != null) {
            callback.showSessionDetail(session);
        }
    }

    public boolean checkSession(@SuppressWarnings("UnusedParameters") View view) {
        if (mySessionsRepository == null) {
            return false;
        }

        if (mySessionsRepository.isExist(session.id)) {
            mySessionsRepository.delete(session)
                    .subscribe((result) -> setCheckVisibility(View.GONE),
                            throwable -> Timber.tag(TAG).e(throwable, "Failed to delete my session"));
        } else {
            mySessionsRepository.save(session)
                    .subscribe(() -> setCheckVisibility(View.VISIBLE),
                            throwable -> Timber.tag(TAG).e(throwable, "Failed to save my session"));
        }
        return true;
    }

    @Override
    public void destroy() {
        callback = null;
    }

    public String getShortStime() {
        return shortStime;
    }

    public String getFormattedDate() {
        return formattedDate;
    }

    public String getTitle() {
        return title;
    }

    public String getSpeakerName() {
        return speakerName;
    }

    String getRoomName() {
        return roomName;
    }

    public String getLanguageId() {
        return languageId;
    }

    public String getMinutes() {
        return minutes;
    }

    public int getRowSpan() {
        return rowSpan;
    }

    public int getColSpan() {
        return colSpan;
    }

    public int getBackgroundResId() {
        return backgroundResId;
    }

    public boolean isClickable() {
        return isClickable;
    }

    public int getTitleMaxLines() {
        return titleMaxLines;
    }

    public int getSpeakerNameMaxLines() {
        return speakerNameMaxLines;
    }

    public int getTopicColorResId() {
        return topicColorResId;
    }

    public int getNormalSessionItemVisibility() {
        return normalSessionItemVisibility;
    }

    public int getLanguageVisibility() {
        return languageVisibility;
    }

    @Bindable
    public int getCheckVisibility() {
        return checkVisibility;
    }

    private void setCheckVisibility(int visibility) {
        checkVisibility = visibility;
        notifyPropertyChanged(BR.checkVisibility);
    }

    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    public interface Callback {

        void showSessionDetail(@NonNull Session session);
    }
}


text:
<?php

namespace League\OAuth2\Client\Provider;

use League\OAuth2\Client\Token\AccessToken;
use Psr\Http\Message\ResponseInterface;

class YouTube extends AbstractProvider
{
    /**
     * Default scopes
     *
     * @var array
     */
    public $defaultScopes = ['https://www.googleapis.com/auth/youtube.readonly', 'https://www.googleapis.com/auth/userinfo.profile'];

    /**
     * Get the string used to separate scopes.
     *
     * @return string
     */
    protected function getScopeSeparator()
    {
        return ' ';
    }

    /**
     * Returns the base URL for authorizing a client.
     *
     * @return string
     */
    public function getBaseAuthorizationUrl()
    {
        return 'https://accounts.google.com/o/oauth2/auth';
    }

    /**
     * Returns the base URL for requesting an access token.
     *
     * @param  array $params
     * @return string
     */
    public function getBaseAccessTokenUrl(array $params)
    {
        return 'https://accounts.google.com/o/oauth2/token';
    }

    /**
     * Returns the URL for requesting the resource owner's details.
     *
     * @param  AccessToken $token
     * @return string
     */
    public function getResourceOwnerDetailsUrl(AccessToken $token)
    {
        return "https://www.googleapis.com/youtube/v3/channels?mine=true&part=id,snippet&access_token=".$token;
    }

    /**
     * Get the default scopes used by this provider.
     *
     * This should not be a complete list of all scopes, but the minimum
     * required for the provider user interface!
     *
     * @return array
     */
    protected function getDefaultScopes()
    {
        return $this->defaultScopes;
    }

    /**
     * Check a provider response for errors.
     *
     * @throws IdentityProviderException
     * @param  ResponseInterface $response
     * @param  string $data Parsed response data
     * @return void
     */
    protected function checkResponse(ResponseInterface $response, $data)
    {
        return false;
    }

    /**
     * Generate a user object from a successful user details request.
     *
     * @param array $response
     * @param AccessToken $token
     * @return ResourceOwnerInterface
     */
    protected function createResourceOwner(array $response, AccessToken $token)
    {
        return new YouTubeResourceOwner($response);
    }
}


text: Student Version of Exam Software, for Andriod devices.