/**********************************************************************
* 
*
*
**********************************************************************/


/*********************************************************************/
// Hepers...
//
var quoteRegExp =
RegExp.quoteRegExp =
	RegExp.quoteRegExp 
		|| function(str){
			return str
				.replace(/([\.\\\/\(\)\[\]\$\*\+\-\{\}\@\^\&\?\<\>])/g, '\\$1') }

var path2lst = function(path){ 
	return (path instanceof Array ? 
			path 
			: path.split(/[\\\/]+/g))
		// handle '..' (lookahead) and trim path elements...
		// NOTE: this will not touch the leading '.' or '..'
		.map(function(p, i, l){
			return (i > 0 
					&& (p.trim() == '..' || p.trim() == '.')
					|| (l[i+1] || '').trim() == '..') ? 
				null 
				: p.trim() })
		// cleanup and clear '.'...
		.filter(function(p){ 
			return p != null 
				&& p != '' })}

var normalizePath = function(path){
	return path2lst(path).join('/') }


var clearWikiWords = function(elem){
	// clear existing...
	elem.find('.wikiword').each(function(){
		$(this).attr('bracketed') == 'yes' ? 
			$(this).replaceWith(['['].concat(this.childNodes, [']']))
			: $(this).replaceWith(this.childNodes) })
	return elem } 

var setWikiWords = function(text, show_brackets, skip){
	skip = skip || []
	skip = skip instanceof Array ? 
		skip 
		: [skip]
	return text 
		// set new...
		.replace(
			Wiki.__wiki_link__,
			function(l){
				// check if wikiword is escaped...
				if(l[0] == '\\'){
					return l.slice(1) }

				var path = l[0] == '[' ? l.slice(1, -1) : l
				var i = [].slice.call(arguments).slice(-2)[0]

				// XXX HACK check if we are inside a tag...
				var rest = text.slice(i+1)
				if(rest.indexOf('>') < rest.indexOf('<')){
					return l }

				return skip.indexOf(l) < 0 ? 
					('<a '
						+'class="wikiword" '
						+'href="#'+ path +'" '
						+'bracketed="'+ (show_brackets && l[0] == '[' ? 'yes' : 'no') +'" '
						//+'onclick="event.preventDefault(); go($(this).attr(\'href\').slice(1))" '
						+'>'
							+ (!!show_brackets ? path : l) 
						+'</a>')
					: l })}



/*********************************************************************/

function Macro(doc, args, func){
	func.doc = doc
	func.macro_args = args
	return func }



// XXX should inline macros support named args???
var macro = {

	__include_marker__: '{{{INCLUDE-MARKER}}}',

	// Abstract macro syntax:
	// 	Inline macro:
	// 		@macro(arg ..)
	//
	// 	HTML-like:
	// 		<macro arg=value ../>
	//
	// 	HTML-like with body:
	// 		<macro arg=value ..>
	// 			..text..
	// 		</macro>
	//
	// XXX should inline macros support named args???
	__macro__pattern__: 
		[[
			// @macro(arg ..)
			'\\\\?@([a-zA-Z-_]+)\\(([^)]*)\\)'
		].join('|'), 'mg'],

	// default filters...
	//
	// NOTE: these are added AFTER the user defined filters...
	__filters__: [
		'wikiword',
		'noscript',
	],
	__post_filters__: [
		//'noscript',
		'title',
		'editor',
	],

	// Macros...
	//
	// XXX add support for sort and reverse attrs in all relavant macros
	// 		(see: macro for details)
	macro: {
		"pwiki-comment": Macro('hide in pWiki',
			[],
			function(context, elem, state){ 
				return '' }),
		now: Macro('Create a now id',
			[],
			function(context, elem, state){ 
				return ''+Date.now() }),
		// select filter to post-process text...
		filter: Macro('Filter to post-process text',
			['name'],
			function(context, elem, state){
				var filter = $(elem).attr('name')
				filter[0] == '-' ?
					// disabled -- keep at head of list...
					state.filters.unshift(filter)
					// normal -- tail...
					: state.filters.push(filter)
				return '' }),

		// include page/slot...
		//
		// NOTE: this will render the page in the caller's context.
		// NOTE: included pages are rendered completely independently 
		// 		from the including page.
		include: Macro('Include page',
			['src', 'isolated', 'text'],
			function(context, elem, state){
				var path = $(elem).attr('src')
				// get and prepare the included page...
				state.include
					.push([elem, context.get(path)])
				// return the marker...
				return this.__include_marker__ }),

		// NOTE: this is similar to include, the difference is that this
		// 		includes the page source to the current context while 
		// 		include works in an isolated context
		source: Macro('Include page source (without parsing)',
			['src'], 
			function(context, elem, state){
				var path = $(elem).attr('src')
				return context.get(path)
					.map(function(page){ 
						return page.raw })
					.join('\n') }),

		quote: Macro('Include quoted page source (without parsing)',
			['src'], 
			function(context, elem, state){
				elem = $(elem)
				var path = elem.attr('src')
				return $(context.get(path)
					.map(function(page){
						return elem
							.clone()
							.attr('src', page.path)
							.text(page.raw)[0] })) }),

		/*
		// fill/define slot (stage 1)...
		//
		// XXX which should have priority the arg text or the content???
		_slot: Macro('Define/fill slot',
			['name', 'text'],
			function(context, elem, state, parse){
				var name = $(elem).attr('name')

				// XXX
				text = $(elem).html()
				text = text == '' ? 
					$(elem).attr('text') 
					: text
				text = this.parse(context, text, state, true)
				//text = parse(elem)

				if(state.slots[name] == null){
					state.slots[name] = text
					// return a slot macro parsable by stage 2...
					//return '<_slot name="'+name+'">'+ text +'</slot>'
					return elem

				} else if(name in state.slots){
					state.slots[name] = text
					return '' } }),
		//*/
		// convert @ macro to html-like + parse content...
		slot: Macro('Define/fill slot',
			['name', 'text'],
			function(context, elem, state, parse){
				elem = $(elem)
				var name = elem.attr('name')

				// XXX
				text = elem.html()
				text = text.trim() == '' ? 
					elem.html(elem.attr('text') || '').html() 
					: text
				text = parse(elem)

				elem.attr('text', null)
				//elem.html(text)

				return elem }),

		// XXX revise macro definition rules -- see inside...
		// XXX do we need macro namespaces or context isolation (for inculdes)???
		macro: Macro('Define/fill macro',
			['name', 'src', 'sort'],
			function(context, elem, state, parse){
				elem = $(elem)
				var name = elem.attr('name')
				var path = elem.attr('src')
				var sort = elem.attr('sort')

				state.templates = state.templates || {}

				// get named macro...
				if(name){
					// XXX not sure which definition rules to use for macros...
					// 		- first define -- implemented now
					// 		- last define -- as in slots
					// 		- first contenr -- original
					//if(elem.html().trim() != ''){
					if(elem.html().trim() != '' 
							// do not redefine...
							&& state.templates[name] == null){
						state.templates[name] = elem.clone()

					} else if(name in state.templates) {
						elem = state.templates[name] } }

				// fill macro...
				if(path){
					var pages = context.get(path)

					// no matching pages -- show the else block or nothing...
					if(pages.length == 0){
						var e = elem
							.find('else').first().clone()
								.attr('src', path)
						parse(e, context)
						return e } 

					// see if we need to overload attrs...
					sort = sort == null ? (elem.attr('sort') || '') : sort
					sort = sort
							.split(/\s+/g)
							.filter(function(e){ return e && e != '' })

					// do the sorting...
					pages = sort.length > 0 ? pages.sort(sort) : pages

					// fill with pages...
					elem = elem.clone()
						.find('else')
							.remove()
						.end()
					return $(pages
						.map(function(page){
							var e = elem.clone()
								.attr('src', page.path)
							parse(e, page)
							return e[0] })) }

				return '' }),
	},
	
	// Post macros... 
	//
	// XXX this is disabled for now, see end of .parse(..)
	post_macro: {
		'*': Macro('cleanup...',
			[],
			function(context, elem, state, parse, match){
				if(match != null){
					return match[0] == '\\' ? 
						match.slice(1) 
						: match }
				return elem }),
		/*
		_slot: Macro('',
			['name'],
			function(context, elem, state){
				var name = $(elem).attr('name')

				if(state.slots[name] == null){
					return $(elem).html()

				} else if(name in state.slots){
					return state.slots[name] } }),
		//*/

		/*
		// XXX rename to post-include and post-quote
		'page-text': Macro('',
			['src'],
			function(context, elem, state){
				return $(elem)
					.html(context.get(elem.attr('src')).text) }),
		'page-raw': Macro('',
			['src'],
			function(context, elem, state){
				return $(elem)
					.text(context.get(elem.attr('src')).text) }),
		//*/
	},

	// Filters...
	//
	// Signature:
	// 	filter(text) -> html
	//
	filter: {
		default: 'html',

		html: function(context, elem){ 
			return $(elem) },
		text: function(context, elem){ 
			return $('<span>')
				.append($('<pre>')
					.html($(elem).html())) },
		// XXX expperimental...
		json: function(context, elem){ 
			return $('<span>')
				.html($(elem).text()
					// remove JS comments...
					.replace(/\s*\/\/.*$|\s*\/\*(.|[\n\r])*?\*\/\s*/mg, '')) },

		// XXX
		nl2br: function(context, elem){ 
			return $('<div>')
				.html($(elem)
					.html()
					.replace(/\n/g, '<br>\n')) },

		wikiword: function(context, elem){ 
			return $('<span>')
				.html(setWikiWords(
					$(elem).html(), 
					true, 
					this.__include_marker__)) },
		// XXX need to remove all on* event handlers...
		noscript: function(context, elem){ 
			return $(elem)
				// remove script tags...
				.find('script')
					.remove()
					.end()
				// remove js links...
				.find('[href]')
					.filter(function(i, e){ 
							return /javascript:/i.test($(e).attr('href')) })
						.attr('href', '#')
						.end()
					.end()
				// remove event handlers...
				// XXX .off() will not work here as we need to remove on* handlers...
		},

		// XXX move this to a plugin...
		markdown: function(context, elem){
			var converter = new showdown.Converter({
				strikethrough: true,
				tables: true,
				tasklists: true,
			})

			return $('<span>')
				.html(converter.makeHtml($(elem).html()))
				// XXX add click handling to checkboxes...
				.find('[checked]')
					.parent()
						.addClass('checked')
						.end()
					.end() },
	},


	// Post-filters...
	//
	// These are run on the final page.
	//
	// The main goal is to setup editors and other active stuff that the
	// user should not have direct access to, but that should be 
	// configurable per instance...
	//
	// for tech and other details see .filter
	//
	post_filter: {
		noscript: function(context, elem){
			// XXX
			return elem },

		// Setup the page title and .title element...
		//
		// Use the text from:
		// 	1) set it H1 if it is the first tag in .text
		// 	2) set it to .location
		//
		title: function(context, elem){
			elem = $(elem)
			var title = elem.find('.text h1').first()

			// show first H1 as title...
			if(elem.find('.text').text().trim().indexOf(title.text().trim()) == 0){
				title.detach()
				elem.find('.title').html(title.html())
				$('title').html(title.text())

			// show location...
			} else {
				$('title').text(context.location) }

			return elem },
		// XXX this needs save/reload...
		editor: function(context, elem){
			// XXX title
			// 		- on focus set it to .title
			// XXX text
			// XXX raw
			// XXX checkbox

			return elem },
	},


	//
	// Parsing:
	//  1) expand macros
	//  2) apply filters
	//  3) merge and parse included pages:
	//  	1) expand macros
	//  	2) apply filters
	//  4) fill slots
	//  5) expand post-macros
	//
	// NOTE: stage 4 parsing is executed on the final merged page only 
	// 		once. i.e. it is not performed on the included pages.
	// NOTE: included pages are parsed in their own context.
	// NOTE: slots are parsed in the context of their containing page 
	// 		and not in the location they are being placed.
	//
	// XXX support quoted text...
	// XXX need to quote regexp chars of .__include_marker__...
	// XXX include recursion is detected but path recursion is not at 
	// 		this point...
	// 		e.g. the folowing paths resolve to the same page:
	// 			/SomePage
	// 			/SomePage/SomePage
	// 			or any path matching:
	// 				/\/(SomePage\/)+/
	parse: function(context, text, state, skip_post, pattern){
		var that = this

		state = state || {}
		state.filters = state.filters || []
		//state.slots = state.slots || {}
		state.include = state.include || []
		state.seen = state.seen || []

		//pattern = pattern || RegExp('@([a-zA-Z-_]+)\\(([^)]*)\\)', 'mg')
		pattern = pattern 
			|| RegExp.apply(null, this.__macro__pattern__)

		// XXX need to quote regexp chars...
		var include_marker = RegExp(this.__include_marker__, 'g')

		var parsed = 
			typeof(text) == typeof('str') ? 
				$('<span>').html(text) 
				: text

		var _parseText = function(context, text, macro){
			return text.replace(pattern, function(match){
				// quoted macro...
				if(match[0] == '\\' && macro['*'] == null){
					return match.slice(1) }
					//return match }

				// XXX parse match...
				var d = match.match(/@([a-zA-Z-_:]*)\(([^)]*)\)/)

				var name = d[1]

				if(name in macro || '*' in macro){
					var elem = $('<'+name+'/>')

					name = name in macro ? name : '*'

					// format positional args....
					var a = d[2]
						.split(/((['"]).*?\2)|\s+/g)
						// cleanup...
						.filter(function(e){ 
							return e 
								&& e != '' 
								&& !/^['"]$/.test(e)})
						// remove quotes...
						.map(function(e){ 
							return /^(['"]).*\1$/.test(e) ? 
								e.slice(1, -1) 
								: e })

					// add the attrs to the element...
					name != '*' 
						&& a.forEach(function(e, i){
							var k = ((macro[name] || {}).macro_args || [])[i]
							k && elem.attr(k, e) })

					// call macro...
					var res = macro[name]
						.call(that, context, elem, state,
							function(elem, c){ 
								return _parse(c || context, elem, macro) },
							match)

					return res instanceof jQuery ? 
							// merge html of the returned set of elements...
							res
								.map(function(i, e){ 
									return e.outerHTML })
								.toArray()
								.join('\n')
						: typeof(res) != typeof('str') ? 
							res.outerHTML
						: res }

				return match }) }
		// NOTE: this modifies parsed in-place...
		var _parse = function(context, parsed, macro){
			$(parsed).contents().each(function(_, e){
				// #text / comment node -> parse the @... macros...
				if(e.nodeType == e.TEXT_NODE 
						|| e.nodeType == e.COMMENT_NODE){
					// get actual element content...
					var text = $('<div>').append($(e).clone()).html()

					// conditional comment...
					if(e.nodeType == e.COMMENT_NODE 
							&& /^<!--\s*\[pWiki\[(.|\n)*\]\]\s*-->$/.test(text)){
						text = text
							.replace(/^<!--\s*\[pWiki\[/, '')
							.replace(/\]\]\s*-->$/, '') }

					$(e).replaceWith(_parseText(context, text, macro))

				// node -> html-style + attrs...
				} else {
					var name = e.nodeName.toLowerCase()

					// parse attr values...
					for(var i=0; i < e.attributes.length; i++){
						var attr = e.attributes[i]

						attr.value = _parseText(context, attr.value, macro) }

					// macro match -> call macro...
					if(name in  macro){
						$(e).replaceWith(macro[name]
							.call(that, context, e, state, 
								function(elem, c){ 
									return _parse(c || context, elem, macro) }))

					// normal tag -> sub-tree...
					} else {
						_parse(context, e, macro) } } })

			return parsed }
		var _filter = function(lst, filters){
			lst
				// unique -- leave last occurance..
				.filter(function(k, i, lst){ 
					return k[0] != '-'
						// filter dupplicates... 
						&& lst.slice(i+1).indexOf(k) == -1 
							// filter disabled...
						&& lst.slice(0, i).indexOf('-' + k) == -1 })
				// unique -- leave first occurance..
				//.filter(function(k, i, lst){ return lst.slice(0, i).indexOf(k) == -1 })
				// apply the filters...
				.forEach(function(f){
					var k = f
					// get filter aliases...
					var seen = []
					while(typeof(k) == typeof('str') 
							&& seen.indexOf(k) == -1){
						seen.push(k)
						k = filters[k] }
					// could not find the filter...
					if(!k){
						//console.warn('Unknown filter:', f)
						return }
					// use the filter...
					parsed = k.call(that, context, parsed) }) }

		// macro stage...
		_parse(context, parsed, this.macro)

		// filter stage...
		_filter(state.filters.concat(this.__filters__), this.filter)

		// merge includes...
		parsed
			.html(parsed.html()
				.replace(include_marker, 
					function(){
						var page = state.include.shift()
						var elem = $(page.shift())
						page = page.pop()
						var isolated = elem.attr('isolated') == 'true'

						var seen = state.seen.slice()
						if(seen.indexOf(page.path) >= 0){
							return elem.html() }
						seen.push(page.path)

						return page.map(function(page){
							return $('<div>')
								.append(elem
									.clone()
									.attr('src', page.path)
									.append(that
										.parse(page,
											page.raw, 
											{ 
												//slots: !isolated ? state.slots : {},
												templates: state.templates,
												seen: seen,
											}, 
											!isolated)))
											//true)))
								.html() })
								.join('\n') }))

		// post processing...
		if(!skip_post){
			// fill slots...
			// XXX need to prevent this from processing slots in editable
			// 		elements...
			slots = {}
			// get slots...
			parsed.find('slot')
				.each(function(i, e){
					e = $(e)

					// XXX not sure about this...
					// 		...check if it prevents correct slot parsing
					// 		within an isolated include...
					if(e.parents('[isolated="true"]').length > 0){
						return }

					var n = e.attr('name')

					n in slots 
						&& e.detach()

					slots[n] = e })
			// place slots...
			parsed.find('slot')
				.each(function(i, e){
					e = $(e)

					// XXX not sure about this...
					// 		...check if it prevents correct slot parsing
					// 		within an isolated include...
					if(e.parents('[isolated="true"]').length > 0){
						return }

					var n = e.attr('name')

					e.replaceWith(slots[n]) })

			// post-macro...
			// XXX for some odd reason this clears the backslash from 
			// 		quoted macros in raw fields...
			//this.post_macro 
			//	&& _parse(context, parsed, this.post_macro)
		}

		// post-filter stage...
		// XXX get list from context.config...
		_filter(this.__post_filters__, this.post_filter)

		// XXX shuld we get rid of the root span???
		return parsed.contents() },
}



/*********************************************************************/

// XXX not sure about these...
// XXX add docs...
// XXX need to handle case: 
// 		.data is function + function returns a page
// 			-> "redirect" to that page
// 		...is changing .path a good idea for redirecting???
var BaseData = {
	// Macro acces to standard page attributes (paths)...
	'System/title': function(){ 
		return this.get('..').title },
	'System/path': function(){ 
		return this.dir },
	'System/dir': function(){ 
		return this.get('..').dir },
	'System/location': function(){ 
		return this.dir },
	'System/resolved': function(){ 
		return this.get('..').acquire() },

	// page data...
	//
	// NOTE: special case: ./raw is treated a differently when getting .text
	// 		i.e:
	// 			.get('./raw').text
	// 		is the same as:
	// 			.get('.').raw
	'System/raw': function(){ 
		return this.get('..').raw },
	'System/text': function(){ 
		return this.get('..').text },

	// XXX move this to Wiki.children + rename...
	'System/list': function(){
		var p = this.dir

		return Object.keys(this.__wiki_data)
			.map(function(k){
				return k.indexOf(p) == 0 ?
					path2lst(k.slice(p.length)).shift() 
					: null })
			.filter(function(e){ 
				return e != null })
			.sort()
			.map(function(e){ 
				return '['+ e +']' })
			.join('<br>') },
	// list links to this page...
	'System/links': function(){
		var that = this
		var p = this.dir

		var res = []

		var wiki = this.__wiki_data
		Object.keys(wiki).forEach(function(k){
			;(wiki[k].links || [])
				.forEach(function(l){
					;(l == p 
							|| that
								.get(path2lst(l).slice(0, -1))
								.acquire('./'+path2lst(l).pop()) == p)
						&& res.push([l, k]) }) })

		return res
			//.map(function(e){ return '['+ e[0] +'] <i>from page: ['+ e[1] +']</i>' })
			.map(function(e){ 
				return '['+ e[1] +'] <i>-&gt; ['+ e[0] +']</i>' })
			.sort()
			.join('<br>') },

	// Page modifiers/actions...
	// XXX these needs redirecting...
	//'System/sort': function(){ return this.get('..').sort() },
	//'System/reverse': function(){ return this.get('..').reverse() },
	/*
	'System/delete': function(){
		var p = this.dir
		delete this.__wiki_data[p]
		return this.get('..') },
	//*/
}

// data store...
// Format:
// 	{
// 		<path>: {
// 			text: <text>,
//
// 			links: [
// 				<offset>: <link>,
// 			],
// 		}
// 	}
//
// XXX add .json support...
/*
var data = {
	// XXX might be a good idea to use this for outline...
	'Templates/tree': {
		//text: '<macro src="../**"> [@source(./path)]<br> </macro>\n'
		text: ''
			+'<div class="sortable">\n'
				+'<macro src="../*">\n'
					+'<div class="item">\n'
						+'<span class="sort-handle">&#x2630;</span> \n'
						+'<a href="#@source(./path)">@source(./title)</a>\n'
						+'<span class="separator"/>\n'
						+'<a class="button" href="#@source(./path)/delete">&times;</a>\n'
					+'</div>\n'
					+'<div style="padding-left: 30px">\n'
						+'<include '
								+'style="display:block" '
								+'src="@source(./path)/tree" '
							+'/>\n'
					+'</div>\n'
				+'</macro>\n'
			+'</div>\n'
	},
	// XXX this is essentially identical to pages, except for the path...
	'Templates/all_pages': {
		//text: '<macro src="../**"> [@source(./path)]<br> </macro>\n'
		text: ''
			+'<macro src="../**">\n'
				+'<div class="item">\n'
					+'[@source(./path)]\n'
					+'<span class="separator"/>\n'
					+'<a class="button" href="#@source(./path)/delete">&times;</a>\n'
				+'</div>\n'
			+'</macro>\n'
	},
	// XXX experimental...
	// XXX need sorting...
	'Templates/outline': {
		text: ''
			+'<macro name="item-pre-controls"/>\n'
			+'\n'
			+'<macro name="item-content">\n'
				+'<include '
						+'class="raw" '
						+'contenteditable tabindex="0" '
						+'style="display:inline-block" '
						+'saveto="@source(./path)" '
						+'src="."'
					+'/>\n'
			+'</macro>\n'
			+'\n'
			+'<macro name="item-post-controls">\n'
				+'<a class="button" href="#@source(./path)/delete">&times;</a>\n'
			+'</macro>\n'
			+'\n'
			+'\n'
			+'<div>\n'
				// XXX select all on focus...
				+'<span class="raw" contenteditable tabindex="0" '
						+'saveto="@source(../path)/@now()" style="display:inline-block">\n'
					+'+\n'
				+'</span>\n'
			+'</div>\n'
			//+'<br>\n'
			+'<div class="sortable">\n'
				+'<macro src="../*">\n'
					+'<div class="item">\n'
						+'<div>\n'
							+'<span class="sort-handle">&#x2630;</span>\n'
							+'<macro name="item-pre-controls" src="."/>\n'
							+'<macro name="item-content" src="."/>\n'
							+'<span class="separator"/>\n'
							+'<macro name="item-post-controls" src="."/>\n'
						+'</div>\n'
						+'<div style="padding-left: 30px">\n'
							+'<include '
									+'style="display:block" '
									+'src="@source(./path)/outline" '
								+'/>\n'
						+'</div>\n'
					+'</div>\n'
					// XXX do we need this or should we just use CSS???
					//+'<else>\n'
					//	+'<i>No items yet...</i>\n'
					//+'</else>\n'
				+'</macro>\n'
			+'</div>\n'
			+'\n',
	},
	// XXX see inside...
	'Templates/todo': {
		text: ''
			// XXX this feels wrong...
			//		...and this will not wirk well with macro override rules...
			+'<macro name="item-pre-controls">\n'
			+'  <input type="checkbox"/>\n'
			+'</macro>\n'
			+'\n'
			+'<include src="../outline">\n'
	},

	// Views...
	// XXX experimental...
	'Templates/_outline': {
		text: ''
			+'<include src="../_view"/>\n'
			+'\n'
			// XXX temporary until I figure out how to deal with the saveto=".."
			// 		in implicit vs. explicit _view
			+'<slot name="title" class="title" contenteditable saveto="..">'
				+'@source(../title)'
			+'</slot>\n'
			+'\n'
			+'<slot name="page-content">\n'
				+'@include(../outline)'
			+'</slot>'
			+'\n',
	},
	'Templates/_todo': {
		text: ''
			+'<include src="../_view"/>\n'
			+'\n'
			// XXX temporary until I figure out how to deal with the saveto=".."
			// 		in implicit vs. explicit _view
			+'<slot name="title" class="title" contenteditable saveto="..">'
				+'@source(../title)'
			+'</slot>\n'
			+'\n'
			+'<slot name="page-content">\n'
				+'@include(../todo)'
			+'</slot>'
			+'\n'
	},
}
//*/

data = {}
data.__proto__ = BaseData


// XXX experimental...
// 		...for some reason these are called twice...
var PathActions = {
	// XXX
	test: function(){
		var p = path2lst(this.location)

		console.log('!!! TEST !!!')

		this.location = p.slice(0, -1) },
	delete: function(){
		var p = normalizePath(path2lst(this.location).slice(0, -1))

		console.log('!!! DELETE: %s !!!', p)

		delete this.__wiki_data[p]

		this.location = p },
}



/*********************************************************************/

// XXX add .json support...
var Wiki = {
	__wiki_data: data,

	__config_page__: 'System/settings',

	__home_page__: 'WikiHome',

	__default_page__: 'EmptyPage',

	// Special sub-paths to look in on each level...
	__acquesition_order__: [
		'Templates',
	],

	__post_acquesition_order__: [
	],

	// XXX should this be read only???
	__system__: 'System',
	//__redirect_template__: 'RedirectTemplate',

	__wiki_link__: RegExp('('+[
		'\\\\?(\\./|\\.\\./|[A-Z][a-z0-9]+[A-Z/])[a-zA-Z0-9/]*',
		'\\\\?\\[[^\\]]+\\]',
	].join('|') +')', 'g'),

	__macro_parser__: macro,


	// Resolve path variables...
	//
	// Supported vars:
	// 	$NOW		- resolves to 'P'+Date.now()
	//
	resolvePathVars: function(path){
		return path
			.replace(/\$NOW|\$\{NOW\}/g, ''+Date.now()) },
	resolvePathActions: function(){
		// XXX this can happen when we are getting '.../*' of an empty item...
		if(this.path == null){
			return this }

		var p = path2lst(this.path).pop()

		if(p in PathActions){
			return PathActions[p].call(this) }

		return this },
	// Resolve '.' and '..' relative to current page...
	//
	// NOTE: '.' is relative to .path and not to .dir
	// NOTE: this is a method as it needs the context to resolve...
	resolveDotPath: function(path){
		path = normalizePath(path)
		// '.' or './*'
		return path == '.' || /^\.\//.test(path) ? 
				//path.replace(/^\./, this.dir)
				path.replace(/^\./, this.path)
			// '..' or '../*'
			: path == '..' || /^\.\.\//.test(path) ? 
				//path.replace(/^\.\./, 
				//	normalizePath(path2lst(this.dir).slice(0, -1)))
				path.replace(/^\.\./, this.dir)
			: path },
	// Get list of paths resolving '*' and '**'
	//
	// XXX should we list parent pages???
	// XXX should this acquire stuff???
	// XXX should this support sorting and reversing???
	resolveStarPath: function(path){
		// no pattern in path -> return as-is...
		if(path.indexOf('*') < 0){
			return [ path ] }

		// get the tail...
		var tail = path.split(/\*/g).pop()
		tail = tail == path ? '' : tail

		var pattern = RegExp('^'
			+normalizePath(path)
				// quote regexp chars...
				.replace(/([\.\\\/\(\)\[\]\$\+\-\{\}\@\^\&\?\<\>])/g, '\\$1')

				// convert '*' and '**' to regexp...
				.replace(/\*\*/g, '.*')
				.replace(/^\*|([^.])\*/g, '$1[^\\/]*')
			+'$')

		var data = this.__wiki_data
		return Object.keys(data)
			// XXX is this correct???
			.concat(Object.keys(data.__proto__)
				// do not repeat overloaded stuff...
				.filter(function(e){ 
					return !data.hasOwnProperty(e) }))
			.map(function(p){ 
				return tail != '' ? 
					normalizePath(p +'/'+ tail) 
					: p })
			.filter(function(p){ 
				return pattern.test(p) }) },


	// current location...
	get location(){
		return this.__location 
			|| this.__home_page__ },
	set location(value){
		delete this.__order
		delete this.__order_by
		this.__location = this.resolvePathVars(this.resolveDotPath(value))
		this.resolvePathActions() },


	get data(){
		return this.__wiki_data[this.acquire()] },
	attr: function(name, value){
		// no args...
		if(arguments.length == 0){
			return this

		// name...
		} else if(arguments.length == 1 
				&& typeof(name) == typeof('str')){
			return this.data[name]

		// object...
		} else if(arguments.length == 1){
			var that = this
			Object.keys(name).forEach(function(k){
				that.data[k] = name[k] })

		// name value pair...
		} else {
			this.data[name] = value }

		return this },

	// XXX experimental...
	get config(){
		try{
			return JSON.parse(this.get(this.__config_page__).code) || {}

		} catch(err){
			console.error('CONFIG:', err)
			return {} } },


	clone: function(){
		var o = Object.create(Wiki)
		o.location = this.location
		//o.__location_at = this.__location_at
		// XXX
		o.__parent = this

		if(this.__order){
			o.__order = this.__order.slice() }

		return o },
	end: function(){
		return this.__parent 
			|| this },


	// page path...
	//
	// Format:
	// 	<dir>/<title>
	//
	// NOTE: changing this will move the page to the new path and change
	// 		.location acordingly...
	// NOTE: same applies to path parts below...
	// NOTE: changing path will update all the links to the moving page.
	// NOTE: if a link can't be updated without a conflit then it is left
	// 		unchanged, and a redirect page will be created.
	//
	// XXX this can be null if we are getting '.../*' of an empty item...
	get path(){ 
		return (this.__order 
				|| this.resolveStarPath(this.location))[this.at()] },
	// XXX should link updating be part of this???
	// XXX use a template for the redirect page...
	// XXX need to skip explicit '.' and '..' paths...
	set path(value){
		value = this.resolvePathVars(this.resolveDotPath(value))

		var l = this.location

		if(value == l || value == ''){
			return }

		// old...
		var otitle = this.title
		var odir = this.dir

		if(this.exists(l)){
			this.__wiki_data[value] = this.__wiki_data[l] }
		this.location = value

		// new...
		var ntitle = this.title
		var ndir = this.dir

		var redirect = false

		// update links to this page...
		this.pages(function(page){
		//this.get('**').map(function(page){
			// skip the old page...
			if(page.location == l){
				return }
			page.raw = page.raw.replace(page.__wiki_link__, function(lnk){
				var from = lnk[0] == '[' ? lnk.slice(1, -1) : lnk

				// get path/title...
				var p = path2lst(from)
				var t = p.pop()
				p = normalizePath(p)

				var target = page.get(p).acquire('./'+t)
				// page target changed...
				// NOTE: this can happen either when a link was an orphan
				// 		or if the new page path shadowed the original 
				// 		target...
				// XXX should we report the exact condition here???
				if(target == value){
					console.log('Link target changed:', lnk, '->', value)
					return lnk

				// skip links that do not resolve to target...
				} else if(page.get(p).acquire('./'+t) != l){
					return lnk }

				// format the new link...
				var to = p == '' ? ntitle : p +'/'+ ntitle
				to = lnk[0] == '[' ? '['+to+']' : to

				// explicit link change -- replace...
				if(from == l){
					//console.log(lnk, '->', to)
					return to

				// path did not change -- change the title...
				} else if(ndir == odir){
					// conflict: the new link will not resolve to the 
					// 		target page...
					if(page.get(p).acquire('./'+ntitle) != value){
						console.log('ERR:', lnk, '->', to,
							'is shadowed by:', page.get(p).acquire('./'+ntitle))
						// XXX should we add a note to the link???
						redirect = true

					// replace title...
					} else {
						//console.log(lnk, '->', to)
						return to }

				// path changed -- keep link + add redirect page...
				} else {
					redirect = true }

				// no change...
				return lnk }) })

		// redirect...
		//
		// XXX should we use a template here???
		// 		...might be a good idea to set a .redirect attr and either
		// 		do an internal/transparent redirect or show a redirect 
		// 		template
		// 		...might also be good to add an option to fix the link from
		// 		the redirect page...
		if(redirect){
			console.log('CREATING REDIRECT PAGE:', l, '->', value, '')
			this.__wiki_data[l].raw = 'REDIRECT TO: ' + value
				+'<br>'
				+'<br><i>NOTE: This page was created when renaming the target '
					+'page that resulted new link being broken (i.e. resolved '
					+'to a different page from the target)</i>'
			this.__wiki_data[l].redirect = value

		// cleaup...
		} else {
			delete this.__wiki_data[l] } },

	// path parts: directory...
	//
	// NOTE: see .path for details...
	get dir(){
		return path2lst(this.path).slice(0, -1).join('/') },
	set dir(value){
		this.path = value +'/'+ this.title },

	// path parts: title...
	//
	// NOTE: see .path for details...
	get title(){ 
		return path2lst(this.path).pop() },
	set title(value){
		if(value == '' || value == null){
			return }

		this.path = this.dir +'/'+ value },


	// page content...
	//
	get raw(){
		var data = this.data
		data = data instanceof Function ? 
			data.call(this, this) 
			: data

		return typeof(data) == typeof('str') ? 
				data
			: data != null ?
				('raw' in data ? 
					data.raw 
					: data.text)
			: '' },
	set raw(value){
		var l = this.location

		// prevent overwriting actions...
		if(this.data instanceof Function){
			return }

		this.__wiki_data[l] = this.__wiki_data[l] || {}
		this.__wiki_data[l].text = value

		// cache links...
		delete this.__wiki_data[l].links
		this.__wiki_data[l].links = this.links },

	get text(){
		//return this.parse() 
		// special case: if we are getting ./raw then do not parse text...
		return this.title == 'raw' ? 
			this.raw 
			: this.__macro_parser__.parse(this, this.raw) },
	get code(){
		return this.text.text() },


	get checked(){ 
		return this.data.checked },
	set checked(value){ 
		this.data.checked = value },

	// NOTE: this is set by setting .text
	get links(){
		var data = this.data || {}
		var links = data.links = data.links
			|| (this.raw.match(this.__wiki_link__) || [])
				// unwrap explicit links...
				.map(function(e){ 
					return e[0] == '[' ? 
						e.slice(1, -1) 
						: e })
				// unique...
				.filter(function(e, i, l){ 
					return l.slice(0, i).indexOf(e) == -1 })
		return links },


	// navigation...
	get parent(){
		return this.get('..') },
	get children(){
		return this
			.get('./*') },
	get siblings(){
		return this
			.get('../*') },

	// NOTE: .get() is not the same as .clone() in that .get() will resolve
	// 		the path to a specific location while .clone() will keep 
	// 		everything as-is...
	//
	// XXX add prpper insyantiation ( .clone() )...
	get: function(path){
		//var o = Object.create(this)
		var o = this.clone() 
		// NOTE: this is here to resolve path patterns...
		o.location = this.path

		o.location = path || this.path
		return o },


	exists: function(path){
		return normalizePath(path || this.path) in this.__wiki_data },
	// get title from dir and then go up the tree...
	//
	// XXX should we also acquire each path part???
	acquire: function(path, no_default){
		var that = this

		// handle paths and relative paths...
		var p = this.get(path)
		var title = p.title
		path = path2lst(p.dir)

		var acquire_from = this.__acquesition_order__ || []
		var post_acquire_from = this.__post_acquesition_order__ || []
		var data = this.__wiki_data

		var _get = function(path, title, lst){
			lst = (lst == null || lst.length == 0) ? 
				[''] 
				: lst
			for(var i=0; i < lst.length; i++){
				var p = path.concat([lst[i], title])
				if(that.exists(p)){
					p = normalizePath(p)
					return that.__wiki_data[p] 
						&& p } } }

		while(true){
			// get title from path...
			var p = _get(path, title)
				// get title from special paths in path...
				|| _get(path, title, acquire_from)

			if(p != null){
				return p }

			if(path.length == 0){
				break }

			path.pop() }

		// default paths...
		var p = _get(
			path, 
			title, 
			post_acquire_from)
				// system path...
				|| this.__system__ 
				&& _get([this.__system__], 
			title)

		// NOTE: this may be null...
		return p 
			|| ((!no_default && title != this.__default_page__) ? 
				this.acquire('./'+this.__default_page__) 
				: null) },


	// iteration...
	get length(){
		return (this.__order 
				|| this.resolveStarPath(this.location))
			.length },
	// get/set postion in list of pages...
	// XXX do we need to min/max normalize n??
	at: function(n){
		// get position...
		if(n == null){
			return this.__location_at || 0 }

		var l = this.length

		// end of list...
		if(n >= l || n < -l){
			return null }

		var res = this.clone()

		n = n < 0 ? l - n : n
		// XXX do we min/max n???
		n = Math.max(n, 0)
		n = Math.min(l-1, n)

		res.__location_at = n

		return res },
	prev: function(){ 
		var i = this.at() - 1
		// NOTE: need to guard against overflows...
		return i >= 0 ? 
			this.at(i) 
			: null },
	next: function(){ 
		return this.at(this.at() + 1) },

	map: function(func){
		var res = []
		for(var i=0; i < this.length; i++){
			var page = this.at(i)
			res.push(func.call(page, page, i)) }
		return res },
	filter: function(func){
		var res = []
		for(var i=0; i < this.length; i++){
			var page = this.at(i)
			func.call(page, page, i) 
				&& res.push(page) }
		return res },
	forEach: function(func){
		this.map(func)
		return this },


	// sorting...
	// XXX make these not affect the general order unless they have to...
	// XXX add a reverse method...
	__default_sort_methods__: ['path'],
	__sort_methods__: {
		title: function(a, b){
			return a.page.title < b.page.title ? 
					-1
				: a.page.title > b.page.title ? 
					1
				: 0 },
		path: function(a, b){
			return a.page.path < b.page.path ? 
					-1
				: a.page.path > b.page.path ? 
					1
				: 0 },
		// XXX
		checked: function(a, b){
			// XXX chech if with similar states the order is kept....
			return a.page.checked == b.page.checked ? 
					0
				: a.page.checked ? 
					1
				: -1 },
		// XXX date, ...

		// XXX use manual order and palce new items (not in order) at 
		// 		top/bottom (option)...
		// XXX store the order in .__wiki_data
		manual: function(a, b){
			// XXX
			return 0 },
	},

	// Sort siblings...
	//
	//	Sort pages via default method
	//	.sort()
	//		-> page
	//
	//	Sort pages via method
	//	.sort(method)
	//		-> page
	//
	//	Sort pages via method1, then method2, ...
	//	.sort(method1, method2, ...)
	//		-> page
	//		NOTE: the next method is used iff the previous returns 0, 
	//			i.e. the items are equal.
	//
	// To reverse a specific method, prepend it's name with "-", e.g. 
	// "title" will do the default ascending sort while "-title" will do
	// a descending sort.
	// This is different from the "reverse" method which will simply 
	// reverse the result.
	//
	// NOTE: the sort is local to the returned object.
	// NOTE: the sorted object may loose sync form the actual wiki as the
	// 		list of siblings is cached.
	// 		...the resulting object is not to be stored for long.
	sort: function(){
		var that = this
		var res = this.clone()
		var path = res.path

		var methods = arguments[0] instanceof Array ? 
			arguments[0] 
			: [].slice.call(arguments)

		res.__order_by = methods = methods.length == 0 ?
			this.__default_sort_methods__ 
			: methods

		res.update()

		return res },
	reverse: function(){
		var res = this.clone()

		res.__order_by = (this.__order_by || []).slice()

		var i = res.__order_by.indexOf('reverse')

		i >= 0 ? 
			res.__order_by.splice(i, 1) 
			: res.__order_by.push('reverse')

		res.update()

		return res },

	// XXX not sure if this is the way to go...
	update: function(){
		var that = this

		if(this.__order || this.__order_by){
			var path = this.path
			var reverse = false

			var methods = (this.__order_by || this.__default_sort_methods__)
				.map(function(m){
					var reversed = m[0] == '-'
					m = reversed ? m.slice(1) : m

					if(m == 'reverse'){
						reverse = !reverse
						return null }
					m = typeof(m) == typeof('str') ? 
							that.__sort_methods__[m]
						: m instanceof Function ? 
							m
						: null

					return m != null ?
						(reversed ? 
							function(){ 
								return -m.apply(this, arguments) } 
							: m) 
						: m })
				.filter(function(m){ 
					return !!m })

			this.__order = this.resolveStarPath(this.location)

			if(methods.length > 0){
				var method = function(a, b){
						for(var i=0; i < methods.length; i++){
							var res = methods[i].call(that, a, b)

							if(res != 0){
								return res } }
						// keep order if nothing else works...
						return a.i - b.i }

				this.__order = this.__order
					.map(function(t, i){ 
						return {
							i: i, 
							page: that.get(t),
						} })
					.sort(method)
					.map(function(t){ 
						return t.page.path }) }

			reverse 
				&& this.__order.reverse()

			this.__location_at = this.__order.indexOf(path) }
	
		return this },


	// serialization...
	// XXX need to account for '*' and '**' in path...
	// XXX
	json: function(path){
		return path == null ? 
				JSON.parse(JSON.stringify(this.__wiki_data))
			: path == '.' ? 
				{
					path: this.location,
					text: this.raw,
				}
			: {
				path: path,
				text: (this.__wiki_data[path] || {}).raw,
			} },
	// XXX should we inherit from the default???
	load: function(json){
		this.__wiki_data = json },


	// iteration...
	// XXX this is not page specific, might need refactoring...
	pages: function(callback){
		var that = this
		Object.keys(this.__wiki_data)
			.forEach(function(location){
				// XXX not sure if this is the right way to go...
				//var o = Object.create(that)
				var o = that.clone() 
				o.location = location
				callback.call(o, o) })
		return this },
}



/**********************************************************************
* vim:set ts=4 sw=4 :                                                */
