/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr.query.model;

import java.math.BigDecimal;
import java.net.URI;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import javax.jcr.Binary;
import javax.jcr.PropertyType;
import javax.jcr.Value;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.api.value.DateTime;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.Reference;
import org.modeshape.jcr.value.ValueFactories;
import org.modeshape.jcr.value.ValueFactory;

/**
 * A set of common visitors that can be reused or extended, and methods that provide easy construction and calling of visitors.
 */
public class Visitors {

    protected static final char OPEN_SQUARE = '[';
    protected static final char CLOSE_SQUARE = ']';
    protected static final char DOUBLE_QUOTE = '"';
    protected static final char SINGLE_QUOTE = '\'';

    private static final ExecutionContext DEFAULT_CONTEXT = new ExecutionContext();

    /**
     * Visit all objects in the supplied {@link Visitable object} using a {@link NavigationVisitor} (specifically a
     * {@link WalkAllVisitor}), and with each of these visited objects calling the appropriate {@code visit(...)} method on the
     * supplied {@link Visitor}.
     * 
     * @param <StrategyVisitor> the type of strategy visitor
     * @param visitable the top-level object to be visited
     * @param strategyVisitor the visitor that is to be called for each visited objects, but that does <i>not</i> call
     *        {@link Visitable#accept(Visitor)}
     * @return the strategy visitor, allowing the caller to easily invoke operations on the visitor after visitation has completed
     */
    public static <StrategyVisitor extends Visitor> StrategyVisitor visitAll( Visitable visitable,
                                                                              StrategyVisitor strategyVisitor ) {
        if (visitable != null) visitable.accept(new WalkAllVisitor(strategyVisitor));
        return strategyVisitor;
    }

    /**
     * Visit all objects in the supplied {@link Visitable object} using a {@link NavigationVisitor} (specifically a
     * {@link WalkAllVisitor}), and with each of these visited objects calling the appropriate {@code visit(...)} method on the
     * supplied {@link Visitor}.
     * 
     * @param <StrategyVisitor> the type of strategy visitor
     * @param visitables the top-level collection of visitable objects to be visited
     * @param strategyVisitor the visitor that is to be called for each visited objects, but that does <i>not</i> call
     *        {@link Visitable#accept(Visitor)}
     * @return the strategy visitor, allowing the caller to easily invoke operations on the visitor after visitation has completed
     */
    public static <StrategyVisitor extends Visitor> StrategyVisitor visitAll( Iterable<? extends Visitable> visitables,
                                                                              StrategyVisitor strategyVisitor ) {
        if (visitables != null) {
            for (Visitable visitable : visitables) {
                if (visitable != null) {
                    visitable.accept(new WalkAllVisitor(strategyVisitor));
                }
            }
        }
        return strategyVisitor;
    }

    /**
     * Visit the supplied {@link Visitable object} using the supplied {@link Visitor}, which must be responsible for navigation as
     * well as any business logic.
     * 
     * @param <GeneralVisitor> the type of visitor
     * @param visitable the top-level object to be visited
     * @param visitor the visitor that is to be used
     * @return the visitor, allowing the caller to easily invoke operations on the visitor after visitation has completed
     */
    public static <GeneralVisitor extends Visitor> GeneralVisitor visit( Visitable visitable,
                                                                         GeneralVisitor visitor ) {
        if (visitable != null) visitable.accept(visitor);
        return visitor;
    }

    /**
     * Using a visitor, obtain the readable string representation of the supplied {@link Visitable object}
     * 
     * @param visitable the visitable
     * @return the string representation
     */
    public static String readable( Visitable visitable ) {
        // return visit(visitable, new ReadableVisitor()).getString();
        return visit(visitable, new JcrSql2Writer(DEFAULT_CONTEXT)).getString();
    }

    /**
     * Using a visitor, obtain the readable string representation of the supplied {@link Visitable object}
     * 
     * @param visitable the visitable
     * @param context the execution context in which the visitable should be converted to a string
     * @return the string representation
     */
    public static String readable( Visitable visitable,
                                   ExecutionContext context ) {
        // return visit(visitable, new ReadableVisitor()).getString();
        return visit(visitable, new JcrSql2Writer(context)).getString();
    }

    public static String readable( Object[] tuple ) {
        return readable(tuple, DEFAULT_CONTEXT);
    }

    public static String readable( Object[] tuple,
                                   ExecutionContext context ) {
        if (tuple.length == 0) {
            return "||";
        }
        ValueFactory<String> stringFactory = context.getValueFactories().getStringFactory();
        StringBuilder sb = new StringBuilder();
        sb.append("| ");
        for (Object value : tuple) {
            if (value != null) {
                sb.append(' ');
                if (value instanceof Object[]) {
                    Object[] array = (Object[])value;
                    int len = array.length;
                    for (int i = 0; i != len; ++i) {
                        if (i != 0) sb.append(", ");
                        sb.append(stringFactory.create(array[i]));
                    }
                } else {
                    sb.append(stringFactory.create(value));
                }
            }
            sb.append(" |");
        }
        return sb.toString();
    }

    /**
     * Using a visitor, obtain the {@link Subquery} objects that are contained within the supplied {@link Visitable object}. This
     * method does find Subquery objets nested in other Subquery objects.
     * 
     * @param visitable the visitable
     * @param includeNestedSubqueries true if any Subquery objects within other Subquery objects should be included, or false if
     *        only the top-level Subquery objects should be included
     * @return the collection of subqueries; never null but possibly empty if no subqueries were found
     */
    public static Collection<Subquery> subqueries( Visitable visitable,
                                                   final boolean includeNestedSubqueries ) {
        final Collection<Subquery> subqueries = new LinkedList<Subquery>();
        Visitors.visitAll(visitable, new Visitors.AbstractVisitor() {
            @Override
            public void visit( Subquery subquery ) {
                subqueries.add(subquery);
                if (includeNestedSubqueries) {
                    // Now look for any subqueries in the subquery ...
                    subquery.getQuery().accept(this);
                }
            }
        });
        return subqueries;
    }

    /**
     * Get a map of the selector names keyed by their aliases.
     * 
     * @param visitable the object to be visited
     * @return the map from the aliases to the aliased selector name; never null but possibly empty
     */
    public static Map<SelectorName, SelectorName> getSelectorNamesByAlias( Visitable visitable ) {
        // Find all of the selectors that have aliases ...
        final Map<SelectorName, SelectorName> result = new HashMap<SelectorName, SelectorName>();
        Visitors.visitAll(visitable, new Visitors.AbstractVisitor() {
            @Override
            public void visit( AllNodes allNodes ) {
                if (allNodes.hasAlias()) {
                    result.put(allNodes.alias(), allNodes.name());
                }
            }

            @Override
            public void visit( NamedSelector selector ) {
                if (selector.hasAlias()) {
                    result.put(selector.alias(), selector.name());
                }
            }
        });
        return result;
    }

    /**
     * Get a map of the selector aliases keyed by their names.
     * 
     * @param visitable the object to be visited
     * @return the map from the selector names to their alias (or name if there is no alias); never null but possibly empty
     */
    public static Map<SelectorName, SelectorName> getSelectorAliasesByName( Visitable visitable ) {
        // Find all of the selectors that have aliases ...
        final Map<SelectorName, SelectorName> result = new HashMap<SelectorName, SelectorName>();
        Visitors.visitAll(visitable, new Visitors.AbstractVisitor() {
            @Override
            public void visit( AllNodes allNodes ) {
                if (allNodes.hasAlias()) {
                    result.put(allNodes.name(), allNodes.aliasOrName());
                }
            }

            @Override
            public void visit( NamedSelector selector ) {
                if (selector.hasAlias()) {
                    result.put(selector.name(), selector.aliasOrName());
                }
            }
        });
        return result;
    }

    /**
     * Get the names of the selectors referenced by the visitable object.
     * 
     * @param visitable the object to be visited
     * @return the set of selector names referenced in some way by the visitable; never null
     */
    public static Set<SelectorName> getSelectorsReferencedBy( Visitable visitable ) {
        final Set<SelectorName> symbols = new HashSet<SelectorName>();
        // Walk the entire structure, so only supply a StrategyVisitor (that does no navigation) ...
        visitAll(visitable, new AbstractVisitor() {
            @Override
            public void visit( AllNodes allNodes ) {
                if (allNodes.hasAlias()) {
                    symbols.add(allNodes.alias());
                } else {
                    symbols.add(allNodes.name());
                }
            }

            @Override
            public void visit( ChildNode childNode ) {
                symbols.add(childNode.selectorName());
            }

            @Override
            public void visit( ChildNodeJoinCondition joinCondition ) {
                symbols.add(joinCondition.childSelectorName());
                symbols.add(joinCondition.parentSelectorName());
            }

            @Override
            public void visit( Column column ) {
                symbols.add(column.selectorName());
            }

            @Override
            public void visit( DescendantNode descendant ) {
                symbols.add(descendant.selectorName());
            }

            @Override
            public void visit( DescendantNodeJoinCondition joinCondition ) {
                symbols.add(joinCondition.ancestorSelectorName());
                symbols.add(joinCondition.descendantSelectorName());
            }

            @Override
            public void visit( EquiJoinCondition joinCondition ) {
                symbols.add(joinCondition.selector1Name());
                symbols.add(joinCondition.selector2Name());
            }

            @Override
            public void visit( FullTextSearch fullTextSearch ) {
                symbols.add(fullTextSearch.selectorName());
            }

            @Override
            public void visit( FullTextSearchScore fullTextSearchScore ) {
                symbols.add(fullTextSearchScore.selectorName());
            }

            @Override
            public void visit( Length length ) {
                symbols.add(length.selectorName());
            }

            @Override
            public void visit( NodeDepth depth ) {
                symbols.add(depth.selectorName());
            }

            @Override
            public void visit( NodePath path ) {
                symbols.add(path.selectorName());
            }

            @Override
            public void visit( NodeLocalName node ) {
                symbols.add(node.selectorName());
            }

            @Override
            public void visit( NodeName node ) {
                symbols.add(node.selectorName());
            }

            @Override
            public void visit( NamedSelector node ) {
                if (node.hasAlias()) {
                    symbols.add(node.alias());
                } else {
                    symbols.add(node.name());
                }
            }

            @Override
            public void visit( PropertyExistence prop ) {
                symbols.add(prop.selectorName());
            }

            @Override
            public void visit( PropertyValue prop ) {
                symbols.add(prop.selectorName());
            }

            @Override
            public void visit( Subquery obj ) {
                // do nothing ...
            }

            @Override
            public void visit( ReferenceValue ref ) {
                symbols.add(ref.selectorName());
            }

            @Override
            public void visit( SameNode node ) {
                symbols.add(node.selectorName());
            }

            @Override
            public void visit( SameNodeJoinCondition joinCondition ) {
                symbols.add(joinCondition.selector1Name());
                symbols.add(joinCondition.selector2Name());
            }
        });
        return symbols;
    }

    /**
     * A common base class for all visitors, which provides no-op implementations for all {@code visit(...)} methods. Visitor
     * implementations can subclass and implement only those methods that they need to implement.
     * <p>
     * This is often an excellent base class for <i>strategy visitors</i>, which simply are {@link Visitor} implementations that
     * are responsible only for visiting the supplied object but that never call {@link Visitable#accept(Visitor)}. Such strategy
     * visitors can be used in conjunction with separate <i>{@link NavigationVisitor navigation visitors}</i> that do the job of
     * navigating the Visitable objects and, for each, delegating to the strategy visitor. See
     * {@link Visitors#visitAll(Visitable, Visitor)} for an example.
     * </p>
     */
    public static class AbstractVisitor implements Visitor {

        @Override
        public void visit( AllNodes obj ) {
        }

        @Override
        public void visit( And obj ) {
        }

        @Override
        public void visit( ArithmeticOperand obj ) {
        }

        @Override
        public void visit( Between obj ) {
        }

        @Override
        public void visit( BindVariableName obj ) {
        }

        @Override
        public void visit( ChildCount obj ) {
        }

        @Override
        public void visit( ChildNode obj ) {
        }

        @Override
        public void visit( ChildNodeJoinCondition obj ) {
        }

        @Override
        public void visit( Column obj ) {
        }

        @Override
        public void visit( Comparison obj ) {
        }

        @Override
        public void visit( DescendantNode obj ) {
        }

        @Override
        public void visit( DescendantNodeJoinCondition obj ) {
        }

        @Override
        public void visit( EquiJoinCondition obj ) {
        }

        @Override
        public void visit( FullTextSearch obj ) {
        }

        @Override
        public void visit( FullTextSearchScore obj ) {
        }

        @Override
        public void visit( Join obj ) {
        }

        @Override
        public void visit( Length obj ) {
        }

        @Override
        public void visit( Limit limit ) {
        }

        @Override
        public void visit( Literal obj ) {
        }

        @Override
        public void visit( LowerCase obj ) {
        }

        @Override
        public void visit( NodeId obj ) {
        }

        @Override
        public void visit( NodeDepth obj ) {
        }

        @Override
        public void visit( NodePath obj ) {
        }

        @Override
        public void visit( NodeName obj ) {
        }

        @Override
        public void visit( NodeLocalName obj ) {
        }

        @Override
        public void visit( NamedSelector obj ) {
        }

        @Override
        public void visit( Not obj ) {
        }

        @Override
        public void visit( Or obj ) {
        }

        @Override
        public void visit( Ordering obj ) {
        }

        @Override
        public void visit( PropertyExistence obj ) {
        }

        @Override
        public void visit( PropertyValue obj ) {
        }

        @Override
        public void visit( Query obj ) {
        }

        @Override
        public void visit( Subquery obj ) {
        }

        @Override
        public void visit( ReferenceValue obj ) {
        }

        @Override
        public void visit( SameNode obj ) {
        }

        @Override
        public void visit( SameNodeJoinCondition obj ) {
        }

        @Override
        public void visit( SetCriteria obj ) {
        }

        @Override
        public void visit( SetQuery obj ) {
        }

        @Override
        public void visit( UpperCase obj ) {
        }

        @Override
        public void visit( Relike obj ) {
        }
    }

    /**
     * An abstract visitor implementation that performs navigation of the query object.
     * <p>
     * Subclasses should always implement the {@code visit(T object)} methods by performing the following actions:
     * <ol>
     * <li>Call <code>strategy.visit(object);</code></li>
     * <li>Add any children of {@code object} that are to be visited using {@link #enqueue(Visitable)}</li>
     * <li>Call {@link #visitNext()}</code></li>
     * </ol>
     * </p>
     */
    public static abstract class NavigationVisitor implements Visitor {
        protected final Visitor strategy;
        private final LinkedList<? super Visitable> itemQueue = new LinkedList<Visitable>();

        /**
         * Create a visitor that walks all query objects.
         * 
         * @param strategy the visitor that should be called at every node.
         */
        protected NavigationVisitor( Visitor strategy ) {
            assert strategy != null;
            this.strategy = strategy;
        }

        protected void enqueue( Visitable objectToBeVisited ) {
            if (objectToBeVisited != null) {
                itemQueue.add(objectToBeVisited);
            }
        }

        protected void enqueue( Iterable<? extends Visitable> objectsToBeVisited ) {
            for (Visitable objectToBeVisited : objectsToBeVisited) {
                enqueue(objectToBeVisited);
            }
        }

        protected final void visitNext() {
            if (!itemQueue.isEmpty()) {
                Visitable first = (Visitable)itemQueue.removeFirst();
                assert first != null;
                first.accept(this);
            }
        }
    }

    /**
     * A visitor implementation that walks the entire query object tree and delegates to another supplied visitor to do the actual
     * work.
     */
    public static class WalkAllVisitor extends NavigationVisitor {

        /**
         * Create a visitor that walks all query objects.
         * 
         * @param strategy the visitor that should be called at every node.
         */
        public WalkAllVisitor( Visitor strategy ) {
            super(strategy);
        }

        @Override
        public void visit( AllNodes allNodes ) {
            strategy.visit(allNodes);
            visitNext();
        }

        @Override
        public void visit( And and ) {
            strategy.visit(and);
            enqueue(and.left());
            enqueue(and.right());
            visitNext();
        }

        @Override
        public void visit( ArithmeticOperand arithmeticOperation ) {
            strategy.visit(arithmeticOperation);
            enqueue(arithmeticOperation.getLeft());
            enqueue(arithmeticOperation.getRight());
            visitNext();
        }

        @Override
        public void visit( Between between ) {
            strategy.visit(between);
            enqueue(between.getOperand());
            enqueue(between.getLowerBound());
            enqueue(between.getUpperBound());
            visitNext();
        }

        @Override
        public void visit( BindVariableName variableName ) {
            strategy.visit(variableName);
            visitNext();
        }

        @Override
        public void visit( ChildCount childCount ) {
            strategy.visit(childCount);
            visitNext();
        }

        @Override
        public void visit( ChildNode child ) {
            strategy.visit(child);
            visitNext();
        }

        @Override
        public void visit( ChildNodeJoinCondition joinCondition ) {
            strategy.visit(joinCondition);
            visitNext();
        }

        @Override
        public void visit( Column column ) {
            strategy.visit(column);
            visitNext();
        }

        @Override
        public void visit( Comparison comparison ) {
            strategy.visit(comparison);
            enqueue(comparison.getOperand1());
            enqueue(comparison.getOperand2());
            visitNext();
        }

        @Override
        public void visit( Relike relike ) {
            strategy.visit(relike);
            enqueue(relike.getOperand1());
            enqueue(relike.getOperand2());
            visitNext();
        }

        @Override
        public void visit( DescendantNode descendant ) {
            strategy.visit(descendant);
            visitNext();
        }

        @Override
        public void visit( DescendantNodeJoinCondition condition ) {
            strategy.visit(condition);
            visitNext();
        }

        @Override
        public void visit( EquiJoinCondition condition ) {
            strategy.visit(condition);
            visitNext();
        }

        @Override
        public void visit( FullTextSearch fullTextSearch ) {
            strategy.visit(fullTextSearch);
            enqueue(fullTextSearch.getFullTextSearchExpression());
            visitNext();
        }

        @Override
        public void visit( FullTextSearchScore score ) {
            strategy.visit(score);
            visitNext();
        }

        @Override
        public void visit( Join join ) {
            strategy.visit(join);
            enqueue(join.getLeft());
            enqueue(join.getJoinCondition());
            enqueue(join.getRight());
            visitNext();
        }

        @Override
        public void visit( Length length ) {
            strategy.visit(length);
            visitNext();
        }

        @Override
        public void visit( Limit limit ) {
            strategy.visit(limit);
            visitNext();
        }

        @Override
        public void visit( Literal literal ) {
            strategy.visit(literal);
            visitNext();
        }

        @Override
        public void visit( LowerCase lowerCase ) {
            strategy.visit(lowerCase);
            enqueue(lowerCase.getOperand());
            visitNext();
        }

        @Override
        public void visit( NodeDepth depth ) {
            strategy.visit(depth);
            visitNext();
        }

        @Override
        public void visit( NodeId id ) {
            strategy.visit(id);
            visitNext();
        }

        @Override
        public void visit( NodePath path ) {
            strategy.visit(path);
            visitNext();
        }

        @Override
        public void visit( NodeName nodeName ) {
            strategy.visit(nodeName);
            visitNext();
        }

        @Override
        public void visit( NodeLocalName nodeLocalName ) {
            strategy.visit(nodeLocalName);
            visitNext();
        }

        @Override
        public void visit( NamedSelector selector ) {
            strategy.visit(selector);
            visitNext();
        }

        @Override
        public void visit( Not not ) {
            strategy.visit(not);
            enqueue(not.getConstraint());
            visitNext();
        }

        @Override
        public void visit( Or or ) {
            strategy.visit(or);
            enqueue(or.left());
            enqueue(or.right());
            visitNext();
        }

        @Override
        public void visit( Ordering ordering ) {
            strategy.visit(ordering);
            enqueue(ordering.getOperand());
            visitNext();
        }

        @Override
        public void visit( PropertyExistence existence ) {
            strategy.visit(existence);
            visitNext();
        }

        @Override
        public void visit( PropertyValue propertyValue ) {
            strategy.visit(propertyValue);
            visitNext();
        }

        @Override
        public void visit( Query query ) {
            strategy.visit(query);
            enqueue(query.source());
            enqueue(query.columns());
            enqueue(query.constraint());
            enqueue(query.orderings());
            visitNext();
        }

        @Override
        public void visit( Subquery subquery ) {
            strategy.visit(subquery);
            enqueue(subquery.getQuery());
            visitNext();
        }

        @Override
        public void visit( ReferenceValue referenceValue ) {
            strategy.visit(referenceValue);
            visitNext();
        }

        @Override
        public void visit( SameNode sameNode ) {
            strategy.visit(sameNode);
            visitNext();
        }

        @Override
        public void visit( SameNodeJoinCondition condition ) {
            strategy.visit(condition);
            visitNext();
        }

        @Override
        public void visit( SetCriteria setCriteria ) {
            strategy.visit(setCriteria);
            enqueue(setCriteria.leftOperand());
            for (StaticOperand right : setCriteria.rightOperands()) {
                enqueue(right);
            }
            visitNext();
        }

        @Override
        public void visit( SetQuery setQuery ) {
            strategy.visit(setQuery);
            enqueue(setQuery.getLeft());
            enqueue(setQuery.getRight());
            visitNext();
        }

        @Override
        public void visit( UpperCase upperCase ) {
            strategy.visit(upperCase);
            enqueue(upperCase.getOperand());
            visitNext();
        }
    }

    public static class ReadableVisitor implements Visitor {
        protected final StringBuilder sb = new StringBuilder();
        protected final ExecutionContext context;
        protected final NamespaceRegistry registry;

        public ReadableVisitor( ExecutionContext context ) {
            CheckArg.isNotNull(context, "context");
            this.context = context;
            this.registry = context == null ? null : context.getNamespaceRegistry();
        }

        protected ReadableVisitor appendAlias( String columnName ) {
            append(columnName);
            return this;
        }

        protected ReadableVisitor appendColumnName( String columnName ) {
            append(columnName);
            return this;
        }

        protected ReadableVisitor appendPropertyName( String columnName ) {
            append(columnName);
            return this;
        }

        protected ReadableVisitor append( String string ) {
            sb.append(string);
            return this;
        }

        protected ReadableVisitor append( char character ) {
            sb.append(character);
            return this;
        }

        protected ReadableVisitor append( int value ) {
            sb.append(value);
            return this;
        }

        protected ReadableVisitor append( SelectorName name ) {
            sb.append(name.getString());
            return this;
        }

        protected ReadableVisitor append( Name name ) {
            append(SINGLE_QUOTE);
            append(name.getString(registry, null, null));
            append(SINGLE_QUOTE);
            return this;
        }

        protected ReadableVisitor append( Path path ) {
            sb.append(SINGLE_QUOTE);
            sb.append(path.getString(registry));
            sb.append(SINGLE_QUOTE);
            return this;
        }

        /**
         * @return context
         */
        public final ExecutionContext getContext() {
            return context;
        }

        /**
         * Get the string representation of the visited objects.
         * 
         * @return the string representation
         */
        public final String getString() {
            return sb.toString();
        }

        @Override
        public String toString() {
            return sb.toString();
        }

        @Override
        public void visit( AllNodes allNodes ) {
            append(allNodes.name());
            if (allNodes.hasAlias()) {
                append(" AS ").append(allNodes.alias());
            }
        }

        @Override
        public void visit( And and ) {
            append('(');
            and.left().accept(this);
            append(" AND ");
            and.right().accept(this);
            append(')');
        }

        @Override
        public void visit( ArithmeticOperand arithmeticOperand ) {
            append('(');
            arithmeticOperand.getLeft().accept(this);
            append(' ');
            append(arithmeticOperand.operator().symbol());
            append(' ');
            arithmeticOperand.getRight().accept(this);
            append(')');
        }

        @Override
        public void visit( Between between ) {
            between.getOperand().accept(this);
            append(" BETWEEN ");
            between.getLowerBound().accept(this);
            if (!between.isLowerBoundIncluded()) append(" EXCLUSIVE");
            append(" AND ");
            between.getUpperBound().accept(this);
            if (!between.isUpperBoundIncluded()) append(" EXCLUSIVE");
        }

        @Override
        public void visit( BindVariableName variable ) {
            append('$').append(variable.getBindVariableName());
        }

        @Override
        public void visit( ChildCount childCount ) {
            append("CHILDCOUNT(").append(childCount.selectorName()).append(')');
        }

        @Override
        public void visit( ChildNode child ) {
            append("ISCHILDNODE(");
            append(child.selectorName());
            append(',');
            append(SINGLE_QUOTE);
            append(child.getParentPath());
            append(SINGLE_QUOTE);
            append(')');
        }

        @Override
        public void visit( ChildNodeJoinCondition condition ) {
            append("ISCHILDNODE(");
            append(condition.childSelectorName());
            append(',');
            append(condition.parentSelectorName());
            append(')');
        }

        @Override
        public void visit( Column column ) {
            append(column.selectorName());
            if (column.getPropertyName() == null) {
                append(".*");
            } else {
                String propertyName = column.getPropertyName();
                append('.').appendPropertyName(propertyName);
                if (!propertyName.equals(column.getColumnName()) && !propertyName.equals(column.getColumnName())
                    && !(column.selectorName() + "." + propertyName).equals(column.getColumnName())) {
                    append(" AS ").appendAlias(column.getColumnName());
                }
            }
        }

        @Override
        public void visit( Comparison comparison ) {
            comparison.getOperand1().accept(this);
            append(' ').append(comparison.operator().symbol()).append(' ');
            comparison.getOperand2().accept(this);
        }

        @Override
        public void visit( Relike relike ) {
            append("RELIKE(");
            relike.getOperand1().accept(this);
            append(',');
            relike.getOperand2().accept(this);
            append(')');
        }

        @Override
        public void visit( DescendantNode descendant ) {
            append("ISDESCENDANTNODE(");
            append(descendant.selectorName());
            append(',');
            append(SINGLE_QUOTE);
            append(descendant.getAncestorPath());
            append(SINGLE_QUOTE);
            append(')');
        }

        @Override
        public void visit( DescendantNodeJoinCondition condition ) {
            append("ISDESCENDANTNODE(");
            append(condition.descendantSelectorName());
            append(',');
            append(condition.ancestorSelectorName());
            append(')');
        }

        @Override
        public void visit( EquiJoinCondition condition ) {
            append(condition.selector1Name()).append('.').appendPropertyName(condition.getProperty1Name());
            append(" = ");
            append(condition.selector2Name()).append('.').appendPropertyName(condition.getProperty2Name());
        }

        @Override
        public void visit( FullTextSearch fullText ) {
            append("CONTAINS(").append(fullText.selectorName());
            if (fullText.getPropertyName() != null) {
                append('.').appendPropertyName(fullText.getPropertyName());
            }
            sb.append(",'").append(fullText.fullTextSearchExpression()).append("')");
        }

        @Override
        public void visit( FullTextSearchScore score ) {
            append("SCORE(").append(score.selectorName()).append(')');
        }

        @Override
        public void visit( Join join ) {
            join.getLeft().accept(this);
            // if (join.getType() != JoinType.INNER) {
            sb.append(' ').append(join.type().symbol());
            // } else {
            // sb.append(',');
            // }
            append(' ');
            join.getRight().accept(this);
            append(" ON ");
            join.getJoinCondition().accept(this);
        }

        @Override
        public void visit( Length length ) {
            append("LENGTH(");
            length.getPropertyValue().accept(this);
            append(')');
        }

        @Override
        public void visit( Limit limit ) {
            append("LIMIT ").append(limit.getRowLimit());
            if (limit.getOffset() != 0) {
                append(" OFFSET ").append(limit.getOffset());
            }
        }

        @Override
        public void visit( Literal literal ) {
            if (literal instanceof LiteralValue) {
                LiteralValue literalValue = (LiteralValue)literal;
                Value value = literalValue.getLiteralValue();
                String typeName = null;
                ValueFactories factories = context.getValueFactories();
                switch (value.getType()) {
                    case PropertyType.UNDEFINED:
                    case PropertyType.STRING:
                        append(SINGLE_QUOTE);
                        String str = factories.getStringFactory().create(literalValue.value());
                        append(str);
                        append(SINGLE_QUOTE);
                        return;
                    case PropertyType.PATH:
                        append("CAST(");
                        append(factories.getPathFactory().create(literalValue.value()));
                        append(" AS ").append(PropertyType.TYPENAME_PATH.toUpperCase()).append(')');
                        return;
                    case PropertyType.NAME:
                        append("CAST(");
                        append(factories.getNameFactory().create(literalValue.value()));
                        append(" AS ").append(PropertyType.TYPENAME_NAME.toUpperCase()).append(')');
                        return;
                    case PropertyType.REFERENCE:
                        typeName = PropertyType.TYPENAME_REFERENCE;
                        break;
                    case PropertyType.WEAKREFERENCE:
                        typeName = PropertyType.TYPENAME_WEAKREFERENCE;
                        break;
                    case org.modeshape.jcr.api.PropertyType.SIMPLE_REFERENCE:
                        typeName = org.modeshape.jcr.api.PropertyType.TYPENAME_SIMPLE_REFERENCE;
                        break;
                    case PropertyType.BINARY:
                        typeName = PropertyType.TYPENAME_BINARY;
                        break;
                    case PropertyType.BOOLEAN:
                        typeName = PropertyType.TYPENAME_BOOLEAN;
                        break;
                    case PropertyType.DATE:
                        typeName = PropertyType.TYPENAME_DATE;
                        break;
                    case PropertyType.DECIMAL:
                        typeName = PropertyType.TYPENAME_DECIMAL;
                        break;
                    case PropertyType.DOUBLE:
                        typeName = PropertyType.TYPENAME_DOUBLE;
                        break;
                    case PropertyType.LONG:
                        typeName = PropertyType.TYPENAME_LONG;
                        break;
                    case PropertyType.URI:
                        typeName = PropertyType.TYPENAME_URI;
                        break;
                }
                assert typeName != null;
                String str = factories.getStringFactory().create(literalValue.value());
                append("CAST('").append(str).append("' AS ").append(typeName.toUpperCase()).append(')');
            } else {
                Object value = literal.value();
                String typeName = null;
                ValueFactories factories = context.getValueFactories();
                if (value instanceof String || value instanceof Character) {
                    append(SINGLE_QUOTE);
                    String str = factories.getStringFactory().create(value);
                    append(str);
                    append(SINGLE_QUOTE);
                    return;
                }
                if (value instanceof Path) {
                    append("CAST(");
                    append(factories.getPathFactory().create(value));
                    append(" AS ").append(PropertyType.TYPENAME_PATH.toUpperCase()).append(')');
                    return;
                }
                if (value instanceof Name) {
                    append("CAST(");
                    append(factories.getNameFactory().create(value));
                    append(" AS ").append(PropertyType.TYPENAME_NAME.toUpperCase()).append(')');
                    return;
                }
                if (value instanceof Reference) {
                    typeName = ((Reference)value).isWeak() ? PropertyType.TYPENAME_WEAKREFERENCE.toUpperCase() : PropertyType.TYPENAME_REFERENCE.toUpperCase();
                } else if (value instanceof Binary) {
                    typeName = PropertyType.TYPENAME_BINARY.toUpperCase();
                } else if (value instanceof Boolean) {
                    typeName = PropertyType.TYPENAME_BOOLEAN.toUpperCase();
                } else if (value instanceof DateTime) {
                    typeName = PropertyType.TYPENAME_DATE.toUpperCase();
                } else if (value instanceof BigDecimal) {
                    typeName = PropertyType.TYPENAME_DECIMAL.toUpperCase();
                } else if (value instanceof Double || value instanceof Float) {
                    typeName = PropertyType.TYPENAME_DOUBLE.toUpperCase();
                } else if (value instanceof Long || value instanceof Integer || value instanceof Short) {
                    typeName = PropertyType.TYPENAME_LONG.toUpperCase();
                } else if (value instanceof URI) {
                    typeName = PropertyType.TYPENAME_URI.toUpperCase();
                }
                assert typeName != null;
                String str = factories.getStringFactory().create(value);
                append("CAST('").append(str).append("' AS ").append(typeName.toUpperCase()).append(')');
            }
        }

        @Override
        public void visit( LowerCase lowerCase ) {
            append("LOWER(");
            lowerCase.getOperand().accept(this);
            append(')');
        }

        @Override
        public void visit( NodeDepth depth ) {
            append("DEPTH(").append(depth.selectorName()).append(')');
        }

        @Override
        public void visit( NodeId id ) {
            append("ID(").append(id.selectorName()).append(')');
        }

        @Override
        public void visit( NodePath path ) {
            append("PATH(").append(path.selectorName()).append(')');
        }

        @Override
        public void visit( NodeLocalName name ) {
            append("LOCALNAME(").append(name.selectorName()).append(')');
        }

        @Override
        public void visit( NodeName name ) {
            append("NAME(").append(name.selectorName()).append(')');
        }

        @Override
        public void visit( NamedSelector selector ) {
            append(selector.name());
            if (selector.hasAlias()) {
                append(" AS ").append(selector.alias());
            }
        }

        @Override
        public void visit( Not not ) {
            append("NOT ");
            append('(');
            not.getConstraint().accept(this);
            append(')');
        }

        @Override
        public void visit( Or or ) {
            append('(');
            or.left().accept(this);
            append(" OR ");
            or.right().accept(this);
            append(')');
        }

        @Override
        public void visit( Ordering ordering ) {
            ordering.getOperand().accept(this);
            append(' ').append(ordering.order().symbol());
        }

        @Override
        public void visit( PropertyExistence existence ) {
            append(existence.selectorName()).append('.').appendPropertyName(existence.getPropertyName()).append(" IS NOT NULL");
        }

        @Override
        public void visit( PropertyValue value ) {
            append(value.selectorName()).append('.').appendPropertyName(value.getPropertyName());
        }

        @Override
        public void visit( ReferenceValue value ) {
            append("REFERENCE(");
            append(value.selectorName());
            if (value.getPropertyName() != null) {
                append('.').appendPropertyName(value.getPropertyName());
            }
            append(")");
        }

        @Override
        public void visit( Query query ) {
            append("SELECT ");
            if (query.isDistinct()) append("DISTINCT ");
            if (query.columns().isEmpty()) {
                append('*');
            } else {
                boolean isFirst = true;
                for (Column column : query.columns()) {
                    if (isFirst) isFirst = false;
                    else append(", ");
                    column.accept(this);
                }
            }
            append(" FROM ");
            query.source().accept(this);
            if (query.constraint() != null) {
                append(" WHERE ");
                query.constraint().accept(this);
            }
            if (!query.orderings().isEmpty()) {
                append(" ORDER BY ");
                boolean isFirst = true;
                for (Ordering ordering : query.orderings()) {
                    if (isFirst) isFirst = false;
                    else append(", ");
                    ordering.accept(this);
                }
            }
            if (!query.getLimits().isUnlimited()) {
                append(' ');
                query.getLimits().accept(this);
            }
        }

        @Override
        public void visit( Subquery subquery ) {
            append('(');
            subquery.getQuery().accept(this);
            append(')');
        }

        @Override
        public void visit( SameNode sameNode ) {
            append("ISSAMENODE(").append(sameNode.selectorName()).append(",'").append(sameNode.getPath()).append("')");
        }

        @Override
        public void visit( SameNodeJoinCondition condition ) {
            append("ISSAMENODE(").append(condition.selector1Name()).append(',').append(condition.selector2Name());
            if (condition.getSelector2Path() != null) {
                append(",'").append(condition.getSelector2Path()).append('\'');
            }
            append(')');
        }

        @Override
        public void visit( SetCriteria criteria ) {
            criteria.leftOperand().accept(this);
            append(" IN (");
            Iterator<? extends StaticOperand> iter = criteria.rightOperands().iterator();
            if (iter.hasNext()) {
                iter.next().accept(this);
                while (iter.hasNext()) {
                    append(',');
                    iter.next().accept(this);
                }
            }
            append(')');
        }

        @Override
        public void visit( SetQuery query ) {
            query.getLeft().accept(this);
            append(' ').append(query.operation().getSymbol()).append(' ');
            if (query.isAll()) append("ALL ");
            query.getRight().accept(this);
        }

        @Override
        public void visit( UpperCase upperCase ) {
            append("UPPER(");
            upperCase.getOperand().accept(this);
            append(')');
        }

    }

    public static class JcrSql2Writer extends ReadableVisitor {

        public JcrSql2Writer( ExecutionContext context ) {
            super(context);
        }

        protected final boolean needsQuotes( String str ) {
            CharacterIterator iter = new StringCharacterIterator(str);
            for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) {
                if (!Character.isLetterOrDigit(c)) return true;
            }
            return false;
        }

        protected void appendQuoted( char openQuote,
                                     String name,
                                     char closeQuote ) {
            // If the name contains any non-alphanumeric characters, then we'll quote.
            // It's okay (and safer) to quote more often than necessary.
            if (needsQuotes(name)) {
                append(OPEN_SQUARE);
                append(name);
                append(CLOSE_SQUARE);
            } else {
                append(name);
            }
        }

        @Override
        protected ReadableVisitor append( String string ) {
            return super.append(string);
        }

        @Override
        protected ReadableVisitor append( char character ) {
            return super.append(character);
        }

        @Override
        protected ReadableVisitor append( int value ) {
            return super.append(value);
        }

        @Override
        protected ReadableVisitor appendColumnName( String columnName ) {
            appendQuoted(OPEN_SQUARE, columnName, CLOSE_SQUARE);
            return this;
        }

        @Override
        protected ReadableVisitor appendPropertyName( String propertyName ) {
            appendQuoted(OPEN_SQUARE, propertyName, CLOSE_SQUARE);
            return this;
        }

        @Override
        protected ReadableVisitor appendAlias( String alias ) {
            appendQuoted(OPEN_SQUARE, alias, CLOSE_SQUARE);
            return this;
        }

        @Override
        protected ReadableVisitor append( SelectorName name ) {
            appendQuoted(OPEN_SQUARE, name.name(), CLOSE_SQUARE);
            return this;
        }

        @Override
        public void visit( Ordering ordering ) {
            ordering.getOperand().accept(this);
            append(' ').append(ordering.order().symbol());
            append(' ').append(ordering.nullOrder().symbol());
        }
    }
}
