<?php namespace ProcessWire;

/**
 * ProcessWire Language Process, displays languages in Setup > Languages > 
 * 
 * It also contains the hooks for altering the output of the InputfieldFile to hold language info and links.
 * This is the process assigned to processwire/setup/languages/.
 *
 * ProcessWire 3.x, Copyright 2019 by Ryan Cramer
 * https://processwire.com
 *
 * @method string execute()
 * @method string executeDownload()
 * @method bool|int processCSV($csvFile, Language $language, array $options = array())
 *
 */

class ProcessLanguage extends ProcessPageType {

	static public function getModuleInfo() {
		return array(
			'title' => __('Languages', __FILE__),
			'version' => 103,
			'summary' => __('Manage system languages', __FILE__),
			'author' => 'Ryan Cramer',
			'requires' => 'LanguageSupport',
			'icon' => 'language', 
			'useNavJSON' => true,
			'permission' => 'lang-edit',
			'permissions' => array(
				'lang-edit' => 'Administer languages and static translation files'
			)
		);
	}

	/**
	 * The URL to the language-translator page (typically admin/setup/language-translator/)
	 *
	 */
	protected $translationUrl = '';

	/**
	 * Array of messages for language_files, indexed by file basename
	 *
	 */
	protected $fileMessages = array();

	/**
	 * CSV file to process, if present
	 * 
	 * @var Pagefile|null
	 * 
	 */
	protected $csvImportLabel = '';

	/**
	 * Populate the fields shown in the default language list output
	 *
	 */
	public function __construct() {
		parent::__construct();
		
		$showFields = array('name', 'title', 'language_files', 'language_files_site'); 
		$this->set('showFields', $showFields); 
		$this->set('jsonListLabel', 'title|name'); 
		require_once(dirname(__FILE__) . '/LanguageParser.php'); 
	}

	/**
	 * Wired to ProcessWire instance
	 *
	 */
	public function wired() {
		parent::wired();
		$fields = $this->wire()->fields;

		// make sure our files fields have CSV support
		foreach(array('language_files', 'language_files_site') as $fieldName) {
			$field = $fields->get($fieldName);
			if(!$field) continue;
			$extensions = $field->get('extensions');
			if(strpos($extensions, 'csv') === false) {
				$field->set('extensions', "$extensions csv");
				$field->save();
				$this->message("Added CSV support to field $fieldName", Notice::debug);
			}
		}

		$this->csvImportLabel = $this->_('CSV Import:') . ' ';
	}

	/**
	 * Add InputfieldFile hooks 
	 *
	 */
	public function init() {
		$this->addHookBefore('InputfieldFile(name^=language_files)::render', $this, 'renderInputfieldFile');
		$this->addHookAfter('InputfieldFile::renderItem', $this, 'renderInputfieldFileItem'); 
		$this->addHookAfter('InputfieldFile::renderUpload', $this, 'renderInputfieldFileUpload');
		$this->addHookBefore('InputfieldFile::processInput', $this, 'processInputfieldFileInput');
	
		if(!$this->wire()->config->ajax) {
			$this->addHookBefore('InputfieldForm::render', $this, 'renderInputfieldForm');
		}
		parent::init();
	}

	protected function translationUrl() {
		if(!$this->translationUrl) {
			/** @var LanguageSupport $support */
			$support = $this->wire()->modules->get('LanguageSupport'); 
			$this->translationUrl = $this->wire()->pages->get($support->languageTranslatorPageID)->url;
		}
		return $this->translationUrl; 
	}
	
	public function processInputfieldFileInput(HookEvent $event) {
		/** @var InputfieldFile $inputfield */
		$inputfield = $event->object;
		$inputfield->overwrite = true; 
	}
	
	/**
	 * Hook for before InputfieldFile::render
	 *
	 * In this case we add an 'edit' link to the translator and some info about the translation file.
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function renderInputfieldFile(HookEvent $event) {

		$inputfield = $event->object; /** @var InputfieldFile $inputfield */
		$language = $this->wire()->process->getPage(); /** @var Language $language */
		
		/** @var Pagefiles $pagefiles */
		$pagefiles = $inputfield->attr('value');
		
		foreach($pagefiles as $pagefile) {
			/** @var Pagefile $pagefile */
			if($pagefile->ext() != 'csv') continue;
			$pagefiles->remove($pagefile);
			$this->processCSV($pagefile->filename(), $language);
		}
		
		$inputfield->descriptionRows = 0;
		$inputfield->overwrite = true;
		$inputfield->noCollapseItem = true;
		$inputfield->noShortName = true; 
		
	}
	
	public function renderInputfieldForm(HookEvent $event) {

		/** @var InputfieldForm $form */
		$form = $event->object;
		$language = $this->getPage();
		
		if(!$language->id) return;
		
		$file = $language->filesManager()->path . '.phrase-index.txt';
		
		/** @var InputfieldMarkup $inputfield */
		$inputfield = $this->wire()->modules->get('InputfieldMarkup');
		$inputfield->label = $this->_('Live Search');
		$inputfield->icon = 'search';
		$placeholder = $this->_('Text to search for');
		$refreshUrl = "../../language-translator/add/?language_id=$language->id&refresh=1";
		$refreshLabel = $this->_('Refresh search phrase index');
		
		if(!is_file($file)) {
			$inputfield->value = "<p><a href='$refreshUrl'>" . 
				$this->_('Click here to build search phrase index') . "</a></p>";
			
		} else {

			$phrases = file_get_contents($file);
			$phrases = str_replace(array('"', "\n", "<", ">"), ' ', $phrases);
			$script = 'script';

			$inputfield->value =
				"<$script>" .
				"var phraseIndex = \"$phrases\"; " .
				"var phraseLanguageID = $language->id;" .
				"</$script>" .
				"<p class='description' style='margin:0'>" .
				$this->_('Search all translatable files for specific text/phrase.') . ' ' .
				$this->_('Click found matches to edit translation or add file (if not already present).') .
				"<p>" .
				"<p>" .
				"<input type='text' class='language-phrase-search InputfieldIgnoreChanges' style='width:50%' name='_q' placeholder='$placeholder' /> " .
				"<span class='detail language-phrase-search-cnt'></span> &nbsp;" .
				"<a class='pw-tooltip' title='$refreshLabel' href='$refreshUrl'>" . wireIconMarkup('refresh') . "</a>" . 	
				"</p>";
		}
		
		$field = $form->getChildByName('language_files_site');
		if($field) $form->insertBefore($inputfield, $field);
	}

	/**
	 * Hook for InputfieldFile::renderItem
	 *
	 * In this case we add an 'edit' link to the translator and some info about the translation file.
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function renderInputfieldFileItem(HookEvent $event) {

		$translationUrl = $this->translationUrl(); 
		$pagefile = $event->arguments[0]; /** @var Pagefile $pagefile */
		$page = $pagefile->get('page'); /** @var Language $page */
		
		if($pagefile->ext() == 'csv') {
			$event->return .=
				"<div class='InputfieldFileData InputfieldFileLanguageInfo'>" .
					"<span class='InputfieldFileLanguageFilename description'>" .
						$this->_('CSV translation file to be imported after save') . 
					"</span>" . 
				"</div>";
			return;
		}
		
		$textdomain = basename($pagefile->basename, '.json');

		$data = $page->translator->getTextdomain($textdomain);
		$file = $data['file']; 
		$pathname = $this->wire()->config->paths->root . $file;
		$translations =& $data['translations'];
		$total = count($translations); 
		
		/** @var LanguageParser $parser */
		$parser = $this->wire(new LanguageParser($page->translator, $pathname));
		$untranslated = $parser->getUntranslated();
		$alternates = $parser->getAlternates();
		$numPending = 0;
		$numAbandoned = 0;
		$numFallback = 0;

		foreach($untranslated as $hash => $text) {
			if(!isset($translations[$hash]) || !strlen($translations[$hash]['text'])) $numPending++;
		}

		foreach($translations as $hash => $translation) {
			if(isset($untranslated[$hash])) continue;
			$numAbandoned++;
			if($page->isDefault()) continue;
			foreach($alternates as $srcHash => $values) {
				if(isset($values[$hash]) && isset($untranslated[$srcHash])) {
					$numFallback++;
					$numAbandoned--;
					break;
				}
			}
		}

		$total += $numAbandoned; 
		$message = sprintf($this->_n("%d phrase", "%d phrases", $total), $total);

		if($numAbandoned || $numPending || $numFallback) {
			$a = array();
			if($numAbandoned) $a[] = sprintf($this->_('%d abandoned'), $numAbandoned);
			if($numPending) $a[] = sprintf($this->_('%d blank'), $numPending);
			if($numFallback) $a[] = sprintf($this->_('%d fallback'), $numFallback);
			$message = " <span class='ui-state-error-text'>(" . implode(' / ', $a) . ")</span>";
		}

		$editLabel = $this->_x('Edit', 'edit-language-file'); 

		$out = 	
			"<div class='InputfieldFileData InputfieldFileLanguageInfo'>" . 
				"<span class='InputfieldFileLanguageFilename description'>/$file &#8212;</span> <span class='notes'>$message</span> " .
				"<a class='action' href='{$translationUrl}edit/?language_id={$page->id}&amp;textdomain=$textdomain'>&nbsp; " . 
				"<i class='fa fa-edit'></i> $editLabel <i class='fa fa-angle-double-right hover-only'></i></a>" . 
			"</div>";

		$page->translator->unloadTextdomain($textdomain);

		$event->return .= $out; 

	}

	/**
	 * Hook for InputfieldFile::renderUpload
	 *
	 * This just adds a 'new' link to add a new translation file.
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function renderInputfieldFileUpload(HookEvent $event) {
		
		$modules = $this->wire()->modules;
		$translationUrl = $this->translationUrl();
		$pagefiles = $event->arguments(0); /** @var Pagefiles $pagefiles */
		$page = $pagefiles->get('page'); /** @var Page $page */
		$inputfield = $event->object; /** @var InputfieldFile $inputfield */
		$out = '';
	
		/** @var InputfieldButton $btn1 */
		$btn1 = $modules->get('InputfieldButton'); 
		$btn1->href = "{$translationUrl}add/?language_id={$page->id}";
		$btn1->value = $this->_('Find Files to Translate'); 
		$btn1->icon = 'plane';
		if($inputfield->name == 'language_files') $btn1->showInHeader();
		$out .= $btn1->render();
	
		if(count($inputfield->attr('value'))) {
			/** @var InputfieldButton $btn2 */
			$btn2 = $modules->get('InputfieldButton');
			$btn2->href = "../download/?language_id={$page->id}&field=" . $inputfield->attr('name');
			$btn2->value = $this->_('Download ZIP');
			$btn2->icon = 'file-zip-o';
			$btn2->setSecondary();
			$btn2->addClass('download-button'); 
			$out .= $btn2->render();
			
			$btn2->href .= '&csv=1';
			$btn2->value = $this->_('Download CSV');
			$btn2->icon = 'file-excel-o';
			$out .= $btn2->render();
		}
		
		$event->return .= "<p>$out</p>";
	}

	/**
 	 * Modify the output per-field in the PageType list (template-method)
	 *
	 * In this case we make it return a count for the language_files
	 * 
	 * @param string $name
	 * @param mixed $value
	 * @return string
	 *
	 */
	protected function renderListFieldValue($name, $value) {
		if($name == 'language_files' || $name == 'language_files_site') {
			return count($value); 
		} else if($name == 'title') { 
			if(!$value) return '(blank)';
			return (string) $value;
		} else {
			return parent::renderListFieldValue($name, $value); 
		}
	}

	public function ___execute() { 
		// check if 2.5 update needed to add new language_files_site field
		if(!$this->wire()->fields->get('language_files_site')) {
			require_once(dirname(__FILE__) . '/LanguageSupportInstall.php');
			/** @var LanguageSupportInstall $installer */
			$installer = $this->wire(new LanguageSupportInstall());
			$installer->addFilesFields($this->wire()->fieldgroups->get(LanguageSupport::languageTemplateName));
		}
		return parent::___execute();
	}

	/**
	 * Create and send a ZIP of translation files or CSV of translations
	 *
	 */
	public function ___executeDownload() {
		
		$config = $this->wire()->config;
		$input = $this->wire()->input;
		
		$id = (int) $input->get('language_id'); 
		if(!$id) throw new WireException("No language specified"); 
		
		$language = $this->wire()->languages->get($id);
		if(!$language->id) throw new WireException("Unknown language"); 
		
		$fieldName = $input->get('field') == 'language_files_site' ? 'language_files_site' : 'language_files';
		$textdomain = $this->wire()->sanitizer->textdomain($input->get('textdomain')); 
		$textdomains = array();
		$csv = (int) $input->get('csv');
		$path = $language->$fieldName->path();
		$files = array();
		
		if($textdomain) {
			$file = $language->translator->textdomainToFilename($textdomain);
			if($file) {
				$files[] = $file;
				$textdomains[$file] = $textdomain;
			} else {
				$textdomain = '';
			}
		}
	
		if(!count($files)) {
			foreach($language->$fieldName as $file) {
				$files[] = $file->filename;
			}
		}
		
		if(!count($files)) {
			throw new WireException('No translation files specified to download');
		}
		
		if($csv) {
			// CSV
			if($textdomain) {
				// i.e. es-modulename.csv
				$parts = explode('--', $textdomain);
				$basename = array_pop($parts);
				$parts = explode('-', $basename);
				$basename = array_shift($parts);
				$filename = "$language->name-$basename.csv";
			} else {
				// i.e. es-site.csv or es-wire.csv
				$filename = $language->name . "-" . (strpos($fieldName, 'site') ? 'site' : 'wire') . ".csv";
			}
			if($input->get('view')) {
				header("Content-type: text/plain");
			} else {
				header("Content-type: application/force-download");
				header("Content-Transfer-Encoding: Binary");
				header("Content-disposition: attachment; filename=$filename");
			}

			$fp = fopen('php://output', 'w');
			$defaultCol = $language->name == 'en' ? 'default' : 'en';
			$fields = array($defaultCol, $language->name, 'description', 'file', 'hash');
			fputcsv($fp, $fields);
			
			foreach($files as $f) {
			
				if(isset($textdomains[$f])) {
					$textdomain = $textdomains[$f];
				} else {
					$textdomain = basename($f, '.json');
				}
				
				$data = $language->translator->getTextdomain($textdomain);
				if(empty($data)) continue;
				
				$file = $data['file'];
				$pathname = $config->paths->root . $file;
				$translated =& $data['translations'];
				$parser = $this->wire(new LanguageParser($language->translator, $pathname)); /** @var LanguageParser $parser */
				$untranslated = $parser->getUntranslated();
				$comments = $parser->getComments();

				foreach($untranslated as $hash => $text1) {
					$text2 = isset($translated[$hash])  ? $translated[$hash]['text'] : '';
					$comment = isset($comments[$hash]) ? $comments[$hash] : '';
					if(strpos($comment, '//') !== false) list(, $comment) = explode('//', $comment);
					$fields = array($text1, $text2, trim($comment), $file, $hash);
					fputcsv($fp, $fields);
				}
			}
			
			fclose($fp);

		} else {
			// ZIP
			$zipname = $language->name . "-";
			$zipname .= $fieldName == 'language_files' ? 'wire' : 'site';
			$zipfile = "$path$zipname.zip";
			$info = wireZipFile($zipfile, $files, array("overwrite" => true));
			if(!count($info['files'])) {
				$this->error("Error adding files to ZIP");
				$this->session->redirect('../');
			} else {
				wireSendFile($zipfile);
			}
		}
		
		exit(0);
	}

	/**
	 * Process a CSV file to import changes from it
	 * 
	 * Must be in the same format as the one provied by the executeDownload() method
	 * 
	 * @param string $csvFile
	 * @param Language $language
	 * @param array $options Additional options (3.0.181+)
	 *  - `file` (string): Use this path/file (relative to install root) 
	 *  - `quiet` (bool): Suppress generating notifications? (default=false)
	 * @return bool|int Returns false on error or integer on success, where value is number of translations imported
	 * @throws WireException
	 * @since 3.0.195 Previously was not hookable
	 * 
	 */
	public function ___processCSV($csvFile, Language $language, array $options = array()) {
		
		$defaults = array(
			'file' => '',
			'quiet' => false, 
		);
		
		$options = array_merge($defaults, $options);
		
		$fp = fopen($csvFile, "r");
		
		if($fp === false) {
			if(!$options['quiet']) $this->error($this->csvImportLabel . "Unable to open: $csvFile");
			return false;
		}

		$keys = array(
			'original',
			'translated',
			'file',
			'hash',
		);
		
		$n = 0;
		$header = array();
		$translator = new LanguageTranslator($language);
		$textdomain = '';
		$lastTextdomain = '';
		$lastFile = '';
		$numChanges = 0;
		$numTotal = 0;
		$numGross = 0;
		$translations = null;
		$optionsFileBasename = '';
		$halt = false;
		
		$this->wire($translator);
		
		if(!empty($options['file'])) {
			$options['file'] = ltrim($this->wire()->files->unixFileName($options['file']), '/');
			$optionsFileBasename = basename($options['file']);
		}

		while(($csvData = fgetcsv($fp, 8192, ",")) !== FALSE) {
			
			if(++$n === 1) {
				// header row
				$header = $csvData; 
				foreach($header as $key => $value) {
					$header[$key] = strtolower($value);
				}
				// make sure everything we need is present
				foreach($keys as $k => $key) {
					if($k > 1 && !in_array($key, $header)) {
						if($key === 'file' && !empty($options['file'])) {
							// default file provided so not required in CSV data
						} else {
							if(!$options['quiet']) $this->error($this->csvImportLabel . "CSV data missing required column '$key'");
							$halt = true;
						}
					}
				}
				if($halt) break;
				continue;
			} 
			
			$row = array();
			foreach($header as $key => $name) {
				if($key === 0) $name = 'original';
				if($key === 1) $name = 'translated';
				$row[$name] = $csvData[$key];
			}
			
			if($options['file']) {
				if(empty($row['file'])) {
					$row['file'] = $options['file'];
				} else {
					$rowFileBasename = basename($row['file']); 
					if($rowFileBasename === $optionsFileBasename) {
						// i.e. site/modules/Hello/Hello.module
						$row['file'] = $options['file']; 
					} else {
						// i.e. site/modules/Hello/World.module 
						$row['file'] = dirname($options['file']) . '/' . $rowFileBasename;
					}
				}
			}
			
			if(empty($row['original']) || empty($row['file'])) continue;
			
			$file = $row['file'];
			$hash = $row['hash'];
			// $textOriginal = $row['original'];
			$textTranslated = $row['translated'];
			$textdomain = $translator->filenameToTextdomain($file);
			
			if(!$translator->textdomainFileExists($textdomain)) {
				$textdomain = $translator->addFileToTranslate($file, false, false);
			}
			
			if(is_null($translations)) {
				$translations = $translator->getTranslations($textdomain);
			}
			
			if(!$textdomain) {
				if(!$options['quiet']) $this->warning($this->csvImportLabel . sprintf(
					$this->_('Unrecognized textdomain for file: %s'), 
					$this->wire()->sanitizer->entities($file)
				));
				continue;
			}
			
			if($textdomain != $lastTextdomain) {
				if(!$lastFile) $lastFile = $file;
				if(!$lastTextdomain) $lastTextdomain = $textdomain;
				$this->processCSV_saveTextdomain($translator, $lastTextdomain, $lastFile, $numChanges);
				$translations = $translator->getTranslations($textdomain);
				$numChanges = 0;
			}
			
			$translation = isset($translations[$hash]) ? $translations[$hash] : array('text' => '');
			if($translation['text'] != $textTranslated) {
				$translator->setTranslationFromHash($textdomain, $hash, $textTranslated);
				$numChanges++;
				$numTotal++;
			}

			$lastTextdomain = $textdomain;
			$lastFile = $file;
			$numGross++;
		}
		
		if($numChanges) {
			$this->processCSV_saveTextdomain($translator, $textdomain, $lastFile, $numChanges);
		}
		$language->save();
		
		fclose($fp);
		if(!$options['quiet']) {
			$this->message($this->csvImportLabel . sprintf($this->_('%d total translations, %d total changes'), $numGross, $numTotal), Notice::noGroup);
		}
		
		return $halt ? false : $numGross; 
	}

	/**
	 * Save a textdomain, helper for processCSV method
	 * 
	 * @param LanguageTranslator $translator
	 * @param string $textdomain
	 * @param string $filename
	 * @param int $numChanges
	 * 
	 */
	protected function processCSV_saveTextdomain(LanguageTranslator $translator, $textdomain, $filename, $numChanges) {
		if($filename) { /* ignore, not currently used */ }
		$file = $translator->textdomainToFilename($textdomain);
		if($numChanges) {
			try {
				$translator->saveTextdomain($textdomain);
				$this->message($this->csvImportLabel . sprintf($this->_('Saved %d change(s) for file: %s'), $numChanges, $file), Notice::noGroup);
			} catch(\Exception $e) {
				$this->error($e->getMessage());
			}
		} else {
			// no changes
		}
		$translator->unloadTextdomain($textdomain);
	}
}
