/*
 * 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 jena;


import static org.apache.jena.atlas.logging.LogCtl.setLogging;

import java.io.BufferedReader;
import java.io.File;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.apache.jena.rdf.model.InfModel;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.reasoner.Reasoner ;
import org.apache.jena.reasoner.rulesys.* ;
import org.apache.jena.reasoner.rulesys.builtins.BaseBuiltin ;
import org.apache.jena.util.FileUtils ;

/**
 * General command line utility to process one RDF file into another
 * by application of a set of forward chaining rules.
 * <pre>
 * Usage:  RuleMap [-il inlang] [-ol outlang] [-d] rulefile infile
 * </pre>
 * The resulting RDF data is written to stdout in format <code>outlang</code>
 * (default N3). If <code>-d</code> is given then only the deductions
 * generated by the rules are output. Otherwise all data including any input
 * data (other than any removed triples) is output.
 * <p>
 * Rules are permitted an additional action "deduce" which forces triples
 * to be added to the deductions graph even if they are already known (for use
 * in deductions only mode). 
 * </p>
 */
public class RuleMap {
    static { setLogging() ; }
    
    /**
     * Load a set of rule definitions including processing of
     * comment lines and any initial prefix definition lines.
     * Also notes the prefix definitions for adding to a later inf model.
     */
    public static List<Rule> loadRules(String filename, Map<String, String> prefixes) {
        String fname = filename;
        if (fname.startsWith("file:///")) {
            fname = File.separator + fname.substring(8);
        } else if (fname.startsWith("file:/")) {
            fname = File.separator + fname.substring(6);
        } else if (fname.startsWith("file:")) {
            fname = fname.substring(5);
        }

        BufferedReader src = FileUtils.openResourceFile(fname);
        return loadRules(src, prefixes);
    }
    
    /**
     * Load a set of rule definitions including processing of
     * comment lines and any initial prefix definition lines.
     * Also notes the prefix definitions for adding to a later inf model.
     */
    public static List<Rule> loadRules(BufferedReader src, Map<String, String> prefixes) {
        Rule.Parser parser = Rule.rulesParserFromReader(src);
        List<Rule> rules = Rule.parseRules(parser);
        prefixes.putAll(parser.getPrefixMap());
        return rules;
    }
    
    /**
     * Internal implementation of the "deduce" primitve.
     * This takes the form <code> ... -> deduce(s, p, o)</code>
     */
    static class Deduce extends BaseBuiltin {

        /**
         * Return a name for this builtin, normally this will be the name of the 
         * functor that will be used to invoke it.
         */
        @Override
        public String getName() {
            return "deduce";
        }    
   
        /**
         * Return the expected number of arguments for this functor or 0 if the number is flexible.
         */
        @Override
        public int getArgLength() {
            return 3;
        }

        /**
         * This method is invoked when the builtin is called in a rule head.
         * Such a use is only valid in a forward rule.
         * @param args the array of argument values for the builtin, this is an array 
         * of Nodes.
         * @param length the length of the argument list, may be less than the length of the args array
         * for some rule engines
         * @param context an execution context giving access to other relevant data
         */
        @Override
        public void headAction(Node[] args, int length, RuleContext context) {
            if (context.getGraph() instanceof FBRuleInfGraph) {
                Triple t = new Triple(args[0], args[1], args[2]);
                ((FBRuleInfGraph)context.getGraph()).addDeduction(t);
            } else {
                throw new BuiltinException(this, context, "Only usable in FBrule graphs");
            }
        }
    }
    
    /**
     * General command line utility to process one RDF file into another
     * by application of a set of forward chaining rules. 
     * <pre>
     * Usage:  RuleMap [-il inlang] [-ol outlang] -d infile rulefile
     * </pre>
     */
    public static void main(String[] args) {
    	try {

            // Parse the command line
            String usage = "Usage:  RuleMap [-il inlang] [-ol outlang] [-d] rulefile infile (- for stdin)"; 
            final CommandLineParser parser = new DefaultParser();
			Options options = new Options().addOption("il", "inputLang", true, "input language")
					.addOption("ol", "outputLang", true, "output language").addOption("d", "Deductions only?");
			CommandLine cl = parser.parse(options, args);
			final List<String> filenameArgs = cl.getArgList();
            if (filenameArgs.size() != 2) {
                System.err.println(usage);
                System.exit(1);
            }
            
            String inLang = cl.getOptionValue("inputLang");
            String fname = filenameArgs.get(1);
            Model inModel = ModelFactory.createDefaultModel();
            if (fname.equals("-")) {
                inModel.read(System.in, null, inLang);
            } else {
                inModel.read(fname, inLang);
            }
            
            String outLang = cl.hasOption("outputLang") ? cl.getOptionValue("outputLang") : "N3";            
            
            boolean deductionsOnly = cl.hasOption('d');
            
            // Fetch the rule set and create the reasoner
            BuiltinRegistry.theRegistry.register(new Deduce());
            Map<String, String> prefixes = new HashMap<>();
            List<Rule> rules = loadRules(filenameArgs.get(0), prefixes);
            Reasoner reasoner = new GenericRuleReasoner(rules);
            
            // Process
            InfModel infModel = ModelFactory.createInfModel(reasoner, inModel);
            infModel.prepare();
            infModel.setNsPrefixes(prefixes);
            
            // Output
            try ( PrintWriter writer = new PrintWriter(System.out) ) {
                if (deductionsOnly) {
                    Model deductions = infModel.getDeductionsModel();
                    deductions.setNsPrefixes(prefixes);
                    deductions.setNsPrefixes(inModel);
                    deductions.write(writer, outLang);
                } else {
                    infModel.write(writer, outLang);
                }
            }
        } catch (Throwable t) {
            System.err.println("An error occured: \n" + t);
            t.printStackTrace();
        }
    }

}
