Ext.apply(Ext.DomQuery.pseudos, {
	nodeValueCaseInsensitive: function(c, v){
        var r = [], ri = -1;
        v = new RegExp('^'+v+'$', 'i');
        for(var i = 0, ci; ci = c[i]; i++){
            if(ci.firstChild && ci.firstChild.nodeValue.match(v)){
                r[++ri] = ci;
            }
        }
        return r;
    }
});

/**
 * @class Voyeur.Tool.Reader A panel that provides reading and searching capabilities.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.Reader = Ext.extend(Ext.Panel, {
	MINIMUM_LIMIT: 1000,
	documents: new Ext.util.MixedCollection(),
	loading: false,
	constructor : function(config) {

		// register localization variables
		Ext.apply(this, new Voyeur.Tool(config, this));

		// set configuration options for superclass initialization, but allow overriding from skin
		Ext.applyIf(config, {
			layout: 'fit',
			html: '<div class="token_images" style="height: 100%; width: 30px;"> </div><div id="prospect_reader" style="position: absolute; left: 40px; right: 0px; top: 0px; height: 100%; overflow: auto; "> </div>'
		});
		
		this.search = new Ext.ux.TypeSearch({
			parentTool: this,
			width: 100,
			listeners: {
				typeSelected: function(combo, type, record) {
    				this.showProspectHits(type);
        		},
        		scope: this
        	}
		});
		
		Ext.applyIf(config, {
			bbar: [
	     		new Ext.Button({
	                tooltip: this.localize('startText'),
	                overflowText: this.localize('startText'),
	                iconCls: 'x-tbar-page-first',
	                handler: this.moveFirst,
	                scope: this
	            }), new Ext.Button({
	                tooltip: this.localize('previousText'),
	                overflowText: this.localize('previousText'),
	                iconCls: 'x-tbar-page-prev',
	                handler: this.movePrevious,
	                scope: this
	            }), new Ext.Button({
	                tooltip: this.localize('nextText'),
	                overflowText: this.localize('nextText'),
	                iconCls: 'x-tbar-page-next',
	                handler: this.moveNext,
	                scope: this
	            }), new Ext.Button({
	                tooltip: this.localize('endText'),
	                overflowText: this.localize('this.endText'),
	                iconCls: 'x-tbar-page-last',
	                handler: this.moveLast,
	                scope: this
	            }), ' ', '-', ' ',
	            this.search
	        ]
		});
		
		Voyeur.Tool.Reader.superclass.constructor.apply(this, arguments);
		
		this.addListener('afterrender', function(panel) {
			if (this.getCorpus().getSize()>0) {
				this.buildProspect();
				this.fetchText();
			}
			panel.body.addListener('click', function(e) {
				var target = e.getTarget(null,null,true);
				if (target && target.dom.tagName=='IMG' && target.dom.id.indexOf("prospect")==0) {
					var parts = target.dom.id.split(".");
					var docIndex = parseInt(parts[1]);
					var docLine = parseInt(parts[2]);
					var doc = this.documents.get(docIndex);
					var totalWords = doc.document.getTotalTokens();
					var start = parseInt(docLine * totalWords / doc.lines);
//					console.log('fetching', docIndex, docLine);
					this.setApiParams({docIndex: docIndex, start: start});
					Ext.get('prospect_reader').update('');
					this.fetchText();
				}
				else if (target && target.dom.tagName=='SPAN' && target.hasClass('word')) {
					var token = target.dom.innerHTML;
					var segment = this.getSegmentObject(target.parent());
					/**
					 * @event documentTypeSelected
					 * @param {Voyeur.Tool.Reader} tool
					 * @param {Object} params <ul>
					 * <li><b>docIdType</b> : String</li>
					 * </ul>
					 * @type dispatcher
					 */
					this.getApplication().dispatchEvent('documentTypeSelected', this, {
						docIdType: this.getCorpus().getDocument(segment.docIndex).getId()+':'+token.toLowerCase()
					});
				}
			}, this);
			Ext.get('prospect_reader').addListener('scroll', function(ev) {
				var el = ev.target;
				if (this.loading) {return}
				var height = el.offsetHeight;
				var scrollHeight = el.scrollHeight;
				var scrollTop = el.scrollTop;
				el = Ext.get(el);
				if (scrollTop+height > scrollHeight-100 || scrollTop < 1) {
					var start = parseInt(this.getApiParamValue('start'));
					var limit = parseInt(this.getApiParamValue('limit'));
					if (scrollTop < 1) {
						var segmentObject = this.getSegmentObject(el.first());
						if (segmentObject) {
							var doc = segmentObject.docIndex;
							var start = segmentObject.start;
							if (doc==0 && start==0) {return;} // already at start
							if (start==0) { // move to end of previous document
								doc -= 1;
								this.setApiParams({docIndex: doc});
								start = this.getCorpus().getDocument(doc).getTotalTokens()-limit;
								if (start<0) {start=0}
							}
							else {
								if (start-limit>-1) {start -= limit;}
								else {
									limit = parseInt(start);
									start = 0;
								}
							}
							this.setApiParams({start: start});
							this.fetchText(Ext.DomHelper.insertFirst(el, {tag: 'span'}, true), {limit: limit});
						}
					} else {
						var currentSegment = el.last();
						var segmentObject = this.getSegmentObject(currentSegment);
						if (segmentObject && segmentObject.docIndex != null) {
							var doc = segmentObject.docIndex;
							var start = segmentObject.start;
							var lastLimit = segmentObject.lastLimit;
							var numberOfDocs = this.getCorpus().getSize();
							var totalWords = this.getCorpus().getDocument(doc).getTotalTokens();
							if (doc+1==numberOfDocs && start+lastLimit>=totalWords) {return;} // already at end
							if (start+lastLimit>=totalWords) { // move to end of previous document
								doc += 1;
								this.setApiParams({docIndex: doc});
								start = 0;
							}
							else { // move ahead
								start+=limit;
							}
							this.setApiParams({start: start});
							this.fetchText(null, {limit: limit});
						}
					}
				} else {
					var currentSegmentObject = this.getCurrentSegmentObject();
					this.highlightProspect(currentSegmentObject.docIndex, currentSegmentObject.start);
				}
			}, this);
		}, this);

		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, params) {
			if (this.rendered) {
				this.buildProspect();
				this.fetchText();
			}
		}, this);

		/**
		 * @event TokensResultLoaded
		 * @type listener
		 */
		this.addListener('TokensResultLoaded', function(src, data) {
			var content = "";
			var docs = data.tokens.documents;
			var toks, category;
			for (var i=0;i<docs.length;i++) {
				toks = docs[i].tokens;
				for (var j=0;j<toks.length;j++) {
					category = toks[j]['@category'];
					if (category.indexOf("TAG")>-1) {
						if (toks[j]['@newline']) {
							content+='<br />';
						}
					}
					else {content+=toks[j]['@token'];}
				}
			}
			var el = this.body.last();
			el.update(content);
		}, this);
		
		/**
		 * @event tokenSelected
		 * @type listener
		 */
		this.addListener('tokenSelected', function(src, data) {
			var tokenId = data.tokenId;
			var docInfo = data.docIdType.split(':');
			var docId = docInfo[0];
			var type = docInfo[1].toLowerCase();
			var docIndex = this.getCorpus().getDocument(docId).getIndex();
			this.setApiParams({docIndex: docIndex, start: tokenId});
			Ext.get('prospect_reader').update('');
			this.fetchText();
//			this.search.setValue(type);
//			this.showProspectHits(type);
		});
		
		/**
		 * @event corpusTypeSelected
		 * @type listener
		 */
		this.addListener('corpusTypeSelected', function(src, data) {
			var type = data.type.toLowerCase();
			this.search.setValue(type);
			this.showProspectHits(type);
		});
		
		/**
		 * @event documentTypeSelected
		 * @type listener
		 */
		this.addListener('documentTypeSelected', function(src, data) {
			var type = data.docIdType.split(':')[1].toLowerCase();
			this.search.setValue(type);
			this.showProspectHits(type);
		}, this);
	}

	,buildProspect: function() {
		var el = this.body.first();
		var availableLines = parseInt(el.getHeight() / 5); // 5px is line height
		availableLines--; // make sure there's no scrollbar for prospect
		
		var corpus = this.getCorpus();
		var totalTokens = 0;
		corpus.getDocuments().each(function(document) {
			totalTokens += document.getTotalTokens();
		});
		var tokensPerLine = Math.floor(totalTokens / availableLines);
		if (tokensPerLine < this.MINIMUM_LIMIT) {tokensPerLine = this.MINIMUM_LIMIT;}
		this.setApiParams({limit: tokensPerLine});
		
		var docTotalTokens, linesPerDocument;
		var imagesSnippet = "";
		var label;
		this.documents = new Ext.util.MixedCollection();
		
		corpus.getDocuments().each(function(document) {
			label = document.getShortLabel();
			docIndex = document.getIndex();
			imagesSnippet += "<div>";
			docTotalTokens = document.getTotalTokens();
			var percentageOfWhole = docTotalTokens / totalTokens;
			//linesPerDocument = Math.floor(docTotalTokens / tokensPerLine);
			linesPerDocument = Math.floor(availableLines * percentageOfWhole);
			//console.log('linesPerDocument',linesPerDocument,'percentageOfWhole',percentageOfWhole);
			if (linesPerDocument < 1) {linesPerDocument = 1;}
			var colourClass = 'colour_'+(docIndex % 24);
			for (var i = 0; i < linesPerDocument; i++) {
				imagesSnippet += "<img src='"+Ext.BLANK_IMAGE_URL+"' class='prospectLine "+colourClass+"' ext:qtip='"+label+"' id='prospect."+docIndex+'.'+i+"' />";
				//imagesSnippet += "<img src='"+img_src+"' class='prospectLine spacer' style='cursor: default;'/>"; // FIXME: do proper CSS override of cursor
			}
			this.documents.add(document.getIndex(), {
				document: document, lines: linesPerDocument
			});
			imagesSnippet += '</div>';
		}, this);
		el.update(imagesSnippet);
		this.addListener('afterlayout', function(p, l) {
			this.buildProspect.defer(500, this);
			var query = this.search.getValue();
			if (query != '') {
				this.showProspectHits.defer(550, this, [query]);
			}
		}, this, {single: true});
	}

	,fetchText : function(el, overrides, getNextDoc) {
		var params = this.getApiParams();
		getNextDoc = getNextDoc != null ? getNextDoc : true;
		Ext.applyIf(params, {
			start: this.getApiParamValue('start')
			,limit: this.getApiParamValue('limit')
		})
		Ext.apply(params, overrides);
		Ext.apply(params, {
			tool: 'Tokens'
			,template: 'docTokens2html'
			,outputFormat: 'html'
			,snippet: 'true'
		});
		if (params.docId) {
			params.docIndex = this.getCorpus().getDocument(params.docId).getIndex();
			delete params.docId;
		}
		if (!params.docIndex) {params.docIndex=0;}
		
		if (!el) {el=Ext.DomHelper.append(Ext.get('prospect_reader'), {tag: 'span'}, true);}
		el.addClass('segment_'+params.docIndex+'_'+params.start+'_'+params.limit);
		this.loading = true;
		el.load({
			url: this.getTromboneUrl()
			,callback: function(el, success, response, options) {
				var doc = this.getCorpus().getDocument(params.docIndex);
				if (params.start==0) {
					var title = Ext.DomHelper.insertFirst(el, {tag: 'h1'/*, cls: 'x-panel-header'*/}, true);
					title.update(doc.getLabel())
				}
				this.loading = false;
				if (getNextDoc && doc.getTotalTokens() < params.limit) {
					var nextDocIndex = params.docIndex + 1;
					if (nextDocIndex < this.getCorpus().getSize()) {
						this.setApiParams({docIndex: nextDocIndex, start: 0});
						this.fetchText(null, null, false);
					}
				}
				var reader = Ext.get('prospect_reader');
				if (reader.first()==reader.last()) {
					this.highlightProspect(params.docIndex, params.start);
				}
				else {
					if (reader.first()==el) {
						el.last().scrollIntoView(reader).frame("ff0000", 1, { duration: 2 });
						this.highlightProspect(params.docIndex, params.start+params.limit);
					}
					else {
						el.first().frame("ff0000", 1, { duration: 2 });
						this.highlightProspect(params.docIndex, params.start);
					}
				}
				
				var query = this.search.getValue();
				if (query != '') {
					this.highlightKeywords(query, false);
				}
			}
			,params: params
			,scope: this
		});
	}
	
	,moveFirst : function() {
		var el = Ext.get('prospect_reader');
		this.setApiParams({docIndex: 0, start: 0});
		el.update('');
		this.fetchText();
	}
	
	,movePrevious : function() {
		var limit = parseInt(this.getApiParamValue('limit'));
		var el = Ext.get('prospect_reader');
		var segmentObject = this.getSegmentObject(el.first());
		if (segmentObject) {
			var doc = segmentObject.docIndex;
			var start = segmentObject.start;
			if (doc==0 && start==0) {return;} // already at start
			if (start==0) { // move to end of previous document
				doc -= 1;
				this.setApiParams({docIndex: doc});
				start = this.getCorpus().getDocument(doc).getTotalTokens()-limit;
				if (start<0) {start=0}
			} else {
				if (start-limit>-1) {start -= limit;}
				else {
					limit = parseInt(start);
					start = 0;
				}
			}
			this.setApiParams({start: start});
			el.update('');
			this.fetchText();
		}
	}
	
	,moveNext : function() {
		var limit = parseInt(this.getApiParamValue('limit'));
		var el = Ext.get('prospect_reader');
		var segmentObject = this.getSegmentObject(el.last());
		if (segmentObject && segmentObject.docIndex != null) {
			var doc = segmentObject.docIndex;
			var start = segmentObject.start;
			var lastLimit = segmentObject.lastLimit;
			var numberOfDocs = this.getCorpus().getSize();
			var totalWords = this.getCorpus().getDocument(doc).getTotalTokens();
			if (doc+1==numberOfDocs && start+lastLimit>=totalWords) {return;} // already at end
			if (start+lastLimit>=totalWords) { // move to end of previous document
				doc += 1;
				this.setApiParams({docIndex: doc});
				start = 0;
			}
			else { // move ahead
				start+=limit;
			}
			this.setApiParams({start: start});
			el.update('');
			this.fetchText();
		}
	}
	
	,moveLast : function() {
		var el = Ext.get('prospect_reader');
		var limit = parseInt(this.getApiParamValue('limit'));
		var lastDocIndex = this.getCorpus().getSize()-1;
		var totalWords = this.getCorpus().getDocument(lastDocIndex).getTotalTokens();
		var start = totalWords - limit;
		this.setApiParams({docIndex: lastDocIndex, start: start});
		el.update('');
		this.fetchText();
	}
	
	,highlightKeywords: function(query, doScroll) {
		var reader = Ext.get('prospect_reader');
		var nodes = Ext.DomQuery.select('span[class*=keyword]', reader.dom);
		for (var i=0; i<nodes.length;i++) {
			Ext.get(nodes[i]).removeClass('keyword');
		}
		nodes = Ext.DomQuery.select('span.token:nodeValueCaseInsensitive('+query+')', reader.dom);
		if (nodes.length>0) {			
			for (var i=0; i<nodes.length;i++) {
				Ext.get(nodes[i]).addClass('keyword');
			}
			if (doScroll) Ext.get(nodes[0]).scrollIntoView(reader).frame("ff0000", 1, { duration: 2 });
		}
	}
	
	,showProspectHits : function(query) {
		this.highlightKeywords(query, true);

		var docIdTypes = [];
		var maxLines = 1;
		this.documents.each(function(doc, index) {
			docIdTypes.push(doc.document.getId()+':'+query);
			if (doc.lines > maxLines) maxLines = doc.lines;
		}, this);
		if (this.documents.getCount() > 1) {
			// round up to nearest 100
			maxLines /= 100;
			maxLines = Math.ceil(maxLines);
			maxLines *= 100;
		}
		
		Ext.Ajax.request({
		   url: this.getTromboneUrl(),
		   params: {
				tool: 'DocumentTypeFrequencies'
				,docIdType: docIdTypes
				,bins: maxLines
				,corpus: this.getCorpus().getId()
		   },
		   success: function(response, options) {
				var json = Ext.decode(response.responseText);
				var store = new Ext.data.JsonStore({
					fields: Voyeur.data.DocumentTypes.fields
					,data: json.documentTypes.types
				});
				
				var min,				// min value
					max,				// max value
					absMax,				// absolute max
					vals,				// raw frequency distribution values
					index,				// document index
					lines,				// number of lines per document
					valsPerLine,		// number of values per line
					extraValsPerLine,	// the remainder, per line
					extraCount,			// tracker for when to add another value to the current line
					valsCount,			// tracker to ensure all values have been used
					sum,				// sum of values for the current line
					lineSums,			// array to store the value sums for each line
					op,					// opacity
					i, j,				// counters
					valsToUse;			// how many values to sum for the current line
				
				store.each(function(item) {
					max = null;//item.get('rawDistributionMax');
					min = null;//item.get('rawDistributionMin');
					vals = item.get('rawFreqs');
					index = item.get('docIndex');
					
					lines = this.documents.get(index).lines;
					valsPerLine = Math.floor(vals.length / lines);
					extraValsPerLine = vals.length % lines / lines;
					extraCount = 0;
					valsCount = 0;
					
					lineSums = [];
					for (i = 0; i < lines; i++) {
						sum = 0;
						valsToUse = i * valsPerLine + valsPerLine;
						extraCount += extraValsPerLine;
						if (extraCount >= 1) {
							valsToUse++;
							extraCount -= 1;
						}
						for (j = i * valsPerLine; j < valsToUse; j++) {
							valsCount++;
							sum += vals[j];
						}
						lineSums[i] = sum;
						if (max == null || max < sum) max = sum;
						if (min == null || min > sum) min = sum;
					}
					if (valsCount < vals.length) {
						lineSums[lineSums.length-1] += vals[vals.length-1];
					}
					absMax = max - min;
					
					for (i = 0; i < lines; i++) {
						var el = Ext.get('prospect.'+index+'.'+i);
						if (el) {
							op = max > 0 ? (lineSums[i] - min) / absMax : 0;
							if (op < .05) op = .05;
							el.setOpacity(op);
						} else {
//							console.warn(el,op,index,i);
						}
					}
				}, this);
		   },
		   failure: function(response, options) {
			   var imgs = Ext.DomQuery.jsSelect('img', this.body.dom);
			   for (var i = 0; i < imgs.length; i++) {
				   Ext.get(imgs[i]).setOpacity(.05);
			   }
		   },
		   scope: this
		});
	}
	
	,highlightProspect: function(docIndex, offset) {
		var doc = this.documents.get(docIndex);
		var line = Math.round(doc.lines * offset / doc.document.getTotalTokens());
		//console.log('finding', docIndex, offset, line);
		var el = Ext.get('prospect.'+docIndex+'.'+line);
		if (this.currentHighlightedProspect) {
			if (this.currentHighlightedProspect==el) {return;}
			this.currentHighlightedProspect.removeClass('current');
		}
		if (el) {
			el.addClass('current').frame("ff0000", 1, { duration: 2 });
			this.currentHighlightedProspect = el;
		}
	}
	
	,getCurrentSegmentObject: function() {
		var el = Ext.get('prospect_reader');
		var scrollTop = el.dom.scrollTop;
		var segments = el.dom.childNodes;
		for (var i=0;i<segments.length;i++) {
			if (segments[i].nodeType==1) {
				if (scrollTop>=segments[i].offsetTop && scrollTop<=segments[i].offsetTop+segments[i].offsetHeight) {
					return this.getSegmentObject(segments[i]);
				}
			}
		}
		return this.getSegmentObject(el.first())
	}
	
	,getSegmentObject: function(segment) {
		var className = segment.className ? segment.className : segment.dom.className;
		var segmentRegex = /segment_(\d+)_(\d+)_(\d+)/;
		var match = segmentRegex.exec(className)
		if (match) {
			return {
				element: segment
				,docIndex: parseInt(match[1])
				,start: parseInt(match[2])
				,lastLimit: parseInt(match[3])
			}
		}
		return {};
		
	}
	 
	,api: {
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 1000
		 */
		,limit: {'default': 1000}
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default 0
		 */
		,docIndex: {'default': 0}
		,toolType: ['Corpus']
		,listeners: ['CorpusSummaryResultLoaded', 'TokensResultLoaded', 'documentTypeSelected', 'tokenSelected']
		,dispatchers: ['documentTypeSelected']
	}
	
	,thumb: {
		large: 'Reader.png'
	}
	
	,i18n : {
		title : {
			en : 'Corpus Reader'
		},
		help: {en: ''}
		,startText: {en: 'Jump to the start of the text'}
		,previousText: {en: 'Fetch preceding segment of text'}
		,nextText: {en: 'Fetch next segment of text'}
		,endText: {en: 'Jump to the end of the text'}
	}
});
Ext.reg('voyeurReader', Voyeur.Tool.Reader);

