<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Table;

\defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Language\Text;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\ParameterType;

/**
 * Usergroup table class.
 *
 * @since  1.7.0
 */
class Usergroup extends Table
{
	/**
	 * Constructor
	 *
	 * @param   DatabaseDriver  $db  Database driver object.
	 *
	 * @since   1.7.0
	 */
	public function __construct(DatabaseDriver $db)
	{
		parent::__construct('#__usergroups', 'id', $db);
	}

	/**
	 * Method to check the current record to save
	 *
	 * @return  boolean  True on success
	 *
	 * @since   1.7.0
	 */
	public function check()
	{
		try
		{
			parent::check();
		}
		catch (\Exception $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		// Validate the title.
		if ((trim($this->title)) == '')
		{
			$this->setError(Text::_('JLIB_DATABASE_ERROR_USERGROUP_TITLE'));

			return false;
		}

		// The parent_id can not be equal to the current id
		if ($this->id === (int) $this->parent_id)
		{
			$this->setError(Text::_('JLIB_DATABASE_ERROR_USERGROUP_PARENT_ID_NOT_VALID'));

			return false;
		}

		// Check for a duplicate parent_id, title.
		// There is a unique index on the (parent_id, title) field in the table.
		$db       = $this->_db;
		$parentId = (int) $this->parent_id;
		$title    = trim($this->title);
		$id       = (int) $this->id;
		$query    = $db->getQuery(true)
			->select('COUNT(title)')
			->from($this->_tbl)
			->where($db->quoteName('title') . ' = :title')
			->where($db->quoteName('parent_id') . ' = :parentid')
			->where($db->quoteName('id') . ' <> :id')
			->bind(':title', $title)
			->bind(':parentid', $parentId, ParameterType::INTEGER)
			->bind(':id', $id, ParameterType::INTEGER);
		$db->setQuery($query);

		if ($db->loadResult() > 0)
		{
			$this->setError(Text::_('JLIB_DATABASE_ERROR_USERGROUP_TITLE_EXISTS'));

			return false;
		}

		// We do not allow to move non public to root and public to non-root
		if (!empty($this->id))
		{
			$table = self::getInstance('Usergroup', 'JTable', array('dbo' => $this->getDbo()));

			$table->load($this->id);

			if ((!$table->parent_id && $this->parent_id) || ($table->parent_id && !$this->parent_id))
			{
				$this->setError(Text::_('JLIB_DATABASE_ERROR_USERGROUP_PARENT_ID_NOT_VALID'));

				return false;
			}
		}
		// New entry should always be greater 0
		elseif (!$this->parent_id)
		{
			$this->setError(Text::_('JLIB_DATABASE_ERROR_USERGROUP_PARENT_ID_NOT_VALID'));

			return false;
		}

		// The new parent_id has to be a valid group
		if ($this->parent_id)
		{
			$table = self::getInstance('Usergroup', 'JTable', array('dbo' => $this->getDbo()));
			$table->load($this->parent_id);

			if ($table->id != $this->parent_id)
			{
				$this->setError(Text::_('JLIB_DATABASE_ERROR_USERGROUP_PARENT_ID_NOT_VALID'));

				return false;
			}
		}

		return true;
	}

	/**
	 * Method to recursively rebuild the nested set tree.
	 *
	 * @param   integer  $parentId  The root of the tree to rebuild.
	 * @param   integer  $left      The left id to start with in building the tree.
	 *
	 * @return  boolean  True on success
	 *
	 * @since   1.7.0
	 */
	public function rebuild($parentId = 0, $left = 0)
	{
		// Get the database object
		$db       = $this->_db;
		$query    = $db->getQuery(true);
		$parentId = (int) $parentId;

		// Get all children of this node
		$query->clear()
			->select($db->quoteName('id'))
			->from($db->quoteName($this->_tbl))
			->where($db->quoteName('parent_id') . ' = :parentid')
			->bind(':parentid', $parentId, ParameterType::INTEGER)
			->order([$db->quoteName('parent_id'), $db->quoteName('title')]);

		$db->setQuery($query);
		$children = $db->loadColumn();

		// The right value of this node is the left value + 1
		$right = $left + 1;

		// Execute this function recursively over all children
		for ($i = 0, $n = \count($children); $i < $n; $i++)
		{
			// $right is the current right value, which is incremented on recursion return
			$right = $this->rebuild($children[$i], $right);

			// If there is an update failure, return false to break out of the recursion
			if ($right === false)
			{
				return false;
			}
		}

		$left  = (int) $left;
		$right = (int) $right;

		// We've got the left value, and now that we've processed
		// the children of this node we also know the right value
		$query->clear()
			->update($db->quoteName($this->_tbl))
			->set($db->quoteName('lft') . ' = :lft')
			->set($db->quoteName('rgt') . ' = :rgt')
			->where($db->quoteName('id') . ' = :id')
			->bind(':lft', $left, ParameterType::INTEGER)
			->bind(':rgt', $right, ParameterType::INTEGER)
			->bind(':id', $parentId, ParameterType::INTEGER);
		$db->setQuery($query);

		// If there is an update failure, return false to break out of the recursion
		try
		{
			$db->execute();
		}
		catch (ExecutionFailureException $e)
		{
			return false;
		}

		// Return the right value of this node + 1
		return $right + 1;
	}

	/**
	 * Inserts a new row if id is zero or updates an existing row in the database table
	 *
	 * @param   boolean  $updateNulls  If false, null object variables are not updated
	 *
	 * @return  boolean  True if successful, false otherwise and an internal error message is set
	 *
	 * @since   1.7.0
	 */
	public function store($updateNulls = false)
	{
		if ($result = parent::store($updateNulls))
		{
			// Rebuild the nested set tree.
			$this->rebuild();
		}

		return $result;
	}

	/**
	 * Delete this object and its dependencies
	 *
	 * @param   integer  $oid  The primary key of the user group to delete.
	 *
	 * @return  mixed  Boolean or Exception.
	 *
	 * @since   1.7.0
	 * @throws  \RuntimeException on database error.
	 * @throws  \UnexpectedValueException on data error.
	 */
	public function delete($oid = null)
	{
		if ($oid)
		{
			$this->load($oid);
		}

		if ($this->id == 0)
		{
			throw new \UnexpectedValueException('Usergroup not found');
		}

		if ($this->parent_id == 0)
		{
			throw new \UnexpectedValueException('Root usergroup cannot be deleted.');
		}

		if ($this->lft == 0 || $this->rgt == 0)
		{
			throw new \UnexpectedValueException('Left-Right data inconsistency. Cannot delete usergroup.');
		}

		$db = $this->_db;

		$lft = (int) $this->lft;
		$rgt = (int) $this->rgt;

		// Select the usergroup ID and its children
		$query = $db->getQuery(true)
			->select($db->quoteName('c.id'))
			->from($db->quoteName($this->_tbl, 'c'))
			->where($db->quoteName('c.lft') . ' >= :lft')
			->where($db->quoteName('c.rgt') . ' <= :rgt')
			->bind(':lft', $lft, ParameterType::INTEGER)
			->bind(':rgt', $rgt, ParameterType::INTEGER);
		$db->setQuery($query);
		$ids = $db->loadColumn();

		if (empty($ids))
		{
			throw new \UnexpectedValueException('Left-Right data inconsistency. Cannot delete usergroup.');
		}

		// Delete the usergroup and its children
		$query->clear()
			->delete($db->quoteName($this->_tbl))
			->whereIn($db->quoteName('id'), $ids);
		$db->setQuery($query);
		$db->execute();

		// Rebuild the nested set tree.
		$this->rebuild();

		// Delete the usergroup in view levels
		$replace = array();

		foreach ($ids as $id)
		{
			$replace[] = ',' . $db->quote("[$id,") . ',' . $db->quote('[');
			$replace[] = ',' . $db->quote(",$id,") . ',' . $db->quote(',');
			$replace[] = ',' . $db->quote(",$id]") . ',' . $db->quote(']');
			$replace[] = ',' . $db->quote("[$id]") . ',' . $db->quote('[]');
		}

		$query->clear()
			->select(
				[
					$db->quoteName('id'),
					$db->quoteName('rules'),
				]
			)
			->from($db->quoteName('#__viewlevels'));
		$db->setQuery($query);
		$rules = $db->loadObjectList();

		$matchIds = [];

		foreach ($rules as $rule)
		{
			foreach ($ids as $id)
			{
				if (strstr($rule->rules, '[' . $id) || strstr($rule->rules, ',' . $id) || strstr($rule->rules, $id . ']'))
				{
					$matchIds[] = $rule->id;
				}
			}
		}

		if (!empty($matchIds))
		{
			$query->clear()
				->update($db->quoteName('#__viewlevels'))
				->set($db->quoteName('rules') . ' = ' . str_repeat('REPLACE(', 4 * \count($ids)) . $db->quoteName('rules') . implode(')', $replace) . ')')
				->whereIn($db->quoteName('id'), $matchIds);
			$db->setQuery($query);
			$db->execute();
		}

		// Delete the user to usergroup mappings for the group(s) from the database.
		$query->clear()
			->delete($db->quoteName('#__user_usergroup_map'))
			->whereIn($db->quoteName('group_id'), $ids);
		$db->setQuery($query);
		$db->execute();

		return true;
	}
}
