/*
 * 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.util.List;
import java.util.function.Predicate;

import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.Node_ANY;
import org.apache.jena.graph.Triple;
import org.apache.jena.util.PrintUtil ;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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).  Functors play three roles in rules -
 * in heads they represent actions (procedural attachment); in bodies they
 * represent builtin predicates; in TriplePatterns they represent embedded
 * structured literals that are used to cache matched subgraphs such as
 * restriction specifications.
 */
public class Functor implements ClauseEntry {
    /** Functor's name */
    protected String name;

    /** Argument list - an array of nodes */
    protected Node[] args;

    /** A built in that implements the functor */
    protected Builtin implementor;

    /** A static Predicate instance that detects triples with Functor objects */
    public static final Predicate<Triple> acceptFilter = t ->  {
                    if (t.getSubject().isLiteral()) return true;
                    Node n = t.getObject();
                    return n.isLiteral() && n.getLiteralDatatype() == FunctorDatatype.theFunctorDatatype;
            };

    protected static Logger logger = LoggerFactory.getLogger(Functor.class);

    /**
     * Constructor.
     * @param name the name of the functor
     * @param args an array of nodes defining the arguments, this will not be copied so beware of
     * accidental structure sharing
     */
    public Functor(String name, Node[] args) {
        this.name = name;
        this.args = args;
    }

    /**
     * Constructor
     * @param name the name of the functor
     * @param args a list of nodes defining the arguments
     * @param registry a table of builtins to consult to check for
     * implementations of this functor when used as a rule clause
     */
    public Functor(String name, List<Node> args, BuiltinRegistry registry) {
        this(name, args,registry.getImplementation(name));
    }

    /**
     * Constructor
     * @param name the name of the functor
     * @param args an array of nodes defining the arguments
     * @param registry a table of builtins to consult to check for
     * implementations of this functor when used as a rule clause
     */
    public Functor(String name,Node[] args, BuiltinRegistry registry) {
        this(name, args,registry.getImplementation(name));
    }

    /**
     * Constructor
     * @param name the name of the functor
     * @param args a list of nodes defining the arguments
     * @param impl a specific builtin implementation of this functor
     *
     */
    public Functor(String name, List<Node> args, Builtin impl) {
        this.name = name;
        this.args = args.toArray(new Node[]{});
        this.implementor = impl;
    }

    /**
     * Constructor
     * @param name the name of the functor
     * @param args a list of nodes defining the arguments
     * @param impl a specific builtin implementation of this functor
     *
     */
    public Functor(String name, Node[] args, Builtin impl) {
        this.name = name;
        this.args = args;
        this.implementor = impl;
    }

    /**
     * Return the functor name
     */
    public String getName() {
        return name;
    }

    /**
     * Return the functor arguments as an array of nodes
     */
    public Node[] getArgs() {
        return args;
    }

    /**
     * Return the length of the functor argument array.
     */
    public int getArgLength() {
        return args.length;
    }

    /**
     * Returns true if the functor is fully ground, no variables
     */
    public boolean isGround() {
        for ( Node n : args )
        {
            if ( n instanceof Node_RuleVariable || n instanceof Node_ANY )
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if the functor is fully ground in the given environment
     */
    public boolean isGround(BindingEnvironment env) {
        for ( Node n : args )
        {
            if ( env.getGroundVersion( n ).isVariable() )
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Execute the given built in as a body clause.
     * @param context an execution context giving access to other relevant data
     * @return true if the functor has an implementation and that implementation returns true when evaluated
     */
    public boolean evalAsBodyClause(RuleContext context) {
        if (getImplementor() == null) {
            logger.warn("Invoking undefined functor " + getName() + " in " + context.getRule().toShortString());
            return false;
        }
        return implementor.bodyCall(getBoundArgs(context.getEnv()), args.length, context);
    }

    /**
     * Execute the given built in as a body clause, only if it is side-effect-free.
     * @param context an execution context giving access to other relevant data
     * @return true if the functor has an implementation and that implementation returns true when evaluated
     */
    public boolean safeEvalAsBodyClause(RuleContext context) {
        if (getImplementor() == null) {
            logger.warn("Invoking undefined functor " + getName() + " in " + context.getRule().toShortString());
            return false;
        }
        if (implementor.isSafe()) {
            return implementor.bodyCall(getBoundArgs(context.getEnv()), args.length, context);
        } else {
            return false;
        }
    }

    /**
     * Return a new Node array containing the bound versions of this Functor's arguments
     */
    public Node[] getBoundArgs(BindingEnvironment env) {
        Node[] boundargs = new Node[args.length];
        for (int i = 0; i < args.length; i++) {
            boundargs[i] = env.getGroundVersion(args[i]);
        }
        return boundargs;
    }

    /**
     * Return the Builtin that implements this functor
     * @return the Builtin or null if there isn't one
     */
    public Builtin getImplementor() {
        if (implementor == null) {
            implementor = BuiltinRegistry.theRegistry.getImplementation(name);
        }
        return implementor;
    }

    /**
     * Set the Builtin that implements this functor.
     */
    public void setImplementor(Builtin implementor) {
        this.implementor = implementor;
    }

    /**
     * Printable string describing the functor
     */
    @Override
    public String toString() {
        StringBuilder buff = new StringBuilder(name);
        buff.append("(");
        for (int i = 0; i < args.length; i++) {
            buff.append(PrintUtil.print(args[i]));
            if (i < args.length - 1) {
                buff.append(" ");
            }
        }
        buff.append(")");
        return buff.toString();
    }

    /**
     * tests that a given Node represents a functor
     */
    public static boolean isFunctor(Node n) {
        if (n == null) return false;
        return n.isLiteral() && n.getLiteralDatatype() == FunctorDatatype.theFunctorDatatype;
    }

    /**
     * Equality is based on structural comparison
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Functor) {
            Functor f2 = (Functor)obj;
            if (name.equals(f2.name) && args.length == f2.args.length) {
                for (int i = 0; i < args.length; i++) {
                    if (!args[i].sameValueAs(f2.args[i])) return false;
                }
                return true;
            }
        }
        return false;
    }

    /** hash function override */
    @Override
    public int hashCode() {
        return (name.hashCode()) ^ (args.length << 2);
    }

    /**
     * Compare Functors, taking into account variable indices.
     * The equality function ignores differences between variables.
     */
    @Override
    public boolean sameAs(Object o) {
        if (o instanceof Functor) {
            Functor f2 = (Functor)o;
            if (name.equals(f2.name) && args.length == f2.args.length) {
                for (int i = 0; i < args.length; i++) {
                    if (! Node_RuleVariable.sameNodeAs(args[i], f2.args[i])) return false;
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Create a functor and wrap it up as a Literal node
     * @param name the name of the functor
     * @param args an array of nodes defining the arguments, this will not be copied so beware of
     * accidental structure sharing
     */
    public static Node makeFunctorNode(String name, Node[] args) {
        return makeFunctorNode( new Functor( name, args, BuiltinRegistry.theRegistry ) );
    }

    /**
     * Wrap  a functor as a Literal node
     * @param f the functor data structure to be wrapped in a node.
     */
    public static Node makeFunctorNode(Functor f) {
        return NodeFactory.createLiteralByValue(f, FunctorDatatype.theFunctorDatatype);
    }
}
