<?php namespace ProcessWire;

/**
 * Multi-language support fields module
 *
 * ProcessWire 3.x, Copyright 2022 by Ryan Cramer
 * https://processwire.com
 * 
 * @method void languageAdded(Page $language) #pw-hooker
 * @method void languageDeleted(Page $language) #pw-hooker
 * @method void fieldLanguageAdded(Field $field, Page $language) #pw-hooker
 * @method void fieldLanguageRemoved(Field $field, Page $language) #pw-hooker
 *
 */

class LanguageSupportFields extends WireData implements Module {

	/**
	 * Return information about the module
	 *
	 */
	static public function getModuleInfo() {
		return array(
			'title' => 'Languages Support - Fields',
			'version' => 101,
			'summary' => 'Required to use multi-language fields.',
			'author' => 'Ryan Cramer',
			'autoload' => true,
			'singular' => true,
			'requires' => array(
				'LanguageSupport'
			),
			'installs' => array(
				'FieldtypePageTitleLanguage',
				'FieldtypeTextareaLanguage',
				'FieldtypeTextLanguage', 
			)
		);
	}

	/**
	 * Cached names of fields that are dealing in multiple languages. 
	 *
	 */
	protected $multilangFields = array();

	/**
	 * Cached names of fields that have alternate language-specific versions
	 *
	 * Indexed by original field name and value is an array of the language-alternate versions
	 *
	 */
	protected $multilangAlternateFields = array();

	/**
	 * Construct and set our dynamic config vars
	 *
	 */
	public function __construct() {
		parent::__construct();
		// load other required classes
		$dirname = dirname(__FILE__); 
		require_once($dirname . '/LanguagesValueInterface.php'); 
		require_once($dirname . '/FieldtypeLanguageInterface.php'); 
		require_once($dirname . '/LanguagesPageFieldValue.php'); 
	}
	
	public function wired() {
		$this->addHookAfter('FieldtypeLanguageInterface::loadPageField', $this, 'fieldtypeLoadPageField');
		$this->addHookAfter('FieldtypeLanguageInterface::wakeupValue', $this, 'fieldtypeWakeupValue');
		$this->addHookAfter('FieldtypeLanguageInterface::getConfigInputfields', $this, 'fieldtypeGetConfigInputfields'); 
		parent::wired();
	}

	public function init() { 
		// intentionally left blank
	}

	/**
	 * Initialize the language support API vars
	 *
	 */
	public function LS_init() {
		
		$fields = $this->wire()->fields;

		$this->addHookBefore('FieldtypeLanguageInterface::sleepValue', $this, 'fieldtypeSleepValue'); 
		$this->addHookBefore('PageFinder::getQuery', $this, 'pageFinderGetQuery'); 
		$this->addHookBefore('Fieldtype::formatValue', $this, 'hookFieldtypeFormatValue'); 

		$languageNames = array();
		$fieldNames = $fields->getAllNames('name');

		foreach($this->wire()->languages as $language) {
			$languageNames[] = $language->name; 
		}

		// keep track of which fields are multilanguage
		$this->multilangFields = $fields->findByType('FieldtypeLanguageInterface', array(
			'inherit' => true,
			'valueType' => 'name',
			'indexType' => '',
		));
		
		// determine which fields have language alternates, i.e. 'title_es' is an alternate for 'title'
		foreach($fieldNames as $fieldName) {
			foreach($languageNames as $languageName) {
				$altName = $fieldName . '_' . $languageName; 
				if(isset($fieldNames[$altName])) {
					if(!isset($this->multilangAlternateFields[$fieldName])) $this->multilangAlternateFields[$fieldName] = array();
					$this->multilangAlternateFields[$fieldName][] = $altName; 
				}
			}
		}	
	}

	/**
	 * Called by ProcessWire when the API and known $page is ready
	 *
	 */
	public function LS_ready() {
		$languages = $this->wire()->languages;
		$languages->addHook('added', $this, 'hookLanguageAdded'); 
		$languages->addHook('deleted', $this, 'hookLanguageDeleted'); 
	}

	/**
	 * Hook into FieldtypeText::formatValue
	 *
	 * Replace a value of one field with the value from another field that has the same name, but with the language name appended to it. 
	 * Example: title and title_es
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookFieldtypeFormatValue(HookEvent $event) {

		/** @var Field $field */
		$field = $event->arguments[1]; 
		if($field->name === 'language') return;

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

		// exit quickly if we can determine now we don't need to continue
		if(!isset($this->multilangAlternateFields[$field->name])) return; 

		/** @var Page $page */
		$page = $event->arguments[0]; 

		// determine name of language field, if present.
		// note that if the language name contains dashes or dots (- or .) the field name should contain underscores there instead
		$newName = $field->name . '_' . str_replace(array('-', '.'), '_', $language->name); 
		$newField = $page->fieldgroup->getField($newName);
		if(!$newField) return;

		// unformatted so nothing can modify it first
		$value = $page->getUnformatted($newName); 

		// if the page doesn't have a populated language-specific field, then exit
		// this will make it fallback to the default language value
		if($field->type->isEmptyValue($field, $value)) return;
		if(is_object($value)) {
			if($value instanceof WireArray && !$value->count()) return;
			if($value instanceof NullPage) return; 
		}

		// we have a new field: swap $field with $newField in the arguments
		$newValue = $page->get($newName); // get formatted version
		$arguments = $event->arguments; 
		$arguments[1] = $newField; 
		$arguments[2] = $newValue; 
		$event->arguments = $arguments; 
	}

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

		$fields = $this->wire()->fields;
		$language = $event->arguments[0];

		if($language->template->name != LanguageSupport::languageTemplateName) return; 

		foreach($this->multilangFields as $name) {
			$field = $fields->get($name); 	
			if($field) $this->fieldLanguageAdded($field, $language); 
		}

		$this->languageAdded($language); 
	}

	/**
	 * Hookable function called when a new language is added 
	 * 
	 * @param Page|Language $language
	 *
	 */
	public function ___languageAdded(Page $language) {
		// hookable, intentionally blank
	}

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

		$fields = $this->wire()->fields;
		$language = $event->arguments[0];
		if($language->template->name != LanguageSupport::languageTemplateName) return; 

		foreach($this->multilangFields as $name) {
			$field = $fields->get($name); 	
			if($field) $this->fieldLanguageRemoved($field, $language); 
		}

		$this->languageDeleted($language); 
	}

	/**
	 * Hookable function called when a language is deleted
	 * 
	 * @param Page|Language $language
	 *
	 */
	public function ___languageDeleted(Page $language) {
		// hookable, intentionally blank
	}


	/**
	 * Called when a new language is added to the system for each field that implements FieldtypeLanguageInterface
	 * 
	 * @param Field $field
	 * @param Page|Language $language
	 *
	 */
	public function ___fieldLanguageAdded(Field $field, Page $language) {

		if($language->isDefault) return;
		
		if(!($field->type instanceof FieldtypeLanguageInterface)) return;
		
		$schema = $field->type->getDatabaseSchema($field);
		$database = $this->wire()->database;
		$table = $database->escapeTable($field->table);

		foreach($schema as $name => $value) {
			if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue; 
			// field in schema ends with the language ID
			try {
				$database->exec("ALTER TABLE `{$table}` ADD `$name` $value");
			} catch(\Exception $e) {
				$this->error($e->getMessage(), Notice::log); 
			}
		}

		foreach($schema['keys'] as $name => $value) {
			if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue; 
			// index in schema ends with the language ID
			try {
				$database->exec("ALTER TABLE `{$table}` ADD $value");
			} catch(\Exception $e) {
				$this->error($e->getMessage(), Notice::log); 
			}
		}

	}
	
	/**
	 * Called when a language is removed from the system for each field that implements FieldtypeLanguageInterface
	 * 
	 * @param Field $field
	 * @param Page|Language $language
	 *
	 */
	protected function ___fieldLanguageRemoved(Field $field, Page $language) {

		if($language->isDefault) return;
		
		if(!($field->type instanceof FieldtypeLanguageInterface)) return;
		
		$schema = $field->type->getDatabaseSchema($field);
		$database = $this->wire()->database;
		$table = $database->escapeTable($field->table);

		foreach($schema as $name => $value) {
			if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue; 
			try { 
				$database->exec("ALTER TABLE `{$table}` DROP COLUMN `$name`"); 
			} catch(\Exception $e) { 
				// just catch, no need for fatal errors here
			}
		}
	}

	/**
	 * Hook into PageFinder::getQuery
	 *
	 * Adjusts the selectors passed to the query so that find operations search in user's native language version, as well as the default version.
	 *
	 * We may make this behavior configurable later on, as one may want to limit the search to 1 language only.
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function pageFinderGetQuery(HookEvent $event) {

		$user = $this->wire()->user;
		$language = $user->language;
		$database = $this->wire()->database;

		if(!$language || !$language->id || $language->isDefault()) return;

		$arguments = $event->arguments; 
		$selectors = $arguments[0]; 

		foreach($selectors as $selector) {

			$changed = false; 
			$fields = $selector->field;
			$fields = is_array($fields) ? $fields : array($fields);

			foreach($fields as $field) {

				$subfield = '';
				if(strpos($field, '.')) list($field, $subfield) = explode('.', $field); 
				
				$field = $database->escapeCol($field);
				$subfield = $database->escapeCol($subfield);

				if(isset($this->multilangAlternateFields[$field])) {
					// account for multilang alternates like 'title_es' for 'title'
					$altName = $field . '_' . $database->escapeCol($user->language->name); 
					if(in_array($altName, $this->multilangAlternateFields[$field])) {
						if($subfield) $altName .= ".$subfield";
						array_unshift($fields, $altName); 
						$changed = true; 
					}
				}

				// next we account for actual multilang fields
				if(!in_array($field, $this->multilangFields)) continue; 
				if(!$subfield) $subfield = 'data';

				if($subfield === 'data') {
					array_unshift($fields, "$field.$subfield" . (int) $user->language->id); 
					$changed = true; 
				}
			}

			if($changed) $selector->field = $fields;
		}

		$arguments[0] = $selectors; 
		$event->arguments = $arguments; 
	}

	/**
	 * Hook into FieldtypeLanguageInterface::loadPageField
	 *
	 * Converts the value to a LanguagesPageFieldValue
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function fieldtypeLoadPageField(HookEvent $event) {
		$page = $event->arguments[0];
		$field = $event->arguments[1];
		$value = $event->return; 
		if($value instanceof LanguagesPageFieldValue) return; 
		$v = new LanguagesPageFieldValue($page, $field, $value); 
		$this->wire($v);
		$event->return = $v;
	}

	/**
	 * Hook into FieldtypeLanguageInterface::wakeupValue
	 *
	 * Converts the value to a LanguagesPageFieldValue
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function fieldtypeWakeupValue(HookEvent $event) {

		$page = $event->arguments[0];
		$field = $event->arguments[1];
		$value = $event->return; 

		if($value instanceof LanguagesPageFieldValue) {
			$value->setTrackChanges(true); 
			$value->setField($field); 
			// good
		} else if(is_array($value)) {
			$value = new LanguagesPageFieldValue($page, $field, $value); 
			$this->wire($value);
			$value->setTrackChanges(true); 
			$value->setField($field); 
			$event->return = $value;
		}

	}

	/**
	 * Hook into FieldtypeLanguageInterface::sleepValue
	 *
	 * Converts a LanguagesPageFieldValue to an array
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function fieldtypeSleepValue(HookEvent $event) {

		// $page = $event->arguments[0];
		// $field = $event->arguments[1];
		$value = $event->arguments[2]; 
		//$value = $event->return;
		$values = array();

		if(!$value instanceof LanguagesPageFieldValue) return;

		foreach($this->wire()->languages as $language) {
			if($language->isDefault()) $key = 'data';
				else $key = 'data' . $language->id; 
			$values[$key] = $value->getLanguageValue($language->id); 
		}
		
		/*
		if(!strlen($values['data'])) foreach($values as $k => $v) {
			// prevent the possibility of the default language having
			// a blank value while some other language has a populated value
			if(!strlen($v)) continue;
			$values['data'] = $v; 
			break;
		}
		*/
	
		// ensure that sleepValue is getting an array
		$event->setArgument(2, $values);
		// $event->return = $values; 
	}

	public function fieldtypeGetConfigInputfields(HookEvent $event) {

		/** @var Field $field */
		$field = $event->arguments(0); 
		
		/** @var InputfieldWrapper $inputfields */
		$inputfields = $event->return;		
	
		/** @var InputfieldRadios $f */
		$f = $this->wire()->modules->get('InputfieldRadios'); 
		$f->attr('name', 'langBlankInherit'); 
		$f->label = $this->_('Language Support / Blank Behavior'); 
		$f->description = $this->_("What should happen when this field's value is blank?"); 
		$f->notes = $this->_('Applies only to non-default language values on the front-end of your site.'); 
		$f->addOption(LanguagesPageFieldValue::langBlankInheritDefault, $this->_('Inherit value from default language')); 
		$f->addOption(LanguagesPageFieldValue::langBlankInheritNone, $this->_('Remain blank')); 
		$f->attr('value', (int) $field->get('langBlankInherit')); 
		$f->collapsed = Inputfield::collapsedBlank;
		$inputfields->add($f); 
	}

	/**
	 * Given a field name, return an array of alternate language field names
	 *
	 * Returns a blank array if none found
	 *
	 * @param string $fieldName
	 * @return array
	 *
	 */
	public function getAlternateFields($fieldName) {
		if(isset($this->multilangAlternateFields[$fieldName])) {
			return $this->multilangAlternateFields[$fieldName];
		}
		return array();
	}

	/**
	 * Given an alternate field name, return the parent (default-language) version of it
	 *
	 * @param string $altFieldName
	 * @param bool $returnLanguage Specify true if you want this function to return the language rather than the parent field name. 
	 * @return string|Language Returns blank string if none found
	 *
	 */
	public function getAlternateFieldParent($altFieldName, $returnLanguage = false) {
		$pos = strrpos($altFieldName, '_'); 
		if(!$pos) return '';
		$parentName = substr($altFieldName, 0, $pos);
		if(!isset($this->multilangAlternateFields[$parentName])) return ''; 
		if(!in_array($altFieldName, $this->multilangAlternateFields[$parentName])) return '';
		if(!$returnLanguage) return $parentName; 
		$languageName = substr($altFieldName, $pos+1); 
		return $this->wire()->languages->get($languageName); 
	}

	/**
	 * Get the language associated with the alternate field name
	 *
	 * @param string $altFieldName
	 * @return Page|false Language page associated with the field, or blank string or false if not found
	 *
	 */
	public function getAlternateFieldLanguage($altFieldName) {
		return $this->getAlternateFieldParent($altFieldName, true);
	}

	/**
	 * Get multi-language field names (fields that implement FieldtypeLanguageInterface)
	 * 
	 * @return array
	 * @since 3.0.194
	 * 
	 */
	public function getMultilangFieldNames() {
		return $this->multilangFields;
	}

	/**
	 * Is the given field name a language alternate field?
	 * 
	 * If it is, the Language of the field is returned.
	 * If it isn't, then boolean false is returned. 
	 * 
	 * This method also accounts for default language. 
	 * 
	 * @param string $name
	 * @return bool|Language
	 * 
	 */
	public function isAlternateField($name) {
		$name = (string) $name;
		if(isset($this->multilangAlternateFields[$name])) {
			// default language for an alternate field set
			return $this->wire()->languages->getDefault();
		}
		if(!strpos($name, '_')) return false;
		$language = $this->getAlternateFieldParent($name, true);
		if($language && $language->id) return $language; 
		return false;
	}

	/**
	 * Install the module
	 *
	 */
	public function ___install() {
		$this->wire()->modules->get('FieldtypeTextLanguage'); 
	}

	/**
	 * Uninstall the module
	 *
	 */
	public function ___uninstall() {
		// first check if there are any fields using the LanguageInterface
		$errors = '';
		foreach($this->wire()->fields as $field) {
			if($field->type instanceof FieldtypeLanguageInterface) $errors .= $field->name . ", "; 
		}
		if($errors) {
			throw new WireException("Can't uninstall because these fields use the language interface: " . rtrim($errors, ", "));
		}
	}

}
