/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.reasoner.rulesys;

import java.io.BufferedReader ;
import java.io.IOException ;
import java.io.InputStream ;
import java.util.* ;

import org.apache.jena.datatypes.RDFDatatype ;
import org.apache.jena.datatypes.TypeMapper ;
import org.apache.jena.datatypes.xsd.XSDDatatype ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.NodeFactory ;
import org.apache.jena.reasoner.ReasonerException ;
import org.apache.jena.reasoner.TriplePattern ;
import org.apache.jena.shared.JenaException ;
import org.apache.jena.shared.PrefixMapping ;
import org.apache.jena.shared.RulesetNotFoundException ;
import org.apache.jena.shared.WrappedIOException ;
import org.apache.jena.util.FileManager ;
import org.apache.jena.util.FileUtils ;
import org.apache.jena.util.PrintUtil ;
import org.apache.jena.util.Tokenizer ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/**Representation of a generic inference rule. 
 * <p>
 * This represents the rule specification but most engines will 
 * compile this specification into an abstract machine or processing
 * graph. </p>
 * <p>
 * The rule specification comprises a list of antecendents (body) and a list
 * of consequents (head). If there is more than one consequent then a backchainer
 * should regard this as a shorthand for several rules, all with the
 * same body but with a singleton head. </p>
 * <p>
 * Each element in the head or body can be a TriplePattern, a Functor or a Rule.
 * A TriplePattern is just a triple of Nodes but the Nodes can represent
 * variables, wildcards and embedded functors - as well as constant uri or
 * literal graph nodes. A functor comprises a functor name and a list of 
 * arguments. The arguments are Nodes of any type except functor nodes
 * (there is no functor nesting). The functor name can be mapped into a registered
 * java class that implements its semantics. Functors play three roles -
 * in heads they represent actions (procedural attachement); in bodies they
 * represent builtin predicates; in TriplePatterns they represent embedded
 * structured literals that are used to cache matched subgraphs such as
 * restriction specifications. </p>
 * <p>
 * The equality contract for rules is that two rules are equal if each of terms
 * (ClauseEntry objects) are equals and they have the same name, if any.
 * </p>
 * We include a trivial, recursive descent parser but this is just there
 * to allow rules to be embedded in code. External rule syntax based on N3
 * and RDF could be developed. The embedded syntax supports rules such as:
 * <blockindent>    
 * <code>[ (?C rdf:type *), guard(?C, ?P)  -> (?c rb:restriction some(?P, ?D)) ].</code><br />
 * <code>[ (?s owl:foo ?p) -> [ (?s owl:bar ?a) -> (?s ?p ?a) ] ].</code><br />
 * <code>[name: (?s owl:foo ?p) -> (?s ?p ?a)].</code><br />
 * </blockindent>
 * only built in namespaces are recognized as such, * is a wildcard node, ?c is a variable, 
 * name(node ... node) is a functor, (node node node) is a triple pattern, [..] is an
 * embedded rule, commas are ignore and can be freely used as separators. Functor names
 * may not end in ':'.
 * </p>
 */
public class Rule implements ClauseEntry {
    
//=======================================================================
// variables

    /** Rule body */
    protected ClauseEntry[] body;
    
    /** Rule head or set of heads */
    protected ClauseEntry[] head;
    
    /** Optional name for the rule */
    protected String name;
    
    /** The number of distinct variables used in the rule */
    protected int numVars = -1;
    
    /** Flags whether the rule was written as a forward or backward rule */
    protected boolean isBackward = false;
    
    /** Flags whether the rule is monotonic */
    protected boolean isMonotonic = true;
    
    static Logger logger = LoggerFactory.getLogger(Rule.class);
    
    /**
     * Constructor
     * @param body a list of TriplePatterns or Functors.
     * @param head a list of TriplePatterns, Functors or rules
     */
    public Rule(List<ClauseEntry> head, List<ClauseEntry> body) {
        this(null, head, body);
    }
    
    /**
     * Constructor
     * @param name a label for rule
     * @param body a list of TriplePatterns or Functors.
     * @param head a list of TriplePatterns, Functors or rules
     */
    public Rule(String name, List<ClauseEntry> head, List<ClauseEntry> body) {
        this(name, 
                head.toArray(new ClauseEntry[head.size()]),
                body.toArray(new ClauseEntry[body.size()]) );
    }
    
    /**
     * Constructor
     * @param name a label for rule
     * @param body an array of TriplePatterns or Functors.
     * @param head an array of TriplePatterns, Functors or rules
     */
    public Rule(String name, ClauseEntry[] head, ClauseEntry[] body) {
        this.name = name;
        this.head = head;
        this.body = body;
        this.isMonotonic = allMonotonic(head);
    }
    
    // Compute the monotonicity flag
    // Future support for negation would affect this
    private boolean allMonotonic(ClauseEntry[] elts) {
        for ( ClauseEntry elt : elts )
        {
            if ( elt instanceof Functor )
            {
                Builtin b = ( (Functor) elt ).getImplementor();
                if ( b != null )
                {
                    if ( !b.isMonotonic() )
                    {
                        return false;
                    }
                }
                else
                {
                    throw new ReasonerException(
                            "(allMonotonic) Undefined Functor " + ( (Functor) elt ).getName() + " in " + toShortString() );
                }
            }
        }
        return true;
    }
    
//=======================================================================
// accessors

    /**
     * Return the number of body elements
     */
    public int bodyLength() {
        return body.length;
    }
    
    /**
     * Return the n'th body element
     */
    public ClauseEntry getBodyElement(int n) {
        return body[n];
    }
    
    /**
     * return the entire rule body as an array of objects
     */
    public ClauseEntry[] getBody() {
        return body;
    }
        
    
    /**
     * Return the number of head elements
     */
    public int headLength() {
        return head.length;
    }
    
    /**
     * Return the n'th head element
     */
    public ClauseEntry getHeadElement(int n) {
        return head[n];
    }
    
    /**
     * return the entire rule head as an array of objects
     */
    public ClauseEntry[] getHead() {
        return head;
    }
    
    /**
     * Return true if the rule was written as a backward (as opposed to forward) rule.
     */
    public boolean isBackward() {
        return isBackward;
    }
    
    /**
     * Set the rule to be run backwards.
     * @param flag if true the rule should run backwards.
     */
    public void setBackward(boolean flag) {
        isBackward = flag;
    }
    
    /**
     * Get the name for the rule - can be null.
     */
    public String getName() {
        return name;
    }
    
    /**
     * Set the number of distinct variables for this rule.
     * Used internally when cloing rules, not normally required.
     */
    public void setNumVars(int n) {
        numVars = n;
    }
    
    /**
     * Return the number of distinct variables in the rule. Or more precisely, the
     * size of a binding environment needed to represent the rule.
     */
    public int getNumVars() {
        if (numVars == -1) {
            // only have to do this if the rule was generated programatically
            // the parser will have prefilled this in for normal rules
            int max = findVars(body, -1);
            max = findVars(head, max);
            numVars = max + 1;
        }
        return numVars;
    }
    
    /**
     * Find all the variables in a clause array.
     */
    private int findVars(Object[] nodes, int maxIn) {
        int max = maxIn;
        for ( Object node : nodes )
        {
            if ( node instanceof TriplePattern )
            {
                max = findVars( (TriplePattern) node, max );
            }
            else
            {
                max = findVars( (Functor) node, max );
            }
        }
        return max;
    }
    
    /**
     * Find all the variables in a TriplePattern.
     */
    private int findVars(TriplePattern t, int maxIn) {
        int max = maxIn;
        max = maxVarIndex(t.getSubject(), max);
        max = maxVarIndex(t.getPredicate(), max);
        Node obj = t.getObject();
        if (obj instanceof Node_RuleVariable) {
            max = maxVarIndex(obj, max);
        } else if (Functor.isFunctor(obj)) {
            max = findVars((Functor)obj.getLiteralValue(), max);
        }
        return max;
    }
        
    /**
     * Find all the variables in a Functor.
     */
    private int findVars(Functor f, int maxIn) {
        int max = maxIn;
        Node[] args = f.getArgs();
        for ( Node arg : args )
        {
            if ( arg.isVariable() )
            {
                max = maxVarIndex( arg, max );
            }
        }
        return max;
    }
    
    /**
     * Return the maximum node index of the variable and the max so far. 
     */
    private int maxVarIndex(Node var, int max) {
        if (var instanceof Node_RuleVariable) {
            int index = ((Node_RuleVariable)var).index;
            if (index > max) return index;            
        }
        return max;
    }
    
    /**
     * Instantiate a rule given a variable binding environment.
     * This will clone any non-bound variables though that is only needed
     * for trail implementations.
     */
    public Rule instantiate(BindingEnvironment env) {
        HashMap<Node_RuleVariable, Node> vmap = new HashMap<>();
        return new Rule(name, cloneClauseArray(head, vmap, env), cloneClauseArray(body, vmap, env));
    }
    
    /**
     * Clone a rule, cloning any embedded variables.
     */
    public Rule cloneRule() {
        if (getNumVars() > 0) {
            HashMap<Node_RuleVariable, Node> vmap = new HashMap<>();
            return new Rule(name, cloneClauseArray(head, vmap, null), cloneClauseArray(body, vmap, null));
        } else {
            return this;
        }
    }
    
    /**
     * Clone a clause array.
     */
    private ClauseEntry[] cloneClauseArray(ClauseEntry[] clauses, Map<Node_RuleVariable, Node> vmap, BindingEnvironment env) {
        ClauseEntry[] cClauses = new ClauseEntry[clauses.length];
        for (int i = 0; i < clauses.length; i++ ) {
            cClauses[i] = cloneClause(clauses[i], vmap, env);
        }
        return cClauses;
    }
    
    /**
     * Clone a clause, cloning any embedded variables.
     */
    private ClauseEntry cloneClause(ClauseEntry clause, Map<Node_RuleVariable, Node> vmap, BindingEnvironment env) {
        if (clause instanceof TriplePattern) {
            TriplePattern tp = (TriplePattern)clause;
            return new TriplePattern (
                            cloneNode(tp.getSubject(), vmap, env),
                            cloneNode(tp.getPredicate(), vmap, env),
                            cloneNode(tp.getObject(), vmap, env)
                        );
        } else {
            return cloneFunctor((Functor)clause, vmap, env);
        }
    }
    
    /**
     * Clone a functor, cloning any embedded variables.
     */
    private Functor cloneFunctor(Functor f, Map<Node_RuleVariable, Node> vmap, BindingEnvironment env) {
        Node[] args = f.getArgs();
        Node[] cargs = new Node[args.length];
        for (int i = 0; i < args.length; i++) {
            cargs[i] = cloneNode(args[i], vmap, env);
        }
        Functor fn = new Functor(f.getName(), cargs, f.getImplementor());
        fn.setImplementor(f.getImplementor());
        return fn;
    }
    
    /**
     * Close a single node.
     */
    private Node cloneNode(Node nIn, Map<Node_RuleVariable, Node> vmap, BindingEnvironment env) {
        Node n = (env == null) ? nIn : env.getGroundVersion(nIn);
        if (n instanceof Node_RuleVariable) {
            Node_RuleVariable nv = (Node_RuleVariable)n;
            Node c = vmap.get(nv);
            if (c == null) {
                c = ((Node_RuleVariable)n).cloneNode();
                vmap.put(nv, c);
            }
            return c;
        } else if (Functor.isFunctor(n)) {
            Functor f = (Functor)n.getLiteralValue();
            return Functor.makeFunctorNode(cloneFunctor(f, vmap, env));
        } else {
            return n;
        }
    }
    
    /**
     * Returns false for rules which can affect other rules non-monotonically (remove builtin
     * or similar) or are affected non-monotonically (involve negation-as-failure).
     */
    public boolean isMonotonic() {
        return isMonotonic;
    }
    
    /**
     * Returns true if the rule does not depend on any data, and so should 
     * be treated as an axiom.
     */
    public boolean isAxiom() {
        if (isBackward() && body.length > 0) return false;
        for ( ClauseEntry aBody : body )
        {
            if ( aBody instanceof TriplePattern )
            {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Printable string describing the rule
     */
    @Override
    public String toString() {
        StringBuilder buff = new StringBuilder();
        buff.append("[ ");
        if (name != null) {
            buff.append(name);
            buff.append(": ");
        }
        if (isBackward) {
            for ( ClauseEntry aHead : head )
            {
                buff.append( PrintUtil.print( aHead ) );
                buff.append( " " );
            }
            buff.append("<- ");
            for ( ClauseEntry aBody : body )
            {
                buff.append( PrintUtil.print( aBody ) );
                buff.append( " " );
            }
        } else {
            for ( ClauseEntry aBody : body )
            {
                buff.append( PrintUtil.print( aBody ) );
                buff.append( " " );
            }
            buff.append("-> ");
            for ( ClauseEntry aHead : head )
            {
                buff.append( PrintUtil.print( aHead ) );
                buff.append( " " );
            }
        }
        buff.append("]");
        return buff.toString();
    }
    
    /**
     * Print a short description of the rule, just its name if it
     * has one, otherwise the whole rule description.
     */
    public String toShortString() {
        if (name != null) {
            return name;
        } else {
            return toString();
        }
    }
    
//=======================================================================
// parser access

    /**
     * Parse a string as a rule,  using the default BuiltinRegistry to resolve functor names
     *
     * @throws ParserException if there is a problem
     */
    public static Rule parseRule(String source) throws ParserException {
        return parseRule(source,BuiltinRegistry.theRegistry);
    }

    /**
     * Parse a string as a rule using a specified BuiltinRegistry to resolve functor names
     *
     * @param source the source string for the rule
     * @param registry the BuiltinRegistry used to resolve functor names
     * @throws ParserException if there is a problem
     */
    public static Rule parseRule(String source,BuiltinRegistry registry) throws ParserException {
        Parser parser = new Parser(source,registry);
        return parser.parseRule();
    }

    /**
     * Answer the list of rules parsed from the given URL.
     * @throws RulesetNotFoundException
     */
    public static List<Rule> rulesFromURL( String uri,BuiltinRegistry registry) {
        BufferedReader br = null;
        try {
            InputStream in = FileManager.get().open(uri);
            if (in == null) throw new RulesetNotFoundException( uri );
            br = FileUtils.asBufferedUTF8( in );
            return parseRules( Rule.rulesParserFromReader( br ) );
        } finally {
            if (br != null) try { br.close(); } catch (IOException e2) {}
        }
    }

    /**
     * Answer the list of rules parsed from the given URL.
     * @throws RulesetNotFoundException
     */
    public static List<Rule> rulesFromURL( String uri) {
        return rulesFromURL(uri,BuiltinRegistry.theRegistry);
    }
        
    /**
     * Processes the source reader stripping off comment lines and noting prefix
     * definitions (@prefix) and rule inclusion commands (@include).
     * Returns a parser which is bound to the stripped source text with 
     * associated prefix and rule inclusion definitions.
    */
    public static Parser rulesParserFromReader( BufferedReader src, BuiltinRegistry registry ) {
       try {
           StringBuilder result = new StringBuilder();
           String line;
           Map<String, String> prefixes = new HashMap<>();
           List<Rule> preloadedRules = new ArrayList<>();
           while ((line = src.readLine()) != null) {
               if (line.startsWith("#")) continue;     // Skip comment lines
               line = line.trim();
               if (line.startsWith("//")) continue;    // Skip comment lines
               if (line.startsWith("@prefix")) {
                   line = line.substring("@prefix".length());
                   String prefix = nextArg(line);
                   String rest = nextAfterArg(line);
                   if (prefix.endsWith(":")) prefix = prefix.substring(0, prefix.length() - 1);
                   String url = extractURI(rest);
                   prefixes.put(prefix, url);

               } else if (line.startsWith("@include")) {
                   // Include referenced rule file, either URL or local special case
                   line = line.substring("@include".length());
                   String url = extractURI(line);
                   // Check for predefined cases
                   if (url.equalsIgnoreCase("rdfs")) {
                       preloadedRules.addAll( RDFSFBRuleReasoner.loadRules() );
                       
                   } else if (url.equalsIgnoreCase("owl")) {
                       preloadedRules.addAll( OWLFBRuleReasoner.loadRules() ) ;
                       
                   } else if (url.equalsIgnoreCase("owlmicro")) {
                       preloadedRules.addAll( OWLMicroReasoner.loadRules() ) ;
                       
                   } else if (url.equalsIgnoreCase("owlmini")) {
                       preloadedRules.addAll( OWLMiniReasoner.loadRules() ) ;
                       
                   } else {
                       // Just try loading as a URL
                       preloadedRules.addAll( rulesFromURL(url) );
                   }

               } else {
                   result.append(line);
                   result.append("\n");
               }
           }
           Parser parser = new Parser(result.toString(),registry);
           parser.registerPrefixMap(prefixes);
           parser.addRulesPreload(preloadedRules);
           return parser;
       }
       catch (IOException e) 
           { throw new WrappedIOException( e ); }
   }

    /**
     * Processes the source reader stripping off comment lines and noting prefix
     * definitions (@prefix) and rule inclusion commands (@include).
     * Returns a parser which is bound to the stripped source text with
     * associated prefix and rule inclusion definitions.
     */
    public static Parser rulesParserFromReader( BufferedReader src) {
       return rulesParserFromReader(src,BuiltinRegistry.theRegistry);
    }

    /** 
     * Helper function find a URI argument in the current string,
     * optionally surrounded by matching <>.
     */
    private static String extractURI(String lineSoFar) {
        String token = lineSoFar.trim();
        if (token.startsWith("<")) {
            int split = token.indexOf('>');
            token = token.substring(1, split);
        }
        return token;
    }

    /** 
     * Helper function to return the next whitespace delimited argument
     * from the string
     */
    private static String nextArg(String token) {
        int start = nextSplit(0, false, token);
        int stop = nextSplit(start, true, token);
        return token.substring(start, stop);
    }
    
    /** 
     * Helper function to return the remainder of the line after
     * stripping off the next whitespace delimited argument
     * from the string
     */
    private static String nextAfterArg(String token) {
        int start = nextSplit(0, false, token);
        int stop = nextSplit(start, true, token);
        int rest = nextSplit(stop, false, token);
        return token.substring(rest);
    }
    
    /**
     * Helper function - find index of next whitespace or non white
     * after the start index. 
     */
    private static int nextSplit(int start, boolean white, String line) {
        int i = start;
        while (i < line.length()) {
            boolean isWhite = Character.isWhitespace(line.charAt(i));
            if ((white & isWhite) || (!white & !isWhite)) {
                return i;
            }
            i++;
        }
        return i;
    }

    /**
     * Run a pre-bound rule parser to extract it's rules
     * @return a list of rules
     * @throws ParserException if there is a problem
     */
    public static List<Rule> parseRules(Parser parser) throws ParserException {
        boolean finished = false;
        List<Rule> ruleset = new ArrayList<>();
        ruleset.addAll(parser.getRulesPreload());
        while (!finished) {
            try {
                parser.peekToken();
            } catch (NoSuchElementException e) {
                finished = true;
                break;
            }
            Rule rule = parser.parseRule();
            ruleset.add(rule);
        }
        return ruleset;
    }

    /**
     * Parse a string as a list a rules.
     * @return a list of rules
     * @throws ParserException if there is a problem
     */
    public static List<Rule> parseRules(String source,BuiltinRegistry registry) throws ParserException {
        return parseRules(new Parser(source,registry));
    }

    /**
     * Parse a string as a list a rules.
     * @return a list of rules
     * @throws ParserException if there is a problem
     */
    public static List<Rule> parseRules(String source) throws ParserException {
        return parseRules(source,BuiltinRegistry.theRegistry);
    }
    

//=======================================================================
// parser support

    /**
     * Inner class which provides minimalist parsing support based on
     * tokenisation with depth 1 lookahead. No sensible error reporting on offer.
     * No embedded spaces supported.
     */
    public static class Parser {


        /** Tokenizer */
        private Tokenizer stream;
        
        /** Look ahead, null if none */
        private String lookahead;

        // registry for builtin name lookup
        private final BuiltinRegistry registry;

        // Literal parse state flags
        private static final int NORMAL = 0;
        private static final int STARTED_LITERAL = 1;
        
        /** Literal parse state */
        private int literalState = NORMAL;
        
        /** Trace back of recent tokens for error reporting */
        protected List<String> priorTokens = new ArrayList<>();
        
        /** Maximum number of recent tokens to remember */
        private static final int maxPriors = 20;
        
        /** Variable table */
        private Map<String, Node_RuleVariable> varMap;
        
        /** Local prefix map */
        private PrefixMapping prefixMapping = PrefixMapping.Factory.create();
        
        /** Pre-included rules */
        private List<Rule> preloadedRules = new ArrayList<>();
        
        /**
         * Constructor
         * @param source the string to be parsed
         */
        Parser(String source,BuiltinRegistry registry) {
            stream = new Tokenizer(source, "()[], \t\n\r", "'\"", true);
            lookahead = null;
            this.registry=registry;
        }
        
        /**
         * Register a new namespace prefix with the parser
         */
        public void registerPrefix(String prefix, String namespace ) {
            prefixMapping.setNsPrefix(prefix, namespace);
        }
        
        /**
         * Register a set of prefix to namespace mappings with the parser
         */
        public void registerPrefixMap(Map<String, String> map) {
            prefixMapping.setNsPrefixes(map);
        }
        
        /**
         * Return a map of all the discovered prefixes
         */
        public Map<String, String> getPrefixMap() {
            return prefixMapping.getNsPrefixMap();
        }
        
        /**
         * Add a new set of preloaded rules.
         */
        void addRulesPreload(List<Rule> rules) {
            preloadedRules.addAll(rules);
        }
        
        /**
         * Return the complete set of preloaded rules;
         */
        public List<Rule> getRulesPreload() {
            return preloadedRules;
        }
        
        /**
         * Return the next token
         */
        String nextToken() {
            if (lookahead != null) {
                String temp = lookahead;
                lookahead = null;
                return temp;
            } else {
                String token = stream.nextToken();
                if (literalState == NORMAL) {
                    // Skip separators unless within a literal
                    while (isSeparator(token)) {
                        token = stream.nextToken();
                    }
                }
                if (token.equals("'")) {
                    if (literalState == NORMAL) {
                        literalState = STARTED_LITERAL;
                    } else {
                        literalState = NORMAL;
                    }
                }
                priorTokens.add(0, token);
                if (priorTokens.size() > maxPriors) {
                    priorTokens.remove(priorTokens.size()-1);
                }
                return token;
            }
        }
                
        /**
         * Return a trace of the recently seen tokens, for use
         * in error reporting
         */
        public String recentTokens() {
            StringBuilder trace = new StringBuilder();
            for (int i = priorTokens.size()-1; i >= 0; i--) {
                trace.append(priorTokens.get(i));
                trace.append(" ");
            }
            return trace.toString();
        }
        
        /**
         * Peek ahead one token.
         */
        String peekToken() {
            if (lookahead == null) {
                lookahead = nextToken();
            }
            return lookahead;
        }
        
        /**
         * Push back a previously fetched token. Only depth 1 supported.
         */
        void pushback(String token) {
            lookahead = token;
        }
        
        /**
         * Returns true if token is an skippable separator
         */
        boolean isSeparator(String token) {
            if (token.length() == 1) {
                char c = token.charAt(0);
                return (c == ',' || Character.isWhitespace(c));
            }
            return false;
        }
        
        /**
         * Returns true if token is a syntax element ()[]
         */
        boolean isSyntax(String token) {
            if (token.length() == 1) {
                char c = token.charAt(0);
                return (c == '(' || c == ')' || c == '[' || c == ']');
            }
            return false;
        }
        
        /**
         * Find the variable index for the given variable name
         * and return a Node_RuleVariable with that index.
         */
        Node_RuleVariable getNodeVar(String name) {
            Node_RuleVariable node = varMap.get(name);
            if (node == null) {
                node = new Node_RuleVariable(name, varMap.size());
                varMap.put(name, node);
            }
            return node;
        }
        
        /**
         * Translate a token to a node.
         */
        Node parseNode(String token) {
            if (token.startsWith("?")) {
                return getNodeVar(token);
                // Dropped support for anon wildcards until the implementation is better resolved
            } else if (token.equals("*") || token.equals("_")) {
                throw new ParserException("Wildcard variables no longer supported", this);
////                return Node_RuleVariable.ANY;
//                return Node_RuleVariable.WILD;
            } else if (token.startsWith("<") && token.endsWith(">")) {
                String uri = token.substring(1, token.length()-1);
                return NodeFactory.createURI(uri);
            } else if (token.startsWith( "_" )) { // TODO rationalise [this is for the RIF code]
                return NodeFactory.createBlankNode( token.substring( 1 ) );
            } else if (token.indexOf(':') != -1) {
                String exp = prefixMapping.expandPrefix(token); // Local map first
                exp = PrintUtil.expandQname(exp);  // Retain global map for backward compatibility
                if (exp == token) {
                    // No expansion was possible
                    String prefix = token.substring(0, token.indexOf(':'));
                    if (prefix.equals("http") || prefix.equals("urn") || prefix.equals("file")
                     || prefix.equals("ftp") || prefix.equals("mailto")) {
                        // assume it is all OK and fall through
                    } else {
                        // Likely to be a typo in a qname or failure to register
                        throw new ParserException("Unrecognized qname prefix (" + prefix + ") in rule", this);
                    }
                }
                return NodeFactory.createURI(exp);
            } else if (peekToken().equals("(")) {
                Functor f = new Functor(token, parseNodeList(), registry);
                return Functor.makeFunctorNode( f );
            } else if (token.equals("'") || token.equals("\"")) {
                // A plain literal
                String lit = nextToken();
                // Skip the trailing quote
                nextToken();
                // Check for an explicit datatype
                if (peekToken().startsWith("^^")) {
                    String dtURI = nextToken().substring(2);
                    if (dtURI.indexOf(':') != -1) {
                        // Thanks to Steve Cranefield for pointing out the need for prefix expansion here
                        String exp = prefixMapping.expandPrefix(dtURI); // Local map first
                        exp = PrintUtil.expandQname(exp);  // Retain global map for backward compatibility
                        if (exp == dtURI) {
                            // No expansion was possible
                            String prefix = dtURI.substring(0, dtURI.indexOf(':'));
                            if (prefix.equals("http") || prefix.equals("urn") 
                             || prefix.equals("ftp") || prefix.equals("mailto")) {
                                // assume it is all OK and fall through
                            } else {
                                // Likely to be a typo in a qname or failure to register
                                throw new ParserException("Unrecognized qname prefix (" + prefix + ") in rule", this);
                            }
                        } else {
                            dtURI = exp;
                        }
                    } 
                    RDFDatatype dt = TypeMapper.getInstance().getSafeTypeByName(dtURI);
                    return NodeFactory.createLiteral(lit, dt);
                } else {
                    return NodeFactory.createLiteral(lit, "");
                }                
            } else  if ( Character.isDigit(token.charAt(0)) || 
                         (token.charAt(0) == '-' && token.length() > 1 && Character.isDigit(token.charAt(1))) ) {
                // A number literal
               return parseNumber(token);
            } else {
                // A  uri
                return NodeFactory.createURI(token);
            }
        }
        
        /**
         * Turn a possible numeric token into typed literal else a plain literal
         * @return the constructed literal node
         */
        Node parseNumber(String lit) {
            if ( Character.isDigit(lit.charAt(0)) || 
                (lit.charAt(0) == '-' && lit.length() > 1 && Character.isDigit(lit.charAt(1))) ) {
                if ( lit.contains( "." ) ) {
                    // Float?
                    if (XSDDatatype.XSDfloat.isValid(lit)) {
                        return NodeFactory.createLiteral(lit, XSDDatatype.XSDfloat);
                    }
                } else {
                    // Int?
                    if (XSDDatatype.XSDint.isValid(lit)) {
                        return NodeFactory.createLiteral(lit, XSDDatatype.XSDint);
                    }
                }
            }
            // Default is a plain literal
            return NodeFactory.createLiteral(lit, "");
        }
        
        /**
         * Parse a list of nodes delimited by parentheses
         */
        List<Node> parseNodeList() {
            String token = nextToken();
            if (!token.equals("(")) {
                throw new ParserException("Expected '(' at start of clause, found " + token, this);
            }
            token = nextToken();
            List<Node> nodeList = new ArrayList<>();
            while (!isSyntax(token)) {
                nodeList.add(parseNode(token));
                token = nextToken();
            }
            if (!token.equals(")")) {
                throw new ParserException("Expected ')' at end of clause, found " + token, this);
            }
            return nodeList;
        }
        
        /**
         * Parse a clause, could be a triple pattern, a rule or a functor
         */
        ClauseEntry parseClause() {
            String token = peekToken();
            if (token.equals("(")) {
                List<Node> nodes = parseNodeList();
                if (nodes.size() != 3) {
                    throw new ParserException("Triple with " + nodes.size() + " nodes!", this);
                }
                if (Functor.isFunctor(nodes.get(0))) {
                    throw new ParserException("Functors not allowed in subject position of pattern", this);
                }
                if (Functor.isFunctor(nodes.get(1))) {
                    throw new ParserException("Functors not allowed in predicate position of pattern", this);
                }
                return new TriplePattern(nodes.get(0), nodes.get(1), nodes.get(2));
            } else if (token.equals("[")) {
                nextToken();
                return doParseRule(true);
            } else {
                String name = nextToken();
                List<Node> args = parseNodeList();
                Functor clause = new Functor(name, args, registry);
                if (clause.getImplementor() == null) {
                    // Not a.error error becase later processing can add this
                    // implementation to the registry
                    logger.warn("Rule references unimplemented functor: " + name);
                }
                return clause;
            }
        }
        
        
        /**
         * Parse a rule, terminated by a "]" or "." character.
         */
        public Rule parseRule() {
            return doParseRule(false);
        }
        
        /**
         * Parse a rule, terminated by a "]" or "." character.
         * @param retainVarMap set to true to ccause the existing varMap to be left in place, which
         * is required for nested rules.
         */
        private Rule doParseRule(boolean retainVarMap) {
            try {
                // Skip initial '[' if present
                if (peekToken().equals("[")) {
                    nextToken();
                }
                // Check for optional name
                String name = null;
                String token = peekToken();
                if (token.endsWith(":")) {
                    name = token.substring(0, token.length()-1);
                    nextToken();
                }
                // Start rule parsing with empty variable table
                if (!retainVarMap) varMap = new HashMap<>();
                // Body
                List<ClauseEntry> body = new ArrayList<>();
                token = peekToken();
                while ( !(token.equals("->") || token.equals("<-")) ) {
                    body.add(parseClause());
                    token = peekToken();
                }
                boolean backwardRule = token.equals("<-");
                List<ClauseEntry> head = new ArrayList<>();
                token = nextToken();   // skip -> token
                token = peekToken();
                while ( !(token.equals(".") || token.equals("]")) ) {
                    head.add(parseClause());
                    token = peekToken();
                } 
                nextToken();        // consume the terminating token
                Rule r = null;
                if (backwardRule) {
                    r =  new Rule(name, body, head);
                } else {
                    r = new Rule(name, head, body);
                }
                r.numVars = varMap.keySet().size();
                r.isBackward = backwardRule;
                return r;
            } catch (NoSuchElementException e) {
                throw new ParserException("Malformed rule", this);
            }
        }

    }
   
    /** Equality override */
    @Override
    public boolean equals(Object o) {
        // Pass 1 - just check basic shape
        if (! (o instanceof Rule) ) return false;
        Rule other = (Rule) o;
        if (other.head.length != head.length) return false;
        if (other.body.length != body.length) return false;
        // Pass 2 - check clause by clause matching
        for (int i = 0; i < body.length; i++) {
            if (! (body[i]).sameAs(other.body[i]) ) return false;
        }
        for (int i = 0; i < head.length; i++) {
            if (! (head[i]).sameAs(other.head[i]) ) return false;
        }
        // Also include the rule name in the equality contract
        if (name != null) {
            if ( !name.equals(other.name) ) return false;
        } else {
            if (other.name != null) return false;
        }
        return true;
    }
        
    /** hash function override */
    @Override
    public int hashCode() {
        int hash = 0;
        for ( ClauseEntry aBody : body )
        {
            hash = ( hash << 1 ) ^ aBody.hashCode();
        }
        for ( ClauseEntry aHead : head )
        {
            hash = ( hash << 1 ) ^ aHead.hashCode();
        }
        return hash;
    }
    
    /**
     * Compare clause entries, taking into account variable indices.
     * The equality function ignores differences between variables.
     */
    @Override
    public boolean sameAs(Object o) {
        return equals(o);
    }
    
//=======================================================================
// Other supporting inner classes

    /**
     * Inner class. Exception raised if there is a problem
     * during rule parsing.
     */
    public static class ParserException extends JenaException {
        private static final long serialVersionUID = -7297703783308663168L;

        /** constructor */
        public ParserException(String message, Parser parser) {
            super(constructMessage(message, parser));
        }
        
        /**
         * Extract context trace from prior tokens stack
         */
        private static String constructMessage(String baseMessage, Parser parser) {
            StringBuilder message = new StringBuilder();
            message.append(baseMessage);
            message.append("\nAt '");
            message.append(parser.recentTokens());
            message.append("'");
            return message.toString();
        }
        
    }
    
}
