<?php namespace ProcessWire;

/**
 * An Inputfield for handling a single checkbox
 * 
 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
 * https://processwire.com
 *
 * Note: if you want a checkbox already checked, you need to add a setAttribute('checked', 'checked'); 
 * 
 * @property string $checkedValue Value when checked (default=1)
 * @property string $uncheckedValue Value when not checked (default='', blank string)
 * @property string $label2 Alterate label to display next to checkbox (default=use regular label)
 * @property string $checkboxLabel Same as label2, but used as part of field config rather than API-only config.
 * @property bool $checkboxOnly Show only the checkbox without label text next to it? (default=false) @since 3.0.144
 * @property array $labelAttrs Optional attributes for <label> element that surrounds checkbox (default=[]) @since 3.0.141
 * @property int $autocheck When set to 1, setting value attribute to non-blank/non-zero automatically triggers checked. 
 * 
 *
 */
class InputfieldCheckbox extends Inputfield {

	public static function getModuleInfo() {
		return array(
			'title' => __('Checkbox', __FILE__), // Module Title
			'summary' => __('Single checkbox toggle', __FILE__), // Module Summary
			'version' => 106,
			'permanent' => true, 
		);
	}

	/**
	 * Default checked value
	 * 
	 */
	const checkedValueDefault = 1;

	/**
	 * Default unchecked value
	 * 
	 */
	const uncheckedValueDefault = '';

	/**
	 * True if the $checkedValue set manually (and should be used as a label), false if it was inherited from $value attribute
	 *
	 */
	protected $checkedValueIsLabel = false;

	/**
	 * Construct and set default settings
	 * 
	 */
	public function __construct() {
		
		$this->set('checkedValue', self::checkedValueDefault); 
		$this->checkedValueIsLabel = false; // cancel line above
		$this->set('uncheckedValue', self::uncheckedValueDefault); 

		// when autocheck set to 1, setting the value attribute to non-zero automatically triggered checked=checked attribute
		$this->set('autocheck', 0);

		// alternate label for checkbox (both do the same thing but for different config context)
		$this->set('label2', ''); // typically specified by API
		$this->set('checkboxOnly', false); // render checkbox only, without showing label text
		$this->set('checkboxLabel', ''); // typically specified by interactive config
		$this->set('labelAttrs', array()); // Optional attributes for <label> element that surrounds checkbox (3.0.141+)
		
		parent::__construct();

		// Use an undefined skipLabel setting so we can detect when to apply a custom default in ___render()
		// according to runtime conditions (placement after parent::__construct() is required)
		$this->set('skipLabel', -1); 
	}

	/**
	 * Wired to ProcessWire instance
	 * 
	 */
	public function wired() {
		$languages = $this->wire()->languages;
		if($languages) {
			foreach($languages as $language) {
				if(!$language->isDefault()) $this->set("checkboxLabel$language", "");
			}
		}
		parent::wired();
	}

	/**
	 * Init
	 * 
	 */
	public function init() {
		parent::init();
		$this->attr('checked', ''); 
	}

	/**
	 * Render checkbox input
	 * 
	 * @return string
	 * 
	 */
	public function ___render() {

		$user = $this->wire()->user;
		
		// label placed to the right of the checkbox
		$checkboxLabel = '';
		
		if(!$this->checkboxOnly) {
			// we will be displaying a label next to the checkbox
			if($user->language) $checkboxLabel = $this->getSetting("checkboxLabel$user->language");
			if(!$checkboxLabel) $checkboxLabel = $this->getSetting("checkboxLabel");
			if(!$checkboxLabel && $this->checkedValueIsLabel) $checkboxLabel = $this->checkedValue;
			if(!$checkboxLabel) $checkboxLabel = $this->getSetting('label2');
			$checkboxLabel = trim($checkboxLabel);
		}

		if($this->getSetting('skipLabel') === -1) {
			// nothing has modified the skipLabel setting so apply an automatic setting
			if($checkboxLabel || $this->description) {
				$this->set('skipLabel', Inputfield::skipLabelFor); 
			} else {
				$this->set('skipLabel', Inputfield::skipLabelHeader);
			}
		}
		
		if(!$checkboxLabel && !$this->checkboxOnly) {
			$checkboxLabel = $this->label;
		}
		
		$checkboxAttrs = $this->getAttributes();
		$checkboxAttrs['value'] = $this->checkedValue; 
		unset($checkboxAttrs['type']); 
		$checkboxAttrs = $this->getAttributesString($checkboxAttrs);
		
		if(strlen($checkboxLabel)) { 
			if($this->getSetting('entityEncodeLabel') !== false) {
				$checkboxLabel = $this->entityEncode($checkboxLabel, Inputfield::textFormatBasic);
			}
			$checkboxLabel = "<span class='pw-no-select'>$checkboxLabel</span>";
		}
		
		$labelAttrs = $this->getSetting('labelAttrs');
		$labelAttrs = empty($labelAttrs) ? '' : ' ' . $this->getAttributesString($labelAttrs);
		
		$out = 	
			"<label$labelAttrs>" . 
				"<input type='checkbox' $checkboxAttrs />" . 
				$checkboxLabel . 
			"</label>"; 
		
		return $out; 
	}
	
	/**
	 * Render value only
	 * 
	 * @return string
	 * 
	 */
	public function ___renderValue() {
		$value = $this->val();
		if($value != self::uncheckedValueDefault && $value != $this->uncheckedValue) {
			$value = $this->wire()->sanitizer->entities($this->checkedValue); 
			$value = $value === "1" ? $this->_('Checked') : $value;
			$value = wireIconMarkup('check-square-o') . " $value";
		} else {
			$value = $this->wire()->sanitizer->entities($this->uncheckedValue);
			$value = empty($value) ? $this->_('Not checked') : $value;
			$value = wireIconMarkup('square-o') . " $value";
		}
		return $value; 
	}

	/**
	 * Set attribute
	 * 
	 * @param array|string $key
	 * @param array|int|string $value
	 * @return Inputfield|InputfieldCheckbox
	 * 
	 */
	public function setAttribute($key, $value) {

		if($key === 'value' && $value && "$value" !== "$this->uncheckedValue") {
			if("$value" !== (string) self::checkedValueDefault) {
				$this->checkedValue = $value; 
				$this->checkedValueIsLabel = false;
			}
			// autocheck mode: when non-zero 'value' set, then 'checked=checked' is assumed
			if($this->autocheck || $this->getSetting('formBuilder')) $this->attr('checked', 'checked');
		}

		return parent::setAttribute($key, $value); 
	}

	/**
	 * Set property
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @return InputfieldCheckbox|Inputfield
	 * 
	 */
	public function set($key, $value) {
		if($key === 'checkedValue' && $value != self::checkedValueDefault) {
			$this->checkedValueIsLabel = true;
		}
		return parent::set($key, $value); 
	}

	/**
	 * Get or set current checkbox boolean attribute state
	 * 
	 * ~~~~~
	 * // the following two lines are equivalent to GET checkbox state
	 * $checked = $f->checked(); 
	 * $checked = !empty($f->attr('checked'));
	 * 
	 * // the following two lines are equivalent to SET checkbox state
	 * $f->checked(true);
	 * $f->attr('checked', 'checked'); 
	 * ~~~~~
	 *
	 * @param bool|null Specify boolean to set checkbox state
	 * @return bool
	 * @since 3.0.133
	 *
	 */
	public function checked($checked = null) {
		if($checked !== null) {
			if($checked) {
				$this->attr('checked', 'checked');
				$checked = true;
			} else {
				$this->removeAttr('checked');
				$checked = false;
			}
		} else {
			$checked = $this->attr('checked');
			$checked = empty($checked) ? false : true;
		}
		return $checked;
	}

	/**
	 * Is checkbox currently checked?
	 * 
	 * #pw-internal
	 * 
	 * @return bool
	 * 
	 */
	public function isChecked() {
		return $this->checked();
	}

	/**
	 * Is empty (checkbox not checked)?
	 * 
	 * @return bool
	 * 
	 */
	public function isEmpty() {
		return !$this->checked();
	}

	/**
	 * Process input
	 * 
	 * @param WireInputData $input
	 * @return $this
	 * 
	 */
	public function ___processInput(WireInputData $input) {

		$value = $input[$this->name];
		$checked = $this->isChecked();
		
		if(!empty($value)) {
			if(!$checked) $this->trackChange('value', $this->uncheckedValue, $this->checkedValue); 
			parent::attr('checked', 'checked'); 
			parent::attr('value', $this->checkedValue); 
		} else {
			if($checked) $this->trackChange('value', $this->checkedValue, $this->uncheckedValue); 
			parent::attr('checked', ''); 
			parent::attr('value', $this->uncheckedValue); 
		}

		return $this; 
	}

	/**
	 * Configure checkbox field
	 * 
	 * @return InputfieldWrapper
	 * 
	 */
	public function ___getConfigInputfields() {

		$inputfields = parent::___getConfigInputfields();
		$languages = $this->wire()->languages;
	
		/** @var InputfieldText $f */
		$f = $this->wire()->modules->get('InputfieldText'); 
		$f->attr('name', 'checkboxLabel'); 
		$f->label = $this->_('Checkbox label');
		$f->description = $this->_('If you want to have separate field and checkbox labels, specify the label that will appear next to the checkbox here.');
		$f->notes = $this->_('If not specified, the field label will be used instead.'); 
		$f->attr('value', $this->getSetting('checkboxLabel')); 
		$f->icon = 'check-square';
		$f->collapsed = Inputfield::collapsedBlank;
		if($languages) {
			$f->useLanguages = true; 
			foreach($languages as $language) {
				if(!$language->isDefault()) $f->set("value$language", $this->getSetting("checkboxLabel$language")); 
			}
		}
		$inputfields->add($f); 

		// if working with fieldtype, no additional settings are applicable
		if($this->hasFieldtype) return $inputfields;

		/** @var InputfieldText $f */
		$f = $this->wire()->modules->get('InputfieldText');
		$f->attr('name', 'checkedValue');
		$f->attr('value', $this->checkedValue);
		$f->label = $this->_('Checked Value');
		$f->collapsed = $this->checkedValue == self::checkedValueDefault ? Inputfield::collapsedYes : Inputfield::collapsedNo;
		$f->description = $this->_('When populated with something other than "1", this will appear as a label directly next to the checkbox.'); 
		$f->required = true; 
		$inputfields->add($f);

		/** @var InputfieldText $f */
		$f = $this->wire()->modules->get('InputfieldText');
		$f->attr('name', 'uncheckedValue');
		$f->attr('value', "$this->uncheckedValue");
		$f->label = $this->_('Unchecked Value');
		$f->collapsed = $this->uncheckedValue == self::uncheckedValueDefault ? Inputfield::collapsedYes : Inputfield::collapsedNo;
		$f->description = $this->_('This only appears in result entries, not in the form itself. You should leave this blank unless you want it to hold a specific value.');
		$inputfields->add($f);

		return $inputfields;
	}
}
