<?php namespace HashOver;

// Copyright (C) 2010-2019 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/>.


// Read and count comments
class CommentFiles extends DataFiles
{
protected $setup;
protected $thread;

public function __construct (Setup $setup, Thread $thread)
{
// Construct parent class
parent::__construct ($setup);

// Store parameters as properties
$this->setup = $setup;
$this->thread = $thread;
}

// Gets the appropriate thread directory path
protected function getThreadRoot ($thread)
{
// Use thread directory path if told to
if ($thread !== 'auto') {
return $this->setup->joinPaths ($this->setup->threadsPath, $thread);
}

// Otherwise, use thread from setup
return $this->setup->threadPath;
}

// Returns a comment file path for a given file and thread
protected function getCommentPath ($file, $extension, $thread = 'auto')
{
// Get thread root
$thread = $this->getThreadRoot ($thread);

// Construct full file path
$path = $this->setup->joinPaths ($thread, $file . '.' . $extension);

return $path;
}

// Read directory contents, put filenames in array, count files
protected function loadFiles ($extension)
{
// Initial comments to return
$comments = array ();

// File pattern to match against
$pattern = $this->setup->threadPath . '/*.' . $extension;

// Find files matching the pattern, with no sorting
$files = glob ($pattern, GLOB_NOSORT);

// Run through comments
foreach ($files as $file) {
// Get file name without extension
$key = basename ($file, '.' . $extension);

// Add file name to comments
$comments[$key] = (string)($key);
}

return $comments;
}

// Gets the appropriate metadata directory path
protected function getMetaDirectory ($thread, $global)
{
// Check if we're getting metadata for a specific thread
if ($global !== true) {
// If so, use the thread's path
$directory = $this->getThreadRoot ($thread) . '/metadata';
} else {
// If not, use the global metadata path
$directory = $this->setup->commentsPath . '/metadata';
}

// Return metadata directory path
return $directory;
}

// Gets the appropriate metadata file path
protected function getMetaPath ($name, $thread, $global)
{
// Metadata directory path
$directory = $this->getMetaDirectory ($thread, $global);

// Metadata file path
$path = $this->setup->joinPaths ($directory, $name . '.json');

// Return metadata file path
return $path;
}

// Creates metadata directory if it doesn't exist
protected function setupMeta ($thread, $global)
{
// Metadata directory path
$metadata = $this->getMetaDirectory ($thread, $global);

// Check if metadata root directory exists
if (file_exists ($this->getThreadRoot ($thread))) {
// If so, attempt to create metadata directory
if (file_exists ($metadata) or @mkdir ($metadata, 0755, true)) {
// If successful, set permissions to 0755 again
@chmod ($metadata, 0755);
return true;
}

// Otherwise throw exception
throw new \Exception (sprintf (
'Failed to create metadata directory at: %s',
$metadata
));
}
}

// Gets and returns latest comments from metadata files
protected function getLatestComments ($path, $thread, $global)
{
// Latest comments
$comments = array ();

// Attempt to read metadata
$latest = $this->readJSON ($path);

// Do nothing if JSON could not be read
if ($latest === false) {
return $comments;
}

// Run through the latest comments
foreach ($latest as $item) {
// Get comment key
$key = basename ($item);

// Decide proper thread
$thread = $global ? dirname ($item) : $thread;

// Initial comment data
$data = array ('comment' => $key, 'thread' => $thread);

// Attempt to read comment
$comment = $this->thread->data->read ($key, $thread);

// Check if comment read successfully
if ($comment !== false) {
// If so, get comment status
$status = Misc::getArrayItem ($comment, 'status') ?: 'approved';

// Add comment to array if it is approved
if ($status === 'approved') {
$comments[] = array_merge ($data, $comment);
}
}
}

return $comments;
}

// Read and return specific metadata from JSON file
public function readMeta ($name, $thread = 'auto', $global = false)
{
// Metadata JSON file path
$path = $this->getMetaPath ($name, $thread, $global);

// Choose statement for supported metadata
switch ($name) {
// Latest comments
case 'latest-comments': {
return $this->getLatestComments ($path, $thread, $global);
}

// All others, just try to read as-is
default: {
return $this->readJSON ($path);
}
}
}

// Save metadata to specific metadata JSON file
public function saveMeta ($name, array $data, $thread = 'auto', $global = false)
{
// Metadata JSON file path
$metadata_path = $this->getMetaPath ($name, $thread, $global);

// Create metadata directory if it doesn't exist
$this->setupMeta ($thread, $global);

// Check if metadata root directory exists
if (file_exists ($this->getThreadRoot ($thread))) {
// If so, attempt to save data to metadata JSON file
if ($this->saveJSON ($metadata_path, $data) === false) {
// Throw exception on failure
throw new \Exception (
'Failed to save metadata!'
);
}
}

return true;
}

// Check if comment thread directory exists
public function checkThread ()
{
// Attempt to create the directory
if (!file_exists ($this->setup->threadPath)
and !@mkdir ($this->setup->threadPath, 0755, true)
and !@chmod ($this->setup->threadPath, 0755))
{
throw new \Exception (sprintf (
'Failed to create comment thread directory at: %s',
$this->setup->threadPath
));
}

// If yes, check if it is or can be made to be writable
if (!is_writable ($this->setup->threadPath)
and !@chmod ($this->setup->threadPath, 0755))
{
throw new \Exception (sprintf (
'Comment thread directory at %s is not writable.',
$this->setup->threadPath
));
}
}

// Queries an array of directory names
protected function queryDirs ($path, $callback = false)
{
// Directory names output
$names = array ();

// Get comment directories
$dirs = glob ($path . '/*', GLOB_ONLYDIR);

// Convert directories paths to just their names
foreach ($dirs as $name) {
// Check callback conditions
$match = $callback ? $callback ($name) : true;

// And add directory if callback returns true
if ($match === true) {
$names[] = basename ($name);
}
}

return $names;
}

// Queries an array of websites
public function queryWebsites ()
{
// Path to get website directories from
$path = $this->setup->commentsRoot;

// Get website directories
$websites = $this->queryDirs ($path, function ($name) {
return file_exists ($name . '/threads');
});

return $websites;
}

// Queries an array of comment threads
public function queryThreads ()
{
return $this->queryDirs ($this->setup->threadsPath);
}

// Prepends a comment to latest comments metadata file
protected function prependLatestComments ($file, $global = false)
{
// Add thread to file if metadata is global
if ($global === true) {
$file = $this->setup->joinPaths ($this->setup->threadName, $file);
}

// Initial latest comments metadata array
$latest = array ($file);

// Get metadata file path
$path = $this->getMetaPath ('latest-comments', 'auto', $global);

// Attempt to read existing latest comments metadata
$metadata = $this->readJSON ($path);

// Merge existing comments with initial array
if ($metadata !== false) {
$latest = array_merge ($latest, $metadata);
}

// Maximum number of latest comments to store
$latest_max = max (10, $this->setup->latestMax);

// Limit latest comments metadata array to configurable size
$latest = array_slice ($latest, 0, $latest_max);

// Attempt to save latest comments metadata
$this->saveMeta ('latest-comments', $latest, 'auto', $global);
}

// Removes a comment from latest comments metadata file
protected function spliceLatestComments ($file, $global = false)
{
// Add thread to file if metadata is global
if ($global === true) {
$file = $this->setup->joinPaths ($this->setup->threadName, $file);
}

// Get metadata file path
$path = $this->getMetaPath ('latest-comments', 'auto', $global);

// Attempt to read existing latest comments metadata
$latest = $this->readJSON ($path);

// Check if latest comments metadata read successfully
if ($latest !== false) {
// If so, get index of file in array
$index = array_search ($file, $latest);

// Remove comment from latest array
if ($index !== false) {
array_splice ($latest, $index, 1);
}

// Attempt to save latest comments metadata
$this->saveMeta ('latest-comments', $latest, 'auto', $global);
}
}

// Adds a comment to latest comments metadata file
public function addLatestComment ($file)
{
// Add comment to thread-specific latest comments metadata
$this->prependLatestComments ($file);

// Add comment to global latest comments metadata
$this->prependLatestComments ($file, true);
}

// Removes a comment from latest comments metadata file
public function removeFromLatest ($file)
{
// Add comment to thread-specific latest comments metadata
$this->spliceLatestComments ($file);

// Add comment to global latest comments metadata
$this->spliceLatestComments ($file, true);
}
}