<?php namespace ProcessWire;

/**
 * InputfieldTinyMCETools
 *
 * Helper for managing TinyMCE settings and defaults
 *
 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
 * https://processwire.com
 *
 */
class InputfieldTinyMCESettings extends InputfieldTinyMCEClass {

	/**
	 * Runtime caches shared among all instances
	 * 
	 * @var array 
	 * 
	 */
	static protected $caches = array(
		'defaults' => array(),
		'settings' => array(), 
		'alignClasses' => array(),
		'renderReadyInline' => array(), 
		'langSettings' => array(), 
		'addDefaults' => array(), 
		'originalDefaults' => array(), 
	);

	/**
	 * Get settings from Inputfield vary from the $defaults
	 *
	 * @param array|null $defaults Default settings Default settings or omit to pull automatically
	 * @param string $cacheKey Optionally cache with this key
	 * @return array
	 *
	 */
	public function getSettings($defaults = null, $cacheKey = '') {

		$inputfield = $this->inputfield;
		
		if($cacheKey && isset(self::$caches['settings'][$cacheKey])) return self::$caches['settings'][$cacheKey];
		if($defaults === null) $defaults = $this->getDefaults();

		$settings = array();
		$features = $inputfield->features;
		$formats = $this->formats();
		
		foreach($defaults as $name => $defaultValue) {
			if($name === 'menubar') {
				if(in_array($name, $features)) {
					$value = $inputfield->get('menubar');
					if(empty($value) || $value === 'default') $value = $defaultValue;
				} else {
					$value = false;
				}
			} else if($name === 'statusbar') {
				$value = true;
			} else if($name === 'browser_spellcheck') {
				$value = in_array('spellcheck', $features);
			} else if($name === 'toolbar') {
				$value = in_array($name, $features) ? $inputfield->get($name) : '';
			} else if($name === 'toolbar_sticky') {
				$value = in_array('stickybars', $features);
			} else if($name === 'content_css') {
				$value = $inputfield->get($name);
				if($value === 'custom') {
					$value = $inputfield->get('content_css_url');
					if(empty($value)) continue;
				}
			} else if($name === 'directionality') {
				$value = $inputfield->getDirectionality();
			} else if($name === 'style_formats') {
				$value = $formats->getStyleFormats($defaults);
			} else if($name === 'block_formats') {
				$value = $formats->getBlockFormats();
			} else if($name === 'invalid_styles') {
				$value = $formats->getInvalidStyles($inputfield->invalid_styles, $defaultValue);
			} else if($name === 'formats') {
				// overlaps with native formats property so use data rather than get
				$value = $inputfield->data('formats');
			} else if($name === 'templates') {
				// overlaps with API variable
				$value = $inputfield->data($name);
			} else {
				$value = $inputfield->get($name);
				if($value === 'default') $value = $defaultValue;
			}
			if($name === 'removed_menuitems' && strpos($value, 'print') === false) {
				// the print option is not useful in inline mode
				if($inputfield->inlineMode) $value = trim("$value print");
			}
			if($value !== null && $value != $defaultValue) {
				$settings[$name] = $value;
			}
		}

		$this->applySkin($settings, $defaults);
		$this->applyPlugins($settings, $defaults);

		if(isset($defaults['style_formats'])) {
			$styleFormatsCSS = $inputfield->get('styleFormatsCSS');
			if($styleFormatsCSS) {
				$formats->applyStyleFormatsCSS($styleFormatsCSS, $settings, $defaults);
			}
		}

		if($cacheKey) self::$caches['settings'][$cacheKey] = $settings;

		return $settings;
	}

	/**
	 * Default settings for ProcessWire.config.InputfieldTinyMCE
	 *
	 * This should have no field-specific settings (no dynamic values)
	 *
	 * @property string $key
	 * @return array
	 *
	 */
	public function getDefaults($key = '') {
		
		if(!empty(self::$caches['defaults'])) {
			if($key) return isset(self::$caches['defaults'][$key]) ? self::$caches['defaults'][$key] : null;
			return self::$caches['defaults'];
		}

		$config = $this->wire()->config;
		$root = $config->urls->root;
		$url = $config->urls($this->inputfield);
		$tools = $this->tools();

		// root relative, i.e. '/site/modules/InputfieldTinyMCE/'
		$url = substr($url, strlen($root)-1);
		$alignClasses = $this->getAlignClasses();
		$mceSettingNames = $this->inputfield->getSettingNames('tinymce');
		$optionalSettingNames = $this->inputfield->getSettingNames('optionals');
		$optionals = $this->inputfield->optionals;

		// selector of elements that can be used with align commands

		$replacements = array(
			'{url}' => $url,
			'{alignleft}' => $alignClasses['left'], 
			'{aligncenter}' => $alignClasses['center'], 
			'{alignright}' => $alignClasses['right'], 
			'{alignfull}' => $alignClasses['full'],
		);
		
		$json = file_get_contents(__DIR__ . '/defaults.json');
		$json = str_replace(array_keys($replacements), array_values($replacements), $json);
		$defaults = $tools->jsonDecode($json, 'defaults.json');

		// defaults JSON file
		$file = $this->inputfield->defaultsFile;
		if($file) {
			$file = $config->paths->root . ltrim($file, '/');
			$data = $tools->jsonDecodeFile($file, 'default settings file for module');
			if(is_array($data) && !empty($data)) $defaults = array_merge($defaults, $data);
		}
	
		// defaults JSON text
		$json = $this->inputfield->defaultsJSON;
		if($json) {
			$data = $tools->jsonDecode($json, 'defaults JSON module setting'); 
			if(is_array($data) && !empty($data)) $defaults = array_merge($defaults, $data);
		}
	
		// extra CSS module setting
		$extraCSS = $this->inputfield->extraCSS;
		if(strlen($extraCSS)) {
			$contentStyle = isset($defaults['content_style']) ? $defaults['content_style'] : '';
			$contentStyle .= "\n$extraCSS";
			$defaults['content_style'] = $contentStyle;
		}

		// optionals
		foreach($optionalSettingNames as $name) {
			if(in_array($name, $optionals)) continue; // configured with field (not module)
			if(!in_array($name, $mceSettingNames)) continue; // not a direct TinyMCE setting
			$value = $this->inputfield->get($name);
			if($value === 'default' || $value === null) continue;
			if($name === 'invalid_styles' && is_string($value)) {
				$value = $this->formats()->invalidStylesStrToArray($value);
			}
			if(isset($defaults[$name]) && $defaults[$name] !== $value) {
				self::$caches['originalDefaults'][$name] = $defaults[$name];
			}
			$defaults[$name] = $value;
		}	
		
		$languageSettings = $this->getLanguageSettings();
		if(!empty($languageSettings)) $defaults = array_merge($defaults, $languageSettings);
		
		foreach($defaults as $k => $value) {
			if(strpos($k, 'add_') === 0 || strpos($k, 'append_') === 0 || strpos($k, 'replace_') === 0) {
				self::$caches['addDefaults'][$k] = $value;
				unset($defaults[$k]); 
			}
		}
		
		self::$caches['defaults'] = $defaults;
		
		if($key) return isset($defaults[$key]) ? $defaults[$key] : null;
		
		return $defaults;
	}

	/**
	 * Get original defaults from source JSON, prior to being overriden by module default settings
	 * 
	 * @param string $key
	 * @return array|mixed|null
	 * 
	 */
	public function getOriginalDefaults($key = '') {
		$defaults = $this->getDefaults();
		if($key) {
			if(isset(self::$caches['originalDefaults'][$key])) {
				return self::$caches['originalDefaults'][$key];
			} else {
				return isset($defaults[$key]) ? $defaults[$key] : null;
			}
		}
		return array_merge($defaults, self::$caches['originalDefaults']); 
	}

	/**
	 * Get 'add_' or 'replace_' default settings
	 * 
	 * @return array|mixed
	 * 
	 */
	public function getAddDefaults() {
		return self::$caches['addDefaults'];
	}

	/**
	 * Apply plugins settings
	 *
	 * @param array $settings
	 * @param array $defaults
	 *
	 */
	protected function applyPlugins( array &$settings, array $defaults) {
		$extPlugins = $this->inputfield->get('extPlugins');

		if(!empty($extPlugins)) {
			$value = $defaults['external_plugins'];
			foreach($extPlugins as $url) {
				$name = basename($url, '.js');
				$value[$name] = $url;
			}
			$settings['external_plugins'] = $value;
		}

		if(isset($defaults['plugins'])) {
			$plugins = $this->inputfield->get('plugins');
			if(empty($plugins) && !empty($defaults['plugins'])) $plugins = $defaults['plugins'];
			if(!is_array($plugins)) $plugins = explode(' ', $plugins);
			if(!in_array('pwlink', $plugins)) {
				unset($settings['external_plugins']['pwlink']);
				if(isset($settings['menu'])) {
					$settings['menu']['insert']['items'] = str_replace('pwlink', 'link', $settings['menu']['insert']['items']);
				}
			}
			if(!in_array('pwimage', $plugins)) {
				unset($settings['external_plugins']['pwimage']);
				if(isset($settings['menu'])) {
					$settings['menu']['insert']['items'] = str_replace('pwimage', 'image', $settings['menu']['insert']['items']);
				}
			}
			$settings['plugins'] = implode(' ', $plugins);
			if($settings['plugins'] === $defaults['plugins']) unset($settings['plugins']);
		}
	}

	/**
	 * Apply skin or skin_url directly to given settings/defaults
	 * 
	 * @param array $settings
	 * @param array $defaults
	 * 
	 */
	protected function applySkin(&$settings, $defaults) {
		$skin = $this->inputfield->skin;
		if($skin === 'custom') {
			$skinUrl = rtrim($this->inputfield->skin_url, '/');
			if(strlen($skinUrl)) {
				if(strpos($skinUrl, '//') === false) {
					$skinUrl = $this->wire()->config->urls->root . ltrim($skinUrl, '/');
				}
				if(!isset($defaults['skin_url']) || $defaults['skin_url'] != $skinUrl) {
					$settings['skin_url'] = $skinUrl;
				}
				unset($settings['skin']); 
			}
		} else {
			if(isset($defaults['skin']) && $defaults['skin'] != $skin) {
				$settings['skin'] = $skin;
			}
			unset($settings['skin_url']);
		}
	}

	/**
	 * Get image alignment classes
	 * 
	 * @return array
	 * 
	 */
	public function getAlignClasses() {
		if(empty(self::$caches['alignClasses'])) {
			$data = $this->wire()->modules->getModuleConfigData('ProcessPageEditImageSelect');
			self::$caches['alignClasses'] = array(
				'left' => (empty($data['alignLeftClass']) ? 'align_left' : $data['alignLeftClass']),
				'right' => (empty($data['alignRightClass']) ? 'align_right' : $data['alignRightClass']),
				'center' => (empty($data['alignCenterClass']) ? 'align_center' : $data['alignCenterClass']),
				'full' => 'align_full', 
			);
		}
		return self::$caches['alignClasses'];
	}

	/**
	 * Get settings from custom settings file
	 * 
	 * @return array
	 * 
	 */
	protected function getFromSettingsFile() {
		$file = $this->inputfield->get('settingsFile');
		if(empty($file)) return array();
		$file = $this->wire()->config->paths->root . ltrim($file, '/'); 
		return $this->tools()->jsonDecodeFile($file, 'settingsFile');	
	}

	/**
	 * Get settings from custom JSON
	 *
	 * @return array
	 * 
	 */
	protected function getFromSettingsJSON() {
		$json = trim((string) $this->inputfield->get('settingsJSON'));
		if(empty($json)) return array();
		return $this->tools()->jsonDecode($json, 'settingsJSON');
	}

	/**
	 * Get content_css URL
	 * 
	 * @param string $content_css
	 * @return string
	 * 
	 */
	public function getContentCssUrl($content_css = '') {
		
		$config = $this->wire()->config;
		$rootUrl = $config->urls->root;
		$defaultUrl = $config->urls($this->inputfield) . 'content_css/wire.css';
		
		if($this->inputfield->useFeature('document')) {
			$content_css = 'document';
		}

		if(empty($content_css)) {
			if($this->inputfield->useFeature('document')) {
				$content_css = 'document';
			} else {
				$content_css = $this->inputfield->content_css;
			}
		}
		
		if($content_css === 'wire' || empty($content_css)) {
			// default
			$url = $defaultUrl;

		} else if(strpos($content_css, '/') !== false) {
			// custom file
			$url = $rootUrl . ltrim($content_css, '/');

		} else if($content_css === 'custom') {
			// custom file (alternate/fallback)
			$content_css_url = $this->inputfield->content_css_url;
			if(empty($content_css_url) || strpos($content_css_url, '/') === false) {
				$url = $defaultUrl;
			} else {
				$url = $rootUrl . ltrim($content_css_url, '/');
			}

		} else if($content_css) {
			// defined
			$content_css = basename($content_css, '.css');
			$url = $config->urls($this->inputfield) . "content_css/$content_css.css";
			
		} else {
			$url = $defaultUrl;
		}

		return $url;
	}

	/**
	 * Prepare given settings ready for output
	 *
	 * This converts relative URLs to absolute, etc.
	 *
	 * @param array $settings
	 * @return array
	 *
	 */
	public function prepareSettingsForOutput(array $settings) {
		$config = $this->wire()->config;
		$rootUrl = $config->urls->root;
		//$inline = $this->inputfield->inlineMode > 0;

		/*
		if($inline) {
			// content_css not loaded here
			//$settings['content_css'] = '';
		*/
			
		if(isset($settings['content_css'])) {
			// convert content_css setting to URL
			$settings['content_css'] = $this->getContentCssUrl($settings['content_css']);
		}

		if(!empty($settings['external_plugins'])) {
			foreach($settings['external_plugins'] as $name => $url) {
				$settings['external_plugins'][$name] = $rootUrl . ltrim($url, '/');
			}
		}
		if(isset($settings['height'])) {
			$settings['height'] = "$settings[height]px";
		}
	
		if(isset($settings['toolbar']) && is_string($settings['toolbar'])) {
			$splitTools = array('styles', 'blocks'); 
			foreach($splitTools as $name) {
				$settings['toolbar'] = str_replace("$name ", "$name | ", $settings['toolbar']); 
			}
		}
		
		if(empty($settings['invalid_styles'])) {
			// for empty invalid_styles use blank string rather than blank array 
			$settings['invalid_styles'] = '';
		}
		
		if(!empty($settings['content_style'])) {
			// namespace content_style for .mce_content_body
			$contentStyle = $settings['content_style'];
			$contentStyle = str_replace('}', "}\n", $contentStyle);
			$contentStyle = preg_replace('![\s\r\n]+\{!', '{', $contentStyle);
			$lines = explode("\n", $contentStyle);
			foreach($lines as $k => $line) {
				$line = trim($line);
				if(empty($line)) {
					unset($lines[$k]);
				} else if(strpos($line, '.mce-content-body') !== false) {
					continue;
				} else if(strpos($line, '{')) {
					$lines[$k] = ".mce-content-body $line";
				}
			}
			$contentStyle = implode(' ', $lines);
			while(strpos($contentStyle, '  ') !== false) $contentStyle = str_replace('  ', ' ', $contentStyle);
			$contentStyle = str_replace(['{ ', ' }'], ['{', '}'], $contentStyle);
			$contentStyle = str_replace('@', "\\@", $contentStyle);
			$settings['content_style'] = $contentStyle;
		}
	
		/*
		if(isset($settings['plugins']) && is_array($settings['plugins'])) {
			$settings['plugins'] = implode(' ', $settings['plugins']); 
		}
		*/
	
		// ensure blank object properties resolve to {} in JSON rather than []	
		foreach($this->tools()->jsonBlankObjectProperties as $name) {
			if(!isset($settings[$name]) || !empty($settings[$name]) || !is_array($settings[$name])) continue;
			$settings[$name] = (object) $settings[$name];
		}

		return $settings;
	}

	/**
	 * Get language pack code
	 * 
	 * @return string
	 * 
	 */
	public function getLanguagePackCode() {
	
		$default = 'en_US';
		$languages = $this->wire()->languages;
		$sanitizer = $this->wire()->sanitizer;
		$path = __DIR__ . '/langs/';
		
		if(!$languages) return $default;
		
		$language = $this->wire()->user->language;
		
		// attempt to get from module setting
		$value = $this->inputfield->get("lang_$language->name");
		if($value) return $value;
	
		// attempt to get from non-default language name
		if(!$language->isDefault() && is_file("$path$language->name.js")) {
			return $language->name;
		}
	
		// attempt to get from admin theme
		$adminTheme = $this->wire()->adminTheme;
		if($adminTheme) {
			$value = $sanitizer->name($adminTheme->_('en'));
			if($value !== 'en' && is_file("$path$value.js")) return $value;
		}

		$value = $languages->getLocale();
	
		// attempt to get from locale setting
		if($value !== 'C') {
			if(strpos($value, '.')) list($value,) = explode('.', $value, 2);
			if(is_file("$path$value.js")) return $value;
			if(strpos($value, '_')) {
				list($value,) = explode('_', $value, 2);
				if(is_file("$path$value.js")) return $value;
			}
		}
	
		// attempt to get from CKEditor static translation
		$textdomain = '/wire/modules/Inputfield/InputfieldCKEditor/InputfieldCKEditor.module';
		if(is_file($this->wire()->config->paths->root . ltrim($textdomain, '/'))) {
			$value = _x('en', 'language-pack', $textdomain);
			if($value !== 'en') {
				$value = $sanitizer->name($value);
				if($value && is_file("$path$value.js")) return $value;
			}
		}

		return $default;
	}

	/**
	 * Get language pack settings
	 *
	 * @return array
	 * 
	 */
	public function getLanguageSettings() {
		if(!$this->wire()->languages) return array();
		$language = $this->wire()->user->language;
		if(isset(self::$caches['langSettings'][$language->id])) {
			return self::$caches['langSettings'][$language->id];
		}
		$code = $this->getLanguagePackCode();
		if($code === 'en_US') {
			$value = array();
		} else {
			$value = array(
				'language' => $code, 
				'language_url' => $this->wire()->config->urls($this->inputfield) . "langs/$code.js"
			);
		}
		self::$caches['langSettings'][$language->id] = $value;
		return $value;
	}

	/**
	 * Apply 'add_*' settings in $addSettings, plus merge all $addSettings into given $settings 
	 * 
	 * This updates the $settings and $addSettings variables directly
	 * 
	 * @param array $settings
	 * @param array $addSettings
	 * @param array $defaults
	 * 
	 */
	protected function applyAddSettings(array &$settings, array &$addSettings, array $defaults) {
	
		// apply add_style_formats when present
		if(isset($addSettings['add_style_formats'])) {
			$styleFormats = isset($settings['style_formats']) ? $settings['style_formats'] : $defaults['style_formats'];
			$settings['style_formats'] = $this->formats()->mergeStyleFormats($styleFormats, $addSettings['add_style_formats']);
			unset($addSettings['add_style_formats']);
		}
	
		// find other add_* properties, i.e. 'add_formats', 'add_invalid_styles', 'add_plugins'
		// these append rather than replace, i.e. 'add_formats' appends to 'formats'
		// also find any replace_* properties and replace setting values rather than append
		foreach($addSettings as $key => $addValue) {
			if(strpos($key, 'replace_') === 0) {
				list(,$k) = explode('replace_', $key, 2); 
				if(!isset($addSettings[$k]) && $addValue !== null) $addSettings[$k] = $addValue;
				unset($addSettings[$key]); 
				continue;
			}
			if(strpos($key, 'append_') === 0) {
				unset($addSettings[$key]); 
				$key = str_replace('append_', 'add_', $key);
			}
			if(strpos($key, 'add_') !== 0) continue;
			list(,$name) = explode('add_', $key, 2);
			unset($addSettings[$key]); 
			if(isset($settings[$name])) {
				// present in settings
				$value = $settings[$name];
			} else if(isset($defaults[$name])) {
				// present in defaults
				$value = $defaults[$name];
			} else {
				// not present, add it to settings
				$addSettings[$name] = $addValue;
				continue;
			}
			$addSettings[$name] = $this->mergeSetting($value, $addValue);
		}
	
		$settings = array_merge($settings, $addSettings);
	}

	/**
	 * Merge two setting values into one that combines them 
	 * 
	 * @param string|array|mixed $value
	 * @param string|array|mixed $addValue
	 * @return string|array|mixed
	 * 
	 */
	protected function mergeSetting($value, $addValue) {
		if(is_string($value) && is_string($addValue)) {
			$value .= " $addValue";
		} else if(is_array($addValue) && is_array($value)) {
			foreach($addValue as $k => $v) {
				if(is_int($k)) {
					// append
					$value[] = $v;
				} else {
					// append or replace
					$value[$k] = $v;
				}
			}
		} else {
			$value = $addValue;
		}
		return $value;
	}

	/**
	 * Merge all settings in given array and combine those with "add_" prefix
	 * 
	 * @param array $settings1
	 * @param array $settings2 Optionally specify this to merge/combine with those in $settings1
	 * @return array 
	 * 
	 */
	protected function mergeSettings(array $settings1, array $settings2 = array()) {
		$settings = array_merge($settings1, $settings2);
		$addSettings = array();
		foreach($settings1 as $key => $value) {
			if(strpos($key, 'add_') !== 0) continue;
			$addSettings[$key] = $value;
		}
		foreach($settings2 as $key => $value) {
			if(strpos($key, 'add_') !== 0) continue;
			if(isset($addSettings[$key])) {
				$addSettings[$key] = $this->mergeSetting($addSettings[$key], $value);
			} else {
				$addSettings[$key] = $value;
			}
		}
		if(count($addSettings)) $settings = array_merge($settings, $addSettings);
		return $settings;
	}

	/**
	 * Determine which settings go where and apply to Inputfield
	 * 
	 * @param array $addSettings Optionally add this settings on top of those that would otherwise be used
	 * 
	 */
	public function applyRenderReadySettings(array $addSettings = array()) {
	
		$config = $this->wire()->config;
		$inputfield = $this->inputfield;
		$configName = $inputfield->getConfigName();
		
		// default settings
		$defaults = $this->getDefaults();
		$addDefaults = $this->getAddDefaults();
		$fileSettings = $this->getFromSettingsFile();
		$jsonSettings = $this->getFromSettingsJSON();
		
		if(count($fileSettings)) $addDefaults = $this->mergeSettings($addDefaults, $fileSettings);
		if(count($jsonSettings)) $addDefaults = $this->mergeSettings($addDefaults, $jsonSettings);
		if(count($addSettings)) $addDefaults = $this->mergeSettings($addDefaults, $addSettings);
		$addSettings = $addDefaults;

		if($configName && $configName !== 'default') {
			$js = $config->js($inputfield->className());

			// get settings that differ between field and defaults, then set to new named config
			$diffSettings = $this->getSettings($defaults, $configName);
			$mergedSettings = array_merge($defaults, $diffSettings);
			//$contentStyle = isset($mergedSettings['content_style']) ? $mergedSettings['content_style'] : '';

			if(count($addSettings)) {
				// merges $addSettings into $diffSettings
				$this->applyAddSettings($diffSettings, $addSettings, $defaults);
			}

			if(!isset($js['settings'][$configName])) {
				$js['settings'][$configName] = $this->prepareSettingsForOutput($diffSettings);
				$config->js($inputfield->className(), $js);
			}

			// get settings that will go in data-settings attribute 
			// remove settings that cannot be set for field/template context
			unset($mergedSettings['style_formats'], $mergedSettings['content_style'], $mergedSettings['content_css']); 
			$dataSettings = $this->getSettings($mergedSettings);

		} else {
			// no configName in use, data-settings attribute will hold all non-default settings
			$dataSettings = $this->getSettings($defaults);
			//$contentStyle = isset($dataSettings['content_style']) ? $dataSettings['content_style'] : '';
			if(count($addSettings)) {
				$this->applyAddSettings($dataSettings, $addSettings, $defaults);
			}
		}

		if($inputfield->inlineMode) {
			if($inputfield->inlineMode < 2) unset($dataSettings['height']);
			$dataSettings['inline'] = true;
			/*
			if($contentStyle && $adminTheme) {
				$cssName = $configName;
				if(empty($cssName)) {
					$cssName = substr(md5($contentStyle), 0, 4) . strlen($contentStyle);
				}
				$inputfield->addClass("tmcei-$cssName", 'wrapClass');
				if(!isset(self::$caches['renderReadyInline'][$cssName])) {
					// inline mode content_style settings, ensure they are visible before inline init
					//$ns = ".tmcei-$cssName .mce-content-body ";
					//$contentStyle = $ns . str_replace('}', "} $ns", $contentStyle) . '{}';
					//$adminTheme->addExtraMarkup('head', "<style>$contentStyle</style>");
					self::$caches['renderReadyInline'][$cssName] = $cssName;
				}
			}
			*/
		}

		$dataSettings = count($dataSettings) ? $this->prepareSettingsForOutput($dataSettings) : array();
		if($inputfield->renderValueMode) $dataSettings['readonly'] = true;
		
		$features = array('imgUpload', 'imgResize', 'pasteFilter');
		foreach($features as $key => $feature) {
			if(!$inputfield->useFeature($feature)) unset($features[$key]);
		}
		if($inputfield->lazyMode) $features[] = "lazyMode$inputfield->lazyMode";
		
		$inputfield->wrapAttr('data-configName', $configName);
		$inputfield->wrapAttr('data-settings', $this->tools()->jsonEncode($dataSettings, 'data-settings', false));
		$inputfield->wrapAttr('data-features', implode(',', $features));
	}

	/**
	 * Apply settings settings to $this->inputfield to inherit from another field
	 * 
	 * This is called from the main InputfieldTinyMCE class. 
	 *
	 * @param string $fieldName Field name or 'fieldName:id' string
	 * @return bool|Field Returns false or field inherited from
	 *
	 */
	public function applySettingsField($fieldName) {

		$fieldId = 0;
		$error = '';
		$hasField = $this->inputfield->hasField;
		$hasPage = $this->inputfield->hasPage;

		if(strpos($fieldName, ':')) {
			list($fieldName, $fieldId) = explode(':', $fieldName);
		} else if(ctype_digit("$fieldName")) {
			$fieldName = (int) $fieldName; // since fields.get also accepts IDs
		}
	
		// no need to inherit from oneself
		if("$fieldName" === "$hasField") return false;

		$field = $this->wire()->fields->get($fieldName);

		if(!$field) {
			$error = "Cannot find settings field '$fieldName'";
		} else if(!$field->type instanceof FieldtypeTextarea) {
			$error = "Settings field '$fieldName' is not of type FieldtypeTextarea";
			$field = null;
		} else if(!wireInstanceOf($field->get('inputfieldClass'), $this->inputfield->className())) {
			$error = "Settings field '$fieldName' is not using TinyMCE";
			$field = null;
		}

		if(!$field && $fieldId && $fieldName) {
			// try again with field ID only, which won't go recursive again
			return $this->applySettingsField($fieldId);
		}

		if(!$field) {
			if($error) $this->error($this->inputfield->attr('name') . ": $error");
			return false;
		}
		
		if($field->flags & Field::flagFieldgroupContext) {
			// field already in fieldgroup context
		} else if($hasPage && $hasPage->template->fieldgroup->hasFieldContext($field)) {
			// get in context of current page template’s fieldgroup, if applicable
			$field = $hasPage->template->fieldgroup->getFieldContext($field->id);
		}

		// identify settings to apply
		$data = array();

		foreach($this->inputfield->getSettingNames(array('tinymce', 'field')) as $name) {
			$value = $field->get($name);
			if($value !== null) $data[$name] = $value;
		}

		// apply settings
		$this->inputfield->data($data);

		return $field;
	}

}
