<?php
/**
 * @package     Joomla.Site
 * @subpackage  com_banners
 *
 * @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\Component\Banners\Site\Model;

\defined('_JEXEC') or die;

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;

/**
 * Banners model for the Joomla Banners component.
 *
 * @since  1.6
 */
class BannersModel extends ListModel
{
	/**
	 * Method to get a store id based on model configuration state.
	 *
	 * This is necessary because the model is used by the component and
	 * different modules that might need different sets of data or different
	 * ordering requirements.
	 *
	 * @param   string  $id  A prefix for the store id.
	 *
	 * @return  string  A store id.
	 *
	 * @since   1.6
	 */
	protected function getStoreId($id = '')
	{
		// Compile the store id.
		$id .= ':' . $this->getState('filter.search');
		$id .= ':' . $this->getState('filter.tag_search');
		$id .= ':' . $this->getState('filter.client_id');
		$id .= ':' . serialize($this->getState('filter.category_id'));
		$id .= ':' . serialize($this->getState('filter.keywords'));

		return parent::getStoreId($id);
	}

	/**
	 * Method to get a DatabaseQuery object for retrieving the data set from a database.
	 *
	 * @return  DatabaseQuery   A DatabaseQuery object to retrieve the data set.
	 *
	 * @since   1.6
	 */
	protected function getListQuery()
	{
		$db         = $this->getDbo();
		$query      = $db->getQuery(true);
		$ordering   = $this->getState('filter.ordering');
		$tagSearch  = $this->getState('filter.tag_search');
		$cid        = (int) $this->getState('filter.client_id');
		$categoryId = $this->getState('filter.category_id');
		$keywords   = $this->getState('filter.keywords');
		$randomise  = ($ordering === 'random');
		$nowDate    = Factory::getDate()->toSql();

		$query->select(
			[
				$db->quoteName('a.id'),
				$db->quoteName('a.type'),
				$db->quoteName('a.name'),
				$db->quoteName('a.clickurl'),
				$db->quoteName('a.sticky'),
				$db->quoteName('a.cid'),
				$db->quoteName('a.description'),
				$db->quoteName('a.params'),
				$db->quoteName('a.custombannercode'),
				$db->quoteName('a.track_impressions'),
				$db->quoteName('cl.track_impressions', 'client_track_impressions'),
			]
		)
			->from($db->quoteName('#__banners', 'a'))
			->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid'))
			->where($db->quoteName('a.state') . ' = 1')
			->extendWhere(
				'AND',
				[
					$db->quoteName('a.publish_up') . ' IS NULL',
					$db->quoteName('a.publish_up') . ' <= :nowDate1',
				],
				'OR'
			)
			->extendWhere(
				'AND',
				[
					$db->quoteName('a.publish_down') . ' IS NULL',
					$db->quoteName('a.publish_down') . ' >= :nowDate2',
				],
				'OR'
			)
			->extendWhere(
				'AND',
				[
					$db->quoteName('a.imptotal') . ' = 0',
					$db->quoteName('a.impmade') . ' < ' . $db->quoteName('a.imptotal'),
				],
				'OR'
			)
			->bind([':nowDate1', ':nowDate2'], $nowDate);

		if ($cid)
		{
			$query->where(
				[
					$db->quoteName('a.cid') . ' = :clientId',
					$db->quoteName('cl.state') . ' = 1',
				]
			)
				->bind(':clientId', $cid, ParameterType::INTEGER);
		}

		// Filter by a single or group of categories
		if (is_numeric($categoryId))
		{
			$categoryId = (int) $categoryId;
			$type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> ';

			// Add subcategory check
			if ($this->getState('filter.subcategories', false))
			{
				$levels = (int) $this->getState('filter.max_category_levels', '1');

				// Create a subquery for the subcategory list
				$subQuery = $db->getQuery(true);
				$subQuery->select($db->quoteName('sub.id'))
					->from($db->quoteName('#__categories', 'sub'))
					->join(
						'INNER',
						$db->quoteName('#__categories', 'this'),
						$db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft')
						. ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt')
					)
					->where(
						[
							$db->quoteName('this.id') . ' = :categoryId1',
							$db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels',
						]
					);

				// Add the subquery to the main query
				$query->extendWhere(
					'AND',
					[
						$db->quoteName('a.catid') . $type . ':categoryId2',
						$db->quoteName('a.catid') . ' IN (' . $subQuery . ')',
					],
					'OR'
				)
					->bind([':categoryId1', ':categoryId2'], $categoryId, ParameterType::INTEGER)
					->bind(':levels', $levels, ParameterType::INTEGER);
			}
			else
			{
				$query->where($db->quoteName('a.catid') . $type . ':categoryId')
					->bind(':categoryId', $categoryId, ParameterType::INTEGER);
			}
		}
		elseif (is_array($categoryId) && (count($categoryId) > 0))
		{
			$categoryId = ArrayHelper::toInteger($categoryId);

			if ($this->getState('filter.category_id.include', true))
			{
				$query->whereIn($db->quoteName('a.catid'), $categoryId);
			}
			else
			{
				$query->whereNotIn($db->quoteName('a.catid'), $categoryId);
			}
		}

		if ($tagSearch)
		{
			if (!$keywords)
			{
				// No keywords, select nothing.
				$query->where('0 != 0');
			}
			else
			{
				$temp   = array();
				$config = ComponentHelper::getParams('com_banners');
				$prefix = $config->get('metakey_prefix');

				if ($categoryId)
				{
					$query->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id'));
				}

				foreach ($keywords as $key => $keyword)
				{
					$regexp       = '[[:<:]]' . $keyword . '[[:>:]]';
					$valuesToBind = [$keyword, $keyword, $regexp];

					if ($cid)
					{
						$valuesToBind[] = $regexp;
					}

					if ($categoryId)
					{
						$valuesToBind[] = $regexp;
					}

					// Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting.
					$bounded = $query->bindArray($valuesToBind, ParameterType::STRING);

					$condition1 = $db->quoteName('a.own_prefix') . ' = 1'
						. ' AND ' . $db->quoteName('a.metakey_prefix')
						. ' = SUBSTRING(' . $bounded[0] . ',1,LENGTH(' . $db->quoteName('a.metakey_prefix') . '))'
						. ' OR ' . $db->quoteName('a.own_prefix') . ' = 0'
						. ' AND ' . $db->quoteName('cl.own_prefix') . ' = 1'
						. ' AND ' . $db->quoteName('cl.metakey_prefix')
						. ' = SUBSTRING(' . $bounded[1] . ',1,LENGTH(' . $db->quoteName('cl.metakey_prefix') . '))'
						. ' OR ' . $db->quoteName('a.own_prefix') . ' = 0'
						. ' AND ' . $db->quoteName('cl.own_prefix') . ' = 0'
						. ' AND ' . ($prefix == substr($keyword, 0, strlen($prefix)) ? '0 = 0' : '0 != 0');

					$condition2 = $db->quoteName('a.metakey') . ' ' . $query->regexp($bounded[2]);

					if ($cid)
					{
						$condition2 .= ' OR ' . $db->quoteName('cl.metakey') . ' ' . $query->regexp($bounded[3]) . ' ';
					}

					if ($categoryId)
					{
						$condition2 .= ' OR ' . $db->quoteName('cat.metakey') . ' ' . $query->regexp($bounded[4]) . ' ';
					}

					$temp[] = "($condition1) AND ($condition2)";
				}

				$query->where('(' . implode(' OR ', $temp) . ')');
			}
		}

		// Filter by language
		if ($this->getState('filter.language'))
		{
			$query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING);
		}

		$query->order($db->quoteName('a.sticky') . ' DESC, ' . ($randomise ? $query->rand() : $db->quoteName('a.ordering')));

		return $query;
	}

	/**
	 * Get a list of banners.
	 *
	 * @return  array
	 *
	 * @since   1.6
	 */
	public function getItems()
	{
		if ($this->getState('filter.tag_search'))
		{
			// Filter out empty keywords.
			$keywords = array_values(array_filter(array_map('trim', $this->getState('filter.keywords')), 'strlen'));

			// Re-set state before running the query.
			$this->setState('filter.keywords', $keywords);

			// If no keywords are provided, avoid running the query.
			if (!$keywords)
			{
				$this->cache['items'] = array();

				return $this->cache['items'];
			}
		}

		if (!isset($this->cache['items']))
		{
			$this->cache['items'] = parent::getItems();

			foreach ($this->cache['items'] as &$item)
			{
				$item->params = new Registry($item->params);
			}
		}

		return $this->cache['items'];
	}

	/**
	 * Makes impressions on a list of banners
	 *
	 * @return  void
	 *
	 * @since   1.6
	 * @throws  \Exception
	 */
	public function impress()
	{
		$trackDate = Factory::getDate()->toSql();
		$items     = $this->getItems();
		$db        = $this->getDbo();
		$bid       = [];

		if (!count($items))
		{
			return;
		}

		foreach ($items as $item)
		{
			$bid[] = (int) $item->id;
		}

		// Increment impression made
		$query = $db->getQuery(true);
		$query->update($db->quoteName('#__banners'))
			->set($db->quoteName('impmade') . ' = ' . $db->quoteName('impmade') . ' + 1')
			->whereIn($db->quoteName('id'), $bid);
		$db->setQuery($query);

		try
		{
			$db->execute();
		}
		catch (ExecutionFailureException $e)
		{
			throw new \Exception($e->getMessage(), 500);
		}

		foreach ($items as $item)
		{
			// Track impressions
			$trackImpressions = $item->track_impressions;

			if ($trackImpressions < 0 && $item->cid)
			{
				$trackImpressions = $item->client_track_impressions;
			}

			if ($trackImpressions < 0)
			{
				$config           = ComponentHelper::getParams('com_banners');
				$trackImpressions = $config->get('track_impressions');
			}

			if ($trackImpressions > 0)
			{
				// Is track already created?
				// Update count
				$query = $db->getQuery(true);
				$query->update($db->quoteName('#__banner_tracks'))
					->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1')
					->where(
						[
							$db->quoteName('track_type') . ' = 1',
							$db->quoteName('banner_id') . ' = :id',
							$db->quoteName('track_date') . ' = :trackDate',
						]
					)
					->bind(':id', $item->id, ParameterType::INTEGER)
					->bind(':trackDate', $trackDate);

				$db->setQuery($query);

				try
				{
					$db->execute();
				}
				catch (ExecutionFailureException $e)
				{
					throw new \Exception($e->getMessage(), 500);
				}

				if ($db->getAffectedRows() === 0)
				{
					// Insert new count
					$query = $db->getQuery(true);
					$query->insert($db->quoteName('#__banner_tracks'))
						->columns(
							[
								$db->quoteName('count'),
								$db->quoteName('track_type'),
								$db->quoteName('banner_id'),
								$db->quoteName('track_date'),
							]
						)
						->values('1, 1, :id, :trackDate')
						->bind(':id', $item->id, ParameterType::INTEGER)
						->bind(':trackDate', $trackDate);

					$db->setQuery($query);

					try
					{
						$db->execute();
					}
					catch (ExecutionFailureException $e)
					{
						throw new \Exception($e->getMessage(), 500);
					}
				}
			}
		}
	}
}
