<?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 2016 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' => 103,
			'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() {

		$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() {

		// document which pages were already cached at this point, as their values may need 
		// to be reloaded to account for language fields. 
		foreach($this->wire('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 = $this->templates->get('language'); 
		if(!$languageTemplate) return;
		
		if(!$this->languagesPageID) {
			// fallback if LanguageSupport config lost or not accessible for some reason
			$this->languagesPageID = $this->wire('pages')->get("template=admin, name=languages"); 
		}
	
		// prevent fields like 'title' from autojoining until languages are fully loaded
		$this->wire('pages')->setAutojoin(false); 
		
		$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++;
			}
		}
		
		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($this->user->language && $this->user->language->id) {
			// $language = $this->user->language; 
		} else {
			$language = $this->defaultLanguagePage; 
			$this->user->language = $language; 
		}

		$this->wire('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->addHookAfter('Field::getInputfield', $this, 'hookFieldGetInputfield');
		$this->pages->addHook('added', $this, 'hookPageAdded'); 
		$this->pages->addHook('deleteReady', $this, 'hookPageDeleteReady'); 
		$this->addHook('Page::setLanguageValue', $this, 'hookPageSetLanguageValue');
		$this->addHook('Page::getLanguageValue', $this, 'hookPageGetLanguageValue'); 
		

		if($this->wire('modules')->isInstalled('LanguageSupportFields')) {
			$this->LanguageSupportFields = $this->wire('modules')->get('LanguageSupportFields'); 
			$this->LanguageSupportFields->LS_init();
			if($languages->getPageEditPermissions('none') && !$this->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
		$this->wire('pages')->setAutojoin(true);
	}

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

		// if languageSupportFields is here, then we have to deal with pages that loaded before this module did
		if($this->LanguageSupportFields) {
			$fieldNames = array();
			// save the names of all fields that support languages
			foreach($this->wire('fields') as $field) {
				if($field->type instanceof FieldtypeLanguageInterface) $fieldNames[] = $field->name;
			}
			// 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) {
		$alwaysAllowInputfields = array(
			'InputfieldWrapper',
			'InputfieldPageName',
			'InputfieldSubmit',
			'InputfieldButton',
			'InputfieldHidden',
		);
		// ignore this call if in ProcessProfile
		if($this->wire('page')->process == 'ProcessProfile') return true;
		if($this->wire('page')->process == 'ProcessLanguage') return true;
		
		$user = $this->wire('user');
		if($user->isSuperuser()) return true; 
		if($inputfield->getSetting('useLanguages')) return true; 
		if(!$this->LanguageSupportFields) return true; 
		$permissions = $this->wire('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($this->wire('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'; 
		
		/** @var Languages $languages */
		$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); 
		
	}

	/**
	 * 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; 

		/** @var Field $field */
		$field = $event->object;
		/** @var Page $page */
		$page = $event->arguments[0];
		/** @var Template $template */
		$template = $page ? $page->template : null;
		$inputfield = $event->return; 
		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(is_object($value) && $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) {

		$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); 
		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
		$configData = $this->wire('modules')->getModuleConfigData('LanguageSupport'); 
		$configData['otherLanguagePageIDs'][] = $page->id; 
		wire('modules')->saveModuleConfigData('LanguageSupport', $configData); 

	}

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

		$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) {

			$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) {
			$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
		wire('languages')->deleted($page);

		// update the other language module IDs to remove the uninstalled language
		$configData = $this->wire('modules')->getModuleConfigData('LanguageSupport'); 
		$key = array_search($page->id, $configData['otherLanguagePageIDs']); 
		if($key !== false) {
			unset($configData['otherLanguagePageIDs'][$key]);
			$this->wire('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; 
		$language = $event->arguments(0);	
		$field = $event->arguments(1);
		$value = $event->arguments(2);
		$event->return = $page; 
		
		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 set to Page::setLanguageValue'); 
		
		if($field == 'name') {
			// set page name
			if(!$this->wire('modules')->isInstalled('LanguageSupportPageNames')) {
				throw new WireException("Please install LanguageSupportPageNames module before attempting to set multi-language names/paths/URLs."); 
			}
			if($language->isDefault()) {
				$page->set("name", $value); 
			} else {
				$page->set("name$language->id", $value); 
			}
			
		} else {
			
			if(is_object($field)) $field = $field->name;
			$previousValue = $page->get($field);

			if(is_object($previousValue) && $previousValue instanceof LanguagesValueInterface) {
				// utilize existing set methods available in LanguagesValueInterface (which might be slightly quicker than the else condition method
				if(is_object($value) && $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); 	
				}
				
			} else {
				// 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; 
			}
		}	
	}

	/**
	 * 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) {
	
		/** @var Page $page */
		$page = $event->object; 
		/** @var Language $language */
		$language = $event->arguments(0); 
		/** @var Field $field */
		$field = $event->arguments(1);
		$value = null;
		
		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 sent to Page::getLanguageValue'); 
		
		if($field == 'name') {
			// get a page name 
			if($language->isDefault()) {
				$value = $page->name; 
			} else {
				$value = $page->get("name$language->id"); 
			}
			
		} else {
			
			if(is_object($field)) $field = $field->name;
			$value = $page->get($field);

			if(is_object($value) && $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;
			}
		}
		
		$event->return = $value; 
	}

	/**
	 * 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() {
		$this->message('Refreshing other language page IDs', Notice::debug); 
		if(!$this->wire('languages')) return;
		$ids = array();
		foreach($this->wire('languages') as $language) {
			if($language->isDefault()) continue; 
			$ids[] = $language->id; 
		}
		if($this->otherLanguagePageIDs != $ids) {
			$this->set('otherLanguagePageIDs', $ids);
			$configData = $this->wire('modules')->getModuleConfigData('LanguageSupport');
			if($configData['otherLanguagePageIDs'] != $ids) {
				$configData['otherLanguagePageIDs'] = $ids;
				$this->wire('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);
	}

}
