<?php namespace ProcessWire;

/**
 * ProcessWire Page Sort Process
 *
 * Saves moved or sorted pages for the PageList process. 
 * Intended to be executed via an ajax call. 
 * 
 * For more details about how Process modules work, please see: 
 * /wire/core/Process.php 
 * 
 * ProcessWire 3.x, Copyright 2024 by Ryan Cramer
 * https://processwire.com
 *
 *
 */

class ProcessPageSort extends Process {

	protected $ids = array(); 
	protected $parent_id = 0; 
	protected $move_id = 0; 
	protected $user; 
	protected $isMoved = false;
	protected $isTrashed = false;
	protected $isRestored = false;
	protected $isSorted = 0;

	public static function getModuleInfo() {
		return array(
			'title' => __('Page Sort and Move', __FILE__), // getModuleInfo title
			'summary' => __('Handles page sorting and moving for PageList', __FILE__), // getModuleInfo summary
			'version' => 101, 
			'permanent' => true, 
			'permission' => 'page-edit',
		); 
	}

	/**
	 * Install a new permission in addition to the regular ProcessPageSort permission
	 *
	 * The "ProcessPageSortMove" permission refers to changing the page's parent,
	 * whereas the "ProcessPageSort" permission refers to changing the sort within the same parent. 
	 *
	 */
	public function ___install() {
		parent::___install();
	}

	/**
	 * Save a move/sort request
	 *
	 */
	public function ___execute() {
		
		$input = $this->wire()->input;
		$pages = $this->wire()->pages;
		$sort = $input->post('sort');

		if($this->wire()->config->demo) {
			throw new WireException($this->_("Your change was not saved because this site is in demo mode"));
		}
		
		if($sort === null) {
			throw new WireException($this->_("This Process is only accessible via POST"));
		}
		
		$this->wire()->session->CSRF->validate(); // throws exception if invalid
		$this->user = $this->wire()->user; 
		$this->ids = array();
		$ids = explode(',', $sort); 
		foreach($ids as $sort => $id) $this->ids[(int) $sort] = (int) $id; 
		if(!count($this->ids)) return; 
		unset($ids);

		$this->parent_id = (int) $input->post('parent_id'); 
		$this->move_id = (int) $input->post('id');
			
		$parentPage = $pages->get($this->parent_id);
		$movePage = $pages->get($this->move_id); 
		
		if($movePage->id < 2 || !$parentPage->id) return;

		$this->movePage($movePage, $parentPage); 
		$this->sortPages($movePage, $parentPage); 
		
		if($this->isMoved || $this->isSorted) {
			if($this->isRestored) {
				// update page name to match manual drag/drop sort out of trash
				$movePage->name = preg_replace('/^(\d+\.\d+)\.\d+_/', '$1.' . $movePage->sort . '_', $movePage->name);
				$movePage->sortPrevious = null;
			}
			$this->save($movePage, $parentPage);
		}
	}	

	/**
	 * Saves a page that has had it's parent_id changed
	 * 
	 * @param Page $page
	 * @param Page $parent
	 * @throws WirePermissionException
	 *
	 */
	protected function movePage(Page $page, Page $parent) {

		if($page->parent_id == $parent->id) return;

		if(!$page->moveable($parent)) {
			throw new WirePermissionException(
				$this->_("You do not have permission to move pages using this parent") . " - " . 
				$parent->path()
			);
		}

		$page->setOutputFormatting(false); 
		$page->resetTrackChanges(true); 
		$page->parent = $parent; 
		$this->message("Moved page $page to parent $parent"); 
		$this->isMoved = true;
		
		if($parent->isTrash()) {
			$this->isTrashed = true;
		} else if($page->parentPrevious && $page->parentPrevious->isTrash()) {
			$this->isRestored = true;
		}
	}

	/**
	 * Updates the sortfield for all pages having the same parent
	 * 
	 * @param Page $page
	 * @param Page $parent
	 * @throws WirePermissionException|WireException
	 *
	 */
	protected function sortPages(Page $page, Page $parent) {

		if(!$page->sortable()) { 
			if(!$this->isMoved) {
				throw new WirePermissionException(
					$this->_("You do not have permission to sort pages using this parent") . " - " .
					$parent->path()
				);
			}
			return;
		}

		$sortfield = $parent->sortfield();
		if($sortfield && $sortfield != 'sort') {
			$msg = sprintf($this->_("Your sort was not saved because these pages are automatically sorted by %s."), $sortfield);
			if(!$this->isMoved) {
				throw new WireException($msg);
			} else {
				$this->message($msg);
			}
			return;
		}
		
		$changes = 0; 
		$database = $this->wire()->database;

		// locate the 'sort' value of the current first sorted item, to use as our starting point
		// (in case sorting in a pagination other than 1)
		$sql = "SELECT sort FROM pages WHERE parent_id=:parent_id AND id IN("; 
		foreach($this->ids as $id) $sql .= ((int) $id) . ",";
		$sql = rtrim($sql, ",") . ") ";
		if($this->isMoved) $sql .= "AND id!=:move_id ";
		$sql .= "ORDER BY sort LIMIT 1";
		$query = $database->prepare($sql);
		$query->bindValue(":parent_id", $parent->id, \PDO::PARAM_INT);
		if($this->isMoved) $query->bindValue(":move_id", $this->move_id, \PDO::PARAM_INT);
		$query->execute();
		$sortStart = (int) $query->fetchColumn();
		
		$query = $database->prepare("UPDATE pages SET sort=:sort1 WHERE id=:id AND parent_id=:parent_id AND sort!=:sort2");
		$query->bindValue(":parent_id", $parent->id, \PDO::PARAM_INT);
		$pageSort = 0;
		
		foreach($this->ids as $sort => $id) {
			$sort += $sortStart;
			$query->bindValue(":sort1", $sort, \PDO::PARAM_INT);
			$query->bindValue(":sort2", $sort, \PDO::PARAM_INT);
			$query->bindValue(":id", $id, \PDO::PARAM_INT); 
			$query->execute();
			if($query->rowCount() > 0) $changes++;
			if($page->id == $id) $pageSort = $sort; 
		}
	
		if($page->sort != $pageSort) {
			$changes++;
			$page->set('sort', $pageSort);
		}
		
		if($changes) {
			$page->trackChange('sort');
			$this->message("Updated sort for $changes pages", Notice::log);
			$this->isSorted = $changes;
		}
	}

	/**
	 * @param Page $page
	 * @param Page $parent
	 * @throws WireException
	 * 
	 */
	protected function save(Page $page, Page $parent) {
		$page->save();
		if($this->isSorted) {
			$parent->trackChange('children');
			$parent->save();
			$this->wire()->pages->sorted($page, false, $this->isSorted);
		}
	}
	
}
