<commit_before>/**
 * Search
 *
 * Basically just an improved version of utilichart
 *
 * Dependencies: jQuery, battledata, search-index
 * Optional dependencies: pokedex, moves, items, abilities
 *
 * @author  <NAME> <Zarel>
 */

(function (exports, $) {
	'use strict';

	function Search(elem, viewport) {
		this.$el = $(elem);
		this.el = this.$el[0];
		this.$viewport = (viewport ? $(viewport) : $(window));

		this.urlRoot = '';
		this.q = undefined; // uninitialized
		this.qType = '';
		this.defaultResultSet = null;
		this.legalityFilter = null;
		this.legalityLabel = "Illegal";
		this.exactMatch = false;

		this.resultSet = null;
		this.filters = null;
		this.renderedIndex = 0;
		this.renderingDone = true;
		this.cur = {};
		this.$inputEl = null;
		this.gen = 6;

		var self = this;
		this.$el.on('click', '.more button', function (e) {
			e.preventDefault();
			self.updateScroll(true);
		});
		this.$el.on('click', '.filter', function (e) {
			e.preventDefault();
			if (!self.filters) return;
			// delete filter
			for (var i = 0; i < self.filters.length; i++) {
				if (e.currentTarget.value === self.filters[i].join(':')) {
					self.filters.splice(i, 1);
					if (!self.filters.length) self.filters = null;
					self.q = undefined;
					self.find('');
					break;
				}
			}
			if (self.$inputEl) self.$inputEl.focus();
		});
	}

	Search.prototype.$ = function (query) {
		return this.$el.find(query);
	};

	//
	// Search functions
	//

	var typeTable = {
		pokemon: 1,
		type: 2,
		move: 3,
		item: 4,
		ability: 5,
		egggroup: 6,
		category: 7
	};
	var typeName = {
		pokemon: 'Pokemon',
		type: 'Type',
		move: 'Moves',
		item: 'Items',
		ability: 'Abilities',
		egggroup: 'Egg group',
		category: 'Category'
	};
	Search.prototype.find = function (query) {
		query = toId(query);

		if (query === this.q) {
			return false;
		}
		this.q = query;
		this.resultSet = null;
		this.exactMatch = false;
		var qType = this.qType;

		if (!query) {
			// search field is blank, display default results

			if (!this.filters && this.defaultResultSet) {
				this.renderedIndex = 0;
				this.renderingDone = false;
				this.updateScroll();
				return true;
			}
			if (qType === 'pokemon') {
				this.allPokemon();
				return true;
			} else if (qType === 'move') {
				this.allMoves();
				return true;
			}
			this.el.innerHTML = '';
			return true;
		}

		// If qType exists, we're searching mainly for results of that type.
		// We'll still search for results of other types, but those results
		// will only be used to filter results for that type.
		var qTypeIndex = (qType ? typeTable[qType] : -1);

		var qFilterType = '';
		if (query.slice(-4) === 'type') {
			if ((query.charAt(0).toUpperCase() + query.slice(1, -4)) in window.BattleTypeChart) {
				query = query.slice(0, -4);
				qFilterType = 'type';
			}
		}

		// i represents the location of the search index we're looking
		var i = Search.getClosest(query);
		this.exactMatch = (BattleSearchIndex[i][0] === query);

		// Even with output buffer buckets, we make multiple passes through
		// the search index. searchPasses is a queue of which pass we're on:
		// [passType, i, query]

		// pass types:
		// 0 - no pass: time to pop the next pass off the searchPasses queue
		// 1 - normal pass: start at i and stop when results no longer start with query
		// 2 - alias pass: like normal, but output aliases instead of non-alias results
		// 3 - fuzzy match pass: start at i and stop when you have two results

		// By doing an alias pass after the normal pass, we ensure that
		// mid-word matches only display after start matches.
		var passType = 0;
		var searchPasses = [[1, i, query]];

		// For performance reasons, only do an alias pass if query is at
		// least 2 chars long
		if (query.length > 1) searchPasses.push([2, i, query]);

		// If the query matches an official alias in BattleAliases: These are
		// different from the aliases in the search index and are given
		// higher priority. We'll do a normal pass through the index with
		// the alias text before any other passes.
		var queryAlias;
		if (query in BattleAliases) {
			if (toId(BattleAliases[query]).slice(0, query.length) !== query) {
				queryAlias = toId(BattleAliases[query]);
				if (queryAlias.slice(0, 11) !== 'hiddenpower') {
					searchPasses.unshift([1, Search.getClosest(queryAlias), queryAlias]);
				}
			}
			this.exactMatch = true;
		}

		// If there are no matches starting with query: Do a fuzzy match pass
		// Fuzzy matches will still be shown after alias matches
		if (!this.exactMatch && BattleSearchIndex[i][0].substr(0, query.length) !== query) {
			// No results start with this. Do a fuzzy match pass.
			var matchLength = query.length - 1;
			if (!i) i++;
			while (matchLength &&
				BattleSearchIndex[i][0].substr(0, matchLength) !== query.substr(0, matchLength) &&
				BattleSearchIndex[i - 1][0].substr(0, matchLength) !== query.substr(0, matchLength)) {
				matchLength--;
			}
			var matchQuery = query.substr(0, matchLength);
			while (i >= 1 && BattleSearchIndex[i - 1][0].substr(0, matchLength) === matchQuery) i--;
			searchPasses.push([3, i, '']);
		}

		// We split the output buffers into 8 buckets.
		// Bucket 0 is usually unused, and buckets 1-7 represent
		// pokemon, types, moves, etc (see typeTable).

		// When we're done, the buffers are concatenated together to form
		// our resultSet, with each buffer getting its own header, unlike
		// multiple-pass results, which have no header.

		// Notes:
		// - if we have a qType, that qType's buffer will be on top
		var bufs = [[], [], [], [], [], [], [], []];
		var topbufIndex = -1;

		var count = 0;
		var nearMatch = false;

		var instafilter = null;
		var instafilterSort = [0, 1, 2, 5, 4, 3, 6, 7];

		// We aren't actually looping through the entirety of the searchIndex
		for (i = 0; i < BattleSearchIndex.length; i++) {
			if (!passType) {
				var searchPass = searchPasses.shift();
				if (!searchPass) break;
				passType = searchPass[0];
				i = searchPass[1];
				query = searchPass[2];
			}

			var entry = BattleSearchIndex[i];
			var id = entry[0];
			var type = entry[1];

			if (!id) break;

			if (passType === 3) {
				// fuzzy match pass; stop after 2 results
				if (count >= 2) {
					passType = 0;
					continue;
				}
				nearMatch = true;
			} else if (id.substr(0, query.length) !== query) {
				// regular pass, time to move onto our next match
				passType = 0;
				continue;
			}

			if (entry.length > 2) {
				// alias entry
				if (passType !== 2) continue;
			} else {
				// normal entry
				if (passType === 2) continue;
			}

			var typeIndex = typeTable[type];

			// For performance, with a query length of 1, we only fill the first bucket
			if (query.length === 1 && typeIndex !== (qType ? qTypeIndex : 1)) continue;

			// For pokemon queries, accept types/abilities/moves/eggroups as filters
			if (qType === 'pokemon' && (typeIndex === 4 || typeIndex > 6)) continue;
			// For move queries, accept types/categories/pokemon as filters
			if (qType === 'move' && (typeIndex !== 7 && typeIndex > 3)) continue;
			// For move queries in the teambuilder, don't accept pokemon as filters
			if (qType === 'move' && this.legalityFilter && typeIndex === 1) continue;
			// For ability/item queries, don't accept anything else as a filter
			if ((qType === 'ability' || qType === 'item') && typeIndex !== qTypeIndex) continue;
			// Query was a type name followed 'type'; only show types
			if (qFilterType === 'type' && typeIndex !== 2) continue;

			var matchStart = 0;
			var matchLength = 0;
			if (passType === 2) {
				// alias entry
				// [aliasid, type, originalid, matchStart, originalindex]
				matchStart = entry[3];
				var originalIndex = entry[2];
				if (matchStart) {
					matchLength = matchStart + query.length;
					matchStart += (BattleSearchIndexOffset[originalIndex][matchStart] || '0').charCodeAt(0) - 48;
					matchLength += (BattleSearchIndexOffset[originalIndex][matchLength - 1] || '0').charCodeAt(0) - 48 - matchStart;
				}
				id = BattleSearchIndex[originalIndex][0];
			} else {
				matchLength = query.length;
				if (matchLength) matchLength += (BattleSearchIndexOffset[i][matchLength - 1] || '0').charCodeAt(0) - 48;
			}

			// some aliases are substrings
			if (queryAlias === id && query !== id) continue;

			if (qType && qTypeIndex !== typeIndex) {
				// This is a filter, set it as an instafilter candidate
				if (!instafilter || instafilterSort[typeIndex] < instafilterSort[instafilter[2]]) {
					instafilter = [type, id, typeIndex];
				}
			}

			if (this.legalityFilter && typeIndex === qTypeIndex) {
				// Always show illegal results under legal results.
				// This is done by putting legal results (and the type header)
				// in bucket 0, and illegal results in the qType's bucket.
				// qType buckets are always on top (but under bucket 0), so
				// illegal results will be seamlessly right under legal results.
				if (!bufs[typeIndex].length && !bufs[0].length) bufs[0] = [['header', typeName[type]]];
				if (id in this.legalityFilter) typeIndex = 0;
			} else {
				if (!bufs[typeIndex].length) bufs[typeIndex] = [['header', typeName[type]]];
			}

			bufs[typeIndex].push([type, id, matchStart, matchLength]);

			count++;
		}

		var topbuf = [];
		if (nearMatch) topbuf = [['html', '<p><em>No exact match found. The closest matches alphabetically are:</em></p></li>']];
		if (this.filters) {
			topbuf.push(['html', this.getFilterText(1)]);
		}
		if (qTypeIndex >= 0) {
			topbuf = topbuf.concat(bufs[0]);
			topbuf = topbuf.concat(bufs[qTypeIndex]);
			bufs[qTypeIndex] = [];
			bufs[0] = [];
		} else if (topbufIndex >= 0) {
			topbuf = topbuf.concat(bufs[topbufIndex]);
			bufs[topbufIndex] = [];
		}
		if (query === 'grass' || query === 'flying') {
			topbuf = topbuf.concat(bufs[5]);
			bufs[5] = [];
		}

		if (instafilter && count < 20) {
			// There's exactly one filter matched and count is less than 20, instafilter
			bufs.push(this.instafilter(qType, instafilter[0], instafilter[1]));
		}

		this.resultSet = Array.prototype.concat.apply(topbuf, bufs);
		this.renderedIndex = 0;
		this.renderingDone = false;
		this.updateScroll();
		return true;
	};
	Search.prototype.instafilter = function (qType, fType, fId) {
		var buf = [];
		var illegalBuf = [];
		var legal = this.legalityFilter;
		if (qType === 'pokemon') {
			switch (fType) {
			case 'type':
				var type = fId.charAt(0).toUpperCase() + fId.slice(1);
				buf.push(['header', "" + type + "-type Pok&eacute;mon"]);
				for (var id in BattlePokedex) {
					if (!BattlePokedex[id].types) continue;
					if (BattlePokedex[id].types[0] === type || BattlePokedex[id].types[1] === type) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['pokemon', id]);
					}
				}
				break;
			case 'ability':
				var ability = Tools.getAbility(fId).name;
				buf.push(['header', "" + ability + " Pok&eacute;mon"]);
				for (var id in BattlePokedex) {
					if (!BattlePokedex[id].abilities) continue;
					if (BattlePokedex[id].abilities['0'] === ability || BattlePokedex[id].abilities['1'] === ability || BattlePokedex[id].abilities['H'] === ability) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['pokemon', id]);
					}
				}
				break;
			}
		} else if (qType === 'move') {
			switch (fType) {
			case 'type':
				var type = fId.charAt(0).toUpperCase() + fId.slice(1);
				buf.push(['header', "" + type + "-type moves"]);
				for (var id in BattleMovedex) {
					if (BattleMovedex[id].type === type) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['move', id]);
					}
				}
				break;
			case 'category':
				var category = fId.charAt(0).toUpperCase() + fId.slice(1);
				buf.push(['header', "" + category + " moves"]);
				for (var id in BattleMovedex) {
					if (BattleMovedex[id].category === category) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['move', id]);
					}
				}
				break;
			}
		}
		return buf.concat(illegalBuf);
	};
	Search.prototype.addFilter = function (node) {
		if (!node.dataset.entry) return;
		var entry = node.dataset.entry.split(':');
		if (this.qType === 'pokemon') {
			if (entry[0] !== 'type' && entry[0] !== 'move' && entry[0] !== 'ability' && entry[0] !== 'egggroup') return;
			if (entry[0] === 'move') entry[1] = toId(entry[1]);
			if (!this.filters) this.filters = [];
			this.q = undefined;
			for (var i = 0; i < this.filters.length; i++) {
				if (this.filters[i][0] === entry[0] && this.filters[i][1] === entry[1]) {
					return true;
				}
			}
			this.filters.push(entry);
			return true;
		} else if (this.qType === 'move') {
			if (entry[0] !== 'type' && entry[0] !== 'category' && entry[0] !== 'pokemon') return;
			if (entry[0] === 'pokemon') entry[1] = toId(entry[1]);
			if (!this.filters) this.filters = [];
			this.filters.push(entry);
			this.q = undefined;
			return true;
		}
	};
	Search.prototype.removeFilter = function () {
		if (!this.filters) return;
		this.filters.pop();
		if (!this.filters.length) this.filters = null;
		this.q = undefined;
		return true;
	};
	Search.prototype.allPokemon = function () {
		if (this.filters) return this.filteredPokemon();
		var resultSet = [];
		for (var id in BattlePokedex) {
			switch (id) {
			case 'bulbasaur':
				resultSet.push(['header', "Generation 1"]);
				break;
			case 'chikorita':
				resultSet.push(['header', "Generation 2"]);
				break;
			case 'treecko':
				resultSet.push(['header', "Generation 3"]);
				break;
			case 'turtwig':
				resultSet.push(['header', "Generation 4"]);
				break;
			case 'victini':
				resultSet.push(['header', "Generation 5"]);
				break;
			case 'chespin':
				resultSet.push(['header', "Generation 6"]);
				break;
			case 'missingno':
				resultSet.push(['header', "Glitch"]);
				break;
			case 'tomohawk':
				resultSet.push(['header', "CAP"]);
				break;
			case 'pikachucosplay':
				continue;
			}
			resultSet.push(['pokemon', id]);
		}
		this.resultSet = resultSet;
		this.renderedIndex = 0;
		this.renderingDone = false;
		this.updateScroll();
	};
	Search.prototype.allMoves = function () {
		if (this.filters) return this.filteredMoves();
		var resultSet = [];
		resultSet.push(['header', "Moves"]);
		for (var id in BattleMovedex) {
			switch (id) {
			case 'paleowave':
				resultSet.push(['header', "CAP moves"]);
				break;
			case 'magikarpsrevenge':
				continue;
			}
			resultSet.push(['move', id]);
		}
		this.resultSet = resultSet;
		this.renderedIndex = 0;
		this.renderingDone = false;
		this.updateScroll();
	};
	Search.prototype.getFilterText = function (q) {
		var buf = '<p>Filters: ';
		for (var i = 0; i < this.filters.length; i++) {
			var text = this.filters[i][1];
			if (this.filters[i][0] === 'move') text = Tools.getMove(text).name;
			if (this.filters[i][0] === 'pokemon') text = Tools.getTemplate(text).name;
			buf += '<button class="filter" value="' + Tools.escapeHTML(this.filters[i].join(':')) + '">' + text + ' <i class="fa fa-times-circle"></i></button> ';
		}
		if (!q) buf += '<small style="color: #888">(backspace = delete filter)</small>';
		return buf + '</p>';
	};
	Search.prototype.filteredPokemon = function () {
		var resultSet = [['html', this.getFilterText()]];
		resultSet.push(['header', "Filtered results"]);
		var illegalResultSet = [];
		var filters = this.filters;
		var genChar = '' + this.gen;
		for (var id in BattlePokedex) {
			var template = BattlePokedex[id];
			if (template.exists === false) continue;
			for (var i = 0; i < filters.length; i++) {
				if (filters[i][0] === 'type') {
					var type = filters[i][1];
					if (template.types[0] !== type && template.types[1] !== type) break;
				} else if (filters[i][0] === 'egggroup') {
					var egggroup = filters[i][1];
					if (template.eggGroups[0] !== egggroup && template.eggGroups[1] !== egggroup) break;
				} else if (filters[i][0] === 'ability') {
					var ability = filters[i][1];
					if (template.abilities['0'] !== ability && template.abilities['1'] !== ability && template.abilities['H'] !== ability) break;
				} else if (filters[i][0] === 'move') {
					var learned = false;
					var learnsetid = id;
					if (!(learnsetid in BattleTeambuilderTable.learnsets)) {
						learnsetid = toId(BattlePokedex[learnsetid].baseSpecies);
					}
					while (true) {
						var learnset = BattleTeambuilderTable.learnsets[learnsetid];<commit_msg>Teambuilder: Fix crash when filtering egg groups<commit_after>/**
 * Search
 *
 * Basically just an improved version of utilichart
 *
 * Dependencies: jQuery, battledata, search-index
 * Optional dependencies: pokedex, moves, items, abilities
 *
 * @author  <NAME> <Zarel>
 */

(function (exports, $) {
	'use strict';

	function Search(elem, viewport) {
		this.$el = $(elem);
		this.el = this.$el[0];
		this.$viewport = (viewport ? $(viewport) : $(window));

		this.urlRoot = '';
		this.q = undefined; // uninitialized
		this.qType = '';
		this.defaultResultSet = null;
		this.legalityFilter = null;
		this.legalityLabel = "Illegal";
		this.exactMatch = false;

		this.resultSet = null;
		this.filters = null;
		this.renderedIndex = 0;
		this.renderingDone = true;
		this.cur = {};
		this.$inputEl = null;
		this.gen = 6;

		var self = this;
		this.$el.on('click', '.more button', function (e) {
			e.preventDefault();
			self.updateScroll(true);
		});
		this.$el.on('click', '.filter', function (e) {
			e.preventDefault();
			if (!self.filters) return;
			// delete filter
			for (var i = 0; i < self.filters.length; i++) {
				if (e.currentTarget.value === self.filters[i].join(':')) {
					self.filters.splice(i, 1);
					if (!self.filters.length) self.filters = null;
					self.q = undefined;
					self.find('');
					break;
				}
			}
			if (self.$inputEl) self.$inputEl.focus();
		});
	}

	Search.prototype.$ = function (query) {
		return this.$el.find(query);
	};

	//
	// Search functions
	//

	var typeTable = {
		pokemon: 1,
		type: 2,
		move: 3,
		item: 4,
		ability: 5,
		egggroup: 6,
		category: 7
	};
	var typeName = {
		pokemon: 'Pokemon',
		type: 'Type',
		move: 'Moves',
		item: 'Items',
		ability: 'Abilities',
		egggroup: 'Egg group',
		category: 'Category'
	};
	Search.prototype.find = function (query) {
		query = toId(query);

		if (query === this.q) {
			return false;
		}
		this.q = query;
		this.resultSet = null;
		this.exactMatch = false;
		var qType = this.qType;

		if (!query) {
			// search field is blank, display default results

			if (!this.filters && this.defaultResultSet) {
				this.renderedIndex = 0;
				this.renderingDone = false;
				this.updateScroll();
				return true;
			}
			if (qType === 'pokemon') {
				this.allPokemon();
				return true;
			} else if (qType === 'move') {
				this.allMoves();
				return true;
			}
			this.el.innerHTML = '';
			return true;
		}

		// If qType exists, we're searching mainly for results of that type.
		// We'll still search for results of other types, but those results
		// will only be used to filter results for that type.
		var qTypeIndex = (qType ? typeTable[qType] : -1);

		var qFilterType = '';
		if (query.slice(-4) === 'type') {
			if ((query.charAt(0).toUpperCase() + query.slice(1, -4)) in window.BattleTypeChart) {
				query = query.slice(0, -4);
				qFilterType = 'type';
			}
		}

		// i represents the location of the search index we're looking
		var i = Search.getClosest(query);
		this.exactMatch = (BattleSearchIndex[i][0] === query);

		// Even with output buffer buckets, we make multiple passes through
		// the search index. searchPasses is a queue of which pass we're on:
		// [passType, i, query]

		// pass types:
		// 0 - no pass: time to pop the next pass off the searchPasses queue
		// 1 - normal pass: start at i and stop when results no longer start with query
		// 2 - alias pass: like normal, but output aliases instead of non-alias results
		// 3 - fuzzy match pass: start at i and stop when you have two results

		// By doing an alias pass after the normal pass, we ensure that
		// mid-word matches only display after start matches.
		var passType = 0;
		var searchPasses = [[1, i, query]];

		// For performance reasons, only do an alias pass if query is at
		// least 2 chars long
		if (query.length > 1) searchPasses.push([2, i, query]);

		// If the query matches an official alias in BattleAliases: These are
		// different from the aliases in the search index and are given
		// higher priority. We'll do a normal pass through the index with
		// the alias text before any other passes.
		var queryAlias;
		if (query in BattleAliases) {
			if (toId(BattleAliases[query]).slice(0, query.length) !== query) {
				queryAlias = toId(BattleAliases[query]);
				if (queryAlias.slice(0, 11) !== 'hiddenpower') {
					searchPasses.unshift([1, Search.getClosest(queryAlias), queryAlias]);
				}
			}
			this.exactMatch = true;
		}

		// If there are no matches starting with query: Do a fuzzy match pass
		// Fuzzy matches will still be shown after alias matches
		if (!this.exactMatch && BattleSearchIndex[i][0].substr(0, query.length) !== query) {
			// No results start with this. Do a fuzzy match pass.
			var matchLength = query.length - 1;
			if (!i) i++;
			while (matchLength &&
				BattleSearchIndex[i][0].substr(0, matchLength) !== query.substr(0, matchLength) &&
				BattleSearchIndex[i - 1][0].substr(0, matchLength) !== query.substr(0, matchLength)) {
				matchLength--;
			}
			var matchQuery = query.substr(0, matchLength);
			while (i >= 1 && BattleSearchIndex[i - 1][0].substr(0, matchLength) === matchQuery) i--;
			searchPasses.push([3, i, '']);
		}

		// We split the output buffers into 8 buckets.
		// Bucket 0 is usually unused, and buckets 1-7 represent
		// pokemon, types, moves, etc (see typeTable).

		// When we're done, the buffers are concatenated together to form
		// our resultSet, with each buffer getting its own header, unlike
		// multiple-pass results, which have no header.

		// Notes:
		// - if we have a qType, that qType's buffer will be on top
		var bufs = [[], [], [], [], [], [], [], []];
		var topbufIndex = -1;

		var count = 0;
		var nearMatch = false;

		var instafilter = null;
		var instafilterSort = [0, 1, 2, 5, 4, 3, 6, 7];

		// We aren't actually looping through the entirety of the searchIndex
		for (i = 0; i < BattleSearchIndex.length; i++) {
			if (!passType) {
				var searchPass = searchPasses.shift();
				if (!searchPass) break;
				passType = searchPass[0];
				i = searchPass[1];
				query = searchPass[2];
			}

			var entry = BattleSearchIndex[i];
			var id = entry[0];
			var type = entry[1];

			if (!id) break;

			if (passType === 3) {
				// fuzzy match pass; stop after 2 results
				if (count >= 2) {
					passType = 0;
					continue;
				}
				nearMatch = true;
			} else if (id.substr(0, query.length) !== query) {
				// regular pass, time to move onto our next match
				passType = 0;
				continue;
			}

			if (entry.length > 2) {
				// alias entry
				if (passType !== 2) continue;
			} else {
				// normal entry
				if (passType === 2) continue;
			}

			var typeIndex = typeTable[type];

			// For performance, with a query length of 1, we only fill the first bucket
			if (query.length === 1 && typeIndex !== (qType ? qTypeIndex : 1)) continue;

			// For pokemon queries, accept types/abilities/moves/eggroups as filters
			if (qType === 'pokemon' && (typeIndex === 4 || typeIndex > 6)) continue;
			// For move queries, accept types/categories/pokemon as filters
			if (qType === 'move' && (typeIndex !== 7 && typeIndex > 3)) continue;
			// For move queries in the teambuilder, don't accept pokemon as filters
			if (qType === 'move' && this.legalityFilter && typeIndex === 1) continue;
			// For ability/item queries, don't accept anything else as a filter
			if ((qType === 'ability' || qType === 'item') && typeIndex !== qTypeIndex) continue;
			// Query was a type name followed 'type'; only show types
			if (qFilterType === 'type' && typeIndex !== 2) continue;

			var matchStart = 0;
			var matchLength = 0;
			if (passType === 2) {
				// alias entry
				// [aliasid, type, originalid, matchStart, originalindex]
				matchStart = entry[3];
				var originalIndex = entry[2];
				if (matchStart) {
					matchLength = matchStart + query.length;
					matchStart += (BattleSearchIndexOffset[originalIndex][matchStart] || '0').charCodeAt(0) - 48;
					matchLength += (BattleSearchIndexOffset[originalIndex][matchLength - 1] || '0').charCodeAt(0) - 48 - matchStart;
				}
				id = BattleSearchIndex[originalIndex][0];
			} else {
				matchLength = query.length;
				if (matchLength) matchLength += (BattleSearchIndexOffset[i][matchLength - 1] || '0').charCodeAt(0) - 48;
			}

			// some aliases are substrings
			if (queryAlias === id && query !== id) continue;

			if (qType && qTypeIndex !== typeIndex) {
				// This is a filter, set it as an instafilter candidate
				if (!instafilter || instafilterSort[typeIndex] < instafilterSort[instafilter[2]]) {
					instafilter = [type, id, typeIndex];
				}
			}

			if (this.legalityFilter && typeIndex === qTypeIndex) {
				// Always show illegal results under legal results.
				// This is done by putting legal results (and the type header)
				// in bucket 0, and illegal results in the qType's bucket.
				// qType buckets are always on top (but under bucket 0), so
				// illegal results will be seamlessly right under legal results.
				if (!bufs[typeIndex].length && !bufs[0].length) bufs[0] = [['header', typeName[type]]];
				if (id in this.legalityFilter) typeIndex = 0;
			} else {
				if (!bufs[typeIndex].length) bufs[typeIndex] = [['header', typeName[type]]];
			}

			bufs[typeIndex].push([type, id, matchStart, matchLength]);

			count++;
		}

		var topbuf = [];
		if (nearMatch) topbuf = [['html', '<p><em>No exact match found. The closest matches alphabetically are:</em></p></li>']];
		if (this.filters) {
			topbuf.push(['html', this.getFilterText(1)]);
		}
		if (qTypeIndex >= 0) {
			topbuf = topbuf.concat(bufs[0]);
			topbuf = topbuf.concat(bufs[qTypeIndex]);
			bufs[qTypeIndex] = [];
			bufs[0] = [];
		} else if (topbufIndex >= 0) {
			topbuf = topbuf.concat(bufs[topbufIndex]);
			bufs[topbufIndex] = [];
		}
		if (query === 'grass' || query === 'flying') {
			topbuf = topbuf.concat(bufs[5]);
			bufs[5] = [];
		}

		if (instafilter && count < 20) {
			// There's exactly one filter matched and count is less than 20, instafilter
			bufs.push(this.instafilter(qType, instafilter[0], instafilter[1]));
		}

		this.resultSet = Array.prototype.concat.apply(topbuf, bufs);
		this.renderedIndex = 0;
		this.renderingDone = false;
		this.updateScroll();
		return true;
	};
	Search.prototype.instafilter = function (qType, fType, fId) {
		var buf = [];
		var illegalBuf = [];
		var legal = this.legalityFilter;
		if (qType === 'pokemon') {
			switch (fType) {
			case 'type':
				var type = fId.charAt(0).toUpperCase() + fId.slice(1);
				buf.push(['header', "" + type + "-type Pok&eacute;mon"]);
				for (var id in BattlePokedex) {
					if (!BattlePokedex[id].types) continue;
					if (BattlePokedex[id].types[0] === type || BattlePokedex[id].types[1] === type) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['pokemon', id]);
					}
				}
				break;
			case 'ability':
				var ability = Tools.getAbility(fId).name;
				buf.push(['header', "" + ability + " Pok&eacute;mon"]);
				for (var id in BattlePokedex) {
					if (!BattlePokedex[id].abilities) continue;
					if (BattlePokedex[id].abilities['0'] === ability || BattlePokedex[id].abilities['1'] === ability || BattlePokedex[id].abilities['H'] === ability) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['pokemon', id]);
					}
				}
				break;
			}
		} else if (qType === 'move') {
			switch (fType) {
			case 'type':
				var type = fId.charAt(0).toUpperCase() + fId.slice(1);
				buf.push(['header', "" + type + "-type moves"]);
				for (var id in BattleMovedex) {
					if (BattleMovedex[id].type === type) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['move', id]);
					}
				}
				break;
			case 'category':
				var category = fId.charAt(0).toUpperCase() + fId.slice(1);
				buf.push(['header', "" + category + " moves"]);
				for (var id in BattleMovedex) {
					if (BattleMovedex[id].category === category) {
						(legal && !(id in legal) ? illegalBuf : buf).push(['move', id]);
					}
				}
				break;
			}
		}
		return buf.concat(illegalBuf);
	};
	Search.prototype.addFilter = function (node) {
		if (!node.dataset.entry) return;
		var entry = node.dataset.entry.split(':');
		if (this.qType === 'pokemon') {
			if (entry[0] !== 'type' && entry[0] !== 'move' && entry[0] !== 'ability' && entry[0] !== 'egggroup') return;
			if (entry[0] === 'move') entry[1] = toId(entry[1]);
			if (!this.filters) this.filters = [];
			this.q = undefined;
			for (var i = 0; i < this.filters.length; i++) {
				if (this.filters[i][0] === entry[0] && this.filters[i][1] === entry[1]) {
					return true;
				}
			}
			this.filters.push(entry);
			return true;
		} else if (this.qType === 'move') {
			if (entry[0] !== 'type' && entry[0] !== 'category' && entry[0] !== 'pokemon') return;
			if (entry[0] === 'pokemon') entry[1] = toId(entry[1]);
			if (!this.filters) this.filters = [];
			this.filters.push(entry);
			this.q = undefined;
			return true;
		}
	};
	Search.prototype.removeFilter = function () {
		if (!this.filters) return;
		this.filters.pop();
		if (!this.filters.length) this.filters = null;
		this.q = undefined;
		return true;
	};
	Search.prototype.allPokemon = function () {
		if (this.filters) return this.filteredPokemon();
		var resultSet = [];
		for (var id in BattlePokedex) {
			switch (id) {
			case 'bulbasaur':
				resultSet.push(['header', "Generation 1"]);
				break;
			case 'chikorita':
				resultSet.push(['header', "Generation 2"]);
				break;
			case 'treecko':
				resultSet.push(['header', "Generation 3"]);
				break;
			case 'turtwig':
				resultSet.push(['header', "Generation 4"]);
				break;
			case 'victini':
				resultSet.push(['header', "Generation 5"]);
				break;
			case 'chespin':
				resultSet.push(['header', "Generation 6"]);
				break;
			case 'missingno':
				resultSet.push(['header', "Glitch"]);
				break;
			case 'tomohawk':
				resultSet.push(['header', "CAP"]);
				break;
			case 'pikachucosplay':
				continue;
			}
			resultSet.push(['pokemon', id]);
		}
		this.resultSet = resultSet;
		this.renderedIndex = 0;
		this.renderingDone = false;
		this.updateScroll();
	};
	Search.prototype.allMoves = function () {
		if (this.filters) return this.filteredMoves();
		var resultSet = [];
		resultSet.push(['header', "Moves"]);
		for (var id in BattleMovedex) {
			switch (id) {
			case 'paleowave':
				resultSet.push(['header', "CAP moves"]);
				break;
			case 'magikarpsrevenge':
				continue;
			}
			resultSet.push(['move', id]);
		}
		this.resultSet = resultSet;
		this.renderedIndex = 0;
		this.renderingDone = false;
		this.updateScroll();
	};
	Search.prototype.getFilterText = function (q) {
		var buf = '<p>Filters: ';
		for (var i = 0; i < this.filters.length; i++) {
			var text = this.filters[i][1];
			if (this.filters[i][0] === 'move') text = Tools.getMove(text).name;
			if (this.filters[i][0] === 'pokemon') text = Tools.getTemplate(text).name;
			buf += '<button class="filter" value="' + Tools.escapeHTML(this.filters[i].join(':')) + '">' + text + ' <i class="fa fa-times-circle"></i></button> ';
		}
		if (!q) buf += '<small style="color: #888">(backspace = delete filter)</small>';
		return buf + '</p>';
	};
	Search.prototype.filteredPokemon = function () {
		var resultSet = [['html', this.getFilterText()]];
		resultSet.push(['header', "Filtered results"]);
		var illegalResultSet = [];
		var filters = this.filters;
		var genChar = '' + this.gen;
		for (var id in BattlePokedex) {
			var template = BattlePokedex[id];
			if (template.exists === false) continue;
			for (var i = 0; i < filters.length; i++) {
				if (filters[i][0] === 'type') {
					var type = filters[i][1];
					if (template.types[0] !== type && template.types[1] !== type) break;
				} else if (filters[i][0] === 'egggroup') {
					var egggroup = filters[i][1];
					if (!template.eggGroups) continue;
					if (template.eggGroups[0] !== egggroup && template.eggGroups[1] !== egggroup) break;
				} else if (filters[i][0] === 'ability') {
					var ability = filters[i][1];
					if (template.abilities['0'] !== ability && template.abilities['1'] !== ability && template.abilities['H'] !== ability) break;
				} else if (filters[i][0] === 'move') {
					var learned = false;
					var learnsetid = id;
					if (!(learnsetid in BattleTeambuilderTable.learnsets)) {
						learnsetid = toId(BattlePokedex[learnsetid].baseSpecies);
					}
					while (true) {
						var learnset = BattleTeambuilderTable.learnsets[learnsetid];
{'fraction_non_alphanumeric': 0.1667632084387671, 'fraction_numerical': 0.01134111764885217, 'mean_word_length': 3.191413237924866, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 49, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '856972', 'n_tokens_mistral': 13049, 'n_tokens_neox': 10969, 'n_words': 3048}