<?php namespace ProcessWire;

/**
 * Main multi-language support module
 *
 * This module is the front door to all the other language modules and files. 
 *
 * ProcessWire 3.x, Copyright 2024 by Ryan Cramer
 * https://processwire.com
 * 
 * @property int $languagesPageID
 * @property int $defaultLanguagePageID
 * @property int $languageTranslatorPageID
 * @property array $otherLanguagePageIDs Quick reference to non-default language IDs, for when needed before languages loaded
 *
 */

class LanguageSupport extends WireData implements Module, ConfigurableModule {

	/**
	 * Return information about the module
	 *
	 */
	static public function getModuleInfo() {
		return array(
			'title' => 'Languages Support',
			'version' => 104,
			'summary' => 'ProcessWire multi-language support.',
			'author' => 'Ryan Cramer',
			'autoload' => true,
			'singular' => true,
			'installs' => array(
				'ProcessLanguage', 
				'ProcessLanguageTranslator', 
			),
			'addFlag' => Modules::flagsNoUserConfig
		);
	}

	/**
	 * Name of template used for language pages
	 *
	 */
	const languageTemplateName = 'language';

	/**
	 * Name of field used to store the language page ref
	 *
	 */
	const languageFieldName = 'language';

	/**
 	 * This module can possibly be init'd before PW's Modules class fully loads, so we keep this to prevent double initialization
	 *
	 */
	protected $initialized = false; 

	/**
	 * Reference to the default language page
	 * 
	 * @var Language|null
	 *
	 */
	protected $defaultLanguagePage = null;

	/**
	 * Array of pages that were cached before this module was loaded. 
	 * 
	 * @var array
	 *
	 */
	protected $earlyCachedPages = array();

	/**
	 * Instanceof LanguageSupportFields, if installed
	 * 
	 * @var LanguageSupportFields|null
	 *
	 */
	protected $LanguageSupportFields = null;

	/**
	 * Instanceof LanguageTabs, if installed
	 * 
	 * @var LanguageTabs|null
	 *
	 */
	protected $languageTabs = null;

	/**
	 * Construct and set our dynamic config vars
	 *
	 */
	public function __construct() {
		
		parent::__construct();

		$this->set('initialized', false); 

		// load other required classes
		$dirname = dirname(__FILE__); 
		require_once($dirname . '/FieldtypeLanguageInterface.php'); 
		require_once($dirname . '/Language.php'); 
		require_once($dirname . '/Languages.php'); 
		require_once($dirname . '/LanguageTranslator.php');
		require_once($dirname . '/LanguagesValueInterface.php'); 
		require_once($dirname . '/LanguagesPageFieldValue.php'); 


		// set our config var placeholders
		$this->set('languagesPageID', 0); 
		$this->set('defaultLanguagePageID', 0); 
		$this->set('languageTranslatorPageID', 0); 

		// quick reference to non-default language IDs, for when needed before languages loaded
		$this->set('otherLanguagePageIDs', array()); 

	}

	/**
	 * Initialize the language support API vars
	 *
	 */
	public function init() {
		
		$pages = $this->wire()->pages;
		$user = $this->wire()->user;
		$config = $this->wire()->config;
		$templates = $this->wire()->templates;
		$modules = $this->wire()->modules;

		// document which pages were already cached at this point, as their values may need 
		// to be reloaded to account for language fields. 
		foreach($pages->getCache() as $id => $value) {
			$this->earlyCachedPages[$id] = $value;
		}

		// prevent possible double init
		if($this->initialized) return; 
		$this->initialized = true;

		FieldtypePageTitle::$languageSupport = true; 

		$defaultLanguagePageID = $this->defaultLanguagePageID; 

		// create the $languages API var
		$languageTemplate = $templates->get('language'); 
		if(!$languageTemplate) return;
		
		if(!$this->languagesPageID) {
			// fallback if LanguageSupport config lost or not accessible for some reason
			$this->languagesPageID = $pages->get("template=admin, name=languages"); 
		}
	
		// prevent fields like 'title' from autojoining until languages are fully loaded
		$pages->setAutojoin(false); 
	
		/** @var Languages $languages */
		$languages = $this->wire(new Languages($this->wire('wire'), $languageTemplate, $this->languagesPageID)); 
		$_default = null; // just in case

		// ensure all languages are loaded and get instantiated versions of system/default languages
		$numOtherLanguages = 0;
		foreach($languages as $language) {
			if($language->id == $defaultLanguagePageID) {
				$this->defaultLanguagePage = $language; 
			} else if($language->name === 'default') {
				$_default = $language; // backup plan
			} else {
				$numOtherLanguages++;
				PageProperties::$languageProperties["name$language->id"] = array('name', $language->id);
				PageProperties::$languageProperties["status$language->id"] = array('status', $language->id);
			}
		}
		
		if(!$this->defaultLanguagePage) {
			if($_default) {
				$this->defaultLanguagePage = $_default;
			} else {
				$this->defaultLanguagePage = $languages->getAll()->first();
			}
		}
		
		$this->defaultLanguagePage->setIsDefaultLanguage();
		$languages->setDefault($this->defaultLanguagePage);
		
		// set $languages API variable
		$this->wire('languages', $languages); 

		// identify the current language from the user, or set one if it's not already
		if($user->language && $user->language->id) {
			// $language = $this->user->language; 
		} else {
			$language = $this->defaultLanguagePage; 
			$user->language = $language; 
		}

		$config->dateFormat = $this->_('Y-m-d H:i:s'); // Sortable date format used in the admin
		$locale = $this->_('C'); // Value to pass to PHP's setlocale(LC_ALL, 'value') function when initializing this language // Default is 'C'. Specify '0' to skip the setlocale() call (and carry on system default). Specify CSV string of locales to try multiple locales in order.
		if($locale != '0') $languages->setLocale(LC_ALL, $locale); 

		// setup our hooks handled by this class
		$this->addHookBefore('Inputfield::render', $this, 'hookInputfieldBeforeRender');
		$this->addHookBefore('Inputfield::renderValue', $this, 'hookInputfieldBeforeRender'); 
		$this->addHookAfter('Inputfield::render', $this, 'hookInputfieldAfterRender');
		$this->addHookAfter('Inputfield::renderValue', $this, 'hookInputfieldAfterRender'); 
		$this->addHookAfter('Inputfield::processInput', $this, 'hookInputfieldAfterProcessInput'); 
		$this->addHookBefore('Inputfield::processInput', $this, 'hookInputfieldBeforeProcessInput'); 
		$this->addHookMethod('Inputfield::getLanguageValue', $this, 'hookInputfieldGetLanguageValue');
		$this->addHookMethod('Inputfield::setLanguageValue', $this, 'hookInputfieldGetLanguageValue');
		$this->addHookAfter('Field::getInputfield', $this, 'hookFieldGetInputfield');
		$pages->addHook('added', $this, 'hookPageAdded'); 
		$pages->addHook('deleteReady', $this, 'hookPageDeleteReady'); 
		$this->addHook('Page::setLanguageValue', $this, 'hookPageSetLanguageValue');
		$this->addHook('Page::setLanguageValues', $this, 'hookPageSetLanguageValues');
		$this->addHook('Page::getLanguageValue', $this, 'hookPageGetLanguageValue');
		$this->addHook('Page::getLanguageValues', $this, 'hookPageGetLanguageValues');

		if($modules->isInstalled('LanguageSupportFields')) {
			$this->LanguageSupportFields = $modules->get('LanguageSupportFields'); 
			$this->LanguageSupportFields->LS_init();
			if($languages->getPageEditPermissions('none') && !$user->hasPermission('page-edit-lang-none')) {
				$this->addHookBefore('InputfieldWrapper::renderInputfield', $this, 'hookInputfieldWrapperBeforeRenderInputfield');
			}
		}
	
		if($numOtherLanguages && $numOtherLanguages != count($this->otherLanguagePageIDs)) {
			$this->refreshLanguageIDs();
		}
		
		// restore autojoin state for pages
		$pages->setAutojoin(true);
	}

	/**
	 * Called by ProcessWire when API is fully ready with known $page
	 *
	 */
	public function ready() {
		$page = $this->wire()->page;
		
		// styles used by our Inputfield hooks
		if($page->template->name === 'admin') { 
			$config = $this->wire()->config;
			$config->styles->add($config->urls('LanguageSupport') . "LanguageSupport.css"); 
			$language = $this->wire()->user->language;
			if($language) $config->js('LanguageSupport', array(
				'language' => array(
					'id' => $language->id, 
					'name' => $language->name,
					'title' => (string) $language->title, 
				)
			));
			$modules = $this->wire()->modules;
			if($modules->isInstalled('LanguageTabs')) {
				$this->languageTabs = $modules->get('LanguageTabs');
			}
		}

		// if languageSupportFields is here, then we have to deal with pages that loaded before this module did
		if($this->LanguageSupportFields) {
			// save the names of all fields that support languages
			$fieldNames = $this->LanguageSupportFields->getMultilangFieldNames();
			
			// unset the values from all the early cached pages since they didn't recognize languages
			// this will force them to reload when accessed
			foreach($this->earlyCachedPages as /* $id => */ $p) {
				$t = $p->trackChanges();
				if($t) $p->setTrackChanges(false);
				foreach($fieldNames as $name) unset($p->$name); 
				if($t) $p->setTrackChanges(true); 
			}
		}
		
		// release this as we don't need it anymore
		$this->earlyCachedPages = array();

		if($this->LanguageSupportFields) $this->LanguageSupportFields->LS_ready(); 
	}

	/**
	 * Returns whether or not Inputfield is editable for current user in language context
	 * 
	 * Takes the page-edit-lang-none permission into account
	 * 
	 * @param Inputfield $inputfield
	 * @return bool
	 * 
	 */
	protected function editableInputfield(Inputfield $inputfield) {
		
		$page = $this->wire()->page;
		
		$alwaysAllowInputfields = array(
			'InputfieldWrapper',
			'InputfieldPageName',
			'InputfieldSubmit',
			'InputfieldButton',
			'InputfieldHidden',
		);
		// ignore this call if in ProcessProfile
		if($page->process == 'ProcessProfile') return true;
		if($page->process == 'ProcessLanguage') return true;
		
		$user = $this->wire()->user;
		$languages = $this->wire()->languages;
		
		if($user->isSuperuser()) return true; 
		if($inputfield->getSetting('useLanguages')) return true; 
		if(!$this->LanguageSupportFields) return true; 
		$permissions = $languages->getPageEditPermissions();
		if(!isset($permissions['none'])) return true;
		if(!$this->wire()->process instanceof WirePageEditor) return true;
		if($inputfield->name == 'delete_page') return true;
		$allow = false;
		foreach($alwaysAllowInputfields as $type) {
			$type = __NAMESPACE__ . "\\$type";
			if($inputfield instanceof $type) {
				$allow = true;
				break;
			}
		}
		if($allow) return true;
		if($inputfield->hasFieldtype && $this->LanguageSupportFields->isAlternateField($inputfield->name)) return true; 
		if($languages->editable('none')) return true; 
		
		return false;
	}
	
	/**
	 * Hook before Inputfield::render to set proper default language value
	 *
	 * Only applies to Inputfields that have: useLanguages == true
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookInputfieldBeforeRender(HookEvent $event) {

		/** @var Inputfield $inputfield */
		$inputfield = $event->object; 
		if(!$inputfield->useLanguages) return;
		
		$user = $this->wire()->user;
		$userLanguage = $user->language;
		if(!$userLanguage) return;

		// set 'value' attribute to default language values	
		if($userLanguage->id !== $this->defaultLanguagePageID) {
			$t = $inputfield->trackChanges();
			if($t) $inputfield->setTrackChanges(false);
			$inputfield->attr('value', $inputfield->get('value' . $this->defaultLanguagePageID)); 
			if($t) $inputfield->setTrackChanges(true); 
		}
	}
	
	/**
	 * Hook before InputfieldWrapper::renderInputfield 
	 *
	 * Only applies to Inputfields that have: useLanguages == false.
	 * Applies only if page-edit-lang-none permission is installed.
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookInputfieldWrapperBeforeRenderInputfield(HookEvent $event) {

		/** @var Inputfield $inputfield */
		$inputfield = $event->arguments(0);
		if($inputfield->getSetting('useLanguages')) return;
		$renderValueMode = $event->arguments(1);

		if(!$this->editableInputfield($inputfield) && !$renderValueMode) {
			$event->return = '';
			$event->replace = true;
		}
	}

	/**
	 * Wrap the inputfield output with a language name label
	 *
	 * @param string $out Existing inputfield output
	 * @param string $id ID attribute to use
	 * @param Language $language
	 * @return string
	 *
	 */
	public function wrapInputfieldOutput($out, $id, Language $language) {
		
		$label = (string) $language->title;
		if(!strlen($label)) $label = $language->name; 
		$class = 'LanguageSupport';
		$labelClass = 'LanguageSupportLabel detail';
		
		if(!$this->wire()->languages->editable($language)) {
			$labelClass .= ' LanguageNotEditable';
			$class .= ' LanguageNotEditable';
			$label = "<s>$label</s>";
			$out = 
				"<p class='detail'>" . 
				sprintf($this->_('Changes to this field will not be saved because you do not have permission for language: %s.'), 
					$language->get('title|name')) . 
				"</p>" . 
				$out;
		}
		
		$out  = "<div class='$class' id='langTab_$id' data-language='$language->id'>" . 
				"<label for='$id' class='$labelClass'>$label</label>" . $out . 
				"</div>";
		
		return $out; 
	}

	/**
	 * Hook into Inputfield::render to duplicate inputs for other languages
	 *
	 * Only applies to Inputfields that have: useLanguages == true
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookInputfieldAfterRender(HookEvent $event) {

		static $numLanguages = null;
		
		if(!$event->return) return; // if already empty, nothing to do
		
		/** @var Inputfield $inputfield */
		$inputfield = $event->object;
		$name = $inputfield->attr('name'); 
		
		$renderValueMode = $event->method == 'renderValue'; 
		
		$languages = $this->wire()->languages;
		
		if(is_null($numLanguages)) $numLanguages = $languages->count();

		// provide an automatic translation for some system/default fields if they've not been overridden in the fields editor
		if($name == 'language' && $inputfield->label == 'Language') {
			$inputfield->label = $this->_('Language'); // Label for 'language' field in user profile
		} else if($name == 'email' && $inputfield->label == 'E-Mail Address') {
			$inputfield->label = $this->_('E-Mail Address'); // Label for 'email' field in user profile
		} else if($name == 'title' && $inputfield->label == 'Title') {
			$inputfield->label = $this->_('Title'); // Label for 'title' field used throughout ProcessWire
		}

		// check if this is a language alternate field (i.e. title_es or title)
		if($this->LanguageSupportFields) {
			$language = $this->LanguageSupportFields->isAlternateField($name);
			if($language) {
				$event->return = $this->wrapInputfieldOutput($event->return, $inputfield->attr('id'), $language); 			
				return;
			}
		}

		if(!$inputfield->getSetting('useLanguages') || $numLanguages < 2) return;

		// keep originals to restore later (including $name, which we already got above)
		$id = $inputfield->attr('id');
		$value = $inputfield->attr('value');
		$required = $inputfield->required; 
		$collapsed = $inputfield->collapsed;
		$trackChanges = $inputfield->trackChanges(); 
		$inputfield->setTrackChanges(false); 
		if($this->languageTabs) $this->languageTabs->resetTabs();
		$out = '';
		
		foreach($languages as $language) {
			$languageID = (int) $language->id;
			$languages->setLanguage($language);

			if($language->isDefault) { 
				// default language
				$newID = $id;
				$o = $event->return; 
				$inputfield->attr('id', $newID);

			} else {
				// non-default language
				$newID = $id . "__$languageID";
				$newName = $name . "__$languageID";
				$inputfield->attr('id', $newID); 
				$inputfield->attr('name', $newName); 
				$valueAttr = "value$languageID";
				$inputfield->required = false;
				$inputfield->setAttribute('value', $inputfield->$valueAttr); 
				$o = $renderValueMode ? $inputfield->___renderValue() : $inputfield->___render(); 
			}
			$languages->unsetLanguage();
			
			if($collapsed == Inputfield::collapsedBlank && !$inputfield->isEmpty()) {
				$inputfield->collapsed = Inputfield::collapsedNo;
			}
			
			$out .= $this->wrapInputfieldOutput($o, $newID, $language);
			if($this->languageTabs) $this->languageTabs->addTab($inputfield, $language); 
		}

		$inputfield->setAttribute('name', $name);
		$inputfield->setAttribute('id', $id);
		$inputfield->setAttribute('value', $value);
		$inputfield->required = $required; 
		$inputfield->setTrackChanges($trackChanges);
		
		if($this->languageTabs) {
			$out = $this->languageTabs->renderTabs($inputfield, $out); 
		}
		
		$event->return = $out; 
	}

	/**
	 * Hook before Inputfield::processInput to process input for other languages (or prevent it)
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookInputfieldBeforeProcessInput(HookEvent $event) {
		
		/** @var Inputfield $inputfield */
		$inputfield = $event->object;
		$replace = false; 
		
		if($inputfield->getSetting('useLanguages') || $inputfield->getSetting('hasLanguages')) { 
			// multi-language field
			$this->hookInputfieldBeforeRender($event); // ensures default language values are populated
			if(!$this->wire()->languages->editable($this->defaultLanguagePage)) $replace = true; 
			
		} else {
			// not a native multi-language field, check if it's language alternate or not editable
			if(!$this->editableInputfield($inputfield)) {
				$replace = true;
			} else if($inputfield->hasFieldtype && $this->LanguageSupportFields) {
				$language = $this->LanguageSupportFields->isAlternateField($inputfield->name);
				if($language && !$this->wire()->languages->editable($language)) $replace = true;
			}
		}
		
		if($replace) {
			// if field or language not editable, prevent processInput from running
			$event->replace = true;
			$event->return = $inputfield;
		}
	}

	/**
	 * Hook into Inputfield::processInput to process input for other languages
	 *
	 * Only applies to Inputfields that have: useLanguages == true
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookInputfieldAfterProcessInput(HookEvent $event) {

		/** @var Inputfield $inputfield */
		$inputfield = $event->object;
		if(!$inputfield->getSetting('useLanguages')) return;
		
		$post = $event->arguments[0];
		$languages = $this->wire()->languages;	
		
		// originals
		$name = $inputfield->attr('name');
		$id = $inputfield->attr('id');
		$value = $inputfield->attr('value');
		$required = $inputfield->required; 
	
		// process and set value for each language
		foreach($languages as $language) {
			
			// default language was already handled
			if($language->isDefault()) continue; 
			
			// if language isn't editable, don't process it
			if(!$languages->editable($language)) continue; 
			
			$languageID = (int) $language->id; 	
			$newID = $id . "__$languageID";
			$newName = $name . "__$languageID";
			$inputfield->setTrackChanges(false);
			$inputfield->attr('id', $newID); 
			$inputfield->attr('name', $newName); 
			// other language values not required, even if default language value is 
			$inputfield->required = false; 
			$valueAttr = "value$languageID";
			$inputfield->attr('value', $inputfield->$valueAttr); 
			$inputfield->setTrackChanges(true);
			$inputfield->___processInput($post);
			$inputfield->set($valueAttr, $inputfield->attr('value')); 
		}

		// restore originals
		$inputfield->setTrackChanges(false);
		$inputfield->setAttribute('name', $name);
		$inputfield->setAttribute('id', $id);
		$inputfield->setAttribute('value', $value);
		$inputfield->required = $required; 
		$inputfield->setTrackChanges(true); 
	}

	/**
	 * Adds an Inputfield::setLanguageValue($language, $value) method
	 * 
	 * @param HookEvent $event
	 * @since 3.0.238
	 * 
	 */
	public function hookInputfieldSetLanguageValue(HookEvent $event) {
		
		/** @var Inputfield $inputfield */
		$inputfield = $event->object; 
		$language = $event->arguments(0);
		$value = $event->arguments(1);
		$valuePrevious = $inputfield->val();
		
		if($value === $valuePrevious) return; // nothing to do

		if(!$language instanceof Language) {		
			$language = $this->wire()->languages->get($language);
		}
		
		if(!$language || !$language->id) {
			// nothing to do
		} else if($valuePrevious instanceof LanguagesValueInterface) {
			$valuePrevious->setLanguageValue($language->id, $value);
		} else if($language->isDefault()) {
			$inputfield->val($value);
		} else {
			$inputfield->set("value$language", $value);
		}
	}

	/**
	 * Adds an Inputfield::getLanguageValue($language) method
	 *
	 * @param HookEvent $event
	 * @since 3.0.238
	 *
	 */
	public function hookInputfieldGetLanguageValue(HookEvent $event) {
		
		/** @var Inputfield $inputfield */
		$inputfield = $event->object;
		$language = $event->arguments(0);
		$value = $inputfield->val();
		
		if(!$language instanceof Language) {
			$language = $this->wire()->languages->get($language);
		}
		
		if(!$language || !$language->id) {
			$event->return = null;
		} else if($value instanceof LanguagesValueInterface) {
			$event->return = $value->getLanguageValue($language->id);
		} else if($language->isDefault()) {
			$event->return = $value;
		} else {
			$event->return = $inputfield->get("value$language"); 
		}
	}

	/**
	 * Hook into Field::getInputfield to change label/description to proper language
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookFieldGetInputfield(HookEvent $event) {

		$language = $this->wire()->user->language; 
		if(!$language || !$language->id) return; 

		$field = $event->object; /** @var Field $field */
		$page = $event->arguments[0]; /** @var Page $page */
		$template = $page ? $page->template : null; /** @var Template|null $template */
		$inputfield = $event->return; /** @var Inputfield $inputfield */	
		
		if(!$inputfield) return;
		
		$translatable = array('label', 'description', 'notes');
		if($inputfield->attr('placeholder') !== null && $this->wire()->process != 'ProcessField') {
			$translatable[] = 'placeholder';
		}
		$languages = $template ? $template->getLanguages() : $this->wire()->languages; 
		$useLanguages = $template && $template->noLang ? false : true;
		if(!$languages) $languages = $this->wire()->languages; 

		// populate language versions where available
		foreach($translatable as $key) {
			$langKey = $key . $language->id; // i.e. label1234
			$value = $field->$langKey; 
			if(!$value) continue; 
			$inputfield->$key = $value;
		}
		
		// see if this fieldtype supports languages natively
		if($field->type instanceof FieldtypeLanguageInterface && $useLanguages) {
			
			// populate useLanguages in the inputfield so we can detect it elsehwere
			$inputfield->set('useLanguages', true); 

			$value = $page->get($field->name);

			// set values in this field specific to each language
			foreach($languages as $language) {
				$languageValue = '';
				if($value instanceof LanguagesPageFieldValue) {
					$languageValue = $value->getLanguageValue($language->id);
				} else {
					if($language->isDefault) $languageValue = $value; 
				}
				$inputfield->set('value' . $language->id, $languageValue); 	
			}
			
			// following this hookInputfieldBeforeRender() completes the process after
			// Fieldgroup::getPageInputfields() which sets the value attribute of Inputfields
		}

		$event->return = $inputfield; 
	}

	/**
	 * Hook called when new language added
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookPageAdded(HookEvent $event) {

		/** @var Page $page */
		$page = $event->arguments[0];
		if($page->template->name != self::languageTemplateName) return; 

		// trigger hook in $languages
		$ids = $this->otherLanguagePageIDs; 
		$ids[] = $page->id; 
		$this->set('otherLanguagePageIDs', $ids); 
		$this->wire()->languages->added($page);

		// save this as a known language page with module settings
		// this is a shortcut used to identify language pages before the API is fully ready
		$modules = $this->wire()->modules;
		$configData = $modules->getModuleConfigData('LanguageSupport'); 
		$configData['otherLanguagePageIDs'][] = $page->id; 
		$modules->saveModuleConfigData('LanguageSupport', $configData); 

	}

	/**
	 * Hook called when language is deleted
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookPageDeleteReady(HookEvent $event) {

		/** @var Page $page */
		$page = $event->arguments[0];
		if($page->template->name != self::languageTemplateName) return; 
		$language = $page; 

		// remove any language-specific values from any fields
		foreach($this->wire()->fields as $field) {
			/** @var Field $field */
			$changed = false;

			foreach(array('label', 'description', 'notes') as $name) {
				$name = $name . $language->id;	
				if(!isset($field->$name)) continue;
				$field->remove($name);
				$this->message("Removed $language->name $name from field $field->name"); 
				$changed = true;
			}

			if($changed) $field->save();
		}

		// remove template labels
		foreach($this->wire()->templates as $template) {
			/** @var Template $template */
			$name = 'label' . $page->id; 
			if(isset($template->$name)) {
				$template->remove($name); 
				$template->save();
				$this->message("Removed $language->name label from template $template->name"); 
			}
		}

		// trigger hook in $languages
		$this->wire()->languages->deleted($page);

		// update the other language module IDs to remove the uninstalled language
		$modules = $this->wire()->modules;
		$configData = $modules->getModuleConfigData('LanguageSupport'); 
		$key = array_search($page->id, $configData['otherLanguagePageIDs']); 
		if($key !== false) {
			unset($configData['otherLanguagePageIDs'][$key]);
			$modules->saveModuleConfigData('LanguageSupport', $configData); 
		}
	}

	/**
	 * Adds a Page::setLanguageValue($language, $fieldName, $value) method 
	 * 
	 * Provides a common interface for setting all language values to a Page. 
	 * 
	 * This method exists in this class rather than one of the field-specific classes
	 * because it deals with both language fields and page names, and potentially
	 * other types of unknown types that implement LanguagesValueInterface. 
	 * 
	 * @param HookEvent $event
	 * @throws WireException
	 *
	 */
	public function hookPageSetLanguageValue(HookEvent $event) {
	
		$page = $event->object; /** @var Page $page */
		$language = $event->arguments(0); /** @var Language $language */
		$field = $event->arguments(1); /** @var string|Field $field */
		$value = $event->arguments(2);
		$languages = $this->wire()->languages;
		
		$event->return = $page; 
		
		if(!is_object($language)) {
			if(ctype_digit("$language")) $language = (int) $language; 
			$language = $languages ? $languages->get($language) : null; 
		}
		
		if(!$language instanceof Language) {
			throw new WireException('Unknown language set to Page::setLanguageValue');
		}
		
		$this->setPageLanguageValue($page, $language, $field, $value);
	}

	/**
	 * Hook to set implement Page::setLanguageValues() - set multiple language values at once
	 * 
	 * @param HookEvent $event
	 * @throws WireException
	 * @since 3.0.236
	 * 
	 */
	public function hookPageSetLanguageValues(HookEvent $event) {
		$page = $event->object; /** @var Page $page */
		$field = $event->arguments(0);
		$values = $event->arguments(1);
		
		if($field instanceof Field) $field = $field->name;
		
		if(!is_array($values)) {
			throw new WireException(
				'Page::setLanguageValues($field, $values) requires an array $values ' . 
				'argument in format [ lang_name_or_id => value, … ];'
			);
		}

		foreach($this->wire()->languages as $language) {
			if(isset($values[$language->id])) {
				// value provided with language ID
				$value = $values[$language->id];
			} else if(isset($values[$language->name])) {
				// value provided with language name
				$value = $values[$language->name];
			} else {
				// value not provided for this language
				continue;
			}
			$this->setPageLanguageValue($page, $language, $field, $value);
		}
	}

	/**
	 * Adds a Page::getLanguageValue($language, $fieldName) method 
	 * 
	 * Provides a common interface for getting all language values from a Page. 
	 *
	 * This method exists in this class rather than one of the field-specific classes
	 * because it deals with both language fields and page names, and potentially
	 * other types of unknown types that implement LanguagesValueInterface.
	 * 
	 * @param HookEvent $event
	 * @throws WireException
	 *
	 */
	public function hookPageGetLanguageValue(HookEvent $event) {
	
		$page = $event->object; /** @var Page $page */
		$language = $event->arguments(0); /** @var Language|string|int $language */
		$field = $event->arguments(1); /** @var string|Field $field */
		$lang = $language;
		
		if(!is_object($language)) {
			if(ctype_digit("$language")) $language = (int) $language;
			$language = $this->wire()->languages->get($language);
		}
		
		if(!$language instanceof Language) {
			throw new WireException("Unknown language '$lang' sent to Page::getLanguageValue");
		}
		
		$event->return = $this->getPageLanguageValue($page, $language, $field);
	}

	/**
	 * Implementation for Page::getLanguageValues($field, [ languages ])
	 * 
	 * If the languages argument is omitted, values for all languages are returned. 
	 * 
	 * @param HookEvent $event
	 * 
	 */
	public function hookPageGetLanguageValues(HookEvent $event) {
		$page = $event->object; /** @var Page $page */
		$field = $event->arguments(0); /** @var Field|string $field */
		$langs = $event->arguments(1); /** @var null|Language[]|string[]|int[] $languages */
		$languages = $this->wire()->languages; /** @var Languages $languages */
		$values = array();
		
		if(is_array($field) && $langs && !is_array($langs)) {
			// allow for arguments in wrong order
			list($langs, $field) = array($field, $langs);
		}
	
		if($langs && !WireArray::iterable($langs)) $langs = null;
		if(empty($langs)) $langs = $this->wire()->languages;
		
		foreach($langs as $language) {
			if(!$language instanceof Language) {
				$language = $languages->getLanguage($language);
				if(!$language) continue;
			}
			$values[$language->name] = $this->getPageLanguageValue($page, $language, $field);
		}
		
		$event->return = $values;
	}
	
	/**
	 * Get page value in given language
	 *
	 * #pw-internal
	 *
	 * @param Page $page
	 * @param Language $language
	 * @param Field|string $field
	 * @return mixed
	 * @since 3.0.236
	 *
	 */
	public function getPageLanguageValue(Page $page, Language $language, $field) {
		if($field === 'name') {
			// get a page name 
			$value = $page->get($language->isDefault() ? "name" : "name$language->id");

		} else if($field === 'status') {
			// get page status
			$value = $page->get($language->isDefault() ? "status" : "status$language->id");

		} else if($field === '_languageStatus') {
			if($language->isDefault()) {
				$value = !$page->isUnpublished();
			} else {
				$value = (bool) $page->get("status$language->id");
			}

		} else {

			if(is_object($field)) $field = $field->name;
			$value = $page->get($field);

			if($value instanceof LanguagesValueInterface) {
				$value = $value->getLanguageValue($language->id);

			} else {
				// temporarily set user's language to field language, get the field value, then set user's language back	
				$user = $this->wire()->user;
				$userLanguage = $user->language->id != $language->id ? $user->language : null;
				if($userLanguage) $user->language = $language;
				$value = $page->get($field);
				if($userLanguage) $user->language = $userLanguage;
			}
		}

		return $value;
	}


	/**
	 * Set page value in given language
	 *
	 * #pw-internal
	 *
	 * @param Page $page
	 * @param Language $language
	 * @param string|Field $field
	 * @param mixed $value
	 * @throws WireException
	 * @since 3.0.236
	 *
	 */
	public function setPageLanguageValue(Page $page, $language, $field, $value) {

		if($field instanceof Field) {
			$field = $field->name;

		} else if($field === 'status') {
			// Set status (requires LanguageSupportPageNames)
			if(is_bool($value)) {
				$field = '_languageStatus';
			} else if($language->isDefault()) {
				$page->set('status', $value);
				return;
			} else {
				$page->set("status$language->id", $value ? 1 : 0);
				return;
			}
		}

		if($field === '_languageStatus') {
			// Set active status (requires LanguageSupportPageNames)
			if($language->isDefault()) {
				if($value) {
					if($page->isUnpublished()) $page->removeStatus(Page::statusUnpublished);
				} else {
					if(!$page->isUnpublished()) $page->addStatus(Page::statusUnpublished);
				}
			} else {
				$page->set("status$language->id", $value ? 1 : 0);
			}
			return;
		}

		if($field === 'name') {
			// Set name (requires LanguageSupportPageNames)
			if($language->isDefault()) {
				$page->set('name', $value);
			} else {
				$value = $this->wire()->sanitizer->pageNameUTF8($value);
				$page->set("name$language->id", $value);
			}
			return;
		}

		if(empty($field) || !is_string($field)) {
			throw new WireException('Page::setLanguageValue($field, …) requires a valid field/property');
		}

		$previousValue = $page->get($field);

		if($previousValue instanceof LanguagesValueInterface) {
			// utilize existing set methods available in LanguagesValueInterface
			if($value instanceof LanguagesValueInterface) {
				// if given a LanguagesPageFieldValue, then just set it to the page
				$page->set($field, $value);
			} else {
				// otherwise use existing setLanguageValue method provided by LanguagesValueInterface
				$previousValue->setLanguageValue($language->id, $value);
			}
			return;
		}

		// temporarily set user's language to field language, set the field value, then set user's language back	
		// we don't know what exactly $field might be, whether custom field or some other field, but we'll set it anyway
		$user = $this->wire()->user;
		$userLanguage = $user->language->id != $language->id ? $user->language : null;
		if($userLanguage) $user->language = $language;
		$page->set($field, $value);
		if($userLanguage) $user->language = $userLanguage;
	}


	/**
	 * Module configuration screen
	 * 
	 * @param array $data
	 * @return InputfieldWrapper
	 *
	 */
	public function getModuleConfigInputfields(array $data) {
		if($data) { }
		require(dirname(__FILE__) . '/LanguageSupportInstall.php'); 
		/** @var LanguageSupportInstall $installer */
		$installer = $this->wire(new LanguageSupportInstall());
		return $installer->getModuleConfigInputfields();
	}

	/**
	 * Refresh the config stored value for $this->otherLanguagePageIDs
	 * 
	 */
	public function refreshLanguageIDs() {
		$languages = $this->wire()->languages;
		$modules = $this->wire()->modules;
		
		$this->message('Refreshing other language page IDs', Notice::debug); 
		if(!$languages) return;
		
		$ids = array();
		
		foreach($languages as $language) {
			if($language->isDefault()) continue; 
			$ids[] = $language->id; 
		}
		if($this->otherLanguagePageIDs != $ids) {
			$this->set('otherLanguagePageIDs', $ids);
			$configData = $modules->getModuleConfigData('LanguageSupport');
			if($configData['otherLanguagePageIDs'] != $ids) {
				$configData['otherLanguagePageIDs'] = $ids;
				$modules->saveModuleConfigData('LanguageSupport', $configData);
			}
		}
	}
	

	/**
	 * Install or uninstall by loading the LanguageSupportInstall script
	 * 
	 * @param bool $install
	 *
	 */
	protected function installer($install = true) {
		require_once(dirname(__FILE__) . '/LanguageSupportInstall.php'); 
		/** @var LanguageSupportInstall $installer */
		$installer = $this->wire(new LanguageSupportInstall());
		if($install) $installer->install();
			else $installer->uninstall();
	}

	/**
	 * Get the LanguageTabs module instance, if it is installed, or null if not
	 * 
	 * @return LanguageTabs|null
	 * 
	 */
	public function getLanguageTabs() {
		return $this->languageTabs;
	}

	/**
	 * Install the module
	 *
	 */
	public function ___install() {
		$this->installer(true); 
	}

	/**
	 * Uninstall the module
	 *
	 */
	public function ___uninstall() {
		$this->installer(false);
	}

}
