"""
A validator for ASCII MOCs, as per MOC 1.1,

(see http://ivoa.net/documents/MOC/).

To you use it, just send an ASCII MOC to stdin; more or less readable
messages will come out.

To run the embedded tests, call it with a "test" command line argument.

This script requires pyparsing installed (debian package: python-pyparsing
or python3-pyparsing; or do pip install pyparsing).

Written 2019-06-07 by Markus Demleitner <msdemlei@ari.uni-heidelberg.de>

This code is in the public domain.
"""

from pyparsing import (nums, ParserElement, Regex, Optional, NotAny,
                       ZeroOrMore, OneOrMore, Group,
                       ParseException, ParseSyntaxException,
                       ParseFatalException)


def get_grammar():
	"""returns a pyparsing grammar for ASCII MOCs.

	This grammar's form is slightly different from what's printed in the spec
	because of implementation considerations; it should be easy in all
	cases to see the equivalence;  the only really ugly part is the
	negative lookahead to tell the order from a pixel.  It certainly
	would have been nicer to have two different separators for pix and
	ordpix.
	"""

	ParserElement.setDefaultWhitespaceChars('')

	int = Regex("[0-9]+").setName("integer")
	separator = Regex(r"[ ,\n\r]").setName("separator")
	pix = (Group(Optional(
		int + Optional('-' + int)) + NotAny('/'))
	       .setName("pix")
	       .setResultsName('pix', listAllMatches=True))
	pixs = (pix + ZeroOrMore(OneOrMore(separator) + pix)).setName("pixs")
	ordpix = (Group(int.setResultsName('order', listAllMatches=True) +
	                '/' + pixs)
	          .setName("ordpix")
	          .setResultsName('ordpix', listAllMatches=True))
	ordpix.setParseAction(validate_ordpix)
	moc = ordpix + ZeroOrMore(
		OneOrMore(separator) + ordpix).setName("moc")
	return moc


def validate_ordpix(s, loc, toks):
	"""
	Raise a ParseFatalException if the parsed ordpix is not semantically valid.
	So far this just checks that pixel numbers are legal for the order.
	"""
	try:
		d = toks.asDict()
		ordpix = d['ordpix'][0]
		order = int(ordpix['order'][0])
		for pix in ordpix['pix']:
			validate_pix(order, pix)

	except (Exception) as ex:
		raise ParseFatalException(s, loc, str(ex))


def validate_pix(order, pix):
	"""
	Raise an Exception if the given parsed pix is legal at the specified order.
	pix is either a list of one integer string, or a list of
	[int_string, '-', 'int_string].
	"""
	max_pix = num_pix(order) - 1
	if len(pix) == 1:
		# pix is a single pixel
		val = int(pix[0])
		if not (val <= max_pix):
			raise Exception("Illegal pixel {}; order {} pixels must be <= to {}"
			                .format(val, order, max_pix))
	elif len(pix) == 3:
		# pix is a range.
		minval = int(pix[0])
		maxval = int(pix[2])
		if not (minval < maxval <= max_pix):
			raise Exception(
			        "Illegal pixel range {}-{}; min must be < max,"
			        " and order {} pixels must be <= to {}"
			        .format(minval, maxval, order, max_pix))


def num_pix(order):
	"""
	Returns the number of HEALPix pixels at the given order.

	The legal range of pixel numbers at order n is [0, num_pix(n)-1]
	"""
	return 12 * 4**order


def validate(moc, grammar=get_grammar()):
	"""returns either "OK" or a string with "Error" depending on whether
	moc matches the grammar for version 1.1 ASCII MOCs.
	"""
	try:
		res = grammar.parseString(moc, parseAll=True)
	except (ParseException, ParseSyntaxException, ParseFatalException) as ex:
		return "Error: {}".format(ex)
	return "OK"


########## End of validator.  Below here: validator validation

GOOD_MOCS = [
	'1/',
	'0/1',
	'0/1-11',
	'0/1,2 4\n5,\n6,,,, \n\r11',
	'0/1 2/ 23 12,9 5/1-13,, ,29',
	'1/1,2,4 2/12-14,21,23,25 8/',

        # pixel range legal boundary cases
	'0/1 2/ 23 12,9 5/1-13,, ,12287',
	'0/1 2/ 23 12,9 5/1-13,, ,22,25/13510798882111487',
	'0/1 2/ 23 12,9 5/1-13,, ,22,29/3458764513820540927',
]

BAD_MOCS = [
	# These come with expected error messages
	(',', 'Error: Expected integer (at char 0), (line:1, col:1)'),
	('1,', 'Error: Expected "/" (at char 1), (line:1, col:2)'),
	(',1', 'Error: Expected integer (at char 0), (line:1, col:1)'),
	('aber/1', 'Error: Expected integer (at char 0), (line:1, col:1)'),
	('1/aber', 'Error: Expected end of text (at char 2), (line:1, col:3)'),

	('1/12-13-14', 'Error: Expected end of text (at char 7), (line:1, col:8)'),
	('/12', 'Error: Expected integer (at char 0), (line:1, col:1)'),
	('1/12 /29', 'Error: Expected end of text (at char 4), (line:1, col:5)'),
	('1 /12', 'Error: Expected "/" (at char 1), (line:1, col:2)'),
	('1/12 2 /12', 'Error: Expected end of text (at char 6), (line:1, col:7)'),

        # pixel range errors
        ('0/1,2 4\n5,\n6,,,, \n\r12',
         'Error: Illegal pixel 12; order 0 pixels must be <= to 11'
         ' (at char 0), (line:1, col:1)'),
	('0/15 2/ 23 12,9 5/1-13,, ,29',
         'Error: Illegal pixel 15; order 0 pixels must be <= to 11'
         ' (at char 0), (line:1, col:1)'),
	('0/1 2/ 230 12,9 5/1-13,, ,29',
         'Error: Illegal pixel 230; order 2 pixels must be <= to 191'
         ' (at char 4), (line:1, col:5)'),
	('0/1 2/ 23 12,9 5/18-13,, ,29',
         'Error: Illegal pixel range 18-13; min must be < max, and order 5'
         ' pixels must be <= to 12287 (at char 15), (line:1, col:16)'),
	('0/1 2/ 23 12,9 5/1-13,, ,12288',
         'Error: Illegal pixel 12288; order 5 pixels must be <= to 12287'
         ' (at char 15), (line:1, col:16)'),
	('0/1 2/ 23 12,9 5/1-13,, ,22,25/13510798882111488',
         'Error: Illegal pixel 13510798882111488; order 25 pixels must be'
         ' <= to 13510798882111487 (at char 28), (line:1, col:29)'),
	('0/1 2/ 23 12,9 5/1-13,, ,22,29/3458764513820540928',
         'Error: Illegal pixel 3458764513820540928; order 29 pixels must be'
         ' <= to 3458764513820540927 (at char 28), (line:1, col:29)'),
	('1/1,2,4 2/12-14,3000,23,25 8/',
         'Error: Illegal pixel 3000; order 2 pixels must be <= to 191'
         ' (at char 8), (line:1, col:9)'),
]


def _test():
	for literal in GOOD_MOCS:
		res = validate(literal)
		if res != "OK":
			print("Failure for MOC {}: Should be ok, but rejected as {}".format(
				literal, res))

	for literal, message in BAD_MOCS:
		res = validate(literal)
		if res != message:
			print("Failure for MOC {}: Message should have been '{}' but"
			      " was '{}'.".format(literal, message, res))


if __name__ == "__main__":
	import sys
	if len(sys.argv) > 1 and sys.argv[1] == "test":
		_test()
	elif len(sys.argv) > 1:
		with open(sys.argv[1], 'r') as f:
			mocstring = f.read()
		res = validate(mocstring)
		print(res)
	else:
		res = validate(sys.stdin.read())
		print(res)
