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

import java.math.BigInteger ;
import java.util.HashSet ;
import java.util.Set ;

import org.apache.jena.atlas.AtlasException ;
import org.apache.jena.atlas.lib.EscapeStr ;
import org.apache.jena.atlas.logging.Log ;
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.graph.Triple ;
import org.apache.jena.iri.IRI ;
import org.apache.jena.n3.JenaURIException ;
import org.apache.jena.query.ARQ ;
import org.apache.jena.query.QueryParseException ;
import org.apache.jena.riot.checker.CheckerIRI ;
import org.apache.jena.riot.system.ErrorHandler ;
import org.apache.jena.riot.system.ErrorHandlerFactory ;
import org.apache.jena.riot.system.RiotLib ;
import org.apache.jena.sparql.ARQInternalErrorException ;
import org.apache.jena.sparql.core.Prologue ;
import org.apache.jena.sparql.core.TriplePath ;
import org.apache.jena.sparql.core.Var ;
import org.apache.jena.sparql.expr.E_Exists ;
import org.apache.jena.sparql.expr.E_NotExists ;
import org.apache.jena.sparql.expr.Expr ;
import org.apache.jena.sparql.graph.NodeConst ;
import org.apache.jena.sparql.modify.request.QuadAccSink ;
import org.apache.jena.sparql.path.Path ;
import org.apache.jena.sparql.syntax.* ;
import org.apache.jena.sparql.util.ExprUtils ;
import org.apache.jena.sparql.util.LabelToNodeMap ;
import org.apache.jena.vocabulary.RDF ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/** Base class parsers, mainly SPARQL related */
public class ParserBase
{
    // NodeConst
    protected final Node XSD_TRUE       = NodeConst.nodeTrue ;
    protected final Node XSD_FALSE      = NodeConst.nodeFalse ;

    protected final Node nRDFtype       = NodeConst.nodeRDFType ;

    protected final Node nRDFnil        = NodeConst.nodeNil ;
    protected final Node nRDFfirst      = NodeConst.nodeFirst ;
    protected final Node nRDFrest       = NodeConst.nodeRest ;

    protected final Node nRDFsubject    = RDF.Nodes.subject ;
    protected final Node nRDFpredicate  = RDF.Nodes.predicate ;
    protected final Node nRDFobject     = RDF.Nodes.object ;

    // ----
    // Graph patterns, true; in templates, false.
    private boolean bNodesAreVariables = true ;
    // In DELETE, false.
    private boolean bNodesAreAllowed = true ;

    // label => bNode for construct templates patterns
    @SuppressWarnings("deprecation")
    final LabelToNodeMap bNodeLabels = LabelToNodeMap.createBNodeMap() ;

    // label => bNode (as variable) for graph patterns
    final LabelToNodeMap anonVarLabels = LabelToNodeMap.createVarMap() ;

    // This is the map used allocate blank node labels during parsing.
    // 1/ It is different between CONSTRUCT and the query pattern
    // 2/ Each BasicGraphPattern is a scope for blank node labels so each
    //    BGP causes the map to be cleared at the start of the BGP

    LabelToNodeMap activeLabelMap = anonVarLabels ;
    Set<String> previousLabels = new HashSet<>() ;

    // Aggregates are only allowed in places where grouping can happen.
    // e.g. SELECT clause but not a FILTER.
    private boolean allowAggregatesInExpressions = false ;

    //LabelToNodeMap listLabelMap = new LabelToNodeMap(true, new VarAlloc("L")) ;
    // ----

    public ParserBase() {}

    protected Prologue prologue ;
    public void setPrologue(Prologue prologue) { this.prologue = prologue ; }
    public Prologue getPrologue() { return prologue ; }

    protected void setInConstructTemplate(boolean b) {
        setBNodesAreVariables(!b) ;
    }

    protected boolean getBNodesAreVariables()   { return bNodesAreVariables ; }

    protected void setBNodesAreVariables(boolean bNodesAreVariables) {
        this.bNodesAreVariables = bNodesAreVariables ;
        if ( bNodesAreVariables )
            activeLabelMap = anonVarLabels ;
        else
            activeLabelMap = bNodeLabels ;
    }

    protected boolean getBNodesAreAllowed()   { return bNodesAreAllowed ; }

    protected void setBNodesAreAllowed(boolean bNodesAreAllowed) {
        this.bNodesAreAllowed = bNodesAreAllowed ;
    }

    protected boolean getAllowAggregatesInExpressions() {
        return allowAggregatesInExpressions ;
    }

    protected void setAllowAggregatesInExpressions(boolean allowAggregatesInExpressions) {
        this.allowAggregatesInExpressions = allowAggregatesInExpressions;
    }

    protected Element compressGroupOfOneGroup(ElementGroup elg) {
        // remove group of one group.
        if ( elg.size() == 1 ) {
            Element e1 = elg.get(0) ;
            if ( e1 instanceof ElementGroup )
                return e1 ;
        }
        return elg ;
    }

    protected Node createLiteralInteger(String lexicalForm) {
        return NodeFactory.createLiteral(lexicalForm, XSDDatatype.XSDinteger) ;
    }

    protected Node createLiteralDouble(String lexicalForm) {
        return NodeFactory.createLiteral(lexicalForm, XSDDatatype.XSDdouble) ;
    }

    protected Node createLiteralDecimal(String lexicalForm) {
        return NodeFactory.createLiteral(lexicalForm, XSDDatatype.XSDdecimal) ;
    }

    protected Node stripSign(Node node) {
        if ( !node.isLiteral() )
            return node ;
        String lex = node.getLiteralLexicalForm() ;
        String lang = node.getLiteralLanguage() ;
        RDFDatatype dt = node.getLiteralDatatype() ;

        if ( !lex.startsWith("-") && !lex.startsWith("+") )
            throw new ARQInternalErrorException("Literal does not start with a sign: " + lex) ;

        lex = lex.substring(1) ;
        return NodeFactory.createLiteral(lex, lang, dt) ;
    }

    protected Node createLiteral(String lexicalForm, String langTag, String datatypeURI) {
        Node n = null ;
        // Can't have type and lang tag in parsing.
        if ( datatypeURI != null ) {
            RDFDatatype dType = TypeMapper.getInstance().getSafeTypeByName(datatypeURI) ;
            n = NodeFactory.createLiteral(lexicalForm, dType) ;
        } else if ( langTag != null && !langTag.isEmpty() )
            n = NodeFactory.createLiteral(lexicalForm, langTag) ;
        else
            n = NodeFactory.createLiteral(lexicalForm) ;
        return n ;
    }

    protected long integerValue(String s) {
        try {
            if ( s.startsWith("+") )
                s = s.substring(1) ;
            if ( s.startsWith("0x") ) {
                // Hex
                s = s.substring(2) ;
                return Long.parseLong(s, 16) ;
            }
            return Long.parseLong(s) ;
        }
        catch (NumberFormatException ex) {
            try {
                // Possible too large for a long.
                BigInteger integer = new BigInteger(s) ;
                throwParseException("Number '" + s + "' is a valid number but can't not be stored in a long") ;
            }
            catch (NumberFormatException ex2) {}
            throw new QueryParseException(ex, -1, -1) ;
        }
    }

    protected double doubleValue(String s) {
        if ( s.startsWith("+") )
            s = s.substring(1) ;
        double valDouble = Double.parseDouble(s) ;
        return valDouble ;
    }

    /** Remove first and last characters (e.g. ' or "") from a string */
    protected static String stripQuotes(String s) {
        return s.substring(1, s.length() - 1) ;
    }

    /** Remove first 3 and last 3 characters (e.g. ''' or """) from a string */
    protected static String stripQuotes3(String s) {
        return s.substring(3, s.length() - 3) ;
    }

    /** remove the first n characters from the string */
    public static String stripChars(String s, int n) {
        return s.substring(n, s.length()) ;
    }

    protected Var createVariable(String s, int line, int column) {
        s = s.substring(1) ; // Drop the marker

        // This is done by the parser input stream nowadays.
        // s = unescapeCodePoint(s, line, column) ;
        // Check \ u did not put in any illegals.
        return Var.alloc(s) ;
    }

    // ---- IRIs and Nodes

    protected String resolveQuotedIRI(String iriStr, int line, int column) {
        iriStr = stripQuotes(iriStr) ;
        return resolveIRI(iriStr, line, column) ;
    }

    public static final String ParserLoggerName = "SPARQL" ;
    private static Logger parserLog = LoggerFactory.getLogger(ParserLoggerName) ;
    private static ErrorHandler errorHandler = ErrorHandlerFactory.errorHandlerStd(parserLog) ;

    protected String resolveIRI(String iriStr, int line, int column) {
        if ( isBNodeIRI(iriStr) )
            return iriStr ;

        if ( getPrologue() != null ) {
            if ( getPrologue().getResolver() != null )
                try {
                    // Used to be errors (pre Jena 2.12.0)
                    // .resolve(iriStr)
                    IRI iri = getPrologue().getResolver().resolveSilent(iriStr) ;
                    if ( true )
                        CheckerIRI.iriViolations(iri, errorHandler, line, column) ;
                    iriStr = iri.toString() ;
                }
                catch (JenaURIException ex) {
                    throwParseException(ex.getMessage(), line, column) ;
                }
        }
        return iriStr ;
    }

    protected String resolvePName(String prefixedName, int line, int column) {
        // It's legal.
        int idx = prefixedName.indexOf(':') ;

        // -- Escapes in local name
        String prefix = prefixedName.substring(0, idx) ;
        String local = prefixedName.substring(idx + 1) ;
        local = unescapePName(local, line, column) ;
        prefixedName = prefix + ":" + local ;
        // --

        String s = getPrologue().expandPrefixedName(prefixedName) ;
        if ( s == null ) {
            if ( ARQ.isTrue(ARQ.fixupUndefinedPrefixes) )
                return RiotLib.fixupPrefixes.apply(prefixedName) ;
            throwParseException("Unresolved prefixed name: " + prefixedName, line, column) ;
        }
        return s ;
    }

    private boolean skolomizedBNodes = ARQ.isTrue(ARQ.constantBNodeLabels) ;

    protected Node createNode(String iri) {
        if ( skolomizedBNodes )
            return RiotLib.createIRIorBNode(iri) ;
        else
            return NodeFactory.createURI(iri) ;
    }

    protected boolean isBNodeIRI(String iri) {
        return skolomizedBNodes && RiotLib.isBNodeIRI(iri) ;
    }

    // -------- Basic Graph Patterns and Blank Node label scopes

    // A BasicGraphPattern is any sequence of TripleBlocks, separated by filters,
    // but not by other graph patterns.

    protected void startBasicGraphPattern()
    { activeLabelMap.clear() ; }

    protected void endBasicGraphPattern()
    { previousLabels.addAll(activeLabelMap.getLabels()) ; }

    protected void startTriplesBlock()
    { }

    protected void endTriplesBlock()
    { }

    // On entry to a new group, the current BGP is ended.
    protected void startGroup(ElementGroup elg) {
        endBasicGraphPattern() ;
        startBasicGraphPattern() ;
    }

    protected void endGroup(ElementGroup elg) {
        endBasicGraphPattern() ;
    }

    // --------

    protected void checkConcrete(Node n, int line, int column) {
        if ( ! n.isConcrete() )
            throwParseException("Term is not concrete: "+n, line, column) ;
    }
    
    // BNode from a list
//    protected Node createListNode()
//    { return listLabelMap.allocNode() ; }

    protected Node createListNode(int line, int column) { return createBNode(line, column) ; }

    // Unlabelled bNode.
    protected Node createBNode(int line, int column) {
        if ( !bNodesAreAllowed )
            throwParseException("Blank nodes not allowed in DELETE templates", line, column) ;
        return activeLabelMap.allocNode() ;
    }

    // Labelled bNode.
    protected Node createBNode(String label, int line, int column) {
        if ( !bNodesAreAllowed )
            throwParseException("Blank nodes not allowed in DELETE templates: " + label, line, column) ;
        if ( previousLabels.contains(label) )
            throwParseException("Blank node label reuse not allowed at this point: " + label, line, column) ;

        // label = unescapeCodePoint(label, line, column) ;
        return activeLabelMap.asNode(label) ;
    }

    protected Node createTripleTerm(Node s, Node p, Node o, int line, int column) {
        return NodeFactory.createTripleNode(s, p, o);
    }

    protected Expr createExprExists(Element element) {
        return new E_Exists(element) ;
    }

    protected Expr createExprNotExists(Element element) {
        // Could negate here.
        return new E_NotExists(element) ;
    }

    protected String fixupPrefix(String prefix, int line, int column) {
        // \ u processing!
        if ( prefix.endsWith(":") )
            prefix = prefix.substring(0, prefix.length() - 1) ;
        return prefix ;
    }

    protected void setAccGraph(QuadAccSink acc, Node gn) {
        acc.setGraph(gn) ;
    }

    protected void insert(TripleCollector acc, Node s, Node p, Node o) {
        acc.addTriple(new Triple(s, p, o)) ;
    }

    protected void insert(TripleCollectorMark acc, int index, Node s, Node p, Node o) {
        acc.addTriple(index, new Triple(s, p, o)) ;
    }

    protected void insert(TripleCollector acc, Node s, Node p, Path path, Node o) {
        if ( p == null )
            acc.addTriplePath(new TriplePath(s, path, o)) ;
        else
            acc.addTriple(new Triple(s, p, o)) ;
    }

    protected void insert(TripleCollectorMark acc, int index, Node s, Node p, Path path, Node o) {
        if ( p == null )
            acc.addTriplePath(index, new TriplePath(s, path, o)) ;
        else
            acc.addTriple(index, new Triple(s, p, o)) ;
    }

    protected void insert(TripleCollector target, ElementPathBlock source) {
        for ( TriplePath path : source.getPattern() ) {
            if ( path.isTriple() ) {
                target.addTriple(path.asTriple()) ;
            } else {
                target.addTriplePath(path) ;
            }
        }
    }

    protected Expr asExpr(Node n) {
        return ExprUtils.nodeToExpr(n) ;
    }

    protected Expr asExprNoSign(Node n) {
        String lex = n.getLiteralLexicalForm() ;
        String lang = n.getLiteralLanguage() ;
        String dtURI = n.getLiteralDatatypeURI() ;
        n = createLiteral(lex, lang, dtURI) ;
        return ExprUtils.nodeToExpr(n) ;
    }

    // Utilities to remove escapes in strings.

    public static String unescapeStr(String s)
    { return unescape(s, '\\', false, 1, 1) ; }

//    public static String unescapeCodePoint(String s)
//    { return unescape(s, '\\', true, 1, 1) ; }
//
//    protected String unescapeCodePoint(String s, int line, int column)
//    { return unescape(s, '\\', true, line, column) ; }


    // Do we nee dthe line/column versions?
    // Why not catch exceptions and comvert to  QueryParseException

    public static String unescapeStr(String s, int line, int column)
    { return unescape(s, '\\', false, line, column) ; }

    // Worker function
    public static String unescape(String s, char escape, boolean pointCodeOnly, int line, int column) {
        try {
            return EscapeStr.unescape(s, escape, pointCodeOnly) ;
        } catch (AtlasException ex) {
            throwParseException(ex.getMessage(), line, column) ;
            return null ;
        }
    }

    public static String unescapePName(String s, int line, int column) {
        char escape = '\\' ;
        int idx = s.indexOf(escape) ;

        if ( idx == -1 )
            return s ;

        int len = s.length() ;
        StringBuilder sb = new StringBuilder() ;

        for ( int i = 0 ; i < len ; i++ ) {
            char ch = s.charAt(i) ;
            // Keep line and column numbers.
            switch (ch) {
                case '\n' :
                case '\r' :
                    line++ ;
                    column = 1 ;
                    break ;
                default :
                    column++ ;
                    break ;
            }

            if ( ch != escape ) {
                sb.append(ch) ;
                continue ;
            }

            // Escape
            if ( i >= s.length() - 1 )
                throwParseException("Illegal escape at end of string", line, column) ;
            char ch2 = s.charAt(i + 1) ;
            column = column + 1 ;
            i = i + 1 ;

            switch (ch2) {   // PN_LOCAL_ESC
                case '_' :
                case '~' :
                case '.' :
                case '-' :
                case '!' :
                case '$' :
                case '&' :
                case '\'' :
                case '(' :
                case ')' :
                case '*' :
                case '+' :
                case ',' :
                case ';' :
                case '=' :
                case ':' :
                case '/' :
                case '?' :
                case '#' :
                case '@' :
                case '%' :
                    sb.append(ch2) ;
                    break ;
                default :
                    throwParseException("Illegal prefix name escape: " + ch2, line, column) ;
            }
        }
        return sb.toString() ;
    }

    protected void warnDeprecation(String msg) {
        Log.warn(this, msg) ;
    }

    public static void throwParseException(String msg, int line, int column) {
        throw new QueryParseException("Line " + line + ", column " + column + ": " + msg, line, column) ;
    }

    public static void throwParseException(String msg) {
        throw new QueryParseException(msg, -1, -1) ;
    }
}
