/*\
title: $:/core/modules/widgets/list.js
type: application/javascript
module-type: widget

List and list item widgets

\*/
(function(){

/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";

var Widget = require("$:/core/modules/widgets/widget.js").widget;

/*
The list widget creates list element sub-widgets that reach back into the list widget for their configuration
*/

var ListWidget = function(parseTreeNode,options) {
	// Initialise the storyviews if they've not been done already
	if(!this.storyViews) {
		ListWidget.prototype.storyViews = {};
		$tw.modules.applyMethods("storyview",this.storyViews);
	}
	// Main initialisation inherited from widget.js
	this.initialise(parseTreeNode,options);
};

/*
Inherit from the base widget class
*/
ListWidget.prototype = new Widget();

/*
Render this widget into the DOM
*/
ListWidget.prototype.render = function(parent,nextSibling) {
	this.parentDomNode = parent;
	this.computeAttributes();
	this.execute();
	this.renderChildren(parent,nextSibling);
	// Construct the storyview
	var StoryView = this.storyViews[this.storyViewName];
	if(StoryView && !this.document.isTiddlyWikiFakeDom) {
		this.storyview = new StoryView(this)
	} else {
		this.storyview = null;
	}
};

/*
Compute the internal state of the widget
*/
ListWidget.prototype.execute = function() {
	// Get our attributes
	this.template = this.getAttribute("template");
	this.editTemplate = this.getAttribute("editTemplate");
	this.variableName = this.getAttribute("variable","currentTiddler");
	this.storyViewName = this.getAttribute("storyview");
	this.historyTitle = this.getAttribute("history");
	// Compose the list elements
	this.list = this.getTiddlerList();
	var members = [],
		self = this;
	// Check for an empty list
	if(this.list.length === 0) {
		members = this.getEmptyMessage();
	} else {
		$tw.utils.each(this.list,function(title,index) {
			members.push(self.makeItemTemplate(title));
		});
	}
	// Construct the child widgets
	this.makeChildWidgets(members);
	// Clear the last history
	this.history = [];
};

ListWidget.prototype.getTiddlerList = function() {
	var defaultFilter = "[!is[system]sort[title]]";
	return this.wiki.filterTiddlers(this.getAttribute("filter",defaultFilter),this);
};

ListWidget.prototype.getEmptyMessage = function() {
	var emptyMessage = this.getAttribute("emptyMessage",""),
		parser = this.wiki.parseText("text/vnd.tiddlywiki",emptyMessage,{parseAsInline: true});
	if(parser) {
		return parser.tree;
	} else {
		return [];
	}
};

/*
Compose the template for a list item
*/
ListWidget.prototype.makeItemTemplate = function(title) {
	// Check if the tiddler is a draft
	var tiddler = this.wiki.getTiddler(title),
		isDraft = tiddler && tiddler.hasField("draft.of"),
		template = this.template,
		templateTree;
	if(isDraft && this.editTemplate) {
		template = this.editTemplate;
	}
	// Compose the transclusion of the template
	if(template) {
		templateTree = [{type: "transclude", attributes: {tiddler: {type: "string", value: template}}}];
	} else {
		if(this.parseTreeNode.children && this.parseTreeNode.children.length > 0) {
			templateTree = this.parseTreeNode.children;
		} else {
			// Default template is a link to the title
			templateTree = [{type: "element", tag: this.parseTreeNode.isBlock ? "div" : "span", children: [{type: "link", attributes: {to: {type: "string", value: title}}, children: [
					{type: "text", text: title}
			]}]}];
		}
	}
	// Return the list item
	return {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree};
};

/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
ListWidget.prototype.refresh = function(changedTiddlers) {
	var changedAttributes = this.computeAttributes();
	// Completely refresh if any of our attributes have changed
	if(changedAttributes.filter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) {
		this.refreshSelf();
		return true;
	} else {
		// Handle any changes to the list
		var hasChanged = this.handleListChanges(changedTiddlers);
		// Handle any changes to the history stack
		if(this.historyTitle && changedTiddlers[this.historyTitle]) {
			this.handleHistoryChanges();
		}
		return hasChanged;
	}
};

/*
Handle any changes to the history list
*/
ListWidget.prototype.handleHistoryChanges = function() {
	// Get the history data
	var newHistory = this.wiki.getTiddlerData(this.historyTitle,[]);
	// Ignore any entries of the history that match the previous history
	var entry = 0;
	while(entry < newHistory.length && entry < this.history.length && newHistory[entry].title === this.history[entry].title) {
		entry++;
	}
	// Navigate forwards to each of the new tiddlers
	while(entry < newHistory.length) {
		if(this.storyview && this.storyview.navigateTo) {
			this.storyview.navigateTo(newHistory[entry]);
		}
		entry++;
	}
	// Update the history
	this.history = newHistory;
};

/*
Process any changes to the list
*/
ListWidget.prototype.handleListChanges = function(changedTiddlers) {
	// Get the new list
	var prevList = this.list;
	this.list = this.getTiddlerList();
	// Check for an empty list
	if(this.list.length === 0) {
		// Check if it was empty before
		if(prevList.length === 0) {
			// If so, just refresh the empty message
			return this.refreshChildren(changedTiddlers);
		} else {
			// Replace the previous content with the empty message
			for(t=this.children.length-1; t>=0; t--) {
				this.removeListItem(t);
			}
			var nextSibling = this.findNextSiblingDomNode();
			this.makeChildWidgets(this.getEmptyMessage());
			this.renderChildren(this.parentDomNode,nextSibling);
			return true;
		}
	} else {
		// If the list was empty then we need to remove the empty message
		if(prevList.length === 0) {
			this.removeChildDomNodes();
			this.children = [];
		}
		// Cycle through the list, inserting and removing list items as needed
		var hasRefreshed = false;
		for(var t=0; t<this.list.length; t++) {
			var index = this.findListItem(t,this.list[t]);
			if(index === undefined) {
				// The list item must be inserted
				this.insertListItem(t,this.list[t]);
				hasRefreshed = true;
			} else {
				// There are intervening list items that must be removed
				for(var n=index-1; n>=t; n--) {
					this.removeListItem(n);
					hasRefreshed = true;
				}
				// Refresh the item we're reusing
				var refreshed = this.children[t].refresh(changedTiddlers);
				hasRefreshed = hasRefreshed || refreshed;
			}
		}
		// Remove any left over items
		for(t=this.children.length-1; t>=this.list.length; t--) {
			this.removeListItem(t);
			hasRefreshed = true;
		}
		return hasRefreshed;
	}
};

/*
Find the list item with a given title, starting from a specified position
*/
ListWidget.prototype.findListItem = function(startIndex,title) {
	while(startIndex < this.children.length) {
		if(this.children[startIndex].parseTreeNode.itemTitle === title) {
			return startIndex;
		}
		startIndex++;
	}
	return undefined;
};

/*
Insert a new list item at the specified index
*/
ListWidget.prototype.insertListItem = function(index,title) {
	// Create, insert and render the new child widgets
	var widget = this.makeChildWidget(this.makeItemTemplate(title));
	widget.parentDomNode = this.parentDomNode; // Hack to enable findNextSiblingDomNode() to work
	this.children.splice(index,0,widget);
	var nextSibling = widget.findNextSiblingDomNode();
	widget.render(this.parentDomNode,nextSibling);
	// Animate the insertion if required
	if(this.storyview && this.storyview.insert) {
		this.storyview.insert(widget);
	}
	return true;
};

/*
Remove the specified list item
*/
ListWidget.prototype.removeListItem = function(index) {
	var widget = this.children[index];
	// Animate the removal if required
	if(this.storyview && this.storyview.remove) {
		this.storyview.remove(widget);
	} else {
		widget.removeChildDomNodes();
	}
	// Remove the child widget
	this.children.splice(index,1);
};

exports.list = ListWidget;

var ListItemWidget = function(parseTreeNode,options) {
	this.initialise(parseTreeNode,options);
};

/*
Inherit from the base widget class
*/
ListItemWidget.prototype = new Widget();

/*
Render this widget into the DOM
*/
ListItemWidget.prototype.render = function(parent,nextSibling) {
	this.parentDomNode = parent;
	this.computeAttributes();
	this.execute();
	this.renderChildren(parent,nextSibling);
};

/*
Compute the internal state of the widget
*/
ListItemWidget.prototype.execute = function() {
	// Set the current list item title
	this.setVariable(this.parseTreeNode.variableName,this.parseTreeNode.itemTitle);
	// Construct the child widgets
	this.makeChildWidgets();
};

/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
ListItemWidget.prototype.refresh = function(changedTiddlers) {
	return this.refreshChildren(changedTiddlers);
};

exports.listitem = ListItemWidget;

})();
