// @licstart  The following is the entire license notice for the
//  JavaScript code in this page.
//
// Copyright (C) 2018-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/>.
//
// @licend  The above is the entire license notice for the
//  JavaScript code in this page.

"use strict";

// Latest comments API frontend constructor (constructor.js)
function HashOverLatest (options)
{
	// Reference to this HashOver object
	var hashover = this;

	// Get current date and time
	var datetime = new Date ();

	// Start queries with current client time
	var queries = [
		'time=' + HashOverLatest.getClientTime ()
	];

	// Check if options is an object
	if (options && options.constructor === Object) {
		// If so, add website to queries if present
		if (options.url !== undefined) {
			queries.push ('url=' + encodeURIComponent (options.url));
		}

		// Add website to queries if present
		if (options.website !== undefined) {
			queries.push ('website=' + encodeURIComponent (options.website));
		}

		// And add thread to queries if present
		if (options.thread !== undefined) {
			queries.push ('thread=' + encodeURIComponent (options.thread));
		}
	}

	// Backend request path
	var requestPath = HashOverLatest.backendPath + '/latest-ajax.php';

	// Handle backend request
	this.ajax ('POST', requestPath, queries, function (json) {
		// Handle error messages
		if (json.message !== undefined) {
			hashover.displayError (json, 'hashover-latest');
			return;
		}

		// Locales from HashOver backend
		HashOverLatest.prototype.locale = json.locale;

		// Setup information from HashOver back-end
		HashOverLatest.prototype.setup = json.setup;

		// Templatify UI HTML from backend
		HashOverLatest.prototype.ui = hashover.strings.templatify (json.ui);

		// Thread information from HashOver back-end
		hashover.instance = json.instance;

		// Backend execution time and memory usage statistics
		hashover.statistics = json.statistics;

		// Initiate HashOver latest comments
		hashover.init ();
	}, true);
};

// Constructor to add shared methods to (constructor.js)
var HashOverConstructor = HashOverLatest;

// Get the current HashOver script tag (script.js)
HashOverConstructor.script = (function () {
	// Get various scripts
	var loaderScript = document.getElementById ('hashover-loader');
	var scripts = document.getElementsByTagName ('script');

	// Use either the current script or an identified loader script
	var currentScript = document.currentScript || loaderScript;

	// Otherwise, fallback to the last script encountered
	return currentScript || scripts[scripts.length - 1];
}) ();

// Returns current client 24-hour time (getclienttime.js)
HashOverConstructor.getClientTime = function ()
{
	// Get current date and time
	var datetime = new Date ();

	// Get 24-hour current time
	var hours = datetime.getHours ();
	var minutes = datetime.getMinutes ();
	var time = hours + ':' + minutes;

	return time;
};

// Root path (rootpath.js)
HashOverConstructor.rootPath = (function () {
	// Get the HashOver script source URL
	var scriptSrc = HashOverConstructor.script.getAttribute ('src');

	// Get HashOver root path
	var root = scriptSrc.replace (/\/[^\/]*\/?$/, '');

	// And return HashOver root path
	return root;
}) ();

// Backend path (backendpath.js)
HashOverConstructor.backendPath = (function () {
	return HashOverConstructor.rootPath + '/backend';
}) ();

// Execute a callback when the page HTML is parsed and ready (onready.js)
HashOverConstructor.onReady = function (callback)
{
	// Ready state
	var state = document.readyState;

	// Check if document HTML has been parsed
	if (state === 'interactive' || state === 'complete') {
		// If so, execute callback immediately
		callback ();
	} else {
		// If not, execute callback after the DOM is parsed
		document.addEventListener ('DOMContentLoaded', function () {
			callback ();
		}, false);
	}
};

// Returns instantiated pseudo-namespaced ID (prefix.js)
HashOverConstructor.prototype.prefix = function (id)
{
	// Initial prefix
	var prefix = 'hashover';

	// Append instance number to prefix
	if (this.instanceNumber > 1) {
		prefix += '-' + this.instanceNumber;
	}

	// Return prefixed ID if one is given
	if (id) {
		return prefix + '-' + id;
	}

	// Otherwise, return prefix by itself
	return prefix;
};

// Adds properties to an element (createelement.js)
HashOverConstructor.prototype.addProperties = function (element, properties)
{
	// Do nothing if no element or properties were given
	if (!element || !properties || properties.constructor !== Object) {
		return element;
	}

	// Add each property to element
	for (var property in properties) {
		// Do nothing if property was inherited
		if (properties.hasOwnProperty (property) === false) {
			continue;
		}

		// Property value
		var value = properties[property];

		// If the property is an object add each item to existing property
		if (!!value && value.constructor === Object) {
			this.addProperties (element[property], value);
			continue;
		}

		element[property] = value;
	}

	return element;
};

// Creates an element with attributes (createelement.js)
HashOverConstructor.prototype.createElement = function (name, attr)
{
	// Create element
	var element = document.createElement (name || 'span');

	// Add properties to element
	if (attr && attr.constructor === Object) {
		element = this.addProperties (element, attr);
	}

	return element;
};

// Collection of element class related functions (classes.js)
HashOverConstructor.prototype.classes = new (function () {
	// If browser supports classList define wrapper functions
	if (document.documentElement.classList) {
		// classList.contains method
		this.contains = function (element, name) {
			return element.classList.contains (name);
		};

		// classList.add method
		this.add = function (element, name) {
			element.classList.add (name);
		};

		// classList.remove method
		this.remove = function (element, name) {
			element.classList.remove (name);
		};

		// And do nothing else
		return;
	}

	// Otherwise, define reasonable classList.contains fallback
	this.contains = function (element, name)
	{
		// Check if element exists with classes
		if (element && element.className) {
			// If so, compile regular expression for class
			var rx = new RegExp ('(^|\\s)' + name + '(\\s|$)');

			// Test class attribute for class name
			return rx.test (element.className);
		}

		// Otherwise, return false
		return false;
	};

	// Define reasonable classList.add fallback
	this.add = function (element, name)
	{
		// Append class if element doesn't already contain the class
		if (element && !this.contains (element, name)) {
			element.className += (element.className ? ' ' : '') + name;
		}
	};

	// Define reasonable classList.remove fallback
	this.remove = function (element, name)
	{
		// Check if element exists with classes
		if (element && element.className) {
			// If so, compile regular expression for class
			var rx = new RegExp ('(^|\\s)' + name + '(\\s|$)', 'g');

			// Remove class from class attribute
			element.className = element.className.replace (rx, '$2');
		}
	};
}) ();

// Get main HashOver UI element (getmainelement.js)
HashOverConstructor.prototype.getMainElement = function (id)
{
	// Given element ID or default
	id = id || this.prefix ();

	// Attempt to get main HashOver element
	var element = document.getElementById (id);

	// Check if the HashOver element exists
	if (element === null) {
		// If not, get script tag
		var script = this.constructor.script;

		// Create div for comments to appear in
		element = this.createElement ('div', { id: id });

		// Check if script tag is in the body
		if (document.body.contains (script) === true) {
			// If so, place HashOver element before script tag
			script.parentNode.insertBefore (element, script);
		} else {
			// If not, place HashOver element in the body
			document.body.appendChild (element);
		}
	}

	// Add main HashOver class
	this.classes.add (element, 'hashover');

	// Check if backend is ready
	if (this.constructor.backendReady === true) {
		// If so, add class indictating desktop or mobile styling
		this.classes.add (element, 'hashover-' + this.setup['device-type']);

		// Add class for raster or vector images
		if (this.setup['image-format'] === 'svg') {
			this.classes.add (element, 'hashover-vector');
		} else {
			this.classes.add (element, 'hashover-raster');
		}

		// And add class to indicate user login status
		if (this.setup['user-is-logged-in'] === true) {
			this.classes.add (element, 'hashover-logged-in');
		} else {
			this.classes.add (element, 'hashover-logged-out');
		}
	}

	return element;
};

// Get main HashOver UI element (displayerror.js)
HashOverConstructor.prototype.displayError = function (json, id)
{
	// Get main HashOver element
	var mainElement = this.getMainElement (id);

	// Error message HTML code
	var messageHTML = '<b>HashOver</b>: ' + json.message;

	// Display error in main HashOver element
	mainElement.innerHTML = messageHTML;
};

// Array of JSONP callbacks, starting with default error handler (ajax.js)
HashOverConstructor.jsonp = [
	function (json) { alert (json.message); }
];

// Send HTTP requests using JSONP as a fallback (ajax.js)
HashOverConstructor.prototype.jsonp = function (method, path, data, callback, async)
{
	// Get constructor name
	var source = this.constructor.toString ();
	var constructor = source.match (/function (\w+)/)[1];

	// Push callback into JSONP array
	this.constructor.jsonp.push (callback);

	// Add JSONP callback index and constructor to request data
	data.push ('jsonp=' + (this.constructor.jsonp.length - 1));
	data.push ('jsonp_object=' + constructor || 'HashOver');

	// Create request script
	var script = document.createElement ('script');

	// Set request script path
	script.src = path + '?' + data.join ('&');

	// Set request script to load type
	script.async = async;

	// Append request script to page
	document.body.appendChild (script);
};

// Send HTTP requests using either XMLHttpRequest or JSONP (ajax.js)
HashOverConstructor.prototype.ajax = function (method, path, data, callback, async)
{
	// Reference to this object
	var hashover = this;

	// Arguments to this method
	var args = arguments;

	// Successful request handler
	var onSuccess = function ()
	{
		// Parse JSON response
		var json = JSON.parse (this.responseText);

		// And execute callback
		callback.apply (this, [ json ]);
	};

	// CORS error handler
	var onError = function ()
	{
		// Call JSONP fallback
		hashover.jsonp.apply (hashover, args);

		// And set AJAX to use JSONP
		hashover.ajax = hashover.jsonp;
	};

	// Check for XHR with credentials support
	if ('withCredentials' in new XMLHttpRequest ()) {
		// If supported, create XHR request
		var xhr = new XMLHttpRequest ();

		// Set ready state change handler
		xhr.onreadystatechange = function ()
		{
			// Do nothing if request isn't ready
			if (this.readyState !== 4) {
				return;
			}

			// Handle successful request response
			if (this.status === 200) {
				return onSuccess.apply (this);
			}

			// Handle failed request response, likely CORS error
			if (this.status === 0) {
				return onError ();
			}
		};

		// Open XHR request
		xhr.open (method, path, async);

		// Set request headers
		xhr.setRequestHeader ('Content-type', 'application/x-www-form-urlencoded');

		// Set request to include credentials, mostly cookies
		xhr.withCredentials = true;

		// Send XHR request
		xhr.send (data.join ('&'));

		// And do nothing else
		return;
	}

	// Try to fallback to XDomainRequest if supported
	if (typeof (XDomainRequest) !== 'undefined') {
		// If so, create XDR request
		var xdr = new XDomainRequest ();

		// Open request
		xdr.open (method, path);

		// Set successful request response handler
		xdr.onload = onSuccess;

		// Set failed request response handler
		xdr.onerror = onError;

		// Send XDR request
		setTimeout (xdr.send, 0);

		// And do nothing else
		return;
	}

	// If all else fails fallback to JSONP
	onError ();
};

// Pre-compiled regular expressions (regex.js)
HashOverConstructor.prototype.rx = new (function () {
	this.urls		= '((http|https|ftp):\/\/[a-z0-9-@:;%_\+.~#?&\/=]+)',
	this.links		= new RegExp (this.urls + '( {0,1})', 'ig'),
	this.thread		= /^(c[0-9r]+)r[0-9\-pop]+$/,
	this.imageTags		= new RegExp ('\\[img\\](<a.*?>' + this.urls + '</a>)\\[/img\\]', 'ig'),
	this.EOLTrim		= /^[\r\n]+|[\r\n]+$/g,
	this.paragraphs		= /(?:\r\n|\r|\n){2}/g,
	this.email		= /\S+@\S+/,
	this.integer		= /^\d+$/
}) ();

// Trims leading and trailing newlines from a string (eoltrim.js)
HashOverConstructor.prototype.EOLTrim = function (string)
{
	return string.replace (this.rx.EOLTrim, '');
};

// Returns the permalink of a comment's parent (permalinks.js)
HashOverConstructor.prototype.permalinkParent = function (permalink)
{
	// Split permalink by reply 'r'
	var parent = permalink.split ('r');

	// Number of replies
	var length = parent.length - 1;

	// Limit depth if in stream mode
	if (this.setup['stream-mode'] === true) {
		length = Math.min (this.setup['stream-depth'], length);
	}

	// Check if there is a parent after flatten
	if (length > 0) {
		// If so, remove child from permalink
		parent = parent.slice (0, length);

		// Return parent permalink as string
		return parent.join ('r');
	}

	return null;
};

// Find a comment by its permalink (permalinks.js)
HashOverConstructor.prototype.permalinkComment = function (permalink, comments)
{
	// Run through all comments
	for (var i = 0, il = comments.length; i < il; i++) {
		// Current comment
		var comment = comments[i];

		// Return comment if its permalink matches
		if (comment.permalink === permalink) {
			return comment;
		}

		// Recursively check replies when present
		if (comment.replies !== undefined) {
			// Get attempt to get reply by permalink
			var reply = this.permalinkComment (permalink, comment.replies);

			// Return reply if its permalink matches
			if (reply !== null) {
				return reply;
			}
		}
	}

	// Otherwise return null
	return null;
};

// Add markdown regular expressions (markdown.js)
HashOverConstructor.prototype.rx.md = {
	// Matches a markdown code block
	blockCode: /```([\s\S]+?)```/g,

	// Matches markdown inline code
	inlineCode: /(^|[^a-z0-9`])`((?!`)[\s\S]+?)`([^a-z0-9`]|$)/ig,

	// Matches temporary code block placeholder
	blockMarker: /CODE_BLOCK\[([0-9]+)\]/g,

	// Matches temporary inline code placeholder
	inlineMarker: /CODE_INLINE\[([0-9]+)\]/g,

	// Markdown patterns to search for
	search: [
		// Matches **bold** text
		/\*\*([^ *])([\s\S]+?)([^ *])\*\*/g,

		// Matches *italic* text
		/\*([^ *])([\s\S]+?)([^ *])\*/g,

		// Matches _underlined_ text
		/(^|\W)_((?!_)[\s\S]+?)_(\W|$)/g,

		// Matches forced __underlined__ text
		/__([^ _])([\s\S]+?)([^ _])__/g,

		// Matches ~~strikethrough~~ text
		/~~([^ ~])([\s\S]+?)([^ ~])~~/g
	],

	// HTML replacements for markdown patterns
	replace: [
		'<strong>$1$2$3</strong>',
		'<em>$1$2$3</em>',
		'$1<u>$2</u>$3',
		'<u>$1$2$3</u>',
		'<s>$1$2$3</s>'
	]
};

// Parses markdown code (markdown.js)
HashOverConstructor.prototype.parseMarkdown = function (string)
{
	// Reference to this object
	var hashover = this;

	// Initial marker arrays
	var block = { marks: [], count: 0 };
	var inline = { marks: [], count: 0 };

	// Replaces inline code with markers
	var inlineReplacer = function (m, first, code, third)
	{
		// Increase inline code count
		var markCount = inline.count++;

		// Inline code marker
		var marker = 'CODE_INLINE[' + markCount + ']';

		// Add inline code to marker array
		inline.marks[markCount] = hashover.EOLTrim (code);

		// And return first match, inline marker, and third match
		return first + marker + third;
	};

	// Replace code blocks with markers
	string = string.replace (this.rx.md.blockCode, function (m, code) {
		// Increase block code count
		var markCount = block.count++;

		// Add block code to marker array
		block.marks[markCount] = hashover.EOLTrim (code);

		// And return block marker
		return 'CODE_BLOCK[' + markCount + ']';
	});

	// Break string into paragraphs
	var ps = string.split (this.rx.paragraphs);

	// Run through each paragraph replacing markdown patterns
	for (var i = 0, il = ps.length; i < il; i++) {
		// Replace code tags with marker text
		ps[i] = ps[i].replace (this.rx.md.inlineCode, inlineReplacer);

		// Perform each markdown regular expression on the current paragraph
		for (var r = 0, rl = this.rx.md.search.length; r < rl; r++) {
			ps[i] = ps[i].replace (this.rx.md.search[r], this.rx.md.replace[r]);
		}

		// Return the original markdown code with HTML replacement
		ps[i] = ps[i].replace (this.rx.md.inlineMarker, function (marker, number) {
			return '<code class="hashover-inline">' + inline.marks[number] + '</code>';
		});
	}

	// Join paragraphs
	string = ps.join (this.setup['server-eol'] + this.setup['server-eol']);

	// Replace code block markers with original markdown code
	string = string.replace (this.rx.md.blockMarker, function (marker, number) {
		return '<code>' + block.marks[number] + '</code>';
	});

	return string;
};

// Collection of convenient string related functions (strings.js)
HashOverConstructor.prototype.strings = {
	// sprintf specifiers regular expression
	specifiers: /%([cdfs])/g,

	// Curly-brace variable regular expression
	curlyBraces: /(\{.+?\})/g,

	// Curly-brace variable name regular expression
	curlyNames: /\{(.+?)\}/,

	// Simplistic JavaScript port of sprintf function in C
	sprintf: function (string, args)
	{
		var string = string || '';
		var args = args || [];
		var count = 0;

		// Replace specifiers with array items
		return string.replace (this.specifiers, function (match, type)
		{
			// Return original specifier if there isn't an item for it
			if (args[count] === undefined) {
				return match;
			}

			// Switch through each specific type
			switch (type) {
				// Single characters
				case 'c': {
					// Use only first character
					return args[count++][0];
				}

				// Integer numbers
				case 'd': {
					// Parse item as integer
					return parseInt (args[count++]);
				}

				// Floating point numbers
				case 'f': {
					// Parse item as float
					return parseFloat (args[count++]);
				}

				// Strings
				case 's': {
					// Use string as-is
					return args[count++];
				}
			}
		});
	},

	// Converts a string containing {curly} variables into an array
	templatifier: function (text)
	{
		// Split string by curly variables
		var template = text.split (this.curlyBraces);

		// Initial variable indexes
		var indexes = {};

		// Run through template
		for (var i = 0, il = template.length; i < il; i++) {
			// Get curly variable names
			var curly = template[i].match (this.curlyNames);

			// Check if any curly variables exist
			if (curly !== null && curly[1] !== undefined) {
				// If so, store the name
				var name = curly[1];

				// Check if variable was previously encountered
				if (indexes[name] !== undefined) {
					// If so, add index to existing indexes
					indexes[name].push (i);
				} else {
					// If not, create indexes
					indexes[name] = [ i ];
				}

				// And remove curly variable from template
				template[i] = '';
			}
		}

		// Return template and indexes
		return {
			template: template,
			indexes: indexes
		}
	},

	// Templatify UI HTML from backend
	templatify: function (ui)
	{
		// Initial template
		var template = {};

		// Templatify each UI HTML string
		for (var name in ui) {
			if (ui.hasOwnProperty (name) === true) {
				template[name] = this.templatifier (ui[name]);
			}
		}

		return template;
	},

	// Parses an HTML template
	parseTemplate: function (template, data)
	{
		// Clone template
		var textClone = template.template.slice ();

		// Run through template data
		for (var name in data) {
			// Store indexes
			var indexes = template.indexes[name];

			// Do nothing if no indexes exist for data
			if (indexes === undefined) {
				continue;
			}

			// Otherwise, add data at each index of template
			for (var i = 0, il = indexes.length; i < il; i++) {
				textClone[(indexes[i])] = data[name];
			}
		}

		// Merge template clone to string
		var text = textClone.join ('');

		return text;
	}
};

// Calls a method that may or may not exist (optionalmethod.js)
HashOverConstructor.prototype.optionalMethod = function (name, args, object)
{
	var method = object ? this[object][name] : this[name];
	var context = object ? this[object] : this;

	// Check if the method exists
	if (method && typeof (method) === 'function') {
		return method.apply (context, args);
	}
};

// Add comment parsing regular expressions (parsecomment.js)
HashOverConstructor.prototype.rx.html = {
	// URL replacement for automatic hyperlinks
	linksReplace: '<a href="$1" rel="noopener noreferrer" target="_blank">$1</a>',

	// Matches various line ending styles
	lines: /(?:\r\n|\r|\n)/g,

	// For <code> tags
	code: {
		// Matches <code> opening
		open: /<code>/i,

		// Replacement for code tag processing
		replace: /(<code>)([\s\S]*?)(<\/code>)/ig,

		// Matches code tag markers
		marker: /CODE_TAG\[([0-9]+)\]/g
	},

	// For <pre> tags
	pre: {
		// Matches <pre> opening
		open: /<pre>/i,

		// Replacement for pre tag processing
		replace: /(<pre>)([\s\S]*?)(<\/pre>)/ig,

		// Matches pre tag markers
		marker: /PRE_TAG\[([0-9]+)\]/g
	},

	// Tags that will have their inner HTML trimmed
	trimTags: {
		// Matches blockquote/ul/ol tags openings
		open: /<(blockquote|ul|ol)>/,

		// Replacement for blockquote/ul/ol trimming
		replace: /(<(blockquote|ul|ol)>)([\s\S]*?)(<\/\2>)/ig
	}
};

// Add comment content to HTML template (parsecomment.js)
HashOverConstructor.prototype.parseComment = function (comment, parent, collapse, popular)
{
	// Parameter defaults
	parent = parent || null;

	// Reference to this object
	var hashover = this;

	var commentKey = comment.permalink;
	var permalink = this.prefix (commentKey);
	var nameClass = 'hashover-name-plain';
	var commentDate = comment.date;
	var codeTagCount = 0;
	var codeTags = [];
	var preTagCount = 0;
	var preTags = [];
	var classes = '';
	var replies = '';

	// Get instantiated prefix
	var prefix = this.prefix ();

	// Initial template
	var template = {
		hashover: prefix,
		permalink: commentKey
	};

	// Text for avatar image alt attribute
	var permatext = commentKey.slice(1).split('r').pop();

	// Check if this comment is a popular comment
	if (popular === true) {
		// Attempt to get parent comment permalink
		parent = this.permalinkParent (commentKey);

		// Get parent comment by its permalink if it exists
		if (parent !== null) {
			parent = this.permalinkComment (parent, this.instance.comments.primary);
		}

		// And remove "-pop" from text for avatar
		permatext = permatext.replace ('-pop', '');
	} else {
		// Append class to indicate comment is a reply when appropriate
		if (parent !== null) {
			classes += ' hashover-reply';
		}

		// Check if we have comments to collapse
		if (collapse === true && this.instance['total-count'] > 0) {
			// If so, check if we've reached the collapse limit
			if (this.instance.collapseLimit >= this.setup['collapse-limit']) {
				// If so, append class to indicate collapsed comment
				classes += ' hashover-hidden';
			} else {
				// If not, increase collapse limit
				this.instance.collapseLimit++;
			}
		}
	}

	// Add avatar image to template
	template.avatar = this.strings.parseTemplate (
		this.ui['user-avatar'], {
			src: comment.avatar,
			href: permalink,
			text: permatext
		}
	);

	// Check if comment is not a notice
	if (comment.notice === undefined) {
		// If so, define commenter name
		var name = comment.name || this.setup['default-name'];

		// Initial website
		var website = comment.website;

		// Name is Twitter handle indicator
		var isTwitter = (name.charAt (0) === '@');

		// Check if user's name is a Twitter handle
		if (isTwitter === true) {
			// If so, remove the leading "@" character
			name = name.slice (1);

			// Set Twitter name class
			nameClass = 'hashover-name-twitter';

			// Get the name length
			var nameLength = name.length;

			// Check if Twitter handle is valid length
			if (nameLength > 1 && nameLength <= 30) {
				// Set website to Twitter profile if a specific website wasn't given
				if (website === undefined) {
					website = 'http://twitter.com/' + name;
				}
			}
		}

		// Check whether user gave a website
		if (website !== undefined) {
			// If so, set normal website class where appropriate
			if (isTwitter === false) {
				nameClass = 'hashover-name-website';
			}

			// And set name as a hyperlink
			var nameElement = this.strings.parseTemplate (
				this.ui['name-link'], {
					hashover: prefix,
					href: website,
					permalink: commentKey,
					name: name
				}
			);
		} else {
			// If not, set name as plain text
			var nameElement = this.strings.parseTemplate (
				this.ui['name-span'], {
					hashover: prefix,
					permalink: commentKey,
					name: name
				}
			);
		}

		// Construct thread link
		if ((comment.url && comment.title) !== undefined) {
			template['thread-link'] = this.strings.parseTemplate (
				this.ui['thread-link'], {
					url: comment.url,
					title: comment.title
				}
			);
		}

		// Check if comment has a parent
		if (parent !== null && this.ui['parent-link'] !== undefined) {
			// If so, create the parent thread permalink
			var parentThread = 'hashover-' + parent.permalink;

			// Get the parent's name
			var parentName = parent.name || this.setup['default-name'];

			// Add thread parent hyperlink to template
			template['parent-link'] = this.strings.parseTemplate (
				this.ui['parent-link'], {
					hashover: prefix,
					href: comment.url || this.instance['file-path'],
					parent: parentThread,
					permalink: commentKey,
					name: parentName
				}
			);
		}

		// Check if the logged in user owns the comment
		if (comment['user-owned'] !== undefined) {
			// If so, append class to indicate comment is from logged in user
			classes += ' hashover-user-owned';

			// Define "Reply" link with original poster title
			var replyTitle = this.locale['commenter-tip'];
			var replyClass = 'hashover-no-email';
		} else {
			// Check if commenter is subscribed
			if (comment.subscribed === true) {
				// If so, set subscribed title
				var replyTitle = name + ' ' + this.locale['subscribed-tip'];
				var replyClass = 'hashover-has-email';
			} else{
				// If not, set unsubscribed title
				var replyTitle = name + ' ' + this.locale['unsubscribed-tip'];
				var replyClass = 'hashover-no-email';
			}
		}

		// Check if the comment is editable for the user
		if ((comment['editable'] && this.ui['edit-link']) !== undefined) {
			// If so, add "Edit" hyperlink to template
			template['edit-link'] = this.strings.parseTemplate (
				this.ui['edit-link'], {
					hashover: prefix,
					href: comment.url || this.instance['file-path'],
					permalink: commentKey
				}
			);
		}

		// Add like link and count to template if likes are enabled
		if (this.setup['allows-likes'] !== false) {
			this.optionalMethod ('addRatings', [
				comment, template, 'like', commentKey
			]);
		}

		// Add dislike link and count to template if dislikes are enabled
		if (this.setup['allows-dislikes'] !== false) {
			this.optionalMethod ('addRatings', [
				comment, template, 'dislike', commentKey
			]);
		}

		// Add name HTML to template
		template.name = this.strings.parseTemplate (
			this.ui['name-wrapper'], {
				class: nameClass,
				link: nameElement
			}
		);

		// Append status text to date
		if (comment['status-text'] !== undefined) {
			commentDate += ' (' + comment['status-text'] + ')';
		}

		// Add date from comment as permalink hyperlink to template
		template.date = this.strings.parseTemplate (
			this.ui['date-link'], {
				hashover: prefix,
				href: comment.url || this.instance['file-path'],
				permalink: 'hashover-' + commentKey,
				title: comment['date-time'],
				date: commentDate
			}
		);

		// Add "Reply" hyperlink to template
		template['reply-link'] = this.strings.parseTemplate (
			this.ui['reply-link'], {
				hashover: prefix,
				href: comment.url || this.instance['file-path'],
				permalink: commentKey,
				class: replyClass,
				title: replyTitle
			}
		);

		// Add reply count to template
		if (comment.replies !== undefined) {
			template['reply-count'] = comment.replies.length;

			if (template['reply-count'] > 0) {
				if (template['reply-count'] !== 1) {
					template['reply-count'] += ' ' + this.locale['replies'];
				} else {
					template['reply-count'] += ' ' + this.locale['reply'];
				}
			}
		}

		// Add HTML anchor tag to URLs
		var body = comment.body.replace (this.rx.links, this.rx.html.linksReplace);

		// Replace [img] tags with placeholders if embedded images are enabled
		if (hashover.setup['allows-images'] !== false) {
			body = body.replace (this.rx.imageTags, function (m, link, url) {
				return hashover.optionalMethod ('embedImage', arguments);
			});
		}

		// Parse markdown in comment if enabled
		if (this.parseMarkdown !== undefined) {
			body = this.parseMarkdown (body);
		}

		// Check if there are code tags in the comment
		if (this.rx.html.code.open.test (body) === true) {
			// If so, define regular expression callback
			var codeReplacer = function (fullTag, open, html, close) {
				// Create code marker
				var codeMarker = open + 'CODE_TAG[' + codeTagCount + ']' + close;

				// Store original HTML for later re-injection
				codeTags[codeTagCount] = hashover.EOLTrim (html);

				// Increase code tag count
				codeTagCount++;

				// Return code tag marker
				return codeMarker;
			};

			// And replace code tags with marker text
			body = body.replace (this.rx.html.code.replace, codeReplacer);
		}

		// Check if there are pre tags in the comment
		if (this.rx.html.pre.open.test (body) === true) {
			// If so, define regular expression callback
			var preReplacer = function (fullTag, open, html, close) {
				// Create pre marker
				var preMarker = open + 'PRE_TAG[' + preTagCount + ']' + close;

				// Store original HTML for later re-injection
				preTags[preTagCount] = hashover.EOLTrim (html);

				// Increase pre tag count
				preTagCount++;

				// Return pre tag marker
				return preMarker;
			};

			// And replace pre tags with marker text
			body = body.replace (this.rx.html.pre.replace, preReplacer);
		}

		// Check if comment has whitespace to be trimmed
		if (this.rx.html.trimTags.open.test (body) === true) {
			// If so, define a regular expression callback
			var tagTrimmer = function (fullTag, open, name, html, close) {
				return open + hashover.EOLTrim (html) + close;
			};

			// And trim whitespace from comment
			body = body.replace (this.rx.html.trimTags.replace, tagTrimmer);
		}

		// Break comment into paragraphs
		var paragraphs = body.split (this.rx.paragraphs);

		// Initial paragraph'd comment
		var pdComment = '';

		// Run through paragraphs
		for (var i = 0, il = paragraphs.length; i < il; i++) {
			// Replace single line breaks with break tags
			var lines = paragraphs[i].replace (this.rx.html.lines, '<br>');

			// Wrap comment in paragraph tags
			pdComment += '<p>' + lines + '</p>' + this.setup['server-eol'];
		}

		// Replace code tag markers with original code tag HTML
		if (codeTagCount > 0) {
			pdComment = pdComment.replace (this.rx.html.code.marker, function (m, i) {
				return codeTags[i];
			});
		}

		// Replace pre tag markers with original pre tag HTML
		if (preTagCount > 0) {
			pdComment = pdComment.replace (this.rx.html.pre.marker, function (m, i) {
				return preTags[i];
			});
		}

		// Add comment data to template
		template.comment = pdComment;
	} else {
		// Append notice class
		classes += ' hashover-notice ' + comment['notice-class'];

		// Add notice to template
		template.comment = comment.notice;

		// Add name HTML to template
		template.name = this.strings.parseTemplate (
			this.ui['name-wrapper'], {
				class: nameClass,
				link: comment.title
			}
		);
	}

	// Comment HTML template
	var html = this.strings.parseTemplate (this.ui['theme'], template);

	// Check if comment has replies
	if (comment.replies !== undefined) {
		// If so, append class to indicate comment has replies
		classes += ' hashover-has-replies';

		// Recursively parse replies
		for (var i = 0, il = comment.replies.length; i < il; i++) {
			replies += this.parseComment (comment.replies[i], comment, collapse);
		}
	}

	// Wrap comment HTML
	var wrapper = this.strings.parseTemplate (
		this.ui['comment-wrapper'], {
			hashover: prefix,
			permalink: commentKey,
			class: classes,
			html: html + replies
		}
	);

	return wrapper;
};

// Callback to close the embedded image (openembeddedimage.js)
HashOverConstructor.prototype.closeEmbeddedImage = function (image)
{
	// Set image load event handler
	image.onload = function ()
	{
		// Reset title
		this.title = hashover.locale['external-image-tip'];

		// Remove loading class from wrapper
		hashover.classes.remove (this.parentNode, 'hashover-loading');

		// Remove open class from wrapper
		hashover.classes.remove (this.parentNode, 'hashover-embedded-image-open');

		// Remove load event handler
		this.onload = null;
	};

	// Reset source
	image.src = image.dataset.placeholder;
};

// Onclick callback function for embedded images (openembeddedimage.js)
HashOverConstructor.prototype.openEmbeddedImage = function (image)
{
	// Reference to this object
	var hashover = this;

	// Check if embedded image is open
	if (image.src === image.dataset.url) {
		// If so, close it
		this.closeEmbeddedImage (image);

		// And return void
		return;
	}

	// Set title
	image.title = this.locale['loading'];

	// Add loading class to wrapper
	this.classes.add (image.parentNode, 'hashover-loading');

	// Set image load event handler
	image.onload = function ()
	{
		// Set title to "Click to close" locale
		this.title = hashover.locale['click-to-close'];

		// Remove loading class from wrapper
		hashover.classes.remove (this.parentNode, 'hashover-loading');

		// Add open class to wrapper
		hashover.classes.add (this.parentNode, 'hashover-embedded-image-open');

		// Remove load event handler
		this.onload = null;
	};

	// Close embedded image if any error occurs
	image.onerror = function () {
		hashover.closeEmbeddedImage (this);
	};

	// Set placeholder image to embedded source
	image.src = image.dataset.url;
};

// Convert URL to embed image HTML (embedimage.js)
HashOverConstructor.prototype.embedImage = function (m, link, url)
{
	// Reference to this object
	var hashover = this;

	// Remove hash from image URL
	var urlExtension = url.split ('#')[0];

	// Remove queries from image URL
	urlExtension = urlExtension.split ('?')[0];

	// Get file extendion
	urlExtension = urlExtension.split ('.');
	urlExtension = urlExtension.pop ();

	// Check if the image extension is an allowed type
	if (this.setup['image-extensions'].indexOf (urlExtension) > -1) {
		// If so, create a wrapper element for the embedded image
		var embeddedImage = this.createElement ('span', {
			className: 'hashover-embedded-image-wrapper'
		});

		// Append an image tag to the embedded image wrapper
		embeddedImage.appendChild (this.createElement ('img', {
			className: 'hashover-embedded-image',
			src: this.setup['image-placeholder'],
			title: this.locale['external-image-tip'],
			alt: 'External Image',

			dataset: {
				placeholder: hashover.setup['image-placeholder'],
				url: url
			}
		}));

		// And return the embedded image HTML
		return embeddedImage.outerHTML;
	}

	// Otherwise, return original link
	return link;
};

// Appends HashOver theme CSS to page head (appendcss.js)
HashOverConstructor.prototype.appendCSS = function (id)
{
	// Get the page head
	var head = document.head || document.getElementsByTagName ('head')[0];

	// Get head link tags
	var links = head.getElementsByTagName ('link');

	// Theme CSS regular expression
	var themeRegex = new RegExp (this.setup['theme-css']);

	// Get the main HashOver element
	var mainElement = this.getMainElement (id);

	// Do nothing if the theme StyleSheet is already in the <head>
	for (var i = 0, il = links.length; i < il; i++) {
		if (themeRegex.test (links[i].href) === true) {
			// Hide HashOver if the theme isn't loaded
			if (links[i].loaded === false) {
				mainElement.style.display = 'none';
			}

			// And do nothing else
			return;
		}
	}

	// Otherwise, create <link> element for theme StyleSheet
	var css = this.createElement ('link', {
		rel: 'stylesheet',
		href: this.setup['theme-css'],
		type: 'text/css',
		loaded: false
	});

	// Check if the browser supports CSS load events
	if (css.onload !== undefined) {
		// CSS load and error event handler
		var onLoadError = function ()
		{
			// Get all HashOver class elements
			var hashovers = document.getElementsByClassName ('hashover');

			// Show all HashOver class elements
			for (var i = 0, il = hashovers.length; i < il; i++) {
				hashovers[i].style.display = '';
			}

			// Set CSS as loaded
			css.loaded = true;
		};

		// Hide HashOver
		mainElement.style.display = 'none';

		// And and CSS load and error event listeners
		css.addEventListener ('load', onLoadError, false);
		css.addEventListener ('error', onLoadError, false);
	}

	// Append theme StyleSheet <link> element to page <head>
	head.appendChild (css);
};

// Add Like/Dislike link and count to template (addratings.js)
HashOverLatest.prototype.addRatings = function (comment, template, action, commentKey)
{
	// Check if the comment has been likes/dislikes
	if (comment[action + 's'] !== undefined) {
		// Add likes/dislikes to HTML template
		template[action + 's'] = comment[action + 's'];

		// Get "X Like/Dislike(s)" locale
		var plural = (comment[action + 's'] === 1 ? 0 : 1);
		var count = comment[action + 's'] + ' ' + this.locale[action][plural];
	}

	// Add like count to HTML template
	template[action + '-count'] = this.strings.parseTemplate (
		this.ui[action + '-count'], {
			permalink: commentKey,
			text: count || ''
		}
	);
};

// Add various events to various elements in each comment (addcontrols.js)
HashOverLatest.prototype.addControls = function (json, popular)
{
	// Reference to this object
	var hashover = this;

	// Set onclick functions for external images
	if (this.setup['allows-images'] !== false) {
		// Main element
		var main = this.instance['main-element'];

		// Get embedded image elements
		var embeds = main.getElementsByClassName ('hashover-embedded-image');

		// Run through each embedded image element
		for (var i = 0, il = embeds.length; i < il; i++) {
			embeds[i].onclick = function () {
				hashover.openEmbeddedImage (this);
			};
		}
	}
};

// HashOver latest comments UI initialization process (init.js)
HashOverLatest.prototype.init = function ()
{
	// Shorthand
	var comments = this.instance.comments.primary;

	// Initial comments HTML
	var html = '';

	// Get the main HashOver element
	var mainElement = this.getMainElement ('hashover-latest');

	// Append theme CSS if enabled
	this.optionalMethod ('appendCSS', [ 'hashover-latest' ]);

	// Add main HashOver element to this HashOver instance
	this.instance['main-element'] = mainElement;

	// Parse every comment
	for (var i = 0, il = comments.length; i < il; i++) {
		html += this.parseComment (comments[i]);
	}

	// Check if we can insert HTML adjacently
	if ('insertAdjacentHTML' in mainElement) {
		// If so, insert comments adjacently
		mainElement.insertAdjacentHTML ('beforeend', html);
	} else {
		// If not, add comments as element's inner HTML
		mainElement.innerHTML = html;
	}

	// Add control events
	for (var i = 0, il = comments.length; i < il; i++) {
		this.addControls (comments[i]);
	}
};

// Instantiate after the DOM is parsed
HashOverLatest.onReady (function () {
	window.hashoverLatest = new HashOverLatest ();
});

/*

	HashOver Statistics

	Execution Time     : 64.63408 ms
	Script Memory Peak : 0.55 MiB
	System Memory Peak : 2 MiB

*/