<?php namespace ProcessWire;

/**
 * ProcessWire Page Paths
 *
 * Keeps a cache of page paths to improve performance and 
 * make paths more queryable by selectors.
 *
 * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
 * https://processwire.com
 * 
 * @property array $rootSegments
 *
 */

class PagePaths extends WireData implements Module, ConfigurableModule {

	public static function getModuleInfo() {
		return array(
			'title' => 'Page Paths', 
			'version' => 4, 
			'summary' => "Enables page paths/urls to be queryable by selectors. Also offers potential for improved load performance. Builds an index at install (may take time on a large site).",
			'singular' => true, 
			'autoload' => true, 
		);
	}

	/**
	 * Table created by this module
	 *
	 */
	const dbTableName = 'pages_paths';

	/**
	 * @var Languages|false
	 *
	 */
	protected $languages = null;

	/**
	 * Construct
	 * 
	 */
	public function __construct() {
		$this->set('rootSegments', array());
		parent::__construct();
	}

	/**
	 * Initialize the hooks
	 *
	 */
	public function init() {
		$pages = $this->wire()->pages;
		$pages->addHook('moved', $this, 'hookPageMoved'); 
		$pages->addHook('renamed', $this, 'hookPageMoved'); 
		$pages->addHook('added', $this, 'hookPageMoved'); 
		$pages->addHook('deleted', $this, 'hookPageDeleted');
	}

	/**
	 * API ready
	 * 
	 */
	public function ready() {
		if($this->wire()->languages) {
			$this->addHookBefore('LanguageSupportFields::languageDeleted', $this, 'hookLanguageDeleted');
		}
	}
	
	/*** HOOKS ******************************************************************************************/

	/**
	 * Hook called when a page is moved or renamed
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookPageMoved(HookEvent $event) {
		$page = $event->arguments[0];
		// $this->updatePagePath($page->id, $page->path);
		$this->updatePagePaths($page); 
	}

	/**
	 * Hook called when a page is deleted
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookPageDeleted(HookEvent $event) {
		$table = self::dbTableName;
		$page = $event->arguments[0];
		$database = $this->wire()->database;
		$query = $database->prepare("DELETE FROM $table WHERE pages_id=:pages_id"); 
		$query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
		$query->execute();
		$this->rebuildRootSegments();
	}

	/**
	 * When a language is deleted
	 * 
	 * @param HookEvent $event
	 * 
	 */
	public function hookLanguageDeleted(HookEvent $event) {
		$languages = $this->getLanguages();
		if(!$languages) return;
		$language = $event->arguments[0]; /** @var Language $language */
		if(!$language->id || $language->isDefault()) return;
		$table = self::dbTableName;
		$database = $this->wire()->database;
		$sql = "DELETE FROM $table WHERE language_id=:language_id";
		$query = $database->prepare($sql);
		$query->bindValue(':language_id', $language->id, \PDO::PARAM_INT);
		$this->executeQuery($query);
	}
	
	/*** PUBLIC API *************************************************************************************/

	/**
	 * Given a page ID, return the page path, NULL if not found, or boolean false if cannot be determined.
	 *
	 * @param int $pageId Page ID
	 * @param int $languageId Optionally specify language ID for path or 0 for default language
	 * @return string|null Returns path or null if not found
	 *
	 */
	public function getPath($pageId, $languageId = 0) {
		
		$table = self::dbTableName;
		$database = $this->wire()->database;
		$sanitizer = $this->wire()->sanitizer;
		
		$languageId = $this->languageId($languageId);
		
		$sql = "SELECT path FROM `$table` WHERE pages_id=:pages_id AND language_id=:language_id";
		$query = $database->prepare($sql); 
		$query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
		$query->bindValue(":language_id", $languageId, \PDO::PARAM_INT);
		$path = null;
		
		if(!$this->executeQuery($query)) return null;
		
		if($query->rowCount()) {
			$path = $query->fetchColumn();
			$path = strlen($path) ? $sanitizer->pagePathName("/$path/", Sanitizer::toUTF8) : '/';
		}
		
		$query->closeCursor();
		
		return $path;
	}
	
	/**
	 * Given a page ID, return all paths found for page 
	 * 
	 * Return value is indexed by language ID (and index 0 for default language)
	 *
	 * @param int $pageId Page ID
	 * @return array
	 *
	 */
	public function getPaths($pageId) {

		$table = self::dbTableName;
		$database = $this->wire()->database;
		$sanitizer = $this->wire()->sanitizer;
		$paths = array();

		$sql = "SELECT path, language_id FROM `$table` WHERE pages_id=:pages_id ";

		$query = $database->prepare($sql);
		$query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
		
		if(!$this->executeQuery($query)) return $paths;

		while($row = $query->fetch(\PDO::FETCH_NUM)) {
			$path = $row[0];
			$languageId = (int) $row[1];
			$path = strlen($path) ? $sanitizer->pagePathName("/$path/", Sanitizer::toUTF8) : '/';
			$paths[$languageId] = $path; 
		}

		$query->closeCursor();

		return $paths;
	}

	/**
	 * Given a page path, return the page ID or NULL if not found.
	 *
	 * @param string $path
	 * @return int|null
	 *
	 */
	public function getID($path) {
		$id = $this->getPageId($path);
		return $id ? $id : null;
	}

	/**
	 * Given a page path, return the page ID or 0 if not found.
	 *
	 * @param string|array $path
	 * @return int|null
	 * @since 3.0.186
	 *
	 */
	public function getPageID($path) {
		$a = $this->getPageAndLanguageId($path);
		return $a[0];
	}

	/**
	 * Given a page path return array of [ page_id, language_id ]
	 * 
	 * If not found, returned page_id and language_id will be 0. 
	 * 
	 * @param string|array $path
	 * @return array
	 * @since 3.0.186
	 * 
	 */
	public function getPageAndLanguageID($path) {
		
		$table = self::dbTableName;
		$database = $this->wire()->database;
		$paths = is_array($path) ? array_values($path) : array($path);
		$bindValues = array();
		$wheres = array();
		
		foreach($paths as $n => $path) {
			$path = $this->wire()->sanitizer->pagePathName($path, Sanitizer::toAscii);
			$path = trim($path, '/');
			$wheres[] = "path=:path$n";
			$bindValues["path$n"] = $path;
		}

		$where = implode(' OR ', $wheres);
		$sql = "SELECT pages_id, language_id FROM $table WHERE $where LIMIT 1";
		$query = $database->prepare($sql);
		$row = array(0, 0);
		
		foreach($bindValues as $bindKey => $bindValue) {
			$query->bindValue(":$bindKey", $bindValue);
		}
	
		if(!$this->executeQuery($query)) return $row;
		
		if($query->rowCount()) {
			$row = $query->fetch(\PDO::FETCH_NUM);
		}
		
		$query->closeCursor();
		
		return array((int) $row[0], (int) $row[1]); 
	}
	
	/**
	 * Get page information about a given path
	 * 
	 * Returned array includes the following:
	 * 
	 *  - `id` (int): ID of page for given path
	 *  - `language_id` (int): ID of language path was for, or 0 for default language
	 *  - `templates_id` (int): ID of template used by page
	 *  - `parent_id` (int): ID of parent page
	 *  - `status` (int): Status value for page ($page->status)
	 *  - `path` (string): Path that was found
	 *
	 * @param string $path
	 * @return array|bool Returns info array on success, boolean false if not found
	 * @since 3.0.186
	 *
	 */
	public function getPageInfo($path) {

		$sanitizer = $this->wire()->sanitizer;
		$database = $this->wire()->database;
		$languages = $this->wire()->languages;
		$config = $this->wire()->config;
		
		$table = self::dbTableName;
		$useUTF8 = $config->pageNameCharset === 'UTF8';
		
		if($languages && !$languages->hasPageNames()) $languages = null;
		
		if($useUTF8) {
			$path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
		}
		
		$columns = array(
			'pages_paths.path AS path', 
			'pages_paths.pages_id AS id', 
			'pages_paths.language_id AS language_id',
			'pages.templates_id AS templates_id',
			'pages.parent_id AS parent_id', 
			'pages.status AS status'
		);
		
		if($languages) {
			foreach($languages as $language) {
				if($language->isDefault()) continue;
				$columns[] = "pages.status$language->id AS status$language->id";
			}
		}
		
		$cols = implode(', ', $columns);

		$sql =
			"SELECT $cols FROM $table " .
			"JOIN pages ON pages_paths.pages_id=pages.id " .
			"WHERE pages_paths.path=:path";

		$query = $database->prepare($sql);
		$query->bindValue(':path', trim($path, '/'));
		
		if(!$this->executeQuery($query)) return false;

		$row = $query->fetch(\PDO::FETCH_ASSOC);
		$query->closeCursor();
		
		if(!$row) return false;
	
		foreach($row as $key => $value) {
			if($key === 'id' || strpos($key, 'status') === 0 || strpos($key, '_id')) {
				$row[$key] = (int) $value;
			}
		}

		if($useUTF8 && $row) {
			$row['path'] = $sanitizer->pagePathName($row['path'], Sanitizer::toUTF8);
		}
				
		return $row;
	}
	/**
	 * Rebuild all paths table starting with $page and descending to its children
	 *
	 * @param Page|null $page Page to start rebuild from or omit to rebuild all
	 * @return int Number of paths added
	 * @since 3.0.186
	 *
	 */
	public function rebuild(?Page $page = null) {
		set_time_limit(3600);
		$table = self::dbTableName;
		if($page === null) {
			// rebuild all
			$this->wire()->database->exec("DELETE FROM $table");
			$page = $this->wire()->pages->get('/');
		}
		$result = $this->updatePagePaths($page, true);
		return $result;
	}

	/**
	 * Perform a path match for use by PageFinder
	 *
	 * @param DatabaseQuerySelect $query
	 * @param Selector $selector
	 * @throws PageFinderSyntaxException
	 *
	 */
	public function getMatchQuery(DatabaseQuerySelect $query, Selector $selector) {

		static $n = 0;
		
		$sanitizer = $this->wire()->sanitizer;
		$database = $this->wire()->database;
		
		$n++;
		$table = self::dbTableName;
		$alias = "$table$n";
		$value = $selector->value;
		$operator = $selector->operator;
		// $joinType = $selector->not ? 'leftjoin' : 'join';

		$query->join("$table AS $alias ON pages.id=$alias.pages_id"); 

		if(in_array($operator, array('=', '!=', '<>', '>', '<', '>=', '<='))) {
			if(!is_array($value)) $value = array($value);
			$where = '';
			foreach($value as $path) {
				if($where) $where .= $selector->not ? " AND " : " OR ";
				$path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
				$path = $database->escapeStr(trim($path, '/')); 
				$where .= ($selector->not ? "NOT " : "") . "$alias.path{$operator}'$path'";
			}
			$query->where("($where)");

		} else {
			if(is_array($value)) {
				$error = "Multi value using '|' is not supported with path/url and '$operator' operator";
				throw new PageFinderSyntaxException($error);
			}
			if($selector->not) {
				$error = "NOT mode isn't yet supported with path/url and '$operator' operator";
				throw new PageFinderSyntaxException($error);
			}
			/** @var DatabaseQuerySelectFulltext $ft */
			$ft = $this->wire(new DatabaseQuerySelectFulltext($query));
			$ft->match($alias, 'path', $operator, trim($value, '/'));
		}
	}
	
	/*** PROTECTED API **********************************************************************************/
	
	/**
	 * Updates path for page and all children
	 *
	 * @param Page|int $page
	 * @param bool|null $hasChildren Does this page have children? Specify false if known not to have children, true otherwise.
	 * @param array $paths Paths indexed by language ID, use index 0 for default language. 
	 * @return int Number of paths updated
	 * @since 3.0.186
	 *
	 */
	protected function updatePagePaths($page, $hasChildren = null, array $paths = array()) {
		
		static $level = 0;

		$rootPageId = $this->wire()->config->rootPageID;
		$database = $this->wire()->database;
		$sanitizer = $this->wire()->sanitizer;
		$languages = $this->getLanguages();
		$table = self::dbTableName;
		$numUpdated = 1;
		$homeDefaultName = '';
		$rebuildRoot = false;
		$level++;
		
		if($hasChildren === null) {
			$hasChildren = $page instanceof Page ? $page->numChildren > 0 : true;
		}

		if(empty($paths)) {
			// determine the paths
			if(!is_object($page) || !$page instanceof Page) {
				throw new WireException('Page object required on first call to updatePagePaths');
			}
			$pageId = $page->id;
			if($page->parent_id === $rootPageId) $rebuildRoot = true;
			if($languages) {
				// multi-language
				foreach($languages as $language) {
					/** @var Language $language */
					$languageId = $language->isDefault() ? 0 : $language->id;
					$paths[$languageId] = $page->localPath($language);
					if($pageId === 1 && !$languageId) $homeDefaultName = $page->name;
				}
			} else {
				// single language
				$paths[0] = $page->path();
			}
		} else {
			// $paths already populated
			$pageId = (int) "$page";
		}
		
		if($pageId === $rootPageId) $rebuildRoot = true;

		// sanitize and prepare paths for DB storage
		foreach($paths as $languageId => $path) {
			$path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
			$paths[$languageId] = trim($path, '/');
		}
		
		$sql = 	
			"INSERT INTO $table (pages_id, language_id, path) " . 
			"VALUES(:pages_id, :language_id, :path) " .
			"ON DUPLICATE KEY UPDATE " . 
			"pages_id=VALUES(pages_id), language_id=VALUES(language_id), path=VALUES(path)";

		$query = $database->prepare($sql);
		$query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
		
		foreach($paths as $languageId => $path) {
			$query->bindValue(":language_id", $languageId, \PDO::PARAM_INT);
			$query->bindValue(":path", $path);
			if($this->executeQuery($query)) $numUpdated += $query->rowCount();
		}

		if($hasChildren) {
			if($homeDefaultName && $homeDefaultName !== 'home' && empty($paths[0])) {
				// for when homepage has a name (lang segment) but it isn’t used on actual homepage
				// but is used on children
				$paths[0] = $homeDefaultName;
			}
			$numUpdated += $this->updatePagePathsChildren($pageId, $paths);
		}
		
		if($level === 1 && $numUpdated > 0) {
			$this->message(
				sprintf($this->_n('Updated %d path', 'Updated %d paths', $numUpdated), $numUpdated),
				Notice::admin
			);
		}
		
		$level--;
		
		if($rebuildRoot && !$level) $this->rebuildRootSegments();

		return $numUpdated;
	}

	/**
	 * Companion to updatePagePaths method to handle children 
	 * 
	 * @param int $pageId
	 * @param array $paths Paths indexed by language ID, index 0 for default language
	 * @return int
	 * @since 3.0.186
	 *
	 */
	protected function updatePagePathsChildren($pageId, array $paths) {
	
		$database = $this->wire()->database;
		$languages = $this->getLanguages();
		$nameColumns = array('pages.name AS name');
		$numUpdated = 0;
		
		if($languages) {
			foreach($languages as $language) {
				/** @var Language $language */
				if($language->isDefault()) continue;
				$nameColumns[] = "pages.name$language->id AS name$language->id";
			}
		}
		
		$sql =
			"SELECT pages.id AS id, " . implode(', ', $nameColumns) . ", " .
			"COUNT(children.id) AS kids " .
			"FROM pages " .
			"LEFT JOIN pages AS children ON children.id=pages.parent_id " .
			// The next line is proposed (PR#281) to replace the above line
			// but we need further confirmation that it is correct:
			// "LEFT JOIN pages AS children ON children.parent_id=pages.id " .
			"WHERE pages.parent_id=:id " .
			"GROUP BY pages.id ";

		$query = $database->prepare($sql);
		$query->bindValue(":id", $pageId, \PDO::PARAM_INT);
		$rows = array();
		
		if(!$this->executeQuery($query)) return $numUpdated;
		
		while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
			$rows[] = $row;
		}
		
		$query->closeCursor();

		foreach($rows as $row) {
			$childPaths = array();
			foreach($paths as $languageId => $path) {
				$key = $languageId ? "name$languageId" : "name";
				$name = !empty($row[$key]) ? $row[$key] : $row["name"];
				$childPaths[$languageId] = "$path/$name";
			}
			$numUpdated += $this->updatePagePaths((int) $row['id'], $row['kids'] > 0, $childPaths);
		}
		
		return $numUpdated;
	}

	/**
	 * Alternate proposed replacement for updatePagePathsChildren() method
	 * 
	 * @param int $pageId
	 * @param array $paths
	 * @return int
	 * @throws WireException
	 * 
	public function updatePagePathsChildren($pageId, array $paths) {
		$numUpdated = 0;
		foreach($this->wire()->pages->findMany("parent=$pageId,include=all") as $childPage) {
			$numUpdated += $this->updatePagePaths($childPage);
		}
		return $numUpdated;
	}
	*/

	
	/*** ROOT SEGMENTS ******************************************************************************/

	/**
	 * Is given segment/page name a root segment?
	 *
	 * A root segment is one that is owned by the homepage or a direct parent of the homepage, i.e.
	 * /about/ might be a root page segment and /de/ might be a root language segment. If it is a
	 * root page segment like /about/ then this will return the ID of that page. If it is a root
	 * language segment like /de/ then it will return the homepage ID (1).
	 *
	 * @param string $segment Page name string or path containing it
	 * @return int Returns page ID or 0 for no match.
	 * @since 3.0.186
	 *
	 */
	public function isRootSegment($segment) {
		$segment = trim($segment, '/');
		if(strpos($segment, '/')) list($segment,) = explode('/', $segment, 2);
		$rootSegments = $this->getRootSegments();
		$key = array_search($segment, $rootSegments);
		if($key === false) return 0;
		$key = ltrim($key, '_');
		if(strpos($key, '.')) {
			list($pageId, /*$languageId*/) = explode('.', $key, 2);
		} else {
			$pageId = $key;
		}
		return (int) $pageId;
	}

	/**
	 * Get root segments
	 *
	 * @param bool $rebuild
	 * @return array
	 * @since 3.0.186
	 *
	 */
	public function getRootSegments($rebuild = false) {
		if(empty($this->rootSegments) || $rebuild) $this->rebuildRootSegments();
		return $this->rootSegments;
	}

	/**
	 * Rebuild root segments stored in module config
	 * 
	 * @since 3.0.186
	 * 
	 */
	protected function rebuildRootSegments() {
		
		$database = $this->wire()->database;
		$config = $this->wire()->config;
		$languages = $this->getLanguages();
		$cols = array('id', 'name');
		$segments = array();
		
		if($languages) {
			foreach($languages as $language) {
				if(!$language->isDefault()) $cols[] = "name$language->id";
			}
		}
		
		$sql = 'SELECT ' . implode(',', $cols) . ' FROM pages WHERE parent_id=:id ';
		if($languages) $sql .= 'OR id=:id';
		$query = $database->prepare($sql);
		$query->bindValue(':id', $config->rootPageID, \PDO::PARAM_INT);
		$query->execute();
	
		while($row = $query->fetch(\PDO::FETCH_ASSOC)){
			$id = (int) $row['id'];
			unset($row['id']);
			foreach($row as $col => $name) {
				if(!strlen("$name")) continue;
				if($id === 1 && $col === 'name' && $name === Pages::defaultRootName) continue; // skip "/home/"
				$col = str_replace('name', '', $col);
				if(strlen($col)) {
					$segments["_$id.$col"] = $name; // _pageID.languageID i.e. 123.456
				} else {
					$segments["_$id"] = $name; // _pageID i.e. 123
				}
			}
		}
		
		$query->closeCursor();
	
		$this->rootSegments = $segments;
		$this->wire()->modules->saveConfig($this, 'rootSegments', $segments);
		
		return $segments;
	}

	
	/*** LANGUAGES **********************************************************************************/
	
	/**
	 * Returns Languages object or false if not available
	 *
	 * @return Languages|Language[]|false
	 *
	 */
	public function getLanguages() {
		if($this->languages !== null) return $this->languages;
		$languages = $this->wire()->languages;
		if(!$languages) {
			$this->languages = false;
		} else if($languages->hasPageNames()) {
			$this->languages = $languages;
		} else {
			$this->languages = false;
		}
		return $this->languages;
	}

	/**
	 * @param Language|int|string $language
	 * @return int Returns language ID or 0 for default language
	 * @since 3.0.186
	 * 
	 */
	protected function languageId($language) {
		$language = $this->language($language);	
		if(!$language->id || $language->isDefault()) return 0;
		return $language->id;
	}

	/**
	 * @param Language|int|string $language
	 * @return Language|NullPage
	 * @since 3.0.186
	 * 
	 */
	protected function language($language) {
		$languages = $this->getLanguages();
		if(!$languages) return new NullPage();
		if(is_object($language)) return ($language instanceof Language ? $language : new NullPage());
		return $languages->get($language);
	}
	
	/*** MODULE MAINT *******************************************************************************/

	/**
	 * Execute a query/PDOStatement
	 * 
	 * @param \PDOStatement $query
	 * @param bool $throw Allow exceptions to be thrown? (default=true)
	 * @return bool
	 * @throws \PDOException
	 * 
	 */
	protected function executeQuery($query, $throw = true) {
		try {
			$result = $query->execute();
		} catch(\Exception $e) {
			if(!$this->checkTableSchema()) {
				if($throw) throw $e;
				$this->error($e->getMessage(), Notice::superuser | Notice::log);
			}
			$result = false;
		}
		return $result;
	}

	/**
	 * Check db schema
	 * 
	 * @return bool True if changes made, false if not
	 * 
	 */
	protected function checkTableSchema() {
		$table = self::dbTableName;
		$database = $this->wire()->database;
		if(!$database->columnExists($table, 'language_id')) {
			$sqls = array(
				"ALTER TABLE $table ADD language_id INT UNSIGNED NOT NULL DEFAULT 0 AFTER pages_id",
				"ALTER TABLE $table DROP PRIMARY KEY, ADD PRIMARY KEY(pages_id, language_id)",
				"ALTER TABLE $table ADD INDEX language_id (language_id)",
				"ALTER TABLE $table DROP INDEX path, ADD UNIQUE KEY path(path(500), language_id)",
			);
			foreach($sqls as $sql) {
				$database->exec($sql);
			}
			$this->message("Added language_id column to table $table", Notice::admin);
			return true;
		}
		return false;
	}

	/**
	 * Upgrade module
	 * 
	 * @param $fromVersion
	 * @param $toVersion
	 * @since 3.0.186
	 * 
	 */
	public function ___upgrade($fromVersion, $toVersion) {
		if($fromVersion && $toVersion) {} // ignore
		$this->checkTableSchema();
		$this->rebuildRootSegments();
	}

	/**
	 * Install the module
	 *
	 */
	public function ___install() {
		
		$table = self::dbTableName;
		$database = $this->wire()->database;
		$engine = $this->wire()->config->dbEngine;
		$charset = $this->wire()->config->dbCharset;

		$database->query("DROP TABLE IF EXISTS $table"); 

		$sql = 	
			"CREATE TABLE $table (" .
				"pages_id int(10) unsigned NOT NULL, " .
				"language_id int unsigned NOT NULL DEFAULT 0, " . 
				"path text CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, " .
				"PRIMARY KEY (pages_id, language_id), " .
				"UNIQUE KEY path (path(500), language_id), " .
				"INDEX language_id (language_id), " . 
				"FULLTEXT KEY path_fulltext (path)" .
			") ENGINE=$engine DEFAULT CHARSET=$charset";

		$database->query($sql); 
	}

	/**
	 * Uninstall the module
	 *
	 */
	public function ___uninstall() {
		$this->wire()->database->query("DROP TABLE " . self::dbTableName); 
	}

	/**
	 * Module config
	 * 
	 * @param InputfieldWrapper $inputfields
	 * 
	 */
	public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
	
		$session = $this->wire()->session;
		$input = $this->wire()->input;
		$numPages = 0;
		$numRows = -1;
		
		if($input->requestMethod('POST')) {
			if($input->post('_rebuild')) $session->setFor($this, 'rebuild', true);
		} else {
			$numPages = $this->wire()->pages->count("id>0, include=all");
			if($session->getFor($this, 'rebuild')) {
				$session->removeFor($this, 'rebuild');
				$timer = Debug::timer();
				$this->rebuild();
				$elapsed = Debug::timer($timer);
				$this->message(sprintf($this->_('Completed rebuild in %d seconds'), $elapsed), Notice::noGroup);
			} else {
				$table = self::dbTableName;
				$query = $this->wire()->database->prepare("SELECT COUNT(*) FROM $table");
				if($this->executeQuery($query, false)) {
					$numRows = (int) $query->fetchColumn();
					$query->closeCursor();
				}
			}
		}
		
		$f = $inputfields->InputfieldCheckbox;
		$f->attr('name', '_rebuild');
		$f->label = sprintf($this->_('Rebuild page paths index for %d pages'), $numPages); 
		$f->label2 = $this->_('Rebuild now');
		if($numPages) $f->description =
			$this->_('Estimated rebuild time is up to 5 seconds per 1000 pages.') . ' ' . 
			sprintf($this->_('There are %d pages to process.'), $numPages); 
		if($numRows > 0) {
			$f->notes = sprintf($this->_('There are currently %d rows stored by this module (path paths and versions of path paths).'), $numRows);
		} else if($numRows === 0 && $input->requestMethod('GET')) {
			$this->warning($this->_('Please choose the “rebuild now” option to create your page paths index.'));
		}
			
		$inputfields->add($f);
	}

}
