/*
 * 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.impl;

import java.util.*;

import org.apache.jena.graph.* ;
import org.apache.jena.reasoner.TriplePattern ;
import org.apache.jena.reasoner.rulesys.* ;

/**
 * Holds the set of backward rules used by an LPEngine. Is responsible
 * for compile the rules into internal byte codes before use.
 */
public class LPRuleStore extends RuleStore {
    
    /** Flag to indicate whether the rules have been compiled into code objects */
    protected boolean isCompiled = false;
    
    /** A map from predicate to a list of RuleClauseCode objects for that predicate.
     *  Uses Node_RuleVariable.WILD for wildcard predicates.
     */ 
    protected Map<Node, List<RuleClauseCode>> predicateToCodeMap;
    
    /** The list of all RuleClauseCode objects, used to implement wildcard queries */
    protected ArrayList<RuleClauseCode> allRuleClauseCodes;

    /** Two level index map - index on predicate then on object */
    protected Map<Node, Map<Node, List<RuleClauseCode>>> indexPredicateToCodeMap;
        
    /** Set of predicates which should be tabled */
    protected HashSet<Node> tabledPredicates = new HashSet<>();
        
    /** Threshold for number of rule entries in a predicate bucket before second level indexing kicks in */
    private static final int INDEX_THRESHOLD = 20;
    
    /** True if all goals should be treated as tabled */
    protected boolean allTabled = false;
    
    /**
     * Construct a rule store containing the given rules.
     * @param rules the rules to initialize the store with.
     */
    public LPRuleStore(List<Rule> rules) {
        super(rules);
    }
    
    /**
     * Construct an empty rule store
     */
    public LPRuleStore() {
        super();
    }
    
    /**
     * Add all the rules and tabling instructions from an existing rulestore into this one.
     */
    public void addAll(LPRuleStore store) {
        super.addAll(store);
        tabledPredicates.addAll(store.tabledPredicates);
        allTabled = tabledPredicates.contains(Node.ANY);
    }
    
    /**
     * Register an RDF predicate as one whose presence in a goal should force
     * the goal to be tabled.
     */
    public synchronized void tablePredicate(Node predicate) {
        tabledPredicates.add(predicate);
        if (predicate == Node.ANY) allTabled = true;
    }    
    
    /**
     * Return an ordered list of RuleClauseCode objects to implement the given 
     * predicate.
     * @param predicate the predicate node or Node_RuleVariable.WILD for wildcards.
     */
    public List<RuleClauseCode> codeFor(Node predicate) {
        if (!isCompiled) {
            compileAll();
        }
        if (predicate.isVariable()) {
            return allRuleClauseCodes;
        } else {
            List<RuleClauseCode> codeList = predicateToCodeMap.get(predicate);
            if (codeList == null) {
                // Uknown predicate, so only the wildcard rules apply
                codeList = predicateToCodeMap.get(Node_RuleVariable.WILD);
            }
            return codeList;
        }
    }
    
    /**
     * Return an ordered list of RuleClauseCode objects to implement the given 
     * query pattern. This may use indexing to narrow the rule set more that the predicate-only case. 
     * @param goal the triple pattern that makes up the query
     */
    public List<RuleClauseCode> codeFor(TriplePattern goal) {
        List<RuleClauseCode> allRules = codeFor(goal.getPredicate());
        if (allRules == null) {
            return allRules;
        }
        Map<Node, List<RuleClauseCode>> indexedCodeTable = indexPredicateToCodeMap.get(goal.getPredicate());
        if (indexedCodeTable != null) {
            List<RuleClauseCode> indexedCode = indexedCodeTable.get(goal.getObject());
            if (indexedCode != null) {
                return indexedCode;
            }
        }
        return allRules;
    }
    
    /**
     * Return true if the given predicate is indexed.
     */
    public boolean isIndexedPredicate(Node predicate) {
        return (indexPredicateToCodeMap.get(predicate) != null);
    }
    
    /**
     * Return true if the given goal is tabled, currently this is true if the
     * predicate is a tabled predicate or the predicate is a wildcard and some
     * tabled predicates exist.
     */
    public boolean isTabled(TriplePattern goal) {
        return isTabled(goal.getPredicate());
    }
    
    /**
     * Return true if the given predicated is tabled, currently this is true if the
     * predicate is a tabled predicate or the predicate is a wildcard and some
     * tabled predicates exist.
     */
    public boolean isTabled(Node predicate) {
        if (allTabled) return true;
        if (predicate.isVariable() && !tabledPredicates.isEmpty()) {
            return true;
        } else {
            return tabledPredicates.contains(predicate);
        }
    }
    
    /**
     * Compile all the rules in a table. initially just indexed on predicate but want to 
     * add better indexing for the particular cases of wildcard rules and type rules. 
     */
    protected void compileAll() {
        isCompiled = true;
        
        predicateToCodeMap = new HashMap<>();
        allRuleClauseCodes = new ArrayList<>();
        indexPredicateToCodeMap = new HashMap<>();
        for ( Rule r : getAllRules() )
        {
            ClauseEntry term = r.getHeadElement( 0 );
            if ( term instanceof TriplePattern )
            {
                RuleClauseCode code = new RuleClauseCode( r );
                allRuleClauseCodes.add( code );
                Node predicate = ( (TriplePattern) term ).getPredicate();
                if ( predicate.isVariable() )
                {
                    predicate = Node_RuleVariable.WILD;
                }
                List<RuleClauseCode> predicateCode = predicateToCodeMap.get( predicate );
                if ( predicateCode == null )
                {
                    predicateCode = new ArrayList<>();
                    predicateToCodeMap.put( predicate, predicateCode );
                }
                predicateCode.add( code );
                if ( predicateCode.size() > INDEX_THRESHOLD )
                {
                    indexPredicateToCodeMap.put( predicate, new HashMap<Node, List<RuleClauseCode>>() );
                }
            }
        }

        // Now add the wild card rules into the list for each non-wild predicate)
        List<RuleClauseCode> wildRules = predicateToCodeMap.get(Node_RuleVariable.WILD);
        if (wildRules != null) {
            for ( Map.Entry<Node, List<RuleClauseCode>> entry : predicateToCodeMap.entrySet() )
            {
                Node predicate = entry.getKey();
                List<RuleClauseCode> predicateCode = entry.getValue();
                if ( predicate != Node_RuleVariable.WILD )
                {
                    predicateCode.addAll( wildRules );
                }
            }
        }
        indexPredicateToCodeMap.put(Node_RuleVariable.WILD, new HashMap<Node, List<RuleClauseCode>>());
                
        // Now built any required two level indices
        for ( Map.Entry<Node, Map<Node, List<RuleClauseCode>>> entry : indexPredicateToCodeMap.entrySet() )
        {
            Node predicate = entry.getKey();
            Map<Node, List<RuleClauseCode>> predicateMap = entry.getValue();
            List<RuleClauseCode> wildRulesForPredicate = new ArrayList<>();
            List<RuleClauseCode> allRulesForPredicate =
                predicate.isVariable() ? allRuleClauseCodes : predicateToCodeMap.get( predicate );
            for ( Iterator<RuleClauseCode> j = allRulesForPredicate.iterator(); j.hasNext(); )
            {
                RuleClauseCode code = j.next();
                ClauseEntry head = code.getRule().getHeadElement( 0 );
                boolean indexed = false;
                if ( head instanceof TriplePattern )
                {
                    Node objectPattern = ( (TriplePattern) head ).getObject();
                    if ( !objectPattern.isVariable() && !Functor.isFunctor( objectPattern ) )
                    {
                        // Index against object
                        List<RuleClauseCode> indexedCode = predicateMap.get( objectPattern );
                        if ( indexedCode == null )
                        {
                            indexedCode = new ArrayList<>();
                            predicateMap.put( objectPattern, indexedCode );
                        }
                        indexedCode.add( code );
                        indexed = true;
                    }
                }
                if ( !indexed )
                {
                    wildRulesForPredicate.add( code );
                }
            }
            // Now fold the rules that apply to any index entry into all the indexed entries
            for ( Iterator<Map.Entry<Node, List<RuleClauseCode>>> k = predicateMap.entrySet().iterator(); k.hasNext(); )
            {
                Map.Entry<Node, List<RuleClauseCode>> ent = k.next();
                List<RuleClauseCode> predicateCode = ent.getValue();
                predicateCode.addAll( wildRulesForPredicate );
            }
        }
        
        // Now compile all the clauses
        for ( RuleClauseCode code : allRuleClauseCodes )
        {
            code.compile( this );
        }
    }
    
    /**
     * Add/remove a single rule from the store. 
     * Overridden in order to reset the "isCompiled" flag.
     * 
     * @param rule the rule, single headed only
     * @param isAdd true to add, false to remove 
     */
    @Override
    protected void doAddRemoveRule(Rule rule, boolean isAdd) {
        isCompiled = false;
        super.doAddRemoveRule(rule, isAdd);
    }

}
