<?php

namespace YahnisElsts\AdminMenuEditor\Customizable\Controls;

use YahnisElsts\AdminMenuEditor\Customizable\Rendering\Renderer;
use YahnisElsts\AdminMenuEditor\Customizable\Settings;
use YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting;
use YahnisElsts\AdminMenuEditor\Customizable\Settings\Setting;

abstract class Control extends UiElement {
	use Toggleable {
		isEnabled as private traitIsEnabled;
	}

	/**
	 * @var string Type of the control
	 */
	protected $type = 'abstract';

	/**
	 * @var \YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting[]
	 */
	protected $settings = [];

	/**
	 * @var null|\YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting
	 */
	protected $mainSetting = null;

	/**
	 * @var string|null Name of the Knockout component that renders this control.
	 */
	protected $koComponentName = null;

	/**
	 * @var string
	 */
	protected $label = '';

	/**
	 * @var bool Whether this control has a specific, well-defined form element
	 * that is the main input and needs an associated label.
	 */
	protected $hasPrimaryInput = false;

	/**
	 * @var array List of CSS classes to apply to the primary input element.
	 */
	protected $inputClasses = [];

	/**
	 * @var array List of HTML attributes to add to the input element.
	 */
	protected $inputAttributes = [];

	protected static $totalInstances = 0;
	protected $instanceNumber = 0;

	public function __construct($settings = [], $params = []) {
		$this->instanceNumber = ++static::$totalInstances;

		parent::__construct($params);

		$this->settings = $settings;
		if ( !empty($this->settings) ) {
			if ( isset($params['mainSetting']) ) {
				$this->mainSetting = $this->settings[$params['mainSetting']];
			} else {
				$this->mainSetting = reset($this->settings);
			}
		}

		//Label
		if ( isset($params['label']) ) {
			$this->label = $params['label'];
		} else if ( !empty($this->mainSetting) ) {
			$this->label = $this->mainSetting->getLabel();
		}
		//Description
		if ( isset($params['description']) ) {
			$this->description = $params['description'];
		} else if ( !empty($this->mainSetting) ) {
			$this->description = $this->mainSetting->getDescription();
		}
		//Enabled/disabled
		$this->parseEnabledParam($params);

		//ID. Note that this is not necessarily the same as the ID attribute
		//of the HTML element generated by this control.
		if ( $this->id === '' ) {
			if ( isset($this->mainSetting) ) {
				$this->id = preg_replace('/[^a-z0-9\-_]+/i', '_', $this->mainSetting->getId());
				//The ID should not start with a number because it *could*
				//be used as an HTML element ID.
				if ( preg_match('/^[0-9]/', $this->id) ) {
					$this->id = 'c' . $this->id;
				}
			} else {
				$this->id = 'ame_' . $this->type . '_' . $this->instanceNumber;
			}
		}

		//Input classes
		if ( isset($params['inputClasses']) ) {
			$this->inputClasses = (array)$params['inputClasses'];
		}

		//Input attributes
		if ( isset($params['inputAttributes']) ) {
			$this->inputAttributes = (array)$params['inputAttributes'];
		}
	}

	abstract public function renderContent(Renderer $renderer);

	/**
	 * @return string
	 */
	public function getLabel() {
		return $this->label;
	}

	public function getLabelTargetId() {
		return $this->getPrimaryInputId();
	}

	/**
	 * Does this control have an input or other form-related element that
	 * could be meaningfully associated with the control's label?
	 *
	 * @return bool
	 */
	public function supportsLabelAssociation() {
		return $this->hasPrimaryInput;
	}

	/**
	 * Whether this control renders its own label as part of its content.
	 *
	 * For example, a checkbox control will usually wrap a label element around
	 * its input element, so it would return true here.
	 *
	 * @return boolean
	 */
	public function includesOwnLabel() {
		return false;
	}

	public function parentLabelEnabled() {
		return $this->supportsLabelAssociation();
	}

	/**
	 * @return \YahnisElsts\AdminMenuEditor\Customizable\Settings\AbstractSetting[]
	 */
	public function getSettings() {
		return $this->settings;
	}

	/**
	 * @return string
	 */
	public function getType() {
		return $this->type;
	}

	/**
	 * @return bool
	 */
	public function isEnabled() {
		$enabled = $this->traitIsEnabled();
		if ( !empty($this->mainSetting) ) {
			$enabled = $enabled && $this->mainSetting->isEditableByUser();
		}
		return $enabled;
	}

	/**
	 * @return string
	 */
	public function getPrimaryInputId() {
		if ( !$this->hasPrimaryInput ) {
			return '';
		}
		return '_ame-cntrl-' . $this->id;
	}

	/**
	 * Generate an HTML tag based on the input settings of this control.
	 *
	 * This is mainly intended for creating input tags, but it also works for textarea tags.
	 *
	 * @param array<string,mixed> $attributes
	 * @param string $tagName
	 * @param string|null $content
	 * @return string
	 */
	protected function buildInputElement(
		$attributes = [],
		$tagName = 'input',
		$content = null
	) {
		//Merge input classes and any classes specified in the $attributes array.
		$classes = $this->inputClasses;
		if ( isset($attributes['class']) ) {
			$classes = array_merge($classes, (array)$attributes['class']);
			unset($attributes['class']);
		}

		$attributes = array_merge(
			[
				'id'                 => $this->hasPrimaryInput ? $this->getPrimaryInputId() : null,
				'class'              => $classes,
				'name'               => $this->getFieldName(),
				'disabled'           => !$this->isEnabled(),
				//Add the setting ID for the admin customizer module.
				'data-ac-setting-id' => $this->getAutoAcSettingId(),
			],
			$attributes,
			$this->inputAttributes
		);

		return $this->buildTag($tagName, $attributes, $content);
	}

	protected function getMainSettingValue($default = null) {
		if ( $this->mainSetting instanceof Setting ) {
			return $this->mainSetting->getValue($default);
		} else if ( $this->mainSetting instanceof Settings\WithSchema\SingularSetting ) {
			return $this->mainSetting->getValue($default);
		}
		return $default;
	}

	/**
	 * Generate the "name" attribute value for a form element associated with
	 * this control.
	 *
	 * @param string|null $compositeChildKey Only for composite settings: the key of the child setting.
	 * @param Settings\AbstractSetting|null $setting The setting shown in the field. Defaults to the main setting.
	 * @return string
	 */
	protected function getFieldName($compositeChildKey = null, $setting = null) {
		if ( $setting === null ) {
			$setting = $this->mainSetting;
		}
		if ( !($setting instanceof Settings\AbstractSetting) ) {
			return $this->id;
		}

		if ( ($compositeChildKey === null) || ($compositeChildKey === '') ) {
			$id = $setting->getId();
		} else {
			$id = $setting->getId() . '.' . $compositeChildKey;
			if ( $setting instanceof Settings\CompositeSetting ) {
				$child = $setting->getChild($compositeChildKey);
				if ( $child ) {
					$id = $child->getId();
				}
			}
		}

		//Convert dot notation to array notation.
		$parts = explode('.', $id);
		$root = array_shift($parts);
		if ( empty($parts) ) {
			return $root;
		}
		return $root . '[' . implode('][', $parts) . ']';
	}

	public function getAutoGroupTitle() {
		if ( $this->mainSetting instanceof Settings\AbstractSetting ) {
			$customTitle = $this->mainSetting->getCustomGroupTitle();
			if ( !empty($customTitle) ) {
				return $customTitle;
			}
		}
		return $this->getLabel();
	}

	/**
	 * Get a version of this control that is compatible with the Admin Customizer module.
	 *
	 * May return this object if it is already compatible or if there is
	 * no better alternative.
	 *
	 * @return Control
	 */
	public function getAdminCustomizerControl() {
		return $this;
	}

	protected function getAutoAcSettingId() {
		if ( ($this->mainSetting instanceof AbstractSetting) && $this->hasPrimaryInput ) {
			return $this->mainSetting->getId();
		}
		return null;
	}

	protected function getKoObservableExpression($defaultValue, ?AbstractSetting $setting = null) {
		if ( $setting === null ) {
			$setting = $this->mainSetting;
		}
		if ( !($setting instanceof AbstractSetting) ) {
			return wp_json_encode($defaultValue);
		}

		$settingId = $setting->getId();
		return sprintf(
			'$root.getSettingObservable(%s, %s)',
			wp_json_encode($settingId),
			wp_json_encode($defaultValue)
		);
	}

	protected function makeKoDataBind($bindingValues) {
		if ( empty($bindingValues) ) {
			return '';
		}

		$bindings = [];
		foreach ($bindingValues as $binding => $value) {
			$bindings[] = sprintf('%s: %s', $binding, $value);
		}
		return implode(', ', $bindings);
	}

	protected function getJsUiElementType() {
		return 'control';
	}

	public function serializeForJs() {
		$result = parent::serializeForJs();

		$label = $this->getLabel();
		if ( !empty($label) ) {
			$result['label'] = $label;
		}

		if ( $this->koComponentName === null ) {
			return $result;
		}
		$result['component'] = $this->koComponentName;

		//Map setting names to IDs.
		$settingIds = [];
		$settings = $this->settings;
		//The main setting is always available as "value". This means that a setting
		//could be duplicated in the "settings" array, but that should be fine.
		if ( $this->mainSetting instanceof AbstractSetting ) {
			$settings['value'] = $this->mainSetting;
		}
		foreach ($settings as $name => $setting) {
			//Skip non-string keys. Controls that care about accessing multiple
			//distinct settings should have custom keys.
			if ( !is_string($name) ) {
				continue;
			}

			//Recurse into structs and add all child settings separately as "name.child".
			//However, don't split up composite settings.
			if (
				($setting instanceof Settings\AbstractStructSetting)
				&& !($setting instanceof Settings\CompositeSetting)
			) {
				foreach (
					AbstractSetting::recursivelyIterateSettings($setting, false, $name)
					as $key => $childSetting
				) {
					$settingIds[$key] = $childSetting->getId();
				}
			} else if ( $setting instanceof AbstractSetting ) {
				$settingIds[$name] = $setting->getId();
			}
		}
		$result['settings'] = $settingIds;

		//Enabled state or condition.
		$result['enabled'] = $this->serializeConditionForJs();

		//Label association settings for components that display this control as a child.
		$includesOwnLabel = $this->includesOwnLabel();
		if ( $includesOwnLabel ) {
			$result['includesOwnLabel'] = true;
		}
		$labelTargetId = $this->getLabelTargetId();
		if ( $this->supportsLabelAssociation() && !empty($labelTargetId) ) {
			$result['labelTargetId'] = $labelTargetId;
		}

		return $result;
	}

	protected function getKoComponentParams() {
		$params = parent::getKoComponentParams();

		//Primary input ID.
		if ( $this->hasPrimaryInput ) {
			$params['primaryInputId'] = $this->getPrimaryInputId();
		}

		//Input classes.
		if ( !empty($this->inputClasses) ) {
			$params['inputClasses'] = $this->inputClasses;
		}

		//Input attributes.
		if ( !empty($this->inputAttributes) ) {
			$params['inputAttributes'] = $this->inputAttributes;
		}

		return $params;
	}
}