<?php
namespace Aelia\WC;
if(!defined('ABSPATH')) { exit; } // Exit if accessed directly

use \InvalidArgumentException;

/**
 * Implements a class that will render the settings page.
 */
class Settings_Renderer {
	// @var Aelia\WC\Settings The settings controller which will handle the settings.
	protected $_settings_controller;
	// @var string The text domain to be used for localisation.
	protected $_textdomain = '';
	// @var string The key to identify plugin settings amongst WP options.
	protected $_settings_key;

	// @var string The default tab ID used if none is specified when class is instantiated.
	const DEFAULT_SETTINGS_TAB_ID = 'default';

	// @var string The ID of the WC menu in the admin section. Used to add submenus.
	const WC_MENU_ITEM_ID = 'woocommerce';

	// @var array A list of tabs used to organise the settings sections.
	protected $_settings_tabs = array();

	// @var string The ID of the default tab where to store the settings sections.
	protected $_default_tab;

	/**
	 * Attaches a settings controller to the renderer.
	 *
	 * @param Aelia\WC\Settings settings_controller The settings controller.
	 */
	public function set_settings_controller(\Aelia\WC\Settings $settings_controller) {
		$this->_settings_controller = $settings_controller;
		$this->_settings_key = $this->_settings_controller->settings_key;
		$this->_textdomain = $this->_settings_controller->textdomain;
	}

	/*** Auxiliary functions ***/
	/**
	 * Returns current plugin settings, or the value a specific setting.
	 *
	 * @param string key If specified, method will return only the setting identified
	 * by the key.
	 * @param mixed default The default value to return if the setting requested
	 * via the "key" argument is not found.
	 * @return array|mixed The plugin settings, or the value of the specified
	 * setting.
	 *
	 * @see Aelia\WC\Settings::current_settings().
	 */
	public function current_settings($key = null, $default = null) {

		return $this->_settings_controller->current_settings($key, $default);
	}

	/**
	 * Returns the default settings for the plugin. Used mainly at first
	 * installation.
	 *
	 * @param string key If specified, method will return only the setting identified
	 * by the key.
	 * @param mixed default The default value to return if the setting requested
	 * via the "key" argument is not found.
	 * @return array|mixed The default settings, or the value of the specified
	 * setting.
	 *
	 * @see Aelia\WC\Settings::default_settings().
	 */
	protected function default_settings($key = null, $default = null) {
		return $this->_settings_controller->default_settings($key, $default);
	}

	/**
	 * Class constructor.
	 *
	 * @param string default_tab the default tab inside which sections should be
	 * rendered, unless a different tab is specified for them.
	 * @return Aelia\WC\Settings.
	 */
	public function __construct($default_tab = self::DEFAULT_SETTINGS_TAB_ID) {
		$this->_default_tab = $default_tab;

		add_action('admin_menu', array($this, 'add_settings_page'));
	}

	/**
	* Add a new section to a settings page. This method relies on standard
	* WordPress add_settings_section() function, with the difference that it takes
	* a "Tab" argument, which can be used to display settings divided into Tabs
	* without having to implement multiple validations or having to figure out
	* which page was posted and what data should be validated each time.
	*
	* @global $wp_settings_sections Storage array of all settings sections added to admin pages.
	*
	* @param string id Slug-name to identify the section. Used in the 'id' attribute of tags.
	* @param string title Formatted title of the section. Shown as the heading for the section.
	* @param string callback Function that echos out any content at the top of the section (between heading and fields).
	* @param string page The slug-name of the settings page on which to show the section. Built-in pages include 'general', 'reading', 'writing', 'discussion', 'media', etc. Create your own using add_options_page();
	* @param string tab_id Slug-name of the Tab where the settings section should be rendered.
	*/
	protected function add_settings_section($id, $title, $callback, $page, $tab_id = self::DEFAULT_SETTINGS_TAB_ID) {
		if(isset($this->_settings_tabs[$page][$tab_id])) {
			$tab = &$this->_settings_tabs[$page][$tab_id];
		}
		else {
			$tab = &$this->get_default_tab($page);
		}

		$tab['sections'][] = $id;

		add_settings_section($id, $title, $callback, $page);
	}

	/**
	 * Adds a settings tab, which will contain settings sections and fields.
	 *
	 * @param string page The slug-name of the settings page on which to show the section. Built-in pages include 'general', 'reading', 'writing', 'discussion', 'media', etc. Create your own using add_options_page();
	 * @param string tab_id Slug-name of the Tab where the settings section should be rendered.
	 */
	protected function add_settings_tab($page, $tab_id, $tab_label) {
		if(!isset($this->_settings_tabs[$page])) {
			$this->_settings_tabs[$page] = array();
		}

		$this->_settings_tabs[$page][$tab_id] = array(
			'label' => $tab_label,
			'sections' => array(),
		);
	}

	/**
	 * Returns the default tab where the settings sections will be put when they
	 * are not set to be displayed in a specific tab.
	 *
	 * @param string page The slug-name of the settings page on which to show the section. Built-in pages include 'general', 'reading', 'writing', 'discussion', 'media', etc. Create your own using add_options_page();
	 */
	protected function &get_default_tab($page) {
		if(!get_value(self::DEFAULT_SETTINGS_TAB_ID, $this->_settings_tabs[$page])) {
			$this->add_settings_tab($page,
															self::DEFAULT_SETTINGS_TAB_ID,
															__('Default', $this->_textdomain));
		}

		return $this->_settings_tabs[$page][self::DEFAULT_SETTINGS_TAB_ID];
	}

	/**
	 * Given an array of items with a "priority" attribute, sorts such items in an ascending order,
	 * depending on the value of such attribute.
	 *
	 * @param array $items An array of items. Each item should be an array itself, with a "priority" element.
	 * @param string $priority_attr The name of the attribute containing the priority.
	 * @param integer $default_priority The default priority to assign to an item, if none was specified.
	 * @return array
	 * @since 2.1.0.201112
	 */
	protected static function sort_items_by_priority($items, $priority_attr = 'priority', $default_priority = 100) {
		uasort($items, function($elem1, $elem2) use ($priority_attr, $default_priority) {
			$elem1_priority = $elem1[$priority_attr] ?? $default_priority;
			$elem2_priority = $elem2[$priority_attr] ?? $default_priority;

			return $elem1_priority <=> $elem2_priority;
		});
		return $items;
	}

	/**
	 * Returns the tabs to be used to render the Settings page.
	 *
	 * @return array
	 * @since 2.1.0.201112
	 */
	protected function get_settings_tabs() {
		// To be implemented by descendant class
		return array();
	}

	/**
	 * Sets the tabs to be used to render the Settings page.
	 */
	protected function add_settings_tabs() {
		// Add the tabs to the settings page, after sorting them by priority
		// @since 2.1.0.201112
		foreach(self::sort_items_by_priority(apply_filters('wc_aelia_' . $this->_settings_key . '_settings_tabs', $this->get_settings_tabs())) as $tab) {
			$this->add_settings_tab($this->_settings_key,	$tab['id'], $tab['label']);
		}
	}

	/**
	 * Returns the sections to be used to render the Settings page.
	 *
	 * @return array
	 * @since 2.1.0.201112
	 */
	protected function get_settings_sections() {
		// To be implemented by descendant class
		return array();
	}

	/**
	 * Empty callback function, to be used when a callback is not used for
	 * a section.
	 *
	 * @since 2.1.0.201112
	 */
	public function void_callback() {
		// Deliberately left empty
	}

	/**
	 * Configures the plugin settings sections.
	 */
	protected function add_settings_sections() {
		foreach(apply_filters('wc_aelia_' . $this->_settings_key . '_settings_sections', $this->get_settings_sections()) as $tab_id => $tab_sections) {
			// Sort the sections by priority
			// @since 2.1.0.201112
			foreach(self::sort_items_by_priority($tab_sections) as $section) {
				$this->add_settings_section(
					$section['id'],
					$section['label'],
					isset($section['callback']) ? $section['callback'] : array($this, 'void_callback'),
					$this->_settings_key,
					$tab_id
				);
			}
		}
	}

	/**
	 * Returns the setting fields to be used to render the Settings page.
	 *
	 * @return array
	 * @since 2.1.0.201112
	 */
	protected function get_settings_fields() {
		// To be implemented by descendant class
		return array();
	}

	/**
	 * Configures the plugin settings fields.
	 */
	protected function add_settings_fields() {
		foreach(apply_filters('wc_aelia_' . $this->_settings_key . '_settings_fields', $this->get_settings_fields()) as $section_id => $section_fields) {
			foreach(self::sort_items_by_priority($section_fields) as $field) {
				$field = array_merge(array(
					'label' => 'text',
					'description' => 'text',
					'css_class' => '',
					'attributes' => array(),
					'type' => 'text',
				), $field);

				// If a field name has been specified, pass it with the rest of the attributes. If not,
				// the field ID will be used to generate a field name automatically
				// @since 2.1.0.201112
				if(!empty($field['name'])) {
					$field['attributes']['name'] = $field['name'];
				}

				// If a settingss key has been specified, pass it with the rest of the attributes. If not,
				// the default settings key from the settings controller will be used automatically
				// @since 2.1.1.201208
				if(!empty($field['settings_key'])) {
					$field['attributes']['settings_key'] = $field['settings_key'];
				}

				// If a field value has been specified, pass it with the rest of the attributes. If not,
				// the settings controller will try to fetch the value from the settings loaded from the database
				// @since 2.1.1.201208
				if(!empty($field['value'])) {
					$field['attributes']['value'] = $field['value'];
				}

				switch($field['type']) {
					case 'checkbox':
						$this->render_checkbox_field(
							$section_id,
							$field['id'],
							$field['label'],
							$field['description'],
							$field['css_class'],
							$field['attributes']
						);
						break;
					case 'dropdown':
						$this->render_dropdown_field(
							$section_id,
							$field['id'],
							$field['label'],
							$field['options'],
							$field['description'],
							$field['css_class'],
							$field['attributes']
						);
						break;
					case 'custom':
						$this->render_custom_field(
							$section_id,
							$field['id'],
							$field['label'],
							$field['attributes'],
							$field['render_callback']
						);
						break;
					case 'text':
					default:
							$this->render_text_field(
								$section_id,
								$field['id'],
								$field['label'],
								$field['description'],
								$field['css_class'],
								$field['attributes'],
								$field['type']
							);
						break;
				}
			}
		}
	}

	/**
	 * Returns the title for the menu item that will bring to the plugin's
	 * settings page.
	 *
	 * @return string
	 */
	protected function menu_title() {
		return __('WC - Aelia Foundation Classes', $this->_textdomain);
	}

	/**
	 * Returns the slug for the menu item that will bring to the plugin's
	 * settings page.
	 *
	 * @return string
	 */
	protected function menu_slug() {
		return 'wc-aelia-foundation-classes';
	}

	/**
	 * Returns the title for the settings page.
	 *
	 * @return string
	 */
	protected function page_title() {
		return __('Aelia Foundation Classes for WC - Settings', $this->_textdomain);
	}

	/**
	 * Returns the description for the settings page.
	 *
	 * @return string
	 */
	protected function page_description() {
		return __('Sample page description', $this->_textdomain);
	}

	/**
	 * Returns the permission required to access the menu link that brings to the
	 * plugin settings page.
	 *
	 * @param string menu_slug The menu ID for which the permissions are set.
	 * @return string
	 */
	protected function get_plugin_settings_menu_permission($menu_slug = '') {
		return apply_filters('wc_aelia_' . $menu_slug . '_menu_permissions', 'manage_woocommerce', $menu_slug);
	}

	/**
	 * Renders all settings sections added to a particular settings page. This
	 * method is an almost exact clone of global do_settings_sections(), the main
	 * difference is that each section is wrapped in its own <div>.
	 *
	 * Part of the Settings API. Use this in a settings page callback function
	 * to output all the sections and fields that were added to that $page with
	 * add_settings_section() and add_settings_field().
	 *
	 * @global $wp_settings_sections Storage array of all settings sections added to admin pages
	 * @global $wp_settings_fields Storage array of settings fields and info about their pages/sections
	 * @since 2.7.0
	 *
	 * @param string $page The slug name of the page whos settings sections you want to output
	 */
	protected function render_settings_sections($page) {
		global $wp_settings_sections, $wp_settings_fields;

		$settings_sections = get_value($page, $wp_settings_sections);
		if(empty($settings_sections)) {
			return;
		}

		//foreach((array)$wp_settings_sections[$page] as $section) {
		$settings_tabs = get_value($page, $this->_settings_tabs);
		$output_tabs = count($settings_tabs) > 1;

		if($output_tabs) {
			echo '<div class="settings-page tabs">';
			echo "<ul>\n";
			foreach($settings_tabs as $tab_id => $tab_info) {
				echo "<li><a href=\"#tab-{$tab_id}\">{$tab_info['label']}</a></li>\n";
			}
			echo "</ul>\n";
		}
		else {
			echo '<div class="settings-page">';
		}

		foreach($settings_tabs as $tab_id => $tab_info) {
			$sections = get_value('sections', $tab_info, array());

			echo "<div id=\"tab-{$tab_id}\" class=\"aelia settings-tab\">";
			foreach($sections as $section_id) {
				$section = get_value($section_id, $wp_settings_sections[$page]);
				echo '<div id="section-'. $section_id .'" class="aelia settings-section">';
				if($section['title']) {
					echo "<h3>{$section['title']}</h3>\n";
				}

				if($section['callback']) {
					call_user_func($section['callback'], $section);
				}

				$section_id = get_value('id', $section);
				if(isset($wp_settings_fields[$page]) && get_value($section_id, $wp_settings_fields[$page], false)) {
					echo '<table class="form-table">';
					do_settings_fields($page, $section['id']);
					echo '</table>';
				}

				echo '</div>';
			}
			echo '</div>';
		}
		echo '</div>';
	}

	/**
	 * Renders the buttons at the bottom of the settings page.
	 */
	protected function render_buttons() {
		submit_button(__('Save Changes', $this->_textdomain),
							'primary',
							'submit',
							false);
	}

	/**
	 * Renders the Options page for the plugin.
	 */
	public function render_options_page() {
		// Prepare settings page for rendering
		$this->init_settings_page();

		echo '<div class="wrap">';
		echo '<div class="icon32" id="icon-options-general"></div>';
		echo '<h2>' . $this->page_title() . '</h2>';
		echo '<p>' . $this->page_description() . '</p>';

		settings_errors();
		echo '<form id="' . $this->_settings_key . '_form" method="post" action="options.php" class="wc-aelia-plugin-settings">';
		settings_fields($this->_settings_key);

		$this->render_settings_sections($this->_settings_key);
		echo '<div class="buttons">';
		$this->render_buttons();
		echo '</div>';
		echo '</form>';
		echo '</div>'; // Closing <div class="wrap">
	}

	/**
	 * Adds a link to Settings Page in WC Admin menu.
	 */
	public function add_settings_page() {
		$settings_page = add_submenu_page(
			self::WC_MENU_ITEM_ID,
	    $this->page_title(),
	    $this->menu_title(),
			$this->get_plugin_settings_menu_permission($this->menu_slug()),
			$this->menu_slug(),
			array($this, 'render_options_page')
		);

		add_action('load-' . $settings_page, array($this, 'options_page_load'));
	}

	/**
   * Takes an associative array of attributes and returns them as a string of
   * param="value" sets to be placed in an input, select, textarea, etc tag.
   *
   * @param array attributes An associative array of attribute key => value
   * pairs to be converted to a string. A number of "reserved" keys will be
   * ignored.
   * @return string
   */
	protected function attributes_to_string(array $attributes) {
		$reserved_attributes = array(
			'id',
			'name',
			'value',
			'method',
			'action',
			'type',
			'for',
			'multiline',
			'default',
			'textfield',
			'valuefield',
			'includenull',
			'yearrange',
			'fields',
			'inlineerrors',
			'description');

		$result = array();
    // Build string from array
    if(is_array($attributes)) {
			foreach($attributes as $attribute => $value) {
				// Ignore reserved attributes
				if(!in_array(strtolower($attribute), $reserved_attributes)) {
					$result[] = $attribute . '="' . $value . '"';
				}
			}
		}
		return implode(' ', $result);
	}

	/**
	 * Extracts the Field ID and Field Name for an input element from the arguments
	 * originally passed to a rendering function.
	 *
	 * @param array field_args The arguments passed to the rendering function which
	 * is going to render the input field.
	 * @param string field_id Output argument. It will contain the ID of the field.
	 * @param string field_name Output argument. It will contain the name of the field.
	 */
	protected function get_field_ids(array $field_args, &$field_id, &$field_name) {
		// Determine field ID and Name
		$field_id = $field_args['id'];
		if(empty($field_id)) {
			throw new InvalidArgumentException(__('Field ID must be specified.', $this->_textdomain));
		}
		$field_name = $field_args['name'] ?? $field_args['attributes']['name'] ?? $field_id;

		// If a Settings Key has been passed, modify field ID and Name to make them
		// part of the Settings Key array
		$settings_key = $field_args['settings_key'] ?? null;
		$field_id = $this->group_field($field_id, $settings_key);
		$field_name = $this->group_field($field_name, $settings_key);
	}

	/**
	 * Takes a field ID and transforms it so that it becomes part of a field group.
	 * Example
	 * - Field ID: MyField
	 * - Group: SomeGroup
	 *
	 * Result: SomeGroup[MyField]
	 * This allows to group fields together and access them as an array.
	 *
	 * @param string id The Field ID.
	 * @param string group The group to which the field should be added.
	 * @return string The new field name.
	 */
	protected function group_field($id, $group) {
		return empty($group) ? $id : $group . '[' . $id . ']';
	}

	/*** Rendering methods ***/
	/**
	 * Renders a select box.
	 *
	 * @param array args An array of arguments passed by add_settings_field().
	 * @param bool display Indicates if the HTML for the field should be printed
	 * out directly (true) or just returned (false).
	 * @see add_settings_field().
	 */
	public function render_dropdown($args, $display = true) {
		$this->get_field_ids($args, $field_id, $field_name);

		// Retrieve the options that will populate the dropdown
		$dropdown_options = $args['options'];
		if(!is_array($dropdown_options)) {
			throw new InvalidArgumentException(__('Argument "options" must be an array.', $this->_textdomain));
		}

		// Retrieve the selected Option elements
		$selected_options = get_value('selected', $args, array());
		if(!is_array($selected_options)) {
			$selected_options = array($selected_options);
		}

		// Retrieve the HTML attributes
		$attributes = get_value('attributes', $args, array());

		// If we are about to render a multi-select dropdown, add two square brackets
		// so that all selected values will be returned as an array
		// TODO Make search in array case-insensitive
		if(in_array('multiple', $attributes)) {
			$field_name .= '[]';
		}

		$html = '<select ' .
			'id="' . $field_id . '" ' .
			'name="' . $field_name . '" ' .
			$this->attributes_to_string($attributes) .
			'>';
		foreach($dropdown_options as $value => $label) {
			$selected_attr = in_array($value, $selected_options) ? 'selected="selected"' : '';
			$html .= '<option value="' . esc_html($value) . '" ' . $selected_attr . '>' . $label . '</option>';
		}
		$html .= '</select>';
		if(!empty($attributes['description'])) {
			$html .= '<p class="description">' . $attributes['description'] . '</p>';
		}

		if($display) {
			echo $html;
		}
		return $html;
	}

	/**
	 * Build the HTML to represent an <input> element.
	 *
	 * @param string type The type of input. It can be text, password, hidden,
	 * checkbox or radio.
	 * @param string field_id The ID of the field.
	 * @param string value The field value.
	 * @param array attribues Additional field attributes.
	 * @param string field_name The name of the field. If unspecified, the field
	 * ID will be taken.
	 * @return string The HTML representation of the field.
	 */
	protected function get_input_html($type, $field_id, $value, $attributes, $field_name = null) {
		$field_name = !empty($field_name) ? $field_name : $field_id;

		$html =
			'<input type="' . $type . '" ' .
			'id="' . $field_id . '" ' .
			'name="' . $field_name . '" ' .
			'value="' . esc_html($value) . '" ' .
			$this->attributes_to_string($attributes) .
			' />';
		if(!empty($attributes['description'])) {
			if($type == 'checkbox') {
				$element = 'span';
			}
			else {
				$element = 'p';
			}

			$html .= '<' . $element . ' class="description">' . $attributes['description'] . '</' . $element . '>';
		}

		return $html;
	}

	/**
	 * Build the HTML to represent a <textarea> element.
	 *
	 * @param string field_id The ID of the field.
	 * @param string value The field value.
	 * @param array attribues Additional field attributes.
	 * @param string field_name The name of the field. If unspecified, the field
	 * ID will be taken.
	 * @return string The HTML representation of the field.
	 */
	protected function get_textarea_html($field_id, $value, $attributes, $field_name = null) {
		$field_name = !empty($field_name) ? $field_name : $field_id;

		$html =
			'<textarea ' .
			'id="' . $field_id . '" ' .
			'name="' . $field_name . '" ' .
			$this->attributes_to_string($attributes) .
			'>' . esc_html($value) . '</textarea>';
		if(!empty($attributes['description'])) {
			$html .= '<p class="description">' . $attributes['description'] . '</p>';
		}

		return $html;
	}

	/**
	 * Renders a hidden field.
	 *
	 * @param array args An array of arguments passed by add_settings_field().
	 * @param bool display Indicates if the HTML for the field should be printed
	 * out directly (true) or just returned (false).
	 * @see add_settings_field().
	 */
	public function render_hidden($args, $display = true) {
		$this->get_field_ids($args, $field_id, $field_name);

		// Retrieve the HTML attributes
		$attributes = get_value('attributes', $args, array());
		$value = get_value('value', $args, '');

		$html = $this->get_input_html('hidden', $field_id, $value, $attributes, $field_name);
		if($display) {
			echo $html;
		}
		return $html;
	}

	/**
	 * Renders a text box (input or textarea). To render a textarea, pass an
	 * attribute named "multiline" set to true.
	 *
	 * @param array args An array of arguments passed by add_settings_field().
	 * @param bool display Indicates if the HTML for the field should be printed
	 * out directly (true) or just returned (false).
	 * @see add_settings_field().
	 */
	public function render_textbox($args, $display = true) {
		$this->get_field_ids($args, $field_id, $field_name);

		// Retrieve the HTML attributes
		$attributes = get_value('attributes', $args, array());
		$value = get_value('value', $args, '');

		$multiline = get_value('multiline', $attributes);

		if($multiline) {
			$html = $this->get_textarea_html($field_id, $value, $attributes, $field_name);
		}
		else {
			$field_type = get_value('type', $args, 'text');
			$html = $this->get_input_html($field_type, $field_id, $value, $attributes, $field_name);
		}

		if($display) {
			echo $html;
		}
		return $html;
	}

	/**
	 * Renders a checkbox.
	 *
	 * @param array args An array of arguments passed by add_settings_field().
	 * @param bool display Indicates if the HTML for the field should be printed
	 * out directly (true) or just returned (false).
	 * @see add_settings_field().
	 */
	public function render_checkbox($args, $display = true) {
		$this->get_field_ids($args, $field_id, $field_name);

		// Retrieve the HTML attributes
		$attributes = get_value('attributes', $args, array());

		if(get_value('checked', $attributes, false)) {
			$attributes['checked'] = 'checked';
		}
		else {
			unset($attributes['checked']);
		}
		$value = get_value('value', $args, 1);

		/* The hidden input is a "trick" to get a value also when a checkbox is not
		 * ticked. Unticked checkboxes are not POSTed with a form, therefore the only
		 * way to get a "non selected" value would be to check if each checkbox field
		 * was posted. By adding a hidden field just before it, with a value of zero,
		 * we can ensure that something is always POSTed, whether the checkbox field
		 * is ticked or not:
		 * - When it's ticked, its value is sent with the form, overriding the hidden field.
		 * - When it's not ticked, the value of the hidden field is sent instead.
		 *
		 * This allows to skip a lot of checks, and to just take the field value.
		 */
		$html = $this->get_input_html('hidden', $field_id . '_unticked', 0, array(), $field_name);
		$html .= $this->get_input_html('checkbox', $field_id, $value, $attributes, $field_name);

		if($display) {
			echo $html;
		}
		return $html;
	}

	/**
	 * Event handler, fired when setting page is loaded.
	 */
	public function options_page_load() {
		if(get_value('settings-updated', $_GET)) {
      //plugin settings have been saved. Display a message, or do anything you like.
		}
	}

	/**
	 * Initialises the settings page.
	 */
	public function init_settings_page() {
		$this->add_settings_tabs();
		$this->add_settings_sections();
		$this->add_settings_fields();
	}

	/**
	 * Renders a simple text field.
	 *
	 * @param string section The section where the field will be rendered.
	 * @param string field_id The field ID.
	 * @param string label The field label.
	 * @param string description The field description.
	 * @param string css_class The CSS class to give to the rendered input field.
	 * @param array attributes Additional HTML attributes.
	 * @param string type The field type.
	 */
	protected function render_text_field($section, $field_id, $label, $description = '', $css_class = '', $attributes = array(), $type = 'text') {
		// Fetch the value from the attributes passed with the field definition, if present. If absent, fetch
		// the value from current settings. This allows 3rd parties to inject custom fields whose value has
		// to be determined using some specific logic
		// @since 2.1.1.201208
		$value = $attributes['value'] ?? $this->current_settings($field_id, $this->default_settings($field_id, ''));

		add_settings_field(
			$field_id,
	    $label,
	    array($this, 'render_textbox'),
	    $this->_settings_key,
	    $section,
	    array(
				'settings_key' => $attributes['settings_key'] ?? $this->_settings_key,
				'id' => $field_id,
				'label_for' => $field_id,
				'value' => $value,
				// Field type
				// @since 2.0.9.191108
				'type' => $type,
				// Input field attributes
				'attributes' => array_merge(array(
					'class' => $css_class . ' ' . $field_id,
					'description' => $description,
				), $attributes),
			)
		);
	}

	/**
	 * Renders a simple checkbox field.
	 *
	 * @param string section The section where the field will be rendered.
	 * @param string field_id The field ID.
	 * @param string label The field label.
	 * @param string description The field description.
	 * @param string css_class The CSS class to give to the rendered input field.
	 * @param array attributes Additional HTML attributes.
	 */
	protected function render_checkbox_field($section, $field_id, $label, $description = '', $css_class = '', $attributes = array()) {
		// Fetch the value from the attributes passed with the field definition, if present. If absent, fetch
		// the value from current settings. This allows 3rd parties to inject custom fields whose value has
		// to be determined using some specific logic
		// @since 2.1.1.201208
		$value = $attributes['value'] ?? $this->current_settings($field_id, $this->default_settings($field_id, ''));

		add_settings_field(
			$field_id,
	    $label,
	    array($this, 'render_checkbox'),
	    $this->_settings_key,
	    $section,
	    array(
				'settings_key' => $attributes['settings_key'] ?? $this->_settings_key,
				'id' => $field_id,
				'label_for' => $field_id,
				// Input field attributes
				'attributes' => array_merge(array(
					'class' => $css_class . ' ' . $field_id,
					'description' => $description,
					'checked' => $value,
				), $attributes),
			)
		);
	}

	/**
	 * Renders a dropdown field.
	 *
	 * @param string section The section where the field will be rendered.
	 * @param string field_id The field ID.
	 * @param string label The field label.
	 * @param array options An associative array of value => label entries. It
	 * will be used to populate the options available for the dropdown field.
	 * @param string description The field description.
	 * @param string css_class The CSS class to give to the rendered input field.
	 * @param array attributes Additional HTML attributes (e.g. "multiple").
	 */
	protected function render_dropdown_field($section, $field_id, $label, $options, $description = '', $css_class = '', $attributes = array()) {
		// Fetch the value from the attributes passed with the field definition, if present. If absent, fetch
		// the value from current settings. This allows 3rd parties to inject custom fields whose value has
		// to be determined using some specific logic
		// @since 2.1.1.201208
		$value = $attributes['value'] ?? $this->current_settings($field_id, $this->default_settings($field_id, ''));

		add_settings_field(
			$field_id,
	    $label,
	    array($this, 'render_dropdown'),
	    $this->_settings_key,
	    $section,
	    array(
				'settings_key' => $attributes['settings_key'] ?? $this->_settings_key,
				'id' => $field_id,
				'label_for' => $field_id,
				'options' => $options,
				'selected' => $value,
				// Input field attributes
				'attributes' => array_merge(array(
					'class' => $css_class . ' ' . $field_id,
					'description' => $description,
				), $attributes),
			)
		);
	}

	/**
	 * Handlesa custom field, which will be rendered using a dedicated callback function.
	 *
	 * @param string $section
	 * @param string $field_id
	 * @param string $field_label
	 * @param array $attributes
	 * @param callable $render_callback
	 * @since 2.1.0.201112
	 */
	protected function render_custom_field($section, $field_id, $field_label, array $attributes, $render_callback) {
		if(!is_callable($render_callback)) {
			// Invalid callback, report issue
			return;
		}

		// Add "Exchange Rates" table
		add_settings_field(
			$field_id,
			$field_label,
			$render_callback,
			$this->_settings_key,
			$section,
			$attributes
		);
	}
}
