<?php namespace ProcessWire;

/**
 * Comments Manager
 *
 * Manage all comments field data in chronological order. 
 *
 * ProcessWire 3.x 
 * Copyright (C) 2019 by Ryan Cramer 
 * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
 * 
 * https://processwire.com
 *
 */

class ProcessCommentsManager extends Process {

	/**
	 * Return information about this module (required)
	 *
	 */
	public static function getModuleInfo() {
		return array(
			'title' => __('Comments', __FILE__), 
			'summary' => __('Manage comments in your site outside of the page editor.', __FILE__),
			'version' => 10, 
			'author' => 'Ryan Cramer', 
			'icon' => 'comments', 
			'requires' => 'FieldtypeComments',
			'searchable' => 'comments', 
			'permission' => 'comments-manager', 
			'permissions' => array(
				'comments-manager' => 'Use the comments manager', 
				),
			'page' => array(
				'name' => 'comments',
				'parent' => 'setup', 
				'title' => 'Comments', 
				),
			'nav' => array(
				array(
					'url' => '?go=approved',
					'label' => __('Approved', __FILE__),
					),
				array(
					'url' => '?go=pending',
					'label' => __('Pending', __FILE__),
					), 
				array(
					'url' => '?go=spam',
					'label' => __('Spam', __FILE__),
					),
				array(
					'url' => '?go=all',
					'label' => __('All', __FILE__),
					)
				)
			); 
	}


	/**
	 * Statuses and names that a Comment can have
	 *
	 */
	protected $statuses = array();

	/**
	 * Translated statuses
	 *
	 */
	protected $statusTranslations = array();

	/**
	 * Translated flags 
	 * 
	 * @var array
	 * 
	 */
	protected $notifyFlagsTranslations = array();

	/**
	 * Number of comments to show per page
	 *
	 */
	protected $limit = 10;

	/**
	 * Headline for masthead
	 *
	 */
	protected $headline = '';

	/**
	 * Translated 'All' label
	 * 
	 * @var string
	 * 
	 */
	protected $labelAll = 'All';

	/**
	 * Initialize the comments manager and define the statuses
	 *
	 */
	public function init() {
		$this->wire('modules')->get('FieldtypeComments');
		parent::init();
		$this->statuses = array(
			Comment::statusFeatured => 'featured',
			Comment::statusApproved => 'approved',
			Comment::statusPending => 'pending',
			Comment::statusSpam => 'spam',
			Comment::statusDelete => 'delete'
		);
		$this->statusTranslations = array(
			Comment::statusFeatured => $this->_('Featured'),
			Comment::statusApproved => $this->_('Approved'),
			Comment::statusPending => $this->_('Pending'),
			Comment::statusSpam => $this->_('Spam'),
			Comment::statusDelete => $this->_('Delete')
		);
		$this->notifyFlagsTranslations = array(
			0 => $this->_('No'), 
			Comment::flagNotifyAll => $this->_('All'),
			Comment::flagNotifyReply => $this->_('Replies'), 
		);
		$this->labelAll = $this->_('All'); 
	}

	/**
	 * Ask the user to select which comments field they want to manage
 	 *
	 * Or, redirect to the comments field if there is only 1.
	 * 
	 * @return string
	 *
	 */
	public function ___execute() {
		$this->checkInstall();
		// locate all the FieldtypeComments fields
		$fields = array();
		foreach($this->fields as $field) {
			if($field->type instanceof FieldtypeComments) $fields[] = $field;
		}

		$count = count($fields);

		if(!$count) {
			$error = $this->_('There are no comments fields installed');
			$this->error($error);
			return "<p>$error</p>";
		}
		
		$go = $this->wire('sanitizer')->pageName($this->wire('input')->get('go')); 

		if($count == 1 || $go) {
			$field = reset($fields);
			$to = 'all';
			if($go && in_array($go, $this->statuses)) $to = $go;
			$this->wire('session')->redirect("./list/$field->name/$to/"); 
			return '';
		}

		$out = "<h2>" . $this->_('Please select a comments field') . "</h2><ul>";
		foreach($fields as $field) {
			$out .= "<li><a href='./list/{$field->name}/pending/'>{$field->name}</a></li>";
		}
		$out .= "</ul>";

		return $out;
	}	

	/**
	 * Execute the comments list 
	 * 
	 * @return string
	 *
	 */
	public function ___executeList() {

		if(wireClassExists("CommentStars")) {
			$config = $this->config;
			$cssFile = $config->urls('FieldtypeComments') . 'comments.css';
			$jsFile = $config->urls('FieldtypeComments') . 'comments.js';
			$config->styles->add($cssFile);
			$config->scripts->add($jsFile);
			CommentStars::setDefault('star', wireIconMarkup('star')); 
		}
	
		$session = $this->wire('session'); /** @var Session $session */
		$input = $this->wire('input'); /** @var WireInput $input */
		$sanitizer = $this->wire('sanitizer'); /** @var Sanitizer $sanitizer */
		$page = $this->wire('page'); /** @var Page $page */

		$commentID = (int) $input->get('id');
		$name = $sanitizer->fieldName($input->urlSegment2); 
		if(!$name) return $this->error($this->_('No comments field specified in URL')); 
		$field = $this->fields->get($name); 
		if(!$field || !$field->type instanceof FieldtypeComments) return $this->error($this->_('Unrecognized field')); 
		$status = $input->urlSegment3;
		
		if(empty($status) || ($status != 'all' && !in_array($status, $this->statuses))) {
			$redirectUrl = $page->url() . "list/$field->name/all/";
			if($commentID) $redirectUrl .= "?id=$commentID";
			$session->redirect($redirectUrl); 
		}
		
		$statusNum = array_search($status, $this->statuses); 
		$headline = $statusNum !== false ? $this->statusTranslations[$statusNum] : $status; 
		if($headline === 'all') $headline = $this->labelAll;
		$this->breadcrumb('../', $field->getLabel());

		$limit = (int) $input->get('limit');
		if($limit) {
			$session->setFor($this, 'limit', $limit);
			$session->redirect('./');
		} else {
			$limit = (int) $session->getFor($this, 'limit');
			if(!$limit) $limit = (int) $this->limit;
		}
		$sort = $sanitizer->name($input->get('sort'));
		if($sort) {
			$session->setFor($this, 'sort', $sort);
			$session->redirect('./');
		} else {
			$sort = $session->getFor($this, 'sort');
			if(!$sort) $sort = '-created';
		}
	
		$start = ($input->pageNum() - 1) * $limit;
		$selector = "start=$start, limit=$limit, sort=$sort";
		if($status != 'all') $selector .= ", status=$statusNum";

		$filterOut = '';
		$filterLabels = array(
			'id' => $this->_('ID'),
			'parent_id' => $this->_('Replies to'), 
			'pages_id' => $this->_('Page'), 
		);

		$properties = array(
			'cite', 
			'email', 
			'text', 
			'ip', 
			'id', 
			'pages_id',
			'parent_id', 
			'stars'
		);
		
		$q = $input->get('q');
		if($q !== null) {
			// query $q that contain a selector 
			$q = trim($sanitizer->text($q, array('stripTags' => false)));
			$op = Selectors::stringHasOperator($q, true);
			if($op) {
				list($property, $value) = explode($op, $q, 2); 
				$property = $sanitizer->fieldName($property);
				if(!in_array($property, $properties)) $property = '';
			} else {
				$property = 'text';
				$op = strpos($q, ' ') ? '~=' : '%=';
				$value = $q;
			}
			if($property && $value) {
				$selector .= ", $property$op" . $sanitizer->selectorValue($value);
				$input->whitelist('q', "$property$op$value"); 
			}
			$filterOut .= $sanitizer->entities(", $property$op$value"); 
		} 
		
		foreach($properties as $key) {
			$value = $input->get($key);
			if(is_null($value)) continue;
			if($key == 'id' || $key == 'pages_id' || $key == 'parent_id' || $key == 'stars') {
				$value = (int) $value;
			} else {
				$value = trim($sanitizer->text($value));
			}
			$input->whitelist($key, $value);
			$value = $sanitizer->selectorValue($value);
			$selector .= ", $key=$value";
			$filterLabel = isset($filterLabels[$key]) ? $filterLabels[$key] : ucfirst($key);
			$filterOut .= $sanitizer->entities(", $filterLabel: $value");
		}

		/** @var FieldtypeComments $fieldtype */
		$fieldtype = $field->type; 
		$comments = $fieldtype->find($field, $selector); 
		if($input->post('processComments')) {
			$this->processComments($comments, $field);
		}
		if($filterOut) {
			$this->breadcrumb('./', $headline); 
			$headline = trim($filterOut, ", "); 
		}
		$this->headline = $headline; 
		
		return $this->renderComments($comments); 
	}

	/**
	 * Process changes to posted comments
	 * 
	 * @param CommentArray $comments
	 * @param Field $field
	 *
	 */
	protected function processComments(CommentArray $comments, Field $field) {

		$numDeleted = 0;
		$numChanged = 0;
		$isSuperuser = $this->user->isSuperuser();
		$allowChangeParent = $isSuperuser && $field->get('depth') > 0;
		$allowChangePage = $isSuperuser;
		$commentField = $field instanceof CommentField ? $field : null;
		
		/** @var FieldtypeComments $fieldtype */
		$fieldtype = $field->type;
		
		/** @var WireInput $input */
		$input = $this->wire('input');

		foreach($comments as $comment) {
			/** @var Comment $comment */

			$properties = array();

			$text = $input->post("CommentText{$comment->id}"); 
			if(!is_null($text) && $text != $comment->text) {
				$comment->text = $text; // cleans it
				$properties['text'] = $comment->text;
				$numChanged++;
			}

			if($field->get('useVotes')) { 
				foreach(array("upvotes", "downvotes") as $name) {
					$votes = (int) $input->post("Comment" . ucfirst($name) . $comment->id); 
					if($votes != $comment->$name) {
						$comment->set($name, $votes); 
						$properties[$name] = $comment->$name;
						$numChanged++;
					}
				}
			}
			
			if($field->get('useStars')) {
				$stars = (int) $input->post("CommentStars$comment->id");
				if($stars != $comment->stars) {
					$comment->set('stars', $stars);
					$properties['stars'] = $comment->stars;
					$numChanged++;
				}
			}

			$_status = $input->post("CommentStatus{$comment->id}"); 
			$status = (int) $_status;
			if($status === Comment::statusDelete && (!$commentField || $commentField->allowDeleteComment($comment))) {
				if($fieldtype->deleteComment($comment->getPage(), $field, $comment)) {
					$this->message(sprintf($this->_('Deleted comment #%d'), $comment->id)); 
					$numDeleted++;
				}
				continue; 
			}
			
			if($_status !== null && $status !== (int) $comment->status && array_key_exists($status, $this->statuses)) {
				$comment->status = $status; 
				$numChanged++;
				$properties['status'] = $comment->status;
			}
		
			$notify = $input->post("CommentNotify{$comment->id}"); 
			if($field->useNotify && ctype_digit($notify)) {
				$notify = (int) $notify;
				if($notify === 0 && $comment->flags) {
					if($comment->flags & Comment::flagNotifyAll) $comment->flags = $comment->flags & ~Comment::flagNotifyAll;
					if($comment->flags & Comment::flagNotifyReply) $comment->flags = $comment->flags & ~Comment::flagNotifyReply;
					if($comment->flags & Comment::flagNotifyConfirmed) $comment->flags = $comment->flags & ~Comment::flagNotifyConfirmed;
					$properties['flags'] = $comment->flags;
				} else if($notify === Comment::flagNotifyAll && !($comment->flags & Comment::flagNotifyAll)) {
					$comment->flags = $comment->flags | Comment::flagNotifyAll;
					$properties['flags'] = $comment['flags'];
				} else if($notify === Comment::flagNotifyReply) {
					$comment->flags = $comment->flags | Comment::flagNotifyReply;
					$properties['flags'] = $comment['flags'];
				}
			}
			
			$changePage = null;
			if($allowChangePage) {
				// check for change of Page ID
				$pageID = (int) $input->post("CommentPage{$comment->id}"); 
				if($pageID > 0 && "$pageID" !== "$comment->page") {
					$page = $this->wire('pages')->get($pageID); 
					$parentID = $comment->parent_id;
					if($parentID) $comment->parent_id = 0; // temporarily set to 0 for page change
					if(!$page->id) {
						$this->error(
							sprintf($this->_('Unable to find page: %d'), $pageID)
						); 
					} else if(!$page->hasField($field)) {
						$this->error(
							sprintf($this->_('Page %d does not have field: %s'), $pageID, "$field")
						);
					} else if($commentField && !$commentField->allowCommentPage($comment, $page, true)) {
						// this one reports errors on its own
					} else {
						$this->message(
							sprintf($this->_('Moved comment #%1$d from page %2$d to page %3$d'), $comment->id, $comment->page->id, $pageID)
						);
						$properties['pages_id'] = $pageID;
						if($comment->parent_id) {
							$comment->parent_id = 0;
							$properties['parent_id'] = 0;
						}
						$changePage = $page;
						$numChanged++;
					}
					if($changePage === null) {
						// if page was not changed, restore back to original parent
						if($parentID) $comment->parent_id = $parentID;
					}
				}
			}
		
			$changeParentID = null;
			if($allowChangeParent) {
				// check for change of parent on threaded comment
				$parentID = $input->post("CommentParent$comment->id");
				if(strlen("$parentID") && ctype_digit("$parentID")) {
					// allows for parent_id "0" but ignore blank
					$parentID = (int) $parentID;
					if($parentID != $comment->parent_id) {
						if(!empty($properties['pages_id'])) {
							// we will apply the parent change after the Page change has applied
							$changeParentID = $parentID;
						} else {
							// parent ID has changed to another parent, or to no parent (0)
							if($commentField && $commentField->allowCommentParent($comment, $parentID, true)) {
								$comment->parent_id = $parentID;
								$properties['parent_id'] = $parentID;
								$numChanged++;
							}
						}
					}
				} else {
					$parentID = null;
				}
			}

			if(count($properties)) {
				$fieldtype->updateComment($comment->getPage(), $field, $comment, $properties); 	
				$this->message(sprintf($this->_('Updated comment #%d'), $comment->id) . " (" . implode(', ', array_keys($properties)) . ")"); 
			}
			
			if($changeParentID !== null && $changePage !== null) {
				// parent ID has changed at the same time that Page ID changed, so we apply parentID change afterwards
				$comment->setPage($changePage);
				if($commentField && $commentField->allowCommentParent($comment, $changeParentID, true)) {
					$comment->parent_id = $changeParentID;
					$fieldtype->updateComment($changePage, $field, $comment, array('parent_id' => $changeParentID));
					$numChanged++;
				}
			}
		}

		if($numDeleted || $numChanged) {
			$pageNum = $input->pageNum() > 1 ? 'page' . $input->pageNum() : '';
			$this->session->redirect('./' . $pageNum . $this->getQueryString());
		}
	}

	/**
	 * Render the markup for a single comment
	 * 
	 * @param Comment $comment
	 * @return string
	 *
	 */
	protected function renderComment(Comment $comment) {

		$sanitizer = $this->sanitizer;
		$page = $comment->getPage();
		$pageTitle = $sanitizer->entities1($page->get('title|name'));
		$field = $comment->getField();
		$adminTheme = $this->wire('adminTheme');
		$isSuperuser = $this->user->isSuperuser();
		$allowDepth = $field->depth > 0;
		$allowDepthChange = $isSuperuser && $allowDepth;
		$allowPageChange = $isSuperuser;
		$parent = $comment->parent();
		$numChildren = 0;
		$text = $this->renderCommentText($comment);
		$id = $comment->id;
		
		$icons = array(
			'edit' => 'edit',
			'upvote' => 'arrow-up',
			'downvote' => 'arrow-down',
			'changed' => 'dot-circle-o',
			'reply' => 'angle-double-down',
			'replies' => 'angle-double-right',
		);
		
		$outs = array(
			'status' => '',
			'notify' => '',
			'website' => '',
			'stars' => '',
			'votes' => '',
			'page' => '',
			'parent' => '',
			'reply' => '',
			'where' => '',
			'children' => '',
		);
		
		$classes = array(
			'input' => 'CommentInput',
			'textarea' => '',
			'radio' => '',
			'checkbox' => '',
			'table' => ''
		);
		
		$labels = array(
			'edit' => $this->_('edit'),
			'view' => $this->_('view'),
			'page' => $this->_('Page'),
			'date' => $this->_('When'),
			'status' => $this->_('Status'),
			'cite' => $this->_('Cite'),
			'website' => $this->_('Web'),
			'email' => $this->_('Mail'),
			'none' => $this->_('None'),
			'parent' => $this->_('Parent'),
			'where' => $this->_('Where'),
			'replyTo' => $this->_('Reply to %s'),
			'stars' => $this->_('Stars'),
			'votes' => $this->_('Votes'),
			'notify' => $this->_('Notify'), 
			'commentId' => $this->_('Comment ID'), 
		);

		$values = array(
			'cite' => $comment->cite, 
			'email' => $comment->email,
			'website' => $comment->website, 
			'ip' => $comment->ip, 
			'date' => wireDate($this->_('Y/m/d g:i a'), $comment->created), 
			'dateRelative' => wireDate('relative', $comment->created),
			'parent' => $parent && $parent->id ? $parent->id : '',
			'parentPlaceholder' => $parent && $parent->id ? '' : $labels['none'],
			'parentCite' => sprintf($labels['replyTo'], $labels['commentId']),
			'stars' => $comment->stars ? $comment->stars : '',
			'upvotes' => $comment->upvotes,
			'downvotes' => $comment->downvotes, 
			'page' => (int) "$comment->page",
		);
		
		$urls = array(
			'parent' => $parent ? "../all/?id=$parent->id" : '',
			'children' => "../all/?parent_id=$id",
			'siblings' => "../all/?pages_id=$page->id",
			'pageView' => "$page->url#Comment$id",
			'pageEdit' => $page->editUrl(),
			'email' => "./?email=" . urlencode($values['email']), 
			'cite' => "../all/?cite=" . urlencode($values['cite']),
			'ip' => "../all/?ip=" . urlencode($values['ip']),
		);
		
		$tooltips = array(
			'parent' => $this->_('ID of the Comment that this one is replying to.'), 
			'page' => $this->_('ID of the Page that this comment lives on.'), 
			'viewAll' => $this->_('View all having value'), 
			'edit' => $this->_('Edit value'), 
			'pageFilter' => $this->_('Show only comments from page'), 
		);
		
		foreach($values as $key => $value) {
			$values[$key] = $sanitizer->entities($value);
		}
		
		foreach($icons as $key => $value) {
			$icons[$key] = wireIconMarkup($value);
		}
		
		if($allowDepth) { 
			$children = $comment->children();
			$numChildren = count($children);
		}
		
		if($adminTheme && $adminTheme instanceof AdminThemeFramework) {
			$classes['input'] = trim("$classes[input] " . $adminTheme->getClass('input-small'));
			$classes['textarea'] = $adminTheme->getClass('textarea');
			$classes['radio'] = $adminTheme->getClass('input-radio');
			$classes['checkbox'] = $adminTheme->getClass('input-checkbox');
			$classes['table'] = $adminTheme->getClass('table');
			// if(strpos($classes['input'], 'uk-input') !== false) $classes['input'] .= " uk-form-blank";
		}
		
		foreach($this->statusTranslations as $status => $label) {
			if($status == Comment::statusDelete && $numChildren) continue; 
			$checked = $comment->status == $status ? "checked='checked' " : '';
			$outs['status'] .= 
				"<label class='CommentStatus'>" . 
					"<input class='$classes[radio]' type='radio' name='CommentStatus$id' value='$status' $checked/>&nbsp;" . 
					"<small>$label</small>" . 
				"</label>&nbsp; ";
		}

		if($page->editable()) {
			$text =
				"<div class='CommentTextEditable' id='CommentText$id' data-textarea-class='$classes[textarea]'>" .
					"<p>$text <a class='CommentTextEdit' href='#'>$icons[edit]&nbsp;$labels[edit]</a></p>" .
				"</div>";
		} else {
			$text = "<p>$text</p>";
		}
	
		if($allowPageChange) $outs['page'] =
			"<span class='detail ui-priority-secondary'>Page #</span>&nbsp;" .
			"<input class='$classes[input] pw-tooltip' title='$tooltips[page]' name='CommentPage$id' value='$values[page]' /> ";

		if($allowDepthChange) $outs['parent'] =
			"<span class='detail ui-priority-secondary'>" . $this->_('Reply to #') . "</span>&nbsp;" .
			"<input class='$classes[input] pw-tooltip' title='$tooltips[parent]' placeholder='$values[parentPlaceholder]' name='CommentParent$id' value='$values[parent]' />";
			
		if($outs['parent'] || $outs['page']) $outs['where'] =
			"<tr>" . 
				"<th>$labels[where]</th>" . 
				"<td class='CommentWhere'>$outs[page]$outs[parent]</td>" . 
			"</tr>";	
		
		if($values['website']) $outs['website'] = 
			"<tr>" .
				"<th>$labels[website]</th>" .
				"<td><a target='_blank' href='$values[website]'>$values[website]</a></td>" .
			"</tr>";
		
		if($field->useStars) $outs['stars'] =
			"<tr>" .
				"<th>$labels[stars]</th>" .
				"<td class='CommentStars'>" .
					"<input type='hidden' name='CommentStars$id' value='$values[stars]' />" .
					$comment->renderStars(array('input' => true)) .
				"</td>" .
			"</tr>";
		
		if($field->useVotes) $outs['votes'] =
			"<tr>" .
				"<th>$labels[votes]</th>" .
				"<td class='CommentVotes'>" .
					"<label class='CommentUpvotes'>" .
						"<span>$icons[upvote]</span>" .
						"<input class='$classes[input]' title='upvotes' type='number' min='0' name='CommentUpvotes$id' value='$values[upvotes]' />" .
					"</label> " .
					"<label class='CommentDownvotes'>" .
						"<span>$icons[downvote]</span>" .
						"<input class='$classes[input]' title='downvotes' type='number' min='0' name='CommentDownvotes$id' value='$values[downvotes]' />" .
					"</label> " .
				"</td>" .
			"</tr>";

		if($field->useNotify) {
			foreach($this->notifyFlagsTranslations as $flag => $label) {
				$checked = false;
				if($flag && $comment->flags & $flag) {
					$checked = true; // All or Replies
				} else if(!$flag && !($comment->flags & 2) && !($comment->flags & 4) && !($comment->flags & 8)) {
					$checked = true; // None
				}	
				$checked = $checked ? "checked='checked' " : '';
				$outs['notify'] .=
					"<label class='CommentNotify'>" .
						"<input class='$classes[radio]' type='radio' name='CommentNotify$id' value='$flag' $checked/>&nbsp;" .
						"<small>$label</small>" .
					"</label>&nbsp; ";
			}
			$outs['notify'] = "<tr><th>$labels[notify]</th><td>$outs[notify]</td></tr>";
		}
		
		if($parent) {
			$a = $sanitizer->entities($parent->cite) . " <a href='$urls[parent]'>$parent->id</a>";
			$outs['reply'] = // displayed after table
				"<p class='CommentReplyInfo detail'>" .
					"$icons[reply] " .
					sprintf($labels['replyTo'], $a) .
				"</p>";
		}

		if($numChildren) $outs['children'] = // displayed after table
			"<p class='CommentChildrenInfo detail'>" .
				"<a href='$urls[children]'>" .
					"$icons[replies] " .
					sprintf($this->_n('%d reply', '%d replies', $numChildren), $numChildren) .
				"</a>" .
			"</p>";

		/** @var FieldtypeComments $fieldtype */
		// $fieldtype = $field->type;
		// $who = $fieldtype->getNotifyEmails($page, $field, $comment); 
		// $text .= "<pre>" . htmlentities(print_r($who, true)) . "</pre>";
		$numRows = 0;
		foreach($outs as $out) if(!empty($out)) $numRows++;
		$contentClass = 'CommentContent';
		if($numRows >= 7) $contentClass .= ' CommentContentLarge';

		$out =
			"<div class='CommentItem ui-helper-clearfix CommentItemStatus$comment->status'>" . 
				"<table class='CommentItemInfo $classes[table]' cellspacing='0'>" .
					"<tr class='CommentTitle'>" . 
						"<th>" .
							"<label>" .
								"<input class='CommentCheckbox $classes[checkbox]' type='checkbox' name='comment[]' value='$id' />&nbsp;" .
								"<span class='CommentID'>$id</span>" . 
							"</label>" . 
						"</th>" . 
						"<td>" . 
							"<a href='$urls[siblings]' class='pw-tooltip' title='$tooltips[pageFilter]'><strong>$pageTitle</strong></a> " .
							"<span class='detail'>&nbsp;" . 
								"<a class='detail' href='$urls[pageView]'>$labels[view]</a>&nbsp;/&nbsp;" . 
								"<a class='detail' href='$urls[pageEdit]'>$labels[edit]</a>" . 
							"</span>" . 
							"<span class='CommentChangedIcon'>$icons[changed]</span>" . 
						"</td>" . 
					"</tr>" .
					"<tr class='CommentItemStatus'>" .
						"<th>$labels[status]</th>" .
						"<td class='CommentStatus'>$outs[status]</td>" .
					"</tr>" .
					$outs['notify'] . 
					"<tr>" . 
						"<th>$labels[date]</th>" . 
						"<td>$values[date] <span class='detail'>$values[dateRelative]</span></td>" . 
					"</tr>" . 
					"<tr>" .
						"<th>$labels[cite]</th>" . 
						"<td class='CommentCite'>" . 
							"<a href='$urls[cite]' class='pw-tooltip' title='$tooltips[viewAll]'>$values[cite]</a> " . 
							//"<input type='text' class='$classes[input]' name='CommentCite$id' value='$values[cite]' hidden /> " . // @todo
							//"<a href='#' class='CommentToggleSiblings pw-tooltip' title='$tooltips[edit]'>$icons[edit]</a> " .
							"<a class='detail pw-tooltip' title='$tooltips[viewAll]' href='$urls[ip]'>$values[ip]</a> " .
						"</td>" . 
					"</tr>" . 
					"<tr>" . 
						"<th>$labels[email]</th>" . 
						"<td>" .
							"<a href='$urls[email]' class='pw-tooltip' title='$tooltips[viewAll]'>$values[email]</a> " .
							//"<input type='email' class='$classes[input]' name='CommentEmail$id' value='$values[email]' hidden /> " . // @todo
							//"<a href='#' class='CommentToggleSiblings pw-tooltip' title='$tooltips[edit]'>$icons[edit]</a>" .
						"</td>" . 
					"</tr>" . 
					$outs['website'] . 
					$outs['stars'] . 
					$outs['votes'] . 
					$outs['where'] . 
				"</table>" .
				"<div class='$contentClass'>" .
					"$outs[reply]" .
					"<div class='CommentText'>$text</div>" .
					"$outs[children]" .
				"</div>" . 
			"</div>";
	
		/*
		$out = 	
			"<div class='CommentItem ui-helper-clearfix CommentItemStatus{$comment->status}'>" . 
				"$out " . 
				"<div class='CommentContent'>" . 
					"$outs[parent]" . 
					"<div class='CommentText'>$text</div>" . 
					"$outs[children]" . 
				"</div>" . 
			"</div>";
		*/

		$page->of(false);

		return $out; 
	}

	/**
	 * Prep comment text for output in editor
	 * 
	 * @param Comment $comment
	 * @return string
	 * 
	 */
	protected function renderCommentText(Comment $comment) {
		$text = $this->sanitizer->entities($comment->get('text'));
		$text = str_replace('\r', ' ', $text);
		$text = preg_replace('/\r?(\n)/', '\r', $text);
		$text = str_replace('\r\r', "<br />\n<br />\n", $text);
		$text = str_replace('\r', "<br />\n", $text);
		return $text;
	}

	/**
	 * Render the comments list header
	 * 
	 * @param int $limit
	 * @param bool $useVotes
	 * @param bool $useStars
	 * @return string
	 * 
	 */
	protected function renderCommentsHeader($limit, $useVotes, $useStars) {
		
		$setStatusLabel = $this->_('Set status:');
		$perPageLabel = $this->_('per page');
		$adminTheme = $this->wire('adminTheme');
		$selectClass = $adminTheme && $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('select') : '';
		$checkboxClass = $adminTheme && $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('input-checkbox') : '';
		
		$pagerLimitOut = "
			<select class='$selectClass' id='CommentLimitSelect'>
				<option>10 $perPageLabel</option>
				<option>25 $perPageLabel</option>
				<option>50 $perPageLabel</option>
				<option>100 $perPageLabel</option>
			</select>
		";
		$pagerLimitOut = str_replace("<option>$limit ", "<option selected>$limit ", $pagerLimitOut);

		$checkAllLabel = $this->_('Check/uncheck all');
		$checkAll =
			"<label title='$checkAllLabel'><input class='$checkboxClass' type='checkbox' id='CommentCheckAll' /> " .
			"<span class='detail'></span></label>";

		$noCheckedLabel = $this->_('There are no checked items');
		$actionsOut =
			"<select class='$selectClass' id='CommentActions' data-nochecked='$noCheckedLabel'>" .
			"<option value=''>" . $this->_('Actions (checked items)') . "</option>";

		foreach($this->statusTranslations as $status => $label) {
			$actionsOut .= "<option value='$status'>$setStatusLabel $label</option>";
		}

		if($useVotes) {
			$actionsOut .= "<option value='reset-upvotes'>" . $this->_('Reset: Upvotes') . "</option>";
			$actionsOut .= "<option value='reset-downvotes'>" . $this->_('Reset: Downvotes') . "</option>";
		}
		$actionsOut .= "</select>";

		$sorts = array(
			'-created' => $this->_('Date (new–old)'),
			'created' => $this->_('Date (old–new)'),
		);
		if($useStars) {
			$sorts['-stars'] =  $this->_('Stars (high–low)');
			$sorts['stars'] = $this->_('Stars (low–high)');
		}
		if($useVotes) {
			$sorts['upvotes'] = $this->_('Upvotes');
			$sorts['downvotes'] = $this->_('Downvotes');
		}

		$sortByOut = "<select class='$selectClass' id='CommentListSort'>";
		$sortLabelPrefix = $this->_('Sort:');
		foreach($sorts as $sortKey => $sortLabel) {
			$sortByOut .= "<option value='$sortKey'>$sortLabelPrefix $sortLabel</option>";
		}
		$sortByOut .= "</select>";
		$sort = $this->wire('session')->getFor($this, 'sort');
		if(empty($sort)) $sort = "-created";
		$sortByOut = str_replace("'$sort'", "'$sort' selected", $sortByOut);

		return
			"<p class='CommentCheckAll'>$checkAll</p>" .
			"<p class='CommentActions'>$actionsOut</p>" .
			"<p class='CommentSorts'>$sortByOut</p>" .
			"<p class='CommentLimitSelect'>$pagerLimitOut</p>";
	}

	/**
	 * Render the markup for a list of comments
	 * 
	 * @param CommentArray $comments
	 * @return string
	 *
	 */
	protected function renderComments(CommentArray $comments) {

		$commentsBody = '';
		$cnt = 0;
		$status = $this->input->urlSegment3;
		$start = $comments->getStart();
		$limit = $comments->getLimit();
		$total = $comments->getTotal();
		$pageNumPrefix = $this->config->pageNumUrlPrefix; 
		$pageNum = $this->wire('input')->pageNum; 
		$queryString = $this->getQueryString();
		$unsavedChangesLabel = $this->_('You have unsaved changes!');
		$field = $comments->getField();

		foreach($comments as $comment) {
			/** @var Comment $comment */
			if($status && $status != 'all' && $this->statuses[$comment->status] != $status) continue; 
			$commentsBody .= $this->renderComment($comment); 
			$cnt++;
			if($cnt >= $limit) break;
		}

		/** @var MarkupPagerNav $pager */
		$pager = $this->wire('modules')->get('MarkupPagerNav'); 
		$pagerOut = $pager->render($comments, array(
			'queryString' => $queryString,
			'baseUrl' => "./"
		));
		/** @var JqueryWireTabs $wireTabs */
		$wireTabs = $this->modules->get('JqueryWireTabs'); 
		$tabs = array();
		$class = $this->input->urlSegment3 === 'all' ? 'on' : '';
		$tabs["tabStatusAll"] = "<a class='$class' href='../all/'>" . $this->labelAll . "</a>";

		foreach($this->statuses as $status => $name) {
			if($status == Comment::statusDelete) continue;
			$class = $this->input->urlSegment3 === $name ? 'on' : '';
			$label = $this->statusTranslations[$status];
			if($label === $name) $label = ucfirst($label);
    		$tabs["tabStatus$status"] = "<a class='$class' href='../$name/'>$label</a>";
		}

		$tabsOut = $wireTabs->renderTabList($tabs);
		$this->headline .= ' (' . ($start+1) . "–" . ($start + $cnt) . " " . sprintf($this->_('of %d'), $total) . ')';
		$this->headline($this->headline);

		if($cnt) { 
			/** @var InputfieldSubmit $button */
			$button = $this->modules->get('InputfieldSubmit');
			$button->attr('name', 'processComments');
			$button->showInHeader();
			$button = $button->render();
		} else $button = '';

		if($this->input->pageNum > 1) {
			$queryString = "./$pageNumPrefix$pageNum$queryString";
		}

		if(!count($comments)) {
			return
				"<form>" . 
					$tabsOut . 
					"<h2>" . $this->_('None to display') . "</h2>" . 
				"</form>";
		}
		
		$commentsHeader = $this->renderCommentsHeader($limit, $field->get('useVotes'), $field->get('useStars'));
		
		return
			"<form id='CommentListForm' action='$queryString' method='post' data-unsaved='$unsavedChangesLabel'>" . 
				$tabsOut . 
				"<div id='CommentListHeader' class='ui-helper-clearfix'>" . 	
					$pagerOut . 
					$commentsHeader . 
				"</div>" . 
				"<div class='CommentItems ui-helper-clearfix'>" . 
					$commentsBody . 
				"</div>" . 
				$pagerOut . 
				$button . 
			"</form>"; 
	}
	
	protected function getQueryString() {
		$queryString = '';
		foreach($this->input->whitelist as $key => $value) {
			$queryString .= $this->wire('sanitizer')->entities($key) . "=" . $this->wire('sanitizer')->entities($value) . "&";
		}
		$queryString = trim($queryString, '&');
		if($queryString) $queryString = "?$queryString";
		return $queryString;
	}

	protected function checkInstall() {
		if($this->wire('modules')->isInstalled('ProcessLatestComments')) {
			$this->warning('Please uninstall the ProcessLatestComments module (this module replaces it).');
		}
	}

	public function ___install() {
		$this->checkInstall();
		return parent::___install();
	}
	
	/**
	 * Search for items containing $text and return an array representation of them
	 *
	 * Implementation for SearchableModule interface
	 *
	 * @param string $text Text to search for
	 * @param array $options Options to modify behavior:
	 *  - `edit` (bool): True if any 'url' returned should be to edit items rather than view them
	 *  - `multilang` (bool): If true, search all languages rather than just current (default=true).
	 *  - `start` (int): Start index (0-based), if pagination active (default=0).
	 *  - `limit` (int): Limit to this many items, if pagination active (default=0, disabled).
	 * @return array
	 *
	 */
	public function search($text, array $options = array()) {

		/** @var Languages $languages */
		$page = $this->getProcessPage();
		$fields = array();
		foreach($this->wire('fields') as $field) {
			if($field->type instanceof FieldtypeComments) $fields[$field->name] = $field;
		}

		$result = array(
			'title' => $page->id ? (string) $page->title : $this->className(),
			'total' => 0,
			'url' => '',
			'items' => array(),
			'properties' => array(
				'text',
				'cite',
				'email',
				'status',
				'created',
				'website',
				'ip',
				'user_agent',
				'upvotes',
				'downvotes',
				'stars'
			)
		);
		
		if($options['help']) return $result;
		
		$operator = empty($options['operator']) ? '%=' : $options['operator'];
		$value = $this->wire('sanitizer')->selectorValue($text); 	
		$summaryLength = $options['verbose'] ? 1024 : 200;
		
		if(!empty($options['property'])) {
			$selector = "$options[property]$operator$value";
			$q = "$options[property]$operator$text"; 
		} else {
			$selector = "text$operator$value";
			$q = "text{$operator}$text"; 
		}
		
		if(!empty($options['limit'])) $selector .= ", limit=$options[limit]";
		if(!empty($options['start'])) $selector .= ", start=$options[start]";
	
		foreach($fields as $fieldName => $field) {
			/** @var CommentArray $comments */
			$comments = $field->type->find($selector); 
			if(!count($comments)) continue;
			$result['total'] += $comments->getTotal();
			$url = $page->url() . "list/$field->name/all/";
			if(count($fields) == 1) $result['url'] = $url . "?q=" . urlencode($q); 
			foreach($comments as $comment) {
				/** @var Comment $comment */
				$commentPage = $comment->getPage();
				$editUrl = $url . "?id=$comment->id";
				$item = array(
					'id' => $comment->id,
					'name' => $comment->cite,
					'title' => $comment->cite, 
					'subtitle' => $commentPage && $commentPage->id ? (string) $commentPage->get('title|name') : '', 
					'summary' => $this->wire('sanitizer')->truncate($comment->text, $summaryLength), 
					'url' => empty($options['edit']) ? $comment->url() : $editUrl
				);
				$result['items'][] = $item;
			}
		}

		/* // for debugging:
		$result['items'][] = array(
			'id' => 0,
			'name' => '',
			'title' => $selector,
			'subtitle' => '',
			'summary' => '',
			'url' => '#'
		);
		*/

		return $result;
	}


}

