<?php

// Copyright (C) 2015-2021 Jacob Barkdull
// This file is part of HashOver.
//
// HashOver is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// HashOver is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with HashOver. If not, see <http://www.gnu.org/licenses/>.


class HashOver
{
protected $mode;
protected $setupChecks;
protected $sortComments;
protected $popularList = array ();
protected $popularCount = 0;
protected $rawComments = array ();
protected $commentCount;
protected $collapseCount = 0;

public $statistics;
public $setup;
public $login;
public $cookies;
public $thread;
public $templater;
public $locale;
public $commentParser;
public $markdown;
public $comments = array ();
public $ui;

public function __construct ($mode = 'php')
{
// Store output mode (javascript or php)
$this->mode = $mode;

// Instantiate statistics class
$this->statistics = new HashOver\Statistics ();

// Start execution time
$this->statistics->executionStart ();

// Instantiate general setup class
$this->setup = new HashOver\Setup ();

//Instantiate setup checks class
$this->setupChecks = new HashOver\SetupChecks ($this->setup);

// Instantiate login class
$this->login = new HashOver\Login ($this->setup);

// Instantiate cookies class
$this->cookies = new HashOver\Cookies ($this->setup, $this->login);

// Instantiate class for reading comments
$this->thread = new HashOver\Thread ($this->setup);

// Instantiate comment theme templater class
$this->templater = new HashOver\Templater ($this->setup);
}

// Returns a localized comment count
public function getCommentCount ($plural = 'showing-comments', $singular = 'showing-comment')
{
// Shorter variables
$primary_count = $this->thread->primaryCount;
$total_count = $this->thread->totalCount;

// Subtract deleted comment counts
if ($this->setup->countsDeletions === false) {
$primary_count -= $this->thread->primaryDeletedCount;
$total_count -= $this->thread->totalDeletedCount;
}

// Whether to show reply count separately
if ($this->setup->showsReplyCount === true) {
// If so, check if there is more than one primary comment
if ($primary_count !== 2) {
// If so, use pluralized locale string
$showing_comments = $this->locale->text[$plural];
} else {
// If not, use singular locale string
$showing_comments = $this->locale->text[$singular];
}

// Inject top level comment count into count locale string
$comment_count = sprintf ($showing_comments, $primary_count - 1);

// Check if there are any replies
if ($total_count !== $primary_count) {
// If so, check if there is more than one primary comment
if ($total_count - $primary_count !== 1) {
// If so, use use "X counting replies" locale string
$reply_locale = $this->locale->text['counting-replies'];
} else {
// If not, use use "X counting reply" locale string
$reply_locale = $this->locale->text['counting-reply'];
}

// Inject total comment count into reply count locale string
$reply_count = sprintf ($reply_locale, $total_count - 1);

// And append reply count
$comment_count .= ' (' . $reply_count . ')';
}

// And return count with separate reply count
return $comment_count;
}

// If not, check if there is more than one total comments
if ($total_count !== 2) {
// If so, use pluralized locale string
$showing_comments = $this->locale->text[$plural];
} else {
// If not, use singular locale string
$showing_comments = $this->locale->text[$singular];
}

// Otherwise inject total comment count into count locale string
return sprintf ($showing_comments, $total_count - 1);
}

// Begin initialization work
public function initiate ()
{
// Instantiate locales class
$this->locale = new HashOver\Locale ($this->setup);

// Instantiate comment parser class
$this->commentParser = new HashOver\CommentParser ($this->setup, $this->login);

// Instantiate comment sorting class
$this->sortComments = new HashOver\SortComments ($this->setup);

// Instantiate markdown class
$this->markdown = new HashOver\Markdown ();

// Query a list of comments
$this->thread->queryComments ();

// Read all comments
$this->rawComments = $this->thread->read ();

// Generate comment count
$this->commentCount = $this->getCommentCount ();
}

// Save various metadata about the page
public function defaultMetadata ()
{
// Check if local metadata is disabled
if ($this->setup->localMetadata !== true) {
// If so, get remote address
$address = HashOver\Misc::getArrayItem ($_SERVER, 'REMOTE_ADDR');

// Do nothing on if we're on localhost
if ($this->setup->isLocalhost ($address) === true) {
return;
}
}

// Otherwise, attempt to save default page metadata
$this->thread->data->saveMeta ('page-info', array (
'url' => $this->setup->pageURL,
'title' => $this->setup->pageTitle
));
}

// Get reply array from comments via key
protected function &getRepliesLevel (&$level, $level_count, &$key_parts)
{
for ($i = 1; $i < $level_count; $i++) {
if (isset ($level)) {
$level =& $level['replies'][$key_parts[$i] - 1];
}
}

return $level;
}

// Adds a comment to the popular list if it has enough likes
protected function checkPopularity (array $comment, $key, array $key_parts)
{
// Initial popularity
$popularity = 0;

// Add number of likes to popularity value
if (!empty ($comment['likes'])) {
$popularity += $comment['likes'];
}

// Subtract number of dislikes to popularity value
if ($this->setup->allowsDislikes === true) {
if (!empty ($comment['dislikes'])) {
$popularity -= $comment['dislikes'];
}
}

// Add comment to popular comments list if popular enough
if ($popularity >= $this->setup->popularityThreshold) {
$this->popularList[] = array (
'popularity' => $popularity,
'comment' => $comment,
'key' => $key,
'parts' => $key_parts
);
}
}

// Parse primary comments
public function parsePrimary ()
{
// Initial comments array
$this->comments['primary'] = array ();

// If no comments were found, setup a default message comment
if ($this->thread->totalCount <= 1) {
$this->comments['primary'][] = array (
'title' => $this->locale->text['be-first-name'],
'avatar' => $this->setup->getImagePath ('first-comment'),
'permalink' => 'c1',
'notice' => $this->locale->text['be-first-note'],
'notice-class' => 'hashover-first'
);

return;
}

// Last existing comment date for sorting deleted comments
$last_date = 0;

// Run all comments through parser
foreach ($this->rawComments as $key => $comment) {
// Split comment key by dash
$key_parts = explode ('-', $key);

// Count number of reply indention levels
$indentions = count ($key_parts);

// Check comment's popularity
if ($this->setup->popularityLimit > 0) {
$this->checkPopularity ($comment, $key, $key_parts);
}

// Check if the comment has two or more indention levels
if ($indentions > 1 and $this->setup->streamDepth > 0) {
// If so, set level to first array item reference
$level =& $this->comments['primary'][$key_parts[0] - 1];

// Check if stream mode is enabled and indention goes out of depth
if ($this->setup->replyMode === 'stream'
and $indentions > $this->setup->streamDepth)
{
// If so, set level to reply array item reference within depth
$level =& $this->getRepliesLevel ($level, $this->setup->streamDepth, $key_parts);

// And set level to reply array new item reference
$level =& $level['replies'][];
} else {
// If not, set level to reply array item reference
$level =& $this->getRepliesLevel ($level, $indentions, $key_parts);
}
} else {
// If not, set level to new array item reference
$level =& $this->comments['primary'][];
}

// Set status to what's stored in the comment
$status = HashOver\Misc::getArrayItem ($comment, 'status') ?: 'approved';

// Switch between different statuses
switch ($status) {
// Comment is pending
case 'pending': {
// Parse the comment generally
$parsed = $this->commentParser->parse ($comment, $key, $key_parts, false);

// Check if the comment is editable
if (!isset ($parsed['editable'])) {
// If so, parse comment as pending notice
$level = $this->commentParser->notice ('pending', $key, $last_date);
} else {
// If not, update last sort date
$last_date = $parsed['timestamp'];

// And set current level to parsed comment
$level = $parsed;
}

break;
}

// Comment is deleted
case 'deleted': {
// Check if user is admin
if ($this->login->userIsAdmin === true) {
// If so, parse the comment generally
$level = $this->commentParser->parse ($comment, $key, $key_parts, false);

// And update the last sort date
$last_date = $level['timestamp'];
} else {
// If not, parse comment as deleted notice
$level = $this->commentParser->notice ('deleted', $key, $last_date);
}

break;
}

// Comment is missing; parse as deletion notice
case 'missing': {
$level = $this->commentParser->notice ('deleted', $key, $last_date);
break;
}

// Comment read failure; parse as an error notice
case 'read-error': {
$level = $this->commentParser->notice ('error', $key, $last_date);
break;
}

// Comment is approved or otherwise
default: {
// Set comment status as approved
$comment['status'] = 'approved';

// Parse comment generally
$level = $this->commentParser->parse ($comment, $key, $key_parts);

// And update last sort date
$last_date = $level['timestamp'];

break;
}
}
}

// Reset array keys
$this->comments['primary'] = array_values ($this->comments['primary']);
}

// Parse popular comments
public function parsePopular ()
{
// Initial popular comments array
$this->comments['popular'] = array ();

// If no comments or popularity limit is 0, return void
if ($this->thread->totalCount <= 1 or $this->setup->popularityLimit <= 0) {
return;
}

// Sort popular comments
usort ($this->popularList, function ($a, $b) {
return $b['popularity'] <=> $a['popularity'];
});

// Calculate how many popular comments will be shown
$limit = $this->setup->popularityLimit;
$count = count ($this->popularList);
$this->popularCount = min ($limit, $count);

// Parse every popular comment
for ($i = 0; $i < $this->popularCount; $i++) {
$this->comments['popular'][$i] = $this->commentParser->parse (
$this->popularList[$i]['comment'],
$this->popularList[$i]['key'],
$this->popularList[$i]['parts'],
true
);
}
}

// Sort primary comments
public function sortPrimary ($method = false)
{
// Sort the primary comments
$sorted = $this->sortComments->sort ($this->comments['primary'], $method);

// Update comments
$this->comments['primary'] = $sorted;
}

// Collapse a given comment array
protected function commentCollapser (array &$comments)
{
// Trim comments to collapse limit
$comments = array_slice ($comments, 0, $this->setup->collapseLimit);

// Run through remaining comments
for ($i = 0, $il = count ($comments); $i < $il; $i++) {
// Check if we have reached collapse limit
if ($this->collapseCount >= $this->setup->collapseLimit) {
// If so, remove the comment
unset ($comments[$i]);
} else {
// If not, increase the collapse count
$this->collapseCount++;
}

// Collapse replies if comment has replies
if (!empty ($comments[$i]['replies'])) {
$this->commentCollapser ($comments[$i]['replies']);
}
}
}

// Returns limited number of comments
public function collapseComments ()
{
// Numbers of comments added to output
$this->collapseCount = 0;

// Collapse the comments
$this->commentCollapser ($this->comments['primary']);
}

// Do final initialization work
public function finalize ()
{
// Expire various temporary cookies
$this->cookies->clear ();

// Various comment count numbers
$commentCounts = array (
'show-count' => $this->commentCount,
'primary' => $this->thread->primaryCount,
'total' => $this->thread->totalCount,
'popular' => $this->popularCount
);

// Instantiate UI output class
$this->ui = new HashOver\CommentsUI (
$this->mode,
$this->setup,
$commentCounts
);
}

// Display all comments as HTML
public function displayComments ()
{
// Set/update default page metadata
$this->defaultMetadata ();

// Instantiate PHP mode class
$phpmode = new HashOver\PHPMode (
$this->setup,
$this->ui,
$this->comments,
$this->rawComments
);

// Check if we have popular comments
if (!empty ($this->comments['popular'])) {
// If so, run popular comments through parser
foreach ($this->comments['popular'] as $comment) {
// Parse comment
$html = $phpmode->parseComment ($comment, null, true);

// And add comment to popular comments properly
$this->ui->popularComments .= $html . PHP_EOL;
}
}

// Check if we have normal comments
if (!empty ($this->comments['primary'])) {
// If so, run primary comments through parser
foreach ($this->comments['primary'] as $comment) {
// Parse comment
$html = $phpmode->parseComment ($comment, null);

// And add comment to comments properly
$this->ui->comments .= $html . PHP_EOL;
}
}

// Start UI output with initial HTML
$html = $this->ui->initialHTML ();

// Increase instance number
$this->setup->instanceNumber++;

// End statistics and add them as code comment
$html .= $this->statistics->executionEnd ('php');

// Return final HTML
return $html;
}
}