#!/usr/bin/python
#########################################################################
#   Copyright 2010 Torsten Grote
#
#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program.  If not, see <http://www.gnu.org/licenses/>.
##########################################################################

from __future__ import with_statement
import sys
import os
import re
import subprocess
import tempfile
from optparse import OptionParser, OptionGroup

if sys.version.rsplit()[0] < '2.5':
	print 'You need at least python 2.5 to run coala'
	sys.exit(1)
	
# defined that early to extract coala version from binary
def checkProgram(prog):
	try:
		(result, error) = subprocess.Popen([prog, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
	except OSError, err:
		print err
		return False
	
	if result != '':
		return result.splitlines()[0]
	else:
		return False

version = checkProgram("coala.bin")
if not version:
	print "There is a problem with the coala binary file. Is it in your $PATH, is it executable, is it compiled for your architecture?"
	sys.exit(1)

# Parse Command Line Options
description = version+" Copyright (C) 2007-2010  Torsten Grote  This program comes with ABSOLUTELY NO WARRANTY. It is free software, and you are welcome to redistribute it under certain conditions."
usage = "%prog [options] file [number]"
parser = OptionParser(usage=usage, description=description, add_help_option=False, version="%prog " + version, conflict_handler="resolve")
parser.add_option("-l", "--language", dest="language", choices=["c","b","al","m","c_taid"], help="the action language (c, c_taid, b, al or m) to be used as input (default: %default)",
		action="store", metavar="LANG")
parser.add_option("-s", "--static-file", dest="static_files", help="file containing static information such as variable types.", action="append", metavar="FILE")
parser.add_option("-c", "--const", dest="const", help="replaces constant t with value v", metavar="t=v")
parser.add_option("-d", "--debug", dest="debug", help="show debugging output", action="store_true")
parser.add_option("", "--version", help="show program's version number and exit", action="version")
parser.add_option("-h", "--help", help="show this help message and exit", action="help")

encoding_group = OptionGroup(parser, "Encoding Options")
encoding_group.add_option("-i", "--incremental", dest="incremental", choices=["yes","no","backwards"], help="use incremental encoding. INC can be yes, no or backwards. Default: %default",
		action="store", metavar="INC")
encoding_group.add_option("-e", "--meta-encoding", dest="meta_encoding", help="use meta-encoding", action="store_true")
encoding_group.add_option("-n", "--negation", dest="negation", help="simulate classical negation instead of using the built-in one", action="store_true")
parser.add_option_group(encoding_group)

solver_group = OptionGroup(parser, "Solver Options")
solver_group.add_option("", "--max-sol", dest="max_sol", help="Compute [number] solutions. Default: %default", type="int", action="store", metavar="NUM")
solver_group.add_option("", "--imax", dest="imax", help="Perform at most NUM incremental steps. Default: %default", type="int", action="store", metavar="NUM")
solver_group.add_option("-t", "--text", dest="text", help="show only coala output and don't look for solutions", action="store_true")
solver_group.add_option("-g", "--ground", dest="ground", help="show only grounded coala output and don't look for solutions", action="store_true")
parser.add_option_group(solver_group)

output_group = OptionGroup(parser, "Output Options")
output_group.add_option("-o", "--output", dest="output", help="change the way the solutions are presented, long, compact or raw. Default: %default",
		action="store", choices=['long','compact', 'raw'], metavar="OUTPUT")
output_group.add_option("-f", "--show-fluents", dest="show_fluents", help="show not only actions, but also fluents in solution", action="store_true")
#output_group.add_option("", "--hide", dest="hide", help="Hide this action or fluent name from output", action="append", metavar="NAME")
parser.add_option_group(output_group)

parser.set_defaults(
	language = "c",
	incremental = "yes",
	meta_encoding = False,
	show_fluents = False,
	negation = False,
	output = "compact",
	text = False,
	ground = False,
	max_sol = 1,
	imax = 99,
	debug = False
)
(opt, args) = parser.parse_args()

if opt.meta_encoding:
	ACTION = re.compile("^occ\((.*?),?(\d+)\)$")
	FLUENT = re.compile("^hol\((.*?),?(\d+)\)$")
elif opt.language == 'b':
	ACTION = re.compile("^occ\((.*?),?(\d+)\)$")
	FLUENT = re.compile("^holds\((.*?),?(\d+)\)$")
else:
	ACTION = re.compile("^-?action_([a-zA-Z0-9_]*\(.*?),? *(\d+) *\)\.?$")
	FLUENT = re.compile("^-?fluent_([a-zA-Z0-9_]*\(.*?),? *(\d+) *\)\.?$")
PARSER = [
	re.compile("^Answer:\ (\d+)$"),
	re.compile("^(-?[a-z_][a-zA-Z0-9_]*(\(.+\))?\ *)+$"),
	re.compile("^SATISFIABLE$"),
	re.compile("^UNSATISFIABLE$"),
	re.compile("^Error: (?P<series>.+)$"),
]


def main():
	checkOptions()
	lp = callCoala("coala.bin")
	if opt.text:
		print lp
	else:
		output = callClingo(lp)
		if opt.ground or opt.output == 'raw':
			print output
		else:
			answer_sets = getAnswerSets(output)
			printAnswerSets(answer_sets)
	return 0


def checkOptions():
	# get last positional argument and check if its a number
	try:
		int(args[len(args)-1])
		opt.max_sol = args.pop(len(args)-1)
	except IndexError:
		print "coala needs at least one file as argument. Run `coala --help` for more information."
		print
		parser.print_usage()
		sys.exit(0)
	except ValueError:
		opt.max_sol = '1'
	
	if(opt.language == "c_taid" and (opt.incremental == "yes" or opt.incremental == "backwards" or opt.meta_encoding)):
		raise RuntimeError("Action Language c_taid does not support these options.")
	if((opt.language == "b" or opt.language == "al") and opt.meta_encoding):
		raise RuntimeWarning("Action Language "+opt.language+" does only support one encoding.")
	if(opt.text and opt.ground):
		raise RuntimeError("The options -g and -t can not be used together.")


def callCoala(coala_binary):
	coala_options = getCoalaOptions()
	if(opt.debug):
		print "Calling Coala with:"
		print "  " + subprocess.list2cmdline([coala_binary] + coala_options)
	(result, error) = subprocess.Popen([coala_binary] + coala_options, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
	if error != "":
		print error
	return result


def getCoalaOptions():
	result = ['-l']
	result.append(opt.language)
	if(opt.meta_encoding):
		result.append('-e')
	elif(opt.incremental == "yes" or opt.incremental == "backward"):
			result.append('-n')
	
	if(opt.incremental == "yes"):
		result.append('-i')
	elif(opt.incremental == "backward"):
		result.append('-ir')
	elif(opt.negation):
		result.append('-n')
		opt.negation = True
	if(opt.debug):
		result.extend(['-d','99'])
	elif(opt.show_fluents):
		result.extend(['-d','1'])

	for file in args:
		if os.path.exists(file):
			result.append(file)
		else:
			raise RuntimeError("Input file %s does not exist." % file)
	return result


def callClingo(lp):
	iclingo_options = ['--imax='+str(opt.imax), opt.max_sol]
	
	iclingo = checkProgram("iclingo")
	if(not iclingo and opt.incremental != "no"):
		raise RuntimeError("Could not find iclingo binary which is needed to process the incremental encoding.")
	elif(not iclingo):
		iclingo = checkProgram("clingo")
		if(not clingo):
			raise RuntimeError("Could not find an iclingo or clingo binary. At least one of them is needed to compute solutions.")
		else:
			iclingo = "clingo"
	else:
		iclingo = "iclingo"
	
	if(opt.incremental == "no"):
		iclingo_options.append('--clingo')
	
	if(opt.const):
		iclingo_options.extend(['-c', opt.const])
	if(opt.ground):
		iclingo_options.append('-t')
		if(opt.const):
			iclingo_options.append('--ifixed=' + opt.const.rsplit('=',1)[1])

	# create tmp file for input
	tmp = tempfile.NamedTemporaryFile()
	tmp.write(lp)
	if opt.static_files:
		for file in opt.static_files:
			if(os.path.exists(file)):
				if(opt.incremental != "no"):
					tmp.write("#base.\n")
				with open(file, 'r') as f:
					for line in f:
						tmp.write(line)
			else:
				raise RuntimeError("Static file '%s' could not be found." % file)
	tmp.seek(0)
	
	if opt.debug:
		print "Calling iClingo with:"
		print "  " + subprocess.list2cmdline([iclingo] + iclingo_options)
		iclingo_options.append('--istats')
	(result, error) = subprocess.Popen([iclingo] + iclingo_options, stdin=tmp, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
	if error != "":
		print error
	return result


def getAnswerSets(output):
	answer_sets = []
	
	for line in output.splitlines():
		matched = False
		for i in range(len(PARSER)):
			match = PARSER[i].match(line)
			if match != None:
				matched = True
				if i == 0 and opt.debug:
					print "Found %s. Answer Set." % match.group(1)
				elif i == 1:
					answer_sets.append(line.split())
				elif i == 2:
					return answer_sets
				elif i == 3:
					raise RuntimeError("The problem doesn't have any solutions. Try to comment out some constraints or queries. " +
							"In case of an LTL query, this message means that no counter examples have been found.")
				elif i == 4:
					raise RuntimeError(line)
		if not matched:
			if opt.debug:
				print line
			#raise SyntaxError("Unkown clasp output read: %s" % line)
	return answer_sets

def printAnswerSets(answer_sets):
	i = 1
	for answer_set in answer_sets:
		if opt.output == "long":
			print "\nAnswer: %d" % i
			print '-' * 78
		else:
			print "Answer: %d" % i
		printAnswerSet(answer_set)
		i += 1
	if i-1 >= int(opt.max_sol) and opt.max_sol != '0':
		print "There might be more solutions. Find out by adding the maximal [number] of"
		print "solutions as a parameter. Use 0 for all."


def printAnswerSet(answer_set):
	"""prints the answer set"""
	times = []
	actions = {}
	fluents = {}
	# stores all actions and fluents in two dictionaries
	for predicate in answer_set:
		for (parser, plan) in [(ACTION, actions), (FLUENT, fluents)]:
			match = parser.match(predicate)
			if match != None:
				t = int(match.group(2))
				if t in plan:
					plan[t].append(predicate)
				else:
					plan[t] = [predicate]
				if not t in times:
					times.append(t)
	times.sort()

	if opt.output == "compact":
		# append fluents to actions and sort both
		for time in times:
			if not time in actions:
				actions[time] = []
			else:
				actions[time].sort()
			if time in fluents:
				fluents[time].sort()
				actions[time].extend(fluents[time])
		# prints predicates in rows
		row_len = getPredicateLength(actions, fluents)
		for time in actions:
			printRow(actions, row_len, time)
	else:
		# append actions to fluents and sort both
		for time in times:
			if not time in fluents:
				fluents[time] = []
			else:
				fluents[time].sort()
			if time in actions:
				actions[time].sort()
				fluents[time].extend(actions[time])
			# print one predicate per row
			for predicate in fluents[time]:
				printPredicate(time, predicate)
			print '-' * 78


def getPredicateLength(actions, fluents):
	row_len = {}
	for plan in [actions, fluents]:
		for time in plan:
			plan[time].sort()
			row = 0
			for predicate in plan[time]:
				predicate = formatPredicate(predicate)[0]
				if row in row_len:
					if len(predicate) > row_len[row]:
						row_len[row] = len(predicate)
				else:
					row_len[row] = len(predicate)
				row += 1
	return row_len


def formatPredicate(predicate):
	pred = ''
	
	match = ACTION.match(predicate)
	if match == None:
		match = FLUENT.match(predicate)
		if match == None:
			pred = predicate
		else:
			pred = match.group(1)
			pred_type = "fluent"
	else:
		pred = match.group(1)
		pred_type = "action"
			
	if not opt.language == 'b' and not opt.meta_encoding:
		# remove opening bracket if time was only argument
		new_pred = pred.rstrip('(')
		# close bracket if needed
		if new_pred == pred:
			new_pred += ')'
		pred = new_pred

	if opt.output == "compact":
		if pred_type == "action":
			pred = "A " + pred
		elif pred_type == "fluent":
			pred = "F " + pred
	elif opt.output == "long":
		pred = pred.replace(',', ', ')

	return (pred, pred_type)


def printRow(plan, row_len, time):
	time_str = str(time).rjust(len(str(len(plan))))
	print "  %s. " % time_str,
	row = 0
	for predicate in plan[time]:
		print formatPredicate(predicate)[0].ljust(row_len[row]+1),
		row += 1
	print ""


def printPredicate(time, predicate):
	(pred, pred_type) = formatPredicate(predicate)
	print str(time).rjust(3) + " ",
	if pred_type == "action":
		print "A",
	elif pred_type == "fluent":
		print "F",
	else:
		print " ",
	print " " + pred


if __name__ == '__main__':
	if opt.debug:
		sys.exit(main())
	else:
		try:
			sys.exit(main())
		except Warning, warn:
			sys.stderr.write('WARNING: %s\n' % str(warn))
		except Exception, err:
			sys.stderr.write('ERROR: %s\n' % str(err))
			sys.exit(1)
