/* This is CLDRPluralRuleParser v1.1, ported to MediaWiki ResourceLoader */

/**
* CLDRPluralRuleParser.js
* A parser engine for CLDR plural rules.
*
* Copyright 2012 GPLV3+, Santhosh Thottingal
*
* @version 0.1.0-alpha
* @source https://github.com/santhoshtr/CLDRPluralRuleParser
* @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
* @author Timo Tijhof
* @author Amir Aharoni
*/

( function ( mw ) {
/**
 * Evaluates a plural rule in CLDR syntax for a number
 * @param {string} rule
 * @param {integer} number
 * @return {boolean} true if evaluation passed, false if evaluation failed.
 */

function pluralRuleParser(rule, number) {
	/*
	Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
	-----------------------------------------------------------------
	condition     = and_condition ('or' and_condition)*
		('@integer' samples)?
		('@decimal' samples)?
	and_condition = relation ('and' relation)*
	relation      = is_relation | in_relation | within_relation
	is_relation   = expr 'is' ('not')? value
	in_relation   = expr (('not')? 'in' | '=' | '!=') range_list
	within_relation = expr ('not')? 'within' range_list
	expr          = operand (('mod' | '%') value)?
	operand       = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
	range_list    = (range | value) (',' range_list)*
	value         = digit+
	digit         = 0|1|2|3|4|5|6|7|8|9
	range         = value'..'value
	samples       = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
	sampleRange   = decimalValue '~' decimalValue
	decimalValue  = value ('.' value)?
	*/

	// we don't evaluate the samples section of the rule. Ignore it.
	rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');

	if (!rule.length) {
		// empty rule or 'other' rule.
		return true;
	}
	// Indicates current position in the rule as we parse through it.
	// Shared among all parsing functions below.
	var pos = 0,
		operand,
		expression,
		relation,
		result,
		whitespace = makeRegexParser(/^\s+/),
		value = makeRegexParser(/^\d+/),
		_n_ = makeStringParser('n'),
		_i_ = makeStringParser('i'),
		_f_ = makeStringParser('f'),
		_t_ = makeStringParser('t'),
		_v_ = makeStringParser('v'),
		_w_ = makeStringParser('w'),
		_is_ = makeStringParser('is'),
		_isnot_ = makeStringParser('is not'),
		_isnot_sign_ = makeStringParser('!='),
		_equal_ = makeStringParser('='),
		_mod_ = makeStringParser('mod'),
		_percent_ = makeStringParser('%'),
		_not_ = makeStringParser('not'),
		_in_ = makeStringParser('in'),
		_within_ = makeStringParser('within'),
		_range_ = makeStringParser('..'),
		_comma_ = makeStringParser(','),
		_or_ = makeStringParser('or'),
		_and_ = makeStringParser('and');

	function debug() {
		// console.log.apply(console, arguments);
	}

	debug('pluralRuleParser', rule, number);

	// Try parsers until one works, if none work return null

	function choice(parserSyntax) {
		return function() {
			for (var i = 0; i < parserSyntax.length; i++) {
				var result = parserSyntax[i]();
				if (result !== null) {
					return result;
				}
			}
			return null;
		};
	}

	// Try several parserSyntax-es in a row.
	// All must succeed; otherwise, return null.
	// This is the only eager one.

	function sequence(parserSyntax) {
		var originalPos = pos;
		var result = [];
		for (var i = 0; i < parserSyntax.length; i++) {
			var res = parserSyntax[i]();
			if (res === null) {
				pos = originalPos;
				return null;
			}
			result.push(res);
		}
		return result;
	}

	// Run the same parser over and over until it fails.
	// Must succeed a minimum of n times; otherwise, return null.

	function nOrMore(n, p) {
		return function() {
			var originalPos = pos;
			var result = [];
			var parsed = p();
			while (parsed !== null) {
				result.push(parsed);
				parsed = p();
			}
			if (result.length < n) {
				pos = originalPos;
				return null;
			}
			return result;
		};
	}

	// Helpers -- just make parserSyntax out of simpler JS builtin types
	function makeStringParser(s) {
		var len = s.length;
		return function() {
			var result = null;
			if (rule.substr(pos, len) === s) {
				result = s;
				pos += len;
			}

			return result;
		};
	}

	function makeRegexParser(regex) {
		return function() {
			var matches = rule.substr(pos).match(regex);
			if (matches === null) {
				return null;
			}
			pos += matches[0].length;
			return matches[0];
		};
	}

	/*
	 * integer digits of n.
	 */
	function i() {
		var result = _i_();
		if (result === null) {
			debug(' -- failed i', parseInt(number, 10));
			return result;
		}
		result = parseInt(number, 10);
		debug(' -- passed i ', result);
		return result;
	}

	/*
	 * absolute value of the source number (integer and decimals).
	 */
	function n() {
		var result = _n_();
		if (result === null) {
			debug(' -- failed n ', number);
			return result;
		}
		result = parseFloat(number, 10);
		debug(' -- passed n ', result);
		return result;
	}

	/*
	 * visible fractional digits in n, with trailing zeros.
	 */
	function f() {
		var result = _f_();
		if (result === null) {
			debug(' -- failed f ', number);
			return result;
		}
		result = (number + '.').split('.')[1] || 0;
		debug(' -- passed f ', result);
		return result;
	}

	/*
	 * visible fractional digits in n, without trailing zeros.
	 */
	function t() {
		var result = _t_();
		if (result === null) {
			debug(' -- failed t ', number);
			return result;
		}
		result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
		debug(' -- passed t ', result);
		return result;
	}

	/*
	 * number of visible fraction digits in n, with trailing zeros.
	 */
	function v() {
		var result = _v_();
		if (result === null) {
			debug(' -- failed v ', number);
			return result;
		}
		result = (number + '.').split('.')[1].length || 0;
		debug(' -- passed v ', result);
		return result;
	}

	/*
	 * number of visible fraction digits in n, without trailing zeros.
	 */
	function w() {
		var result = _w_();
		if (result === null) {
			debug(' -- failed w ', number);
			return result;
		}
		result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
		debug(' -- passed w ', result);
		return result;
	}

	// operand       = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
	operand = choice([n, i, f, t, v, w]);

	// expr          = operand (('mod' | '%') value)?
	expression = choice([mod, operand]);

	function mod() {
		var result = sequence([operand, whitespace, choice([_mod_, _percent_]), whitespace, value]);
		if (result === null) {
			debug(' -- failed mod');
			return null;
		}
		debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
		return parseInt(result[0], 10) % parseInt(result[4], 10);
	}

	function not() {
		var result = sequence([whitespace, _not_]);
		if (result === null) {
			debug(' -- failed not');
			return null;
		}

		return result[1];
	}

	// is_relation   = expr 'is' ('not')? value
	function is() {
		var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
		if (result !== null) {
			debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
			return result[0] === parseInt(result[4], 10);
		}
		debug(' -- failed is');
		return null;
	}

	// is_relation   = expr 'is' ('not')? value
	function isnot() {
		var result = sequence([expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]);
		if (result !== null) {
			debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
			return result[0] !== parseInt(result[4], 10);
		}
		debug(' -- failed isnot');
		return null;
	}

	function not_in() {
		var result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
		if (result !== null) {
			debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
			var range_list = result[4];
			for (var i = 0; i < range_list.length; i++) {
				if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
					return false;
				}
			}
			return true;
		}
		debug(' -- failed not_in');
		return null;
	}

	// range_list    = (range | value) (',' range_list)*
	function rangeList() {
		var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]);
		var resultList = [];
		if (result !== null) {
			resultList = resultList.concat(result[0]);
			if (result[1][0]) {
				resultList = resultList.concat(result[1][0]);
			}
			return resultList;
		}
		debug(' -- failed rangeList');
		return null;
	}

	function rangeTail() {
		// ',' range_list
		var result = sequence([_comma_, rangeList]);
		if (result !== null) {
			return result[1];
		}
		debug(' -- failed rangeTail');
		return null;
	}

	// range         = value'..'value

	function range() {
		var i;
		var result = sequence([value, _range_, value]);
		if (result !== null) {
			debug(' -- passed range');
			var array = [];
			var left = parseInt(result[0], 10);
			var right = parseInt(result[2], 10);
			for (i = left; i <= right; i++) {
				array.push(i);
			}
			return array;
		}
		debug(' -- failed range');
		return null;
	}

	function _in() {
		// in_relation   = expr ('not')? 'in' range_list
		var result = sequence([expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]);
		if (result !== null) {
			debug(' -- passed _in:' + result);
			var range_list = result[5];
			for (var i = 0; i < range_list.length; i++) {
				if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
					return (result[1][0] !== 'not');
				}
			}
			return (result[1][0] === 'not');
		}
		debug(' -- failed _in ');
		return null;
	}

	/*
	 * The difference between in and within is that in only includes integers in the specified range,
	 * while within includes all values.
	 */

	function within() {
		// within_relation = expr ('not')? 'within' range_list
		var result = sequence([expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]);
		if (result !== null) {
			debug(' -- passed within');
			var range_list = result[5];
			if ((result[0] >= parseInt(range_list[0], 10)) &&
				(result[0] < parseInt(range_list[range_list.length - 1], 10))) {
				return (result[1][0] !== 'not');
			}
			return (result[1][0] === 'not');
		}
		debug(' -- failed within ');
		return null;
	}

	// relation      = is_relation | in_relation | within_relation
	relation = choice([is, not_in, isnot, _in, within]);

	// and_condition = relation ('and' relation)*
	function and() {
		var result = sequence([relation, nOrMore(0, andTail)]);
		if (result) {
			if (!result[0]) {
				return false;
			}
			for (var i = 0; i < result[1].length; i++) {
				if (!result[1][i]) {
					return false;
				}
			}
			return true;
		}
		debug(' -- failed and');
		return null;
	}

	// ('and' relation)*
	function andTail() {
		var result = sequence([whitespace, _and_, whitespace, relation]);
		if (result !== null) {
			debug(' -- passed andTail' + result);
			return result[3];
		}
		debug(' -- failed andTail');
		return null;

	}
	//  ('or' and_condition)*
	function orTail() {
		var result = sequence([whitespace, _or_, whitespace, and]);
		if (result !== null) {
			debug(' -- passed orTail: ' + result[3]);
			return result[3];
		}
		debug(' -- failed orTail');
		return null;

	}

	// condition     = and_condition ('or' and_condition)*
	function condition() {
		var result = sequence([and, nOrMore(0, orTail)]);
		if (result) {
			for (var i = 0; i < result[1].length; i++) {
				if (result[1][i]) {
					return true;
				}
			}
			return result[0];

		}
		return false;
	}

	result = condition();
	/*
	 * For success, the pos must have gotten to the end of the rule
	 * and returned a non-null.
	 * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
	 */
	if (result === null) {
		throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
	}

	if (pos !== rule.length) {
		debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
	}

	return result;
}

/* pluralRuleParser ends here */
mw.libs.pluralRuleParser = pluralRuleParser;

} )( mediaWiki );
