<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace Tassos\Framework\Conditions;

defined('_JEXEC') or die;

use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\CMS\Language\Text;

/**
 *  Assignment Class
 */
class Condition
{
	/**
	 *  Application Object
	 *
	 *  @var  object
	 */
	protected $app;

	/**
	 *  Document Object
	 *
	 *  @var  object
	 */
	protected $doc;

	/**
	 *  Date Object
	 *
	 *  @var  object
	 */
	protected $date;

	/**
	 *  Database Object
	 *
	 *  @var  object
	 */
	protected $db;

	/**
	 *  User Object
	 *
	 *  @var  object
	 */
	protected $user;

	/**
	 *  Assignment Selection
	 *
	 *  @var  mixed
	 */
	protected $selection;

	/**
	 *  Assignment Parameters
	 *
	 *  @var  mixed
	 */
	protected $params;

	/**
	 *  Assignment State (Include|Exclude)
	 *
	 *  @var  string
	 */
    public $assignment;
    
    /**
     *  Options
	 * 
	 *  @var  object
     */
    public $options;
    
    /**
     *  Framework factory object
	 * 
	 *  @var  object
     */
    public $factory;

	/**
	 * The default operator that will be used to compare haystack with needle.
	 *
	 * @var string
	 */
	protected $operator;

	/**
	 * Class constructor
	 *
	 * @param	array  $options		The rule options. Expected properties: selection, value, params
	 * @param   object $factory		The framework's factory class.
	 */
	public function __construct($options = null, $factory = null)
	{
        $this->factory = is_null($factory) ? new \Tassos\Framework\Factory() : $factory;

		// Set General Joomla Objects
		$this->db   = $this->factory->getDbo();
		$this->app  = $this->factory->getApplication();
		$this->doc  = $this->factory->getDocument();
		$this->user = $this->factory->getUser();

		$this->options = new Registry($options);

		$this->setParams($this->options->get('params'));
		$this->setOperator($this->options->get('operator', 'includesSome'));

		// For performance reasons we might move this inside the pass() method
		$this->setSelection($this->options->get('selection', ''));
    }

	/**
	 * Set the rule's user selected value
	 *
	 * @param	mixed	$selection
	 * @return 	object
	 */
	public function setSelection($selection)
	{
		$this->selection = $selection;

		if (method_exists($this, 'prepareSelection'))
		{
			$this->selection = $this->prepareSelection();
		}

		return $this;
	}

	/**
	 * Undocumented function
	 *
	 * @return void
	 */
	public function getSelection()
	{
		return $this->selection;
	}

	/**
	 * Set the operator that will be used for the comparison
	 *
	 * @param	string $operator
	 * @return	object
	 */
	public function setOperator($operator)
	{
		$this->operator = $operator;
		return $this;
	}

	/**
	 * Set the rule's parameters
	 *
	 * @param  array $params
	 */
	public function setParams($params)
	{
		$this->params = new Registry($params);
	}

	public function getParams()
	{
		return $this->params;
	}

	/**
	 * Checks the validitity of two values based on the given operator.
	 * 
	 * Consider converting this method as a Trait.
	 * 
	 * @param   mixed   $value
	 * @param   mixed   $selection
	 * @param   string  $operator
	 * @param   array   $options 	ignoreCase: true,false
	 * 
	 * @return  bool
	 */
	public function passByOperator($value = null, $selection = null, $operator = null, $options = null)
	{
		$value = is_null($value) ? $this->value() : $value;

		if (!is_null($selection))
		{
			$this->setSelection($selection);
		}

		$selection = $this->getSelection();

		$options = new Registry($options);
		$ignoreCase = $options->get('ignoreCase', true);

		if (is_object($value))
		{
			$value = (array) $value;
		}

		if (is_object($selection))
		{
			$selection = (array) $selection;
		}

		if ($ignoreCase)
		{
			if (is_string($value))
			{
				$value = strtolower($value);
			}

			if (is_string($selection))
			{
				$selection = strtolower($selection);
			}

			if (is_array($value))
			{
				$value = array_map('strtolower', $value);
			}

			if (is_array($selection))
			{
				$selection = array_map(function($str)
				{
					return is_null($str) ? '' : strtolower($str);
				}, $selection);
			}
		}

		$operator = (is_null($operator) OR empty($operator)) ? $this->operator : $operator;
		$pass = false;

        switch ($operator)
        {
			case 'exists':
				$pass = !is_null($value);
				break;

			// Determines whether haystack is empty. Accepts: array, string
            case 'empty':
				if (is_array($value))
				{
					$pass = empty($value);
				}

				if (is_string($value))
				{
					$pass = $value == '' || trim($value) == '';
				}

				if (is_bool($value))
				{
					$pass = !$value;
				}
				break;

			case 'equals':
				if (is_array($selection) || is_array($value))
				{
					$pass = $this->passByOperator($value, $selection, 'includesSome', $options);
				}
				else
				{
					$pass = $value == $selection;
				}
				break;

			case 'contains':
				if (is_string($value) && is_string($selection))
				{
					$pass = strlen($selection) > 0 && strpos($value, $selection) !== false;
				}

				break;
			// Determine whether haystack is less than needle.
			case 'less_than':
			case 'lowerthan':
			case 'lt':
				$pass = $value < $selection;
				break;

			// Determine whether haystack is less than or equal to needle.
			case 'less_than_or_equal_to':
			case 'lowerthanequal':
			case 'lte':
				$pass = $value <= $selection;
				break;

			// Determine whether haystack is greater than needle.
			case 'greater_than':
			case 'greaterthan':
			case 'gt':
				$pass = $value > $selection;
				break;

			// Determine whether haystack is greater than or equal to needle.
			case 'greater_than_or_equal_to':
			case 'greterthanequal':
			case 'gte':
				$pass = $value >= $selection;
				break;

			// Determine whether haystack contains all elements in needle.
			case 'includesAll':
			case 'containsall':
				$pass = count(array_intersect((array) $selection, (array) $value)) == count((array) $selection);
				break;

			// Determine whether haystack contains at least one element from needle.
			case 'includesSome':
			case 'containsany':
				$pass = !empty(array_intersect((array) $value, (array) $selection));
				break;

			// Determine whether haystack contains at least one element from needle. Accepts; string, array.
			case 'includes':
				if (is_string($value) && $value != '' && is_string($selection) && $selection != '')
				{
					if (StringHelper::strpos($value, $selection) !== false)
					{
						$pass = true;
					}
				}

				if (is_array($value) || is_array($selection))
				{
					$pass = $this->passByOperator($value, $selection, 'includesSome', $options);
				}

				break;

			// Determine whether haystack starts with needle. Accepts: string
            case 'starts_with':
                $pass = StringHelper::substr($value, 0, StringHelper::strlen($selection)) === $selection;
				break;
			
			// Determine whether haystack ends with needle. Accepts: string
            case 'ends_with':
                $pass = StringHelper::substr($value, -StringHelper::strlen($selection)) === $selection;
				break;

			// Determine whether value is in given range
            case 'range':
				$value1 = isset($selection['value1']) ? (float) $selection['value1'] : false;
				$value2 = isset($selection['value2']) ? (float) $selection['value2'] : false;

                $pass = $value1 && $value2 ? (($value >= $value1) && ($value <= $value2)) : false;
				break;
			
			// Determine whether haystack equals to needle. Accepts any object.
			default:
				$pass = $value == $selection;
        }

		return $pass;
	}

    /**
     *  Base assignment check
     * 
     *  @return bool
     */
	public function pass()
	{	
		return $this->passByOperator();
	}

	/**
	 *  Returns all parent rows
	 * 
	 *  This method doesn't belong here. Move it to Functions.php.
	 *
	 *  @param   integer  $id      Row primary key
	 *  @param   string   $table   Table name
	 *  @param   string   $parent  Parent column name
	 *  @param   string   $child   Child column name
	 *
	 *  @return  array             Array with IDs
	 */
	public function getParentIds($id = 0, $table = 'menu', $parent = 'parent_id', $child = 'id')
	{
		if (!$id)
		{
			return [];
		}

		$cache = $this->factory->getCache(); 
		$hash  = md5('getParentIds_' . $id . '_' . $table . '_' . $parent . '_' . $child);

		if ($cache->has($hash))
		{
			return $cache->get($hash);
		}

		$parent_ids = array();

		while ($id)
		{
			$query = $this->db->getQuery(true)
				->select('t.' . $parent)
				->from('#__' . $table . ' as t')
				->where('t.' . $child . ' = ' . (int) $id);
			$this->db->setQuery($query);
			$id = $this->db->loadResult();

			// Break if no parent is found or parent already found before for some reason
			if (!$id || in_array($id, $parent_ids))
			{
				break;
			}

			$parent_ids[] = $id;
		}

		return $cache->set($hash, $parent_ids);
	}

	/**
	 * A one-line text that describes the current value detected by the rule. Eg: The current time is %s.
	 *
	 * @return string
	 */
	public function getValueHint()
	{
		$value = $this->value();

		// If the rule returns an array, use the 1st one.
		$value = is_array($value) ? $value[0] : $value;

		return Text::sprintf('NR_DISPLAY_CONDITIONS_HINT_' . strtoupper($this->getName()), ucfirst(strtolower($value)));
	}

	/**
	 * Return the rule name
	 *
	 * @return string
	 */
	protected function getName()
	{
		$classParts = explode('\\', get_called_class());
		return array_pop($classParts);
	}
}