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

import java.io.OutputStream ;
import java.util.* ;

import org.apache.jena.atlas.io.IndentedLineBuffer ;
import org.apache.jena.atlas.io.IndentedWriter ;
import org.apache.jena.atlas.io.Printable ;
import org.apache.jena.atlas.logging.Log ;
import org.apache.jena.graph.Node ;
import org.apache.jena.sparql.ARQConstants ;
import org.apache.jena.sparql.algebra.table.TableData ;
import org.apache.jena.sparql.core.* ;
import org.apache.jena.sparql.engine.binding.Binding ;
import org.apache.jena.sparql.expr.Expr ;
import org.apache.jena.sparql.expr.ExprAggregator ;
import org.apache.jena.sparql.expr.ExprTransform ;
import org.apache.jena.sparql.expr.ExprVar ;
import org.apache.jena.sparql.expr.aggregate.Aggregator ;
import org.apache.jena.sparql.serializer.QuerySerializerFactory ;
import org.apache.jena.sparql.serializer.SerializerRegistry ;
import org.apache.jena.sparql.syntax.Element ;
import org.apache.jena.sparql.syntax.PatternVars ;
import org.apache.jena.sparql.syntax.Template ;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransform;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransformCopyBase;
import org.apache.jena.sparql.syntax.syntaxtransform.ExprTransformApplyElementTransform;
import org.apache.jena.sparql.syntax.syntaxtransform.QueryTransformOps;
import org.apache.jena.sparql.util.FmtUtils ;
import org.apache.jena.sys.JenaSystem ;

/** The data structure for a query as presented externally.
 *  There are two ways of creating a query - use the parser to turn
 *  a string description of the query into the executable form, and
 *  the programmatic way (the parser is calling the programmatic
 *  operations driven by the query string).  The declarative approach
 *  of passing in a string is preferred.
 *
 * Once a query is built, it can be passed to the QueryFactory to produce a query execution engine.
 * @see QueryExecutionFactory
 * @see ResultSet
 */

public class Query extends Prologue implements Cloneable, Printable
{
    static { JenaSystem.init() ; /* Ensure everything has started properly */ }

    // Old constants. Retain for compatibility.
    public static final int QueryTypeUnknown    = -123 ;
    public static final int QueryTypeSelect     = 111 ;
    public static final int QueryTypeConstruct  = 222 ;
    public static final int QueryTypeDescribe   = 333 ;
    public static final int QueryTypeAsk        = 444 ;
    public static final int QueryTypeJson       = 555 ;

    private QueryType queryType = QueryType.UNKNOWN;

    // If no model is provided explicitly, the query engine will load
    // a model from the URL.  Never a list of zero items.

    private List<String> graphURIs = new ArrayList<>() ;
    private List<String> namedGraphURIs = new ArrayList<>() ;

    // The WHERE clause
    private Element queryPattern = null ;

    // Query syntax
    private Syntax syntax = Syntax.syntaxSPARQL ; // Default

    // LIMIT/OFFSET
    public static final long  NOLIMIT = Long.MIN_VALUE ;
    private long resultLimit   = NOLIMIT ;
    private long resultOffset  = NOLIMIT ;

    // ORDER BY
    private List<SortCondition> orderBy       = null ;
    public static final int ORDER_ASCENDING           = 1 ;
    public static final int ORDER_DESCENDING          = -1 ;
    public static final int ORDER_DEFAULT             = -2 ;    // Not explicitly given.
    public static final int ORDER_UNKNOW              = -3 ;

    // VALUES trailing clause
    protected TableData valuesDataBlock = null ;

    protected boolean strictQuery = true ;

    // SELECT * seen
    protected boolean queryResultStar        = false ;

    protected boolean distinct               = false ;
    protected boolean reduced                = false ;

    // CONSTRUCT
    protected Template constructTemplate  = null ;

    // DESCRIBE
    // Any URIs/QNames in the DESCRIBE clause
    // Also uses resultVars
    protected List<Node> resultNodes               = new ArrayList<>() ;     // Type in list: Node

    /**
     * Creates a new empty query
     */
    public Query()
    {
        syntax = Syntax.syntaxSPARQL ;
    }

    /**
     * Creates a new empty query with the given prologue
     */
    public Query(Prologue prologue)
    {
        this() ;
        Prologue p2 = prologue.copy();
        prefixMap = p2.getPrefixMapping();
        seenBaseURI = false ;
        resolver = p2.getResolver();
    }

    // Allocate variables that are unique to this query.
    private VarAlloc varAlloc = new VarAlloc(ARQConstants.allocQueryVariables) ;
    private Var allocInternVar() { return varAlloc.allocVar() ; }

    public void setQuerySelectType()            { queryType = QueryType.SELECT ; }
    public void setQueryConstructType()         { queryType = QueryType.CONSTRUCT ; queryResultStar = true ; }
    public void setQueryDescribeType()          { queryType = QueryType.DESCRIBE; }
    public void setQueryAskType()               { queryType = QueryType.ASK; }
    public void setQueryJsonType()              { queryType = QueryType.CONSTRUCT_JSON; }

    /** Return the {@link QueryType} */
    public QueryType queryType()                       { return queryType ; }

    /** @deprecated Use {@link #queryType()} which returns a {@link QueryType} */
    @Deprecated
    public int getQueryType() {
        // Old constants.
        switch(queryType) {
            case SELECT :           return QueryTypeSelect;
            case ASK :              return QueryTypeAsk;
            case CONSTRUCT :        return QueryTypeConstruct;
            case CONSTRUCT_JSON :   return QueryTypeJson;
            //case CONSTRUCT_QUADS :  return QueryTypeConstruct;
            case DESCRIBE :         return QueryTypeDescribe;
            default :               return QueryTypeUnknown;
        }
    }

    public boolean isSelectType()               { return queryType == QueryType.SELECT; }

    public boolean isConstructType()            { return queryType == QueryType.CONSTRUCT ; }

    public boolean isDescribeType()             { return queryType == QueryType.DESCRIBE ; }

    public boolean isAskType()                  { return queryType == QueryType.ASK ; }

    public boolean isJsonType()                 { return queryType == QueryType.CONSTRUCT_JSON ; }

    public boolean isUnknownType()              { return queryType == QueryType.UNKNOWN ; }

    public boolean isConstructQuad() {
        return (isConstructType() && constructTemplate.containsRealQuad());
    }

    // It was a mistake to extend Prologue ... but what is done is done.
    public Prologue getPrologue()               { return this ; }

    public void setStrict(boolean isStrict)
    {
        strictQuery = isStrict ;

        if ( strictQuery )
            initStrict() ;
        else
            initLax() ;
    }

    public boolean isStrict()                { return strictQuery ; }

    private void initStrict()
    {
//        if ( prefixMap.getGlobalPrefixMapping() == globalPrefixMap )
//            prefixMap.setGlobalPrefixMapping(null) ;
    }

    private void initLax()
    {
//        if ( prefixMap.getGlobalPrefixMapping() == null )
//            prefixMap.setGlobalPrefixMapping(globalPrefixMap) ;
    }

    public void setDistinct(boolean b) { distinct = b ; }
    public boolean isDistinct()        { return distinct ; }

    public void setReduced(boolean b) { reduced = b ; }
    public boolean isReduced()        { return reduced ; }

    /** @return Returns the syntax. */
    public Syntax getSyntax()         { return syntax ; }

    /** @param syntax The syntax to set. */
    public void setSyntax(Syntax syntax)
    {
        this.syntax = syntax ;
        if ( syntax != Syntax.syntaxSPARQL )
            strictQuery = false ;
    }

    // ---- Limit/offset

    public long getLimit()             { return resultLimit ; }
    public void setLimit(long limit)   { resultLimit = limit ; }
    public boolean hasLimit()          { return resultLimit != NOLIMIT ; }

    public long getOffset()            { return resultOffset ; }
    public void setOffset(long offset) { resultOffset = offset ; }
    public boolean hasOffset()         { return resultOffset != NOLIMIT ; }

    // ---- Order By

    public boolean hasOrderBy()        { return orderBy != null && orderBy.size() > 0 ; }

    public boolean isOrdered()         { return hasOrderBy() ; }

    public void addOrderBy(SortCondition condition)
    {
        if ( orderBy == null )
            orderBy = new ArrayList<>() ;

        orderBy.add(condition) ;
    }
    public void addOrderBy(Expr expr, int direction)
    {
        SortCondition sc = new SortCondition(expr, direction) ;
        addOrderBy(sc) ;
    }

    public void addOrderBy(Node var, int direction)
    {
        if ( ! var.isVariable() )
            throw new QueryException("Not a variable: "+var) ;
        SortCondition sc = new SortCondition(var, direction) ;
        addOrderBy(sc) ;
    }

    public void addOrderBy(String varName, int direction)
    {
        varName = Var.canonical(varName) ;
        SortCondition sc = new SortCondition(new ExprVar(varName), direction) ;
        addOrderBy(sc) ;
    }

    public List<SortCondition> getOrderBy()           { return orderBy ; }

    // ----

    /** Answer whether the query had SELECT/DESCRIBE/CONSTRUCT *
     * @return boolean as to whether a * result form was seen
     */
    public boolean isQueryResultStar() { return queryResultStar ; }

    /** Set whether the query had SELECT/DESCRIBE *
     *
     * @param isQueryStar
     */
    public void setQueryResultStar(boolean isQueryStar)
    {
        queryResultStar = isQueryStar ;
        if ( isQueryStar )
            resultVarsSet = false ;
    }

    public void setQueryPattern(Element elt)
    {
        queryPattern = elt ;
    }

    public Element getQueryPattern() { return queryPattern ; }

    /** Location of the source for the data.  If the model is not set,
     *  then the QueryEngine will attempt to load the data from these URIs
     *  into the default (unnamed) graph.
     */
    public void addGraphURI(String s)
    {
        if ( graphURIs == null )
            graphURIs = new ArrayList<>() ;
        graphURIs.add(s) ;
    }

    /** Location of the source for the data.  If the model is not set,
     *  then the QueryEngine will attempt to load the data from these URIs
     *  as named graphs in the dataset.
     */
    public void addNamedGraphURI(String uri)
    {
        if ( namedGraphURIs == null )
            namedGraphURIs = new ArrayList<>() ;
        if ( namedGraphURIs.contains(uri) )
            throw new QueryException("URI already in named graph set: "+uri) ;
        else
            namedGraphURIs.add(uri) ;
    }

    /** Return the list of URIs (strings) for the unnamed graph
     *
     * @return List of strings
     */

    public List<String> getGraphURIs() { return graphURIs ; }

    /** Test whether the query mentions a URI in forming the default graph (FROM clause)
     *
     * @param uri
     * @return boolean  True if the URI used in a FROM clause
     */
    public boolean usesGraphURI(String uri) { return graphURIs.contains(uri) ; }

    /** Return the list of URIs (strings) for the named graphs (FROM NAMED clause)
     *
     * @return List of strings
     */

    public List<String> getNamedGraphURIs() { return namedGraphURIs ; }

    /** Test whether the query mentions a URI for a named graph.
     *
     * @param uri
     * @return True if the URI used in a FROM NAMED clause
     */
    public boolean usesNamedGraphURI(String uri) { return namedGraphURIs.contains(uri) ; }

    /** Return true if the query has either some graph
     * URIs or some named graph URIs in its description.
     * This does not mean these URIs will be used - just that
     * they are noted as part of the query.
     */

    public boolean hasDatasetDescription()
    {
        if ( getGraphURIs() != null && getGraphURIs().size() > 0 )
            return true ;
        if ( getNamedGraphURIs() != null && getNamedGraphURIs().size() > 0 )
            return true ;
        return false ;
    }

    /** Return a dataset description (FROM/FROM NAMED clauses) for the query. */
    public DatasetDescription getDatasetDescription()
    {
        if ( ! hasDatasetDescription() )
            return null;

        DatasetDescription description = new DatasetDescription() ;

        description.addAllDefaultGraphURIs(getGraphURIs()) ;
        description.addAllNamedGraphURIs(getNamedGraphURIs()) ;
        return description ;
    }

    // ---- SELECT

    protected VarExprList projectVars = new VarExprList() ;

    /** Return a list of the variables requested (SELECT) */
    public List<String> getResultVars()
    {
        // Ensure "SELECT *" processed
        setResultVars() ;
        return Var.varNames(projectVars.getVars()) ;
    }

    /** Return a list of the variables requested (SELECT) */
    public List<Var> getProjectVars()
    {
        // Ensure "SELECT *" processed
        setResultVars() ;
        return projectVars.getVars() ;
    }

    public VarExprList getProject()
    {
        return projectVars ;
    }

    /** Add a collection of projection variables to a SELECT query */
    public void addProjectVars(Collection<?> vars)
    {
        for ( Object obj : vars )
        {
            if ( obj instanceof String )
            {
                this.addResultVar( (String) obj );
                continue;
            }
            if ( obj instanceof Var )
            {
                this.addResultVar( (Var) obj );
                continue;
            }
            throw new QueryException( "Not a variable or variable name: " + obj );
        }
        resultVarsSet = true ;
    }


    /** Add a projection variable to a SELECT query */
    public void addResultVar(String varName)
    {
        varName = Var.canonical(varName) ;
        _addResultVar(varName) ;
    }

    public void addResultVar(Node v)
    {
        if ( !v.isVariable() )
            throw new QueryException("Not a variable: "+v) ;
        _addResultVar(v.getName()) ;
    }

    public void addResultVar(Node v, Expr expr)
    {
        Var var = null ;
        if ( v == null )
            var = allocInternVar() ;
        else
        {
            if ( !v.isVariable() )
                throw new QueryException("Not a variable: "+v) ;
            var = Var.alloc(v) ;
        }
        _addVarExpr(projectVars, var, expr) ;
    }

    /** Add an to a SELECT query (a name will be created for it) */
    public void addResultVar(Expr expr)
    {
        _addVarExpr(projectVars, allocInternVar(), expr) ;
    }

    /** Add a named expression to a SELECT query */
    public void addResultVar(String varName, Expr expr)
    {
        Var var = null ;
        if ( varName == null )
            var = allocInternVar() ;
        else
        {
            varName = Var.canonical(varName) ;
            var = Var.alloc(varName) ;
        }
        _addVarExpr(projectVars, var, expr) ;
    }

    // Add raw name.
    private void _addResultVar(String varName)
    {
        Var v = Var.alloc(varName) ;
        _addVar(projectVars, v) ;
        resultVarsSet = true ;
    }

    private static void _addVar(VarExprList varExprList, Var v)
    {
        if ( varExprList.contains(v) )
        {
            Expr expr = varExprList.getExpr(v) ;
            if ( expr != null )
                // SELECT (?a+?b AS ?x) ?x
                throw new QueryBuildException("Duplicate variable (had an expression) in result projection '"+v+"'") ;
            // SELECT ?x ?x
            if ( ! ARQ.allowDuplicateSelectColumns )
                return ;
            // else drop through and have two variables of the same name.
        }
        varExprList.add(v) ;
    }

    private static void _addVarExpr(VarExprList varExprList, Var v, Expr expr)
    {
        if ( varExprList.contains(v) )
            // SELECT ?x (?a+?b AS ?x)
            // SELECT (2*?a AS ?x) (?a+?b AS ?x)
            throw new QueryBuildException("Duplicate variable in result projection '"+v+"'") ;
        varExprList.add(v, expr) ;
    }

    protected VarExprList groupVars = new VarExprList() ;
    protected List<Expr> havingExprs = new ArrayList<>() ;  // Expressions : Make an ExprList?

    public boolean hasGroupBy()     { return ! groupVars.isEmpty() || getAggregators().size() > 0 ; }
    public boolean hasHaving()      { return havingExprs != null && havingExprs.size() > 0 ; }

    public VarExprList getGroupBy()      { return groupVars ; }

    public List<Expr> getHavingExprs()    { return havingExprs ; }

    public void addGroupBy(String varName)
    {
        varName = Var.canonical(varName) ;
        addGroupBy(Var.alloc(varName)) ;
    }

    public void addGroupBy(Node v)
    {
        _addVar(groupVars, Var.alloc(v)) ;
    }

    public void addGroupBy(Expr expr) { addGroupBy(null, expr) ; }

    public void addGroupBy(Var v, Expr expr)
    {
        if ( v == null )
            v = allocInternVar() ;

        if ( expr.isVariable() && v.isAllocVar() )
        {
            // It was (?x) with no AS - keep the name by adding by variable.
            addGroupBy(expr.asVar()) ;
            return ;
        }

        groupVars.add(v, expr) ;
    }

    public void addHavingCondition(Expr expr)
    {
        havingExprs.add(expr) ;
    }

    // SELECT JSON

    private Map<String, Node> jsonMapping = new LinkedHashMap<>();

    public void addJsonMapping(String key, Node value) {
        jsonMapping.put(key, value);
    }

    public Map<String, Node> getJsonMapping() {
        return Collections.unmodifiableMap(jsonMapping);
    }

    // ---- Aggregates

    // Record allocated aggregations.
    // Later: The same aggregation expression used in a query
    // will always lead to the same aggregator.
    // For now, allocate a fresh one each time (cause the calculation
    // to be done multiple times but (1) it's unusual to have repeated
    // aggregators normally and (2) the actual calculation is cheap.

    // Unlike SELECT expressions, here the expression itself (E_Aggregator) knows its variable
    // Commonality?

    private List<ExprAggregator> aggregators = new ArrayList<>() ;
    private Map<Var, ExprAggregator> aggregatorsMap = new HashMap<>() ;

    // Note any E_Aggregator created for reuse.
    private Map<String, Var> aggregatorsAllocated = new HashMap<>() ;

    public boolean hasAggregators() { return aggregators.size() != 0  ; }
    public List<ExprAggregator> getAggregators() { return aggregators ; }

    public Expr allocAggregate(Aggregator agg)
    {
        // We need to track the aggregators in case one aggregator is used twice, e.g. in HAVING and in SELECT expression
        // (is that much harm to do twice?  Yes, if distinct.)
        String key = agg.key() ;

        Var v = aggregatorsAllocated.get(key);
        if ( v != null )
        {
            ExprAggregator eAgg = aggregatorsMap.get(v) ;
            if ( ! agg.equals(eAgg.getAggregator()) )
                Log.warn(Query.class, "Internal inconsistency: Aggregator: "+agg) ;
            return eAgg ;
        }
        // Allocate.
        v = allocInternVar() ;
        ExprAggregator aggExpr = new ExprAggregator(v, agg) ;
        aggregatorsAllocated.put(key, v) ;
        aggregatorsMap.put(v, aggExpr) ;
        aggregators.add(aggExpr) ;
        return aggExpr ;
    }

    // ---- VALUES

    /** Does the query have a VALUES trailing block? */
    public boolean hasValues()                { return valuesDataBlock != null ; }

    /** Variables from a VALUES trailing block */
    public List<Var> getValuesVariables()     { return valuesDataBlock==null ? null : valuesDataBlock.getVars() ; }

    /** Data from a VALUES trailing block. null for a Node means undef */
    public List<Binding> getValuesData()      { return valuesDataBlock==null ? null : valuesDataBlock.getRows() ; }

    public void setValuesDataBlock(List<Var> variables, List<Binding> values)
    {
        checkDataBlock(variables, values) ;
        valuesDataBlock = new TableData(variables, values) ;
    }

    private static void checkDataBlock(List<Var> variables, List<Binding> values)
    {
        // Check.
        int N = variables.size() ;
        for ( Binding valueRow : values )
        {
            Iterator<Var> iter= valueRow.vars() ;
            for ( ; iter.hasNext() ; )
            {
                Var v = iter.next() ;
                if ( ! variables.contains(v) )
                    throw new QueryBuildException("Variable "+v+" not found in "+variables) ;
            }
        }
    }

    // ---- CONSTRUCT

    /** Get the template pattern for a construct query */
    public Template getConstructTemplate()
    {
        return constructTemplate ;
    }

    /** Set triple patterns for a construct query */
    public void setConstructTemplate(Template templ)  { constructTemplate = templ ; }

    // ---- DESCRIBE

    public void addDescribeNode(Node node)
    {
        if ( node.isVariable() ) { addResultVar(node) ; return ; }
        if ( node.isURI() || node.isBlank() )
        {
            if ( !resultNodes.contains(node) )
                resultNodes.add(node);
            return ;
        }
        if ( node.isLiteral() )
            throw new QueryException("Result node is a literal: "+FmtUtils.stringForNode(node)) ;
        throw new QueryException("Result node not recognized: "+node) ;
    }


    /** Get the result list (things wanted - not the results themselves)
     *  of a DESCRIBE query. */
    public List<Node> getResultURIs() { return resultNodes ; }

    private boolean resultVarsSet = false ;
    /**
     * Set the results variables if necessary, when the query has "*" ({@code SELECT *}
     * or {@code DESCRIBE *}) and for a construct query. This operation is idempotent and can
     * be called to ensure the results variables have been set.
     */
    public void setResultVars()
    {
        if ( resultVarsSet )
            return ;
        synchronized(this) {
            if ( resultVarsSet )
                return;
            // Synchronized in case this query is used in a multithreaded
            // situation calling setResultVars(). JENA-1861.
            resetResultVars();
            resultVarsSet = true ;
        }
    }

    /**
     * If modifying a query, it may be necessary to reset the calculate of the result
     * variables of the query for {@code SELECT *} and {@code DESCRIBE *} and {@code CONSTRUCT}.
     */
    public void resetResultVars() {
        if  ( isQueryResultStar() )
            projectVars.clear();

        if ( getQueryPattern() == null )
        {
            if ( ! this.isDescribeType() )
                Log.warn(this, "setResultVars(): no query pattern") ;
            return ;
        }

        if ( isSelectType() )
        {
            if ( isQueryResultStar() )
                findAndAddNamedVars() ;
            return ;
        }

        if ( isConstructType() )
        {
            // All named variables are in-scope
            findAndAddNamedVars() ;
            return ;
        }

        if ( isDescribeType() )
        {
            if ( isQueryResultStar() )
                findAndAddNamedVars() ;
            return ;
        }
//        if ( isAskType() )
//        {}
    }

    private void findAndAddNamedVars()
    {
        Iterator<Var> varIter = null ;
        if ( hasGroupBy() )
            varIter = groupVars.getVars().iterator() ;
        else
        {
            // Binding variables -- in patterns, not in filters and not in EXISTS
            LinkedHashSet<Var> queryVars = new LinkedHashSet<>() ;
            PatternVars.vars(queryVars, this.getQueryPattern()) ;
            if ( this.hasValues() )
                queryVars.addAll(getValuesVariables()) ;
//            if ( this.hasValues() )
//                queryVars.addAll(getValuesVariables()) ;
            varIter = queryVars.iterator() ;
        }

        // All query variables, including ones from bNodes in the query.

        for ( ; varIter.hasNext() ; )
        {
            Var var = varIter.next() ;
            if ( var.isNamedVar() )
                addResultVar(var) ;
        }
    }

    public void visit(QueryVisitor visitor)
    {
        visitor.startVisit(this) ;
        visitor.visitResultForm(this) ;
        visitor.visitPrologue(this) ;
        if ( this.isSelectType() )
            visitor.visitSelectResultForm(this) ;
        if ( this.isConstructType() )
            visitor.visitConstructResultForm(this) ;
        if ( this.isDescribeType() )
            visitor.visitDescribeResultForm(this) ;
        if ( this.isAskType() )
            visitor.visitAskResultForm(this) ;
        if ( this.isJsonType() )
            visitor.visitJsonResultForm(this) ;
        visitor.visitDatasetDecl(this) ;
        visitor.visitQueryPattern(this) ;
        visitor.visitGroupBy(this) ;
        visitor.visitHaving(this) ;
        visitor.visitOrderBy(this) ;
        visitor.visitOffset(this) ;
        visitor.visitLimit(this) ;
        visitor.visitValues(this) ;
        visitor.finishVisit(this) ;
    }

    @Override
    public Object clone() { return cloneQuery() ; }

    /**
     * Makes a copy of this query using the syntax transform machinery.
     * @return Copy of this query
     */
    public Query cloneQuery() {
        ElementTransform eltTransform = new ElementTransformCopyBase(true);
        ExprTransform exprTransform = new ExprTransformApplyElementTransform(eltTransform, true);

        Query result = QueryTransformOps.transform(this, eltTransform, exprTransform);
        return result;
    }

    // ---- Query canonical syntax

    // Reverse of parsing : should produce a string that parses to an equivalent query
    // "Equivalent" => gives the same results on any model
    @Override
    public String toString()
    { return serialize() ; }

    public String toString(Syntax syntax)
    { return serialize(syntax) ; }


    /** Must align with .equals */
    private int hashcode = -1 ;

    @Override
    public int hashCode()
    {
        if ( hashcode == -1 )
        {
            hashcode = QueryHashCode.calc(this) ;
            if ( hashcode == -1 )
                hashcode = Integer.MIN_VALUE/2 ;
        }
        return hashcode ;
    }

    /** Are two queries equals - tests shape and details.
     * Equality means that the queries do the same thing, including
     * same variables, in the same places.  Being unequals does
     * <b>not</b> mean the queries do different things.
     *
     * For example, reordering a group or union
     * means that a query is different.
     *
     * Two instances of a query parsed from the same string are equal.
     */

    @Override
    public boolean equals(Object other)
    {
        if ( ! ( other instanceof Query ) )
            return false ;
        if ( this == other ) return true ;
        return QueryCompare.equals(this, (Query)other) ;
    }

//    public static boolean sameAs(Query query1, Query query2)
//    { return query1.sameAs(query2) ; }

    @Override
    public void output(IndentedWriter out)
    {
        serialize(out) ;
    }

    /** Convert the query to a string */

    public String serialize()
    {
        IndentedLineBuffer buff = new IndentedLineBuffer() ;
        serialize(buff) ;
        return buff.toString();
    }

    /** Convert the query to a string in the given syntax
     * @param syntax
     */

    public String serialize(Syntax syntax)
    {
        IndentedLineBuffer buff = new IndentedLineBuffer() ;
        serialize(buff, syntax) ;
        return buff.toString();
    }

    /** Output the query
     * @param out  OutputStream
     */
    public void serialize(OutputStream out) { serialize(out, syntax); }

    /** Output the query
     *
     * @param out     OutputStream
     * @param syntax  Syntax URI
     */

    public void serialize(OutputStream out, Syntax syntax) {
        IndentedWriter writer = new IndentedWriter(out) ;
        serialize(writer, syntax) ;
        writer.flush() ;
        try { out.flush() ; } catch (Exception ex) { }
    }

    /** Format the query
     *
     * @param writer  IndentedWriter
     */
    public void serialize(IndentedWriter writer) {
        serialize(writer, syntax);
    }

    /** Format the query
     *
     * @param writer     IndentedWriter
     * @param outSyntax  Syntax URI
     */

    public void serialize(IndentedWriter writer, Syntax outSyntax)
    {
        // Try to use a serializer factory if available
        QuerySerializerFactory factory = SerializerRegistry.get().getQuerySerializerFactory(outSyntax);
        QueryVisitor serializer = factory.create(outSyntax, this, writer);
        this.visit(serializer);
    }
}
