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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.apache.jena.datatypes.BaseDatatype;
import org.apache.jena.datatypes.DatatypeFormatException;
import org.apache.jena.datatypes.RDFDatatype;
import org.apache.jena.datatypes.TypeMapper;
import org.apache.jena.graph.FrontsNode;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.impl.LiteralLabel;
import org.apache.jena.graph.impl.LiteralLabelFactory;
import org.apache.jena.riot.RiotException;
import org.apache.jena.riot.system.PrefixMapFactory;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.sparql.ARQInternalErrorException;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.expr.ExprVar;
import org.apache.jena.sparql.path.P_Link;
import org.apache.jena.sparql.path.Path;
import org.apache.jena.sparql.path.PathParser;
import org.apache.jena.sparql.util.NodeFactoryExtra;

/**
 * A collection of static methods to convert from Objects to various types used
 * in Query and Update construction.
 */
public class Converters {

    private Converters() {
        // do not make instance
    }

    /**
     * Converts any Node_Variable nodes into Var nodes.
     * 
     * @param n the node to check
     * @return the node n or a new Var if n is an instance of Node_Variable
     */
    public static Node checkVar(Node n) {
        if (n.isVariable()) {
            return Var.alloc(n);
        }
        return n;
    }

    /**
     * Creates a literal from an object. If the object type is registered with the
     * TypeMapper the associated literal string is returned. If the object is not
     * registered an IllegalArgumentException is thrown.
     * 
     * @param o the object to convert.
     * @return the literal node.
     * @throws IllegalArgumentException if object type is not registered.
     */
    public static Node makeLiteral(Object o) {

        RDFDatatype dt = TypeMapper.getInstance().getTypeByValue(o);
        if (dt == null) {
            String msg = "No TypeDef defined for %s. Use TypeMapper.getInstance().register() to "
                    + "register one or use makeLiteral() method in query builder instance.";
            throw new IllegalArgumentException(String.format(msg, o.getClass()));
        }
        return NodeFactory.createLiteral(LiteralLabelFactory.createTypedLiteral(o));

    }

    /**
     * Creates a literal from the value and type URI. There are several possible
     * outcomes:
     * <ul>
     * <li>If the URI is registered with TypeMapper and the value is the proper
     * lexical form for the type, the registered TypeMapper is used and calling
     * {@code getLiteralValue()} on the returned node will return a proper object.
     * </li>
     * <li>If the URI is unregistered a Datatype is created but not registered with
     * the TypeMapper. The resulting node is properly constructed for used in output
     * serialization, queries, or updates. Calling {@code getLiteralValue()} on the
     * returned node will throw DatatypeFormatException. Note that if
     * {@code JenaParameters.enableEagerLiteralValidation} is true the
     * DatatypeFormatException will be thrown by this method.</li>
     * <li>If the URI is registered but the value is not a proper lexical form a
     * DatatypeFormatException will be thrown by this method.</li>
     * </ul>
     * 
     * @param value the value for the literal
     * @param typeUri the type URI for the literal node.
     * @return the literal node.
     * @throws DatatypeFormatException on errors noted above
     */
    public static Node makeLiteral(String value, String typeUri) {
        Object oValue = value;
        RDFDatatype dt = TypeMapper.getInstance().getTypeByName(typeUri);
        if (dt == null) {
            dt = new BaseDatatype(typeUri) {

                @Override
                public boolean isValidValue(Object valueForm) {
                    return false;
                }

                @Override
                public Object parse(String lexicalForm) throws DatatypeFormatException {
                    RDFDatatype dt = TypeMapper.getInstance().getTypeByName(uri);
                    if (dt == null) {
                        throw new DatatypeFormatException("no registered Datatype for " + uri);
                    }
                    return dt.parse(lexicalForm);
                }

            };
        } else {
            oValue = dt.parse(value);
        }
        LiteralLabel ll = LiteralLabelFactory.createByValue(oValue, null, dt);
        return NodeFactory.createLiteral(ll);
    }

    /**
     * Makes a node from an object while using the associated prefix mapping.
     * <ul>
     * <li>Will return Node.ANY if object is null.</li>
     * <li>Will return the enclosed Node from a FrontsNode</li>
     * <li>Will return the object if it is a Node.</li>
     * <li>Will call NodeFactoryExtra.parseNode() using the currently defined
     * prefixes if the object is a String</li>
     * <li>Will call makeLiteral() to create a literal representation if the
     * parseNode() fails or for any other object type.</li>
     * </ul>
     * 
     * @param o The object to convert (may be null).
     * @param pMapping The prefix mapping to use for prefix resolution.
     * @return The Node value.
     * @see #makeLiteral(Object)
     */
    public static Node makeNode(Object o, PrefixMapping pMapping) {
        if (o == null) {
            return Node.ANY;
        }
        if (o instanceof FrontsNode) {
            return checkVar(((FrontsNode) o).asNode());
        }

        if (o instanceof Node) {
            return checkVar((Node) o);
        }
        if (o instanceof String) {
            try {
                return checkVar(NodeFactoryExtra.parseNode((String) o, PrefixMapFactory.create(pMapping)));
            } catch (final RiotException e) {
                // expected in some cases -- do nothing
            }

        }
        return makeLiteral(o);
    }

    /**
     * Creates a Path or Node as appropriate.
     * <ul>
     * <li>Will return Node.ANY if object is null.</li>
     * <li>Will return the object if it is a Path
     * <li>Will return the enclosed Node from a FrontsNode</li>
     * <li>Will return the object if it is a Node.</li>
     * <li>Will call PathParser.parse() using the prefix mapping if the object is a
     * String</li>
     * <li>Will call NodeFactoryExtra.parseNode() using the currently defined
     * prefixes if the object is a String and the PathParser.parse() fails.</li>
     * <li>Will call makeLiteral() to create a literal representation if the
     * parseNode() fails or for any other object type.</li>
     * </ul>
     * 
     * @param o the object that should be interpreted as a path or a node.
     * @param pMapping the prefix mapping to resolve path or node with
     * @return the Path or Node
     * @see #makeLiteral(Object)
     */
    public static Object makeNodeOrPath(Object o, PrefixMapping pMapping) {
        if (o == null) {
            return Node.ANY;
        }
        if (o instanceof Path) {
            return o;
        }
        if (o instanceof FrontsNode) {
            return checkVar(((FrontsNode) o).asNode());
        }

        if (o instanceof Node) {
            return checkVar((Node) o);
        }
        if (o instanceof String) {
            try {
                final Path p = PathParser.parse((String) o, pMapping);
                if (p instanceof P_Link) {
                    return ((P_Link) p).getNode();
                }
                return p;
            }

            catch (final Exception e) {
                // expected in some cases -- do nothing
            }

        }
        return makeNode(o, pMapping);
    }

    /**
     * Makes a Var from an object.
     * <ul>
     * <li>Will return Var.ANON if object is null.</li>
     * <li>Will return null if the object is "*" or Node_RuleVariable.WILD</li>
     * <li>Will return the object if it is a Var</li>
     * <li>Will return resolve FrontsNode to Node and then resolve to Var</li>
     * <li>Will return resolve Node if the Node implements Node_Variable, otherwise
     * throws an NotAVariableException (instance of ARQInternalErrorException)</li>
     * <li>Will return ?x if object is "?x"</li>
     * <li>Will return ?x if object is "x"</li>
     * <li>Will return the enclosed Var of a ExprVar</li>
     * <li>For all other objects will return the "?" prefixed to the toString()
     * value.</li>
     * </ul>
     *
     * @param o The object to convert.
     * @return the Var value.
     * @throws ARQInternalErrorException
     */
    public static Var makeVar(Object o) throws ARQInternalErrorException {
        if (o == null) {
            return Var.ANON;
        }
        if (o instanceof Var) {
            return (Var) o;
        }
        Var retval = null;
        if (o instanceof FrontsNode) {
            retval = Var.alloc(((FrontsNode) o).asNode());
        } else if (o instanceof Node) {
            retval = Var.alloc((Node) o);
        } else if (o instanceof ExprVar) {
            retval = Var.alloc((ExprVar) o);
        } else {
            retval = Var.alloc(Var.canonical(o.toString()));
        }
        if ("*".equals(Var.canonical(retval.toString()))) {
            return null;
        }
        return retval;
    }

    /**
     * A convenience method to quote a string.
     * 
     * @param q the string to quote.
     *
     * Will use single quotes if there are no single quotes in the string or if the
     * double quote is before the single quote in the string.
     *
     * Will use double quote otherwise.
     *
     * @return the quoted string.
     */
    public static String quoted(String q) {
        int qt = q.indexOf('"');
        int sqt = q.indexOf("'");
        if (qt == -1) {
            qt = Integer.MAX_VALUE;
        }
        if (sqt == -1) {
            sqt = Integer.MAX_VALUE;
        }

        if (qt <= sqt) {
            return String.format("'%s'", q);
        }
        return String.format("\"%s\"", q);
    }

    /**
     * Creates a collection of nodes from an iterator of Objects.
     * 
     * @param iter the iterator of objects, may be null or empty.
     * @param prefixMapping the PrefixMapping to use when nodes are created.
     * @return a Collection of nodes or null if iter is null or empty.
     */
    public static Collection<Node> makeValueNodes(Iterator<?> iter, PrefixMapping prefixMapping) {
        if (iter == null || !iter.hasNext()) {
            return null;
        }
        final List<Node> values = new ArrayList<Node>();
        while (iter.hasNext()) {
            final Object o = iter.next();
            // handle null as UNDEF
            if (o == null) {
                values.add(null);
            } else {
                values.add(makeNode(o, prefixMapping));
            }
        }
        return values;
    }

}
