<?php
/** 
 * @package     VikWP - Libraries
 * @subpackage  adapter.form
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

/**
 * Form field class to handle XML fields.
 *
 * @since 10.0
 */
#[\AllowDynamicProperties]
abstract class JFormField
{
	/**
	 * A list of paths in which to search for the fields to load.
	 *
	 * @var array
	 */
	private static $_paths = array();

	/**
	 * A map of relationships between the types and the handlers classnames.
	 *
	 * @var array
	 */
	private static $_cache = array();

	/**
	 * The layout identifier.
	 * Overwrite in children classes.
	 *
	 * @var   string
	 * @since 10.1.20
	 */
	protected $layoutId = null;

	/**
	 * The JForm object of the form attached to the form field.
	 *
	 * @var   JForm
	 * @since 10.1.31
	 */
	protected $form;

	/**
	 * Tries to instantiate the class used to handle
	 * the specified field XML element.
	 *
	 * @param 	SimpleXMLElement 	$field 	The field element.
	 *
	 * @return 	JFormField 			A new field instance.
	 */
	public static function getInstance($field)
	{
		// get field type
		$type = (string) $field->attributes()->type;

		// text by default
		if (empty($type))
		{
			$type = 'text';
		}

		// if not cached, search for a handler classname
		if (!isset(static::$_cache[$type]))
		{
			// find the handler
			$res = static::findField($type);

			// on failure, text handler by default
			if (!$res)
			{
				$res = 'JFormFieldText';
			}

			// cache the handler
			static::$_cache[$type] = $res;
		}

		// obtain the classname from the cache
		$classname = static::$_cache[$type];

		// instantiate the handler
		return new $classname($field);
	}

	/**
	 * Tries to search for the specified field type.
	 *
	 * @param 	string 	$type 	The field type.
	 *
	 * @return 	mixed 	The field classname on success, otherwise false.
	 */
	protected static function findField($type)
	{
		// get default fields pool (./fields)
		$paths = array();
		$paths[] = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fields';

		// merge the default and additional paths
		$paths = array_merge($paths, static::$_paths); 
		
		// iterate the paths
		foreach ($paths as $path)
		{
			// try to load the handler
			if (JLoader::import($type, $path))
			{
				// build classname
				$classname = 'JFormField' . ucwords($type);

				// make sure the handler exists
				if (class_exists($classname))
				{
					// file found, return the classname
					return $classname;
				}
			}
		}

		// load text field for unknown types
		JLoader::import('adapter.form.fields.text');

		return false;
	}

	/**
	 * Adds a new path containing custom fields handlers.
	 *
	 * @param 	string 	$path 	The directory path.
	 *
	 * @return 	void
	 */
	public static function addIncludePath($path)
	{
		if (!in_array($path, static::$_paths))
		{
			static::$_paths[] = $path;
		}
	}

	/**
	 * Class constructor.
	 *
	 * @param 	SimpleXMLElement 	$field 	The field element.
	 */
	public function __construct($field)
	{
		$this->setup($field);
	}

	/**
	 * Helper method to setup the field.
	 *
	 * @param 	SimpleXMLElement 	$field 	The field element.
	 *
	 * @return 	void
	 */
	protected function setup($element)
	{
		// workaround for sql fields with no default option
		$this->option = array();

		// iterate the attributes
		foreach ($element->attributes() as $k => $v)
		{
			$this->{$k} = (string) $v;
		}

		if (count($element))
		{
			// iterate the children
			foreach ($element as $k => $child)
			{
				// create the container if not set
				if (!isset($this->{$k}))
				{
					$this->{$k} = array();
				}

				// get the value from attributes
				$value = $child->attributes()->value;

				if (is_null($value))
				{
					// push the element without key if the value is NULL
					$this->{$k}[] = (string) $child;
				}
				else
				{
					// the value is set, make an associative list
					$this->{$k}[(string) $value] = (string) $child;
				}
			}
		}

		/**
		 * In case value is not set, use default one.
		 *
		 * @since 10.1.29
		 */
		if (!isset($this->value) && isset($this->default))
		{
			$this->value = $this->default;
		}

		// register original field name and ID
		$this->fieldName = $this->name;
		$this->fieldId   = $this->id;

		// set up field name
		$this->name = $this->getName($this->fieldName);
		// set up field ID
		$this->id = $this->getId($this->fieldID, $this->fieldName);
	}

	/**
	 * Magic method to access internal properties.
	 *
	 * @param 	string 	$name 	The property to access.
	 *
	 * @return 	mixed 	The property value.
	 */
	public function __get($name)
	{
		// protected properties are not allowed
		$name = ltrim($name, '_');

		// check if the property exists
		if (isset($this->{$name}))
		{
			return $this->{$name};
		}
		// if name is equals to 'element' return an assoc
		// list containing all the field attributes
		else if ($name === 'element')
		{
			// get a list of public properties
			return (array) get_object_vars($this);
		}

		return null;
	}

	/**
	 * Method used to bind the field properties.
	 *
	 * @param 	mixed 	$value 	The property value.
	 * @param 	string 	$key 	The property name (value by default).
	 *
	 * @return 	self 	This object to support chaining.
	 */
	public function bind($value, $key = 'value')
	{
		$this->{$key} = $value;

		return $this;
	}

	/**
	 * Placeholder used to render the form field.
	 *
	 * @return 	string 	The HTML field.
	 *
	 * @uses 	getInput()
	 */
	public function render()
	{
		return $this->getInput();
	}

	/**
	 * Placeholder used to render the form field.
	 *
	 * @return 	string 	The HTML field.
	 *
	 * @uses 	getLayoutData()
	 */
	public function getInput()
	{
		// make sure we have a layout id
		if (!$this->layoutId)
		{
			return;
		}

		// create layout file
		$layout = new JLayoutFile($this->layoutId);

		if (!empty($this->modowner))
		{
			// in case of modowner property, add plugin include path to access layout files properly
			$layout->addIncludePath(implode(DIRECTORY_SEPARATOR, array(WP_PLUGIN_DIR, $this->modowner, 'libraries')));
		}

		// obtain layout data and complete field rendering
		return $layout->render($this->getLayoutData());
	}

	/**
	 * Method to get the data to be passed to the layout for rendering.
	 *
	 * @return 	array 	An associative array of display data.
	 *
	 * @since 	10.1.20
	 */
	public function getLayoutData()
	{
		return array();
	}

	/**
	 * Method to attach a JForm object to the field.
	 *
	 * @param   Form  $form  The JForm object to attach to the form field.
	 *
	 * @return  self  this object to support chaining.
	 *
	 * @since   10.1.31
	 */
	public function setForm(JForm $form)
	{
		$this->form = $form;

		// set up field name
		$this->name = $this->getName($this->fieldName);
		// set up field ID
		$this->id = $this->getId($this->fieldID, $this->fieldName);

		return $this;
	}

	/**
	 * Method to get the name used for the field input tag.
	 *
	 * @param   string  $fieldName  The field element name.
	 *
	 * @return  string  The name to be used for the field input tag.
	 *
	 * @since   10.1.31
	 */
	protected function getName($fieldName)
	{
		$name = '';

		$formControl = $this->form ? $this->form->getFormControl() : '';

		// if there is a form control set for the attached form add it first
		if ($formControl)
		{
			$name .= $formControl;
		}

		// if we already have a name segment add the field name as another level
		if ($name)
		{
			$name .= '[' . $fieldName . ']';
		}
		else
		{
			$name .= $fieldName;
		}

		return $name;
	}

	/**
	 * Method to get the id used for the field input tag.
	 *
	 * @param   string  $fieldId    The field element id.
	 * @param   string  $fieldName  The field element name.
	 *
	 * @return  string  The id to be used for the field input tag.
	 *
	 * @since   10.1.31
	 */
	protected function getId($fieldId, $fieldName)
	{
		$id = '';

		$formControl = $this->form ? $this->form->getFormControl() : '';

		// if there is a form control set for the attached form add it first
		if ($formControl)
		{
			$id .= $formControl . '_';
		}
		else
		{
			// use default one for IDs
			$id .= 'jform_';
		}

		if ($fieldId)
		{
			$id .= $fieldId;
		}
		else
		{
			$id .= preg_replace("/[^a-z0-9_\-]+/i", '', $fieldName);
		}

		return $id;
	}
}
