# $Id: test.py 081917d30609 2010-03-05 mtnyogi $
# coding=utf-8
#
# Copyright © 2008 Bruce Frederiksen
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


from __future__ import with_statement
import types
from pyke import knowledge_engine, krb_traceback, pattern, contexts

def parse(str):
    str = str.strip()
    if str[0] == '(': return parse_tuple(str[1:])
    if str[0] in "0123456789.-+": return parse_number(str)
    if str[0] in "\"'": return parse_string(str)
    if str[0].isalpha() or str[0] in "_$*": return parse_identifier(str)
    else: return parse_symbol(str)

def parse_number(str):
    '''
        >>> parse_number('123abc')
        (123, 'abc')
        >>> parse_number('123e17abc')
        (1.23e+19, 'abc')
        >>> parse_number('-123abc')
        (-123, 'abc')
        >>> parse_number('-1.23e-7abc')
        (-1.23e-07, 'abc')
    '''
    for i in range(1, len(str)):
        if str[i] not in "0123456789.-+e": break
    return eval(str[:i]), str[i:]

def parse_string(str):
    r'''
        >>> parse_string("'hello' mom")
        ('hello', ' mom')
        >>> parse_string(r'"hello\" mom"')
        ('hello" mom', '')
    '''
    quote = str[0]
    end = str.index(quote, 1)
    while str[end - 1] == '\\':
        end = str.index(quote, end + 1)
    return eval(str[:end + 1]), str[end + 1:]

def parse_identifier(str):
    '''
        >>> parse_identifier("abc, def")
        ('abc', ', def')
        >>> parse_identifier("$abc, def")
        ('$abc', ', def')
        >>> parse_identifier("*$abc, def")
        ('*$abc', ', def')
        >>> parse_identifier("*")
        ('*', '')
        >>> parse_identifier("True, 0")
        (True, ', 0')
        >>> parse_identifier("False, 0")
        (False, ', 0')
        >>> parse_identifier("None, 0")
        (None, ', 0')
    '''
    if len(str) == 1: return str, ''
    start = 2 if str.startswith('*$') else 1
    for i in range(start, len(str)):
        if not str[i].isalnum() and str[i] != '_': break
    if str[0] == '*' and (i < 3 or str[1] != '$'): return parse_symbol(str)
    if str[0] == '$' and i < 2: return parse_symbol(str)
    if str[:i] == 'None': return None, str[i:]
    if str[:i] == 'True': return True, str[i:]
    if str[:i] == 'False': return False, str[i:]
    return str[:i], str[i:]

def parse_symbol(str):
    '''
        >>> parse_symbol(", def")
        (',', ' def')
        >>> parse_symbol("$+, def")
        ('$+', ', def')
        >>> parse_symbol("+)")
        ('+', ')')
        >>> parse_symbol("+]")
        ('+', ']')
    '''
    if len(str) == 1: return str, ''
    for i in range(1, len(str)):
        if str[i].isspace() or str[i].isalnum() or str[i] in "\"'()[]{},;_`":
            break
    return str[:i], str[i:]

def parse_tuple(str):
    '''
        >>> parse_tuple("))")
        ((), ')')
        >>> parse_tuple("a, b), c)")
        (('a', 'b'), ', c)')
        >>> parse_tuple("a, (b), c)")
        (('a', ('b',), 'c'), '')
    '''
    ans = []
    str = str.lstrip()
    while str[0] != ')':
        element, str = parse(str)
        ans.append(element)
        str = str.lstrip()
        if str[0] == ',': str = str[1:].lstrip()
    return tuple(ans), str[1:]

def is_pattern(data):
    '''
        >>> is_pattern('abc')
        False
        >>> is_pattern(123)
        False
        >>> is_pattern(())
        False
        >>> is_pattern((1,2,3))
        False
        >>> is_pattern((1,2,'*$_'))
        True
    '''
    if isinstance(data, tuple):
        if data and is_rest_var(data[-1]): return True
        return any(is_pattern(element) for element in data)
    if isinstance(data, types.StringTypes) and len(data) > 1 and \
       data[0] == '$' and (data[1].isalpha() or data[1] == '_'):
        return True
    return False

def is_rest_var(data):
    r'''
    >>> is_rest_var('foo')
    False
    >>> is_rest_var('$foo')
    False
    >>> is_rest_var('*foo')
    False
    >>> is_rest_var('$*foo')
    False
    >>> is_rest_var('*$foo')
    True
    >>> is_rest_var('*$')
    False
    >>> is_rest_var('*')
    False
    >>> is_rest_var('')
    False
    '''
    return isinstance(data, types.StringTypes) and len(data) > 2 and \
           data.startswith('*$') and (data[2].isalpha() or data[2] == '_')

def as_pattern(data):
    if isinstance(data, tuple) and is_pattern(data):
        if is_rest_var(data[-1]):
            name = data[-1][2:]
            if name[0] == '_':
                rest_var = contexts.anonymous(name)
            else:
                rest_var = contexts.variable(name)
            return pattern.pattern_tuple(tuple(as_pattern(element)
                                               for element in data[:-1]),
                                         rest_var)
        return pattern.pattern_tuple(tuple(as_pattern(element)
                                           for element in data))
    if isinstance(data, types.StringTypes) and is_pattern(data):
        name = data[1:]
        if name[0] == '_': return contexts.anonymous(name)
        return contexts.variable(name)
    return pattern.pattern_literal(data)

Did_init = False

def init(*args, **kws):
    global Engine, Did_init
    Engine = knowledge_engine.engine(*args, **kws)
    Did_init = True

def eval_plan(globals, locals):
    while True:
        print
        expr = raw_input("run plan: ").strip()
        if not expr: break
        ans = eval(expr, globals.copy(), locals.copy())
        print "plan returned:", ans

def run(rule_bases_to_activate,
        default_rb = None, init_fn = None, fn_to_run_plan = eval_plan,
        plan_globals = {}):
    if not Did_init: init()

    if not isinstance(rule_bases_to_activate, (tuple, list)):
        rule_bases_to_activate = (rule_bases_to_activate,)

    if default_rb is None: default_rb = rule_bases_to_activate[0]

    while True:
        print
        goal_str = raw_input("goal: ")
        if not goal_str: break
        goal, args_str = parse(goal_str)
        if goal == "trace":
            args = args_str.split()
            if len(args) == 1:
                Engine.trace(default_rb, args[0])
            else:
                Engine.trace(*args)
            continue
        if goal == "untrace":
            args = args_str.split()
            if len(args) == 1:
                Engine.untrace(default_rb, args[0])
            else:
                Engine.untrace(*args)
            continue
        args_str = args_str.strip()
        rb_name = default_rb
        if args_str[0] == '.':
            rb_name = goal
            goal, args_str = parse(args_str[1:])
        args = parse(args_str)[0]
        print "proving: %s.%s%s" % (rb_name, goal, args)
        goal_args = tuple(as_pattern(arg) for arg in args)
        Engine.reset()
        if init_fn: init_fn(Engine)
        context = contexts.simple_context()
        try:
            Engine.activate(*rule_bases_to_activate)
            with Engine.prove(rb_name, goal, context, goal_args) as it:
                for prototype_plan in it:
                    final = {}
                    print "got: %s%s" % \
                          (goal, tuple(arg.as_data(context, True, final)
                                       for arg in goal_args))
                    if not prototype_plan:
                        print "no plan returned"
                    else:
                        plan = prototype_plan.create_plan(final)
                        fn_to_run_plan(plan_globals, locals())
        except:
            krb_traceback.print_exc(100)

