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

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RangeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;
import org.modeshape.common.annotation.NotThreadSafe;
import org.modeshape.common.collection.Collections;
import org.modeshape.common.collection.Problem;
import org.modeshape.common.collection.Problem.Status;
import org.modeshape.jcr.JcrI18n;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.CachedNodeSupplier;
import org.modeshape.jcr.query.NodeSequence.Batch;
import org.modeshape.jcr.query.NodeSequence.Restartable;
import org.modeshape.jcr.query.QueryResults.Columns;
import org.modeshape.jcr.query.engine.process.RestartableSequence;

/**
 * The results of a query. This is not thread-safe because it relies upon JcrSession, which is not thread-safe. Also, although the
 * results of a query never change, the objects returned by the iterators may vary if the session information changes.
 * 
 * @see XPathQueryResult
 * @see JcrSqlQueryResult
 */
@NotThreadSafe
public class JcrQueryResult implements org.modeshape.jcr.api.query.QueryResult {
    public static final String JCR_SCORE_COLUMN_NAME = "jcr:score";
    public static final String JCR_PATH_COLUMN_NAME = "jcr:path";
    public static final String JCR_NAME_COLUMN_NAME = "jcr:name";
    public static final String JCR_UUID_COLUMN_NAME = "jcr:uuid";
    public static final String MODE_LOCALNAME_COLUMN_NAME = "mode:localName";
    public static final String MODE_DEPTH_COLUMN_NAME = "mode:depth";
    protected static final Set<String> PSEUDO_COLUMNS = Collections.unmodifiableSet(JCR_SCORE_COLUMN_NAME, JCR_PATH_COLUMN_NAME,
                                                                                    JCR_NAME_COLUMN_NAME, JCR_UUID_COLUMN_NAME,
                                                                                    MODE_LOCALNAME_COLUMN_NAME,
                                                                                    MODE_DEPTH_COLUMN_NAME);

    protected final JcrQueryContext context;
    protected final QueryResults results;
    protected final String queryStatement;
    private final NodeSequence sequence;
    private final boolean restartable;
    private List<String> warnings;
    private boolean accessed = false;

    protected JcrQueryResult( JcrQueryContext context,
                              String query,
                              QueryResults results,
                              boolean restartable,
                              int numRowsInMemory ) {
        this.context = context;
        this.results = results;
        this.queryStatement = query;
        this.restartable = restartable;
        if (!restartable || results.getRows().isEmpty()) {
            this.sequence = results.getRows();
        } else {
            String workspace = context.getWorkspaceName();
            BufferManager bufferMgr = context.getBufferManager();
            CachedNodeSupplier nodeCache = results.getCachedNodes();
            this.sequence = new RestartableSequence(workspace, results.getRows(), bufferMgr, nodeCache, numRowsInMemory);
        }
        assert this.context != null;
        assert this.results != null;
        assert this.queryStatement != null;
    }

    protected List<String> getColumnNameList() {
        return results.getColumns().getColumnNames();
    }

    protected List<String> getColumnTypeList() {
        return results.getColumns().getColumnTypes();
    }

    protected final NodeSequence sequence() {
        return sequence;
    }

    @Override
    public String[] getColumnNames() /*throws RepositoryException*/{
        List<String> names = getColumnNameList();
        return names.toArray(new String[names.size()]); // make a defensive copy ...
    }

    @Override
    public String[] getColumnTypes() {
        List<String> types = getColumnTypeList();
        return types.toArray(new String[types.size()]); // make a defensive copy ...
    }

    @Override
    public String[] getSelectorNames() {
        List<String> selectorNames = results.getColumns().getSelectorNames();
        return selectorNames.toArray(new String[selectorNames.size()]); // make a defensive copy ...
    }

    @Override
    public NodeIterator getNodes() throws RepositoryException {
        if (accessed) {
            if (!restartable) {
                throw new RepositoryException(JcrI18n.multipleCallsToGetRowsOrNodesIsNotAllowed.text(queryStatement));
            }
            ((Restartable)sequence).restart();
        }
        if (getSelectorNames().length > 1) {
            throw new RepositoryException(JcrI18n.multipleSelectorsAppearInQueryUnableToCallMethod.text(queryStatement));
        }
        // Find all of the nodes in the results...
        accessed = true;
        int defaultSelectorIndex = computeDefaultSelectorIndex();
        return new QueryResultNodeIterator(context, sequence, defaultSelectorIndex);
    }

    protected int computeDefaultSelectorIndex() {
        Columns columns = results.getColumns();
        List<String> selectorNames = columns.getSelectorNames();
        if (selectorNames.size() == 1) {
            return columns.getSelectorIndex(selectorNames.get(0));
        }
        return 0;
    }

    @Override
    public RowIterator getRows() throws RepositoryException {
        if (accessed) {
            if (!restartable) {
                throw new RepositoryException(JcrI18n.multipleCallsToGetRowsOrNodesIsNotAllowed.text(queryStatement));
            }
            ((Restartable)sequence).restart();
        }
        accessed = true;
        final Columns columns = results.getColumns();
        if (columns.getSelectorNames().size() == 1) {
            return new SingleSelectorQueryResultRowIterator(context, queryStatement, sequence, results.getColumns());
        }
        return new QueryResultRowIterator(context, queryStatement, sequence, results.getColumns());
    }

    @Override
    public String getPlan() {
        return results.getPlan();
    }

    @Override
    public Collection<String> getWarnings() {
        if (warnings == null) {
            // Obtain the warnings ...
            if (!results.hasWarnings()) {
                warnings = java.util.Collections.emptyList();
            } else {
                List<String> messages = new LinkedList<String>();
                for (Problem problem : results.getProblems()) {
                    if (problem.getStatus() == Status.WARNING) {
                        String msg = problem.getMessageString();
                        if (!messages.contains(msg)) messages.add(msg);
                    }
                }
                warnings = java.util.Collections.unmodifiableList(messages);
            }
        }
        return warnings;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

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

    @Override
    public void close() {
        sequence.close();
    }

    /**
     * The {@link NodeIterator} implementation returned by the {@link JcrQueryResult}.
     * 
     * @see JcrQueryResult#getNodes()
     */
    @NotThreadSafe
    protected static abstract class QueryResultIterator implements RangeIterator {
        protected final JcrQueryContext context;
        private NodeSequence sequence;
        private long position = 0L;
        private Batch currentBatch;

        protected QueryResultIterator( JcrQueryContext context,
                                       NodeSequence sequence ) {
            this.context = context;
            this.sequence = sequence;
        }

        @Override
        public boolean hasNext() {
            while (findNextBatch() != null) {
                if (currentBatch.hasNext()) return true;
                currentBatch = null;
            }
            return false;
        }

        protected final Batch moveToNextRow() {
            if (findNextBatch() == null || !currentBatch.hasNext()) throw new NoSuchElementException();
            currentBatch.nextRow();
            ++position;
            return currentBatch;
        }

        protected Batch findNextBatch() {
            if (currentBatch == null) {
                currentBatch = sequence.nextBatch();
            }
            return currentBatch;
        }

        @Override
        public long getPosition() {
            return position;
        }

        @Override
        public void skip( long skipNum ) {
            for (long i = 0L; i != skipNum; ++i)
                moveToNextRow();
        }

        @Override
        public final long getSize() {
            return sequence.getRowCount();
        }
    }

    /**
     * The {@link NodeIterator} implementation returned by the {@link JcrQueryResult}.
     * 
     * @see JcrQueryResult#getNodes()
     */
    @NotThreadSafe
    protected static class QueryResultNodeIterator extends QueryResultIterator implements NodeIterator {

        private final int defaultSelectorIndex;

        protected QueryResultNodeIterator( JcrQueryContext context,
                                           NodeSequence sequence,
                                           int defaultSelectorIndex ) {
            super(context, sequence);
            this.defaultSelectorIndex = defaultSelectorIndex;
        }

        @Override
        public Node nextNode() {
            CachedNode cachedNode = moveToNextRow().getNode(defaultSelectorIndex);
            Node node = context.getNode(cachedNode);
            return node;
        }

        @Override
        public Object next() {
            return nextNode();
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * The {@link RowIterator} implementation returned by the {@link JcrQueryResult}.
     * 
     * @see JcrQueryResult#getRows()
     */
    @NotThreadSafe
    protected static class QueryResultRowIterator extends QueryResultIterator implements RowIterator {
        protected final List<String> columnNames;
        private final Set<String> selectorNames;
        protected final Columns columns;
        protected final String query;

        protected QueryResultRowIterator( JcrQueryContext context,
                                          String query,
                                          NodeSequence sequence,
                                          Columns columns ) {
            super(context, sequence);
            this.query = query;
            this.columns = columns;
            this.columnNames = this.columns.getColumnNames();
            this.selectorNames = new HashSet<String>(columns.getSelectorNames());
        }

        public boolean hasSelector( String selectorName ) {
            return this.selectorNames.contains(selectorName);
        }

        @Override
        public Row nextRow() {
            return createRow(moveToNextRow());
        }

        @Override
        public Object next() {
            return nextRow();
        }

        protected Row createRow( Batch batch ) {
            return new MultiSelectorQueryResultRow(this, batch);
        }

        protected String getPropertyNameForColumnName( String columnName ) {
            return columns.getPropertyNameForColumnName(columnName);
        }

        protected int nodeIndexForSelector( String selector ) {
            return columns.getSelectorIndex(selector);
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        protected Value jcrPath( CachedNode node ) {
            assert node != null;
            // Every node has a path ...
            return context.createValue(PropertyType.PATH, context.getPath(node));
        }

        protected Value jcrUuid( CachedNode node ) {
            assert node != null;
            // only referenceable nodes have UUIDs ...
            String uuid = context.getUuid(node);
            return uuid == null ? null : context.createValue(PropertyType.STRING, uuid);
        }

        protected Value jcrName( CachedNode node ) {
            assert node != null;
            // Every node has a name; even the root has a zero-length name ...
            return context.createValue(PropertyType.NAME, context.getName(node));
        }

        protected Value jcrLocalName( CachedNode node ) {
            assert node != null;
            // Every node has a name; even the root has a zero-length name ...
            return context.createValue(PropertyType.NAME, context.getName(node).getLocalName());
        }

        protected Value jcrDepth( CachedNode node ) {
            assert node != null;
            // Every node has a depth ...
            return context.createValue(PropertyType.LONG, context.getDepth(node));
        }

        protected Value jcrPath( String path ) {
            return context.createValue(PropertyType.PATH, path);
        }

        protected Value jcrName( String name ) {
            return context.createValue(PropertyType.NAME, name);
        }

        protected Value jcrName() {
            return context.createValue(PropertyType.NAME, "");
        }

        protected Value jcrString( String name ) {
            return context.createValue(PropertyType.STRING, name);
        }

        protected Value jcrLong( Long value ) {
            return context.createValue(PropertyType.LONG, value);
        }

        protected Value jcrDouble( Double score ) {
            return context.createValue(PropertyType.DOUBLE, score);
        }

        protected Value jcrDouble( Float score ) {
            return context.createValue(PropertyType.DOUBLE, score);
        }
    }

    /**
     * The {@link RowIterator} implementation returned by the {@link JcrQueryResult}.
     * 
     * @see JcrQueryResult#getRows()
     */
    @NotThreadSafe
    protected static class SingleSelectorQueryResultRowIterator extends QueryResultRowIterator {

        private final int selectorIndex;

        protected SingleSelectorQueryResultRowIterator( JcrQueryContext context,
                                                        String query,
                                                        NodeSequence sequence,
                                                        Columns columns ) {
            super(context, query, sequence, columns);
            this.selectorIndex = columns.getSelectorIndex(columns.getSelectorNames().get(0));
        }

        @Override
        protected Row createRow( Batch batch ) {
            return new SingleSelectorQueryResultRow(this, batch, selectorIndex);
        }
    }

    protected static abstract class AbstractRow implements javax.jcr.query.Row {
        protected final QueryResultRowIterator iterator;
        protected final Batch batchAtRow;
        private Value[] values = null;

        protected AbstractRow( QueryResultRowIterator iterator,
                               Batch batchAtRow ) {
            this.iterator = iterator;
            this.batchAtRow = batchAtRow;
            assert this.iterator != null;
            assert this.batchAtRow != null;
        }

        @Override
        public Node getNode( String selectorName ) throws RepositoryException {
            int nodeIndex = iterator.nodeIndexForSelector(selectorName);
            if (nodeIndex < 0) {
                throw new RepositoryException(JcrI18n.selectorNotUsedInQuery.text(selectorName, iterator.query));
            }
            CachedNode cachedNode = batchAtRow.getNode(nodeIndex);
            return cachedNode == null ? null : iterator.context.getNode(cachedNode);
        }

        protected Value getValue( String columnName,
                                  CachedNode cachedNode,
                                  int nodeIndex ) throws ItemNotFoundException, RepositoryException {
            if (cachedNode == null) return null;
            // Get the property name for the column. Note that if the column is aliased, the property name will be different;
            // otherwise, the property name will be the same as the column name ...
            String propertyName = iterator.getPropertyNameForColumnName(columnName);
            if (propertyName == null) return null;

            if (PSEUDO_COLUMNS.contains(propertyName)) {
                if (JCR_PATH_COLUMN_NAME.equals(propertyName)) {
                    return iterator.jcrPath(cachedNode);
                }
                if (JCR_NAME_COLUMN_NAME.equals(propertyName)) {
                    return iterator.jcrName(cachedNode);
                }
                if (MODE_LOCALNAME_COLUMN_NAME.equals(propertyName)) {
                    return iterator.jcrLocalName(cachedNode);
                }
                if (MODE_DEPTH_COLUMN_NAME.equals(propertyName)) {
                    return iterator.jcrDepth(cachedNode);
                }
                if (JCR_SCORE_COLUMN_NAME.equals(propertyName)) {
                    float score = batchAtRow.getScore(nodeIndex);
                    return iterator.jcrDouble(score);
                }
                if (JCR_UUID_COLUMN_NAME.equals(propertyName)) {
                    return iterator.jcrUuid(cachedNode);
                }
            }
            // Get the property's value ...
            Node node = iterator.context.getNode(cachedNode);
            if (node == null || !node.hasProperty(propertyName)) return null;
            Property property = node.getProperty(propertyName);
            Value value = null;
            if (property.isMultiple()) {
                Value[] values = property.getValues();
                // The array of values might be empty ...
                if (values.length > 0) {
                    // Use only the first value of a multi-valued property ...
                    value = property.getValues()[0];
                }
                // Otherwise the value will be null
            } else {
                value = property.getValue();
            }
            return value;
        }

        @Override
        public Value[] getValues() throws RepositoryException {
            if (values == null) {
                int i = 0;
                values = new Value[iterator.columnNames.size()];
                for (String columnName : iterator.columnNames) {
                    values[i++] = getValue(columnName);
                }
            }
            return values;
        }

        @Override
        public Node getNode() throws RepositoryException {
            throw new RepositoryException(
                                          JcrI18n.multipleSelectorsAppearInQueryRequireSpecifyingSelectorName.text(iterator.query));
        }

        @Override
        public String getPath() throws RepositoryException {
            throw new RepositoryException(
                                          JcrI18n.multipleSelectorsAppearInQueryRequireSpecifyingSelectorName.text(iterator.query));
        }

        @Override
        public double getScore() throws RepositoryException {
            throw new RepositoryException(
                                          JcrI18n.multipleSelectorsAppearInQueryRequireSpecifyingSelectorName.text(iterator.query));
        }

        @Override
        public String getPath( String selectorName ) throws RepositoryException {
            Node node = getNode(selectorName);
            return node == null ? null : node.getPath();
        }

        @Override
        public double getScore( String selectorName ) throws RepositoryException {
            if (!iterator.hasSelector(selectorName)) {
                throw new RepositoryException(JcrI18n.selectorNotUsedInQuery.text(selectorName, iterator.query));
            }
            int nodeIndex = iterator.columns.getSelectorNames().indexOf(selectorName);
            return batchAtRow.getScore(nodeIndex);
        }

    }

    protected static class SingleSelectorQueryResultRow extends AbstractRow {
        protected final CachedNode cachedNode;
        protected final Node node;
        protected final int selectorIndex;

        protected SingleSelectorQueryResultRow( QueryResultRowIterator iterator,
                                                Batch batchAtRow,
                                                int selectorIndex ) {
            super(iterator, batchAtRow);
            this.selectorIndex = selectorIndex;
            this.cachedNode = batchAtRow.getNode(selectorIndex);
            this.node = iterator.context.getNode(cachedNode);
        }

        @Override
        public Node getNode( String selectorName ) throws RepositoryException {
            if (!iterator.hasSelector(selectorName)) {
                throw new RepositoryException(JcrI18n.selectorNotUsedInQuery.text(selectorName, iterator.query));
            }
            return node;
        }

        @Override
        public Value getValue( String columnName ) throws ItemNotFoundException, RepositoryException {
            return getValue(columnName, cachedNode, selectorIndex);
        }

        @Override
        public Node getNode() {
            return node;
        }

        @Override
        public String getPath() throws RepositoryException {
            return node.getPath();
        }

        @Override
        public String getPath( String selectorName ) throws RepositoryException {
            if (!iterator.hasSelector(selectorName)) {
                throw new RepositoryException(JcrI18n.selectorNotUsedInQuery.text(selectorName, iterator.query));
            }
            return node.getPath();
        }

        @Override
        public double getScore() {
            return batchAtRow.getScore(selectorIndex);
        }

        @Override
        public double getScore( String selectorName ) throws RepositoryException {
            if (!iterator.hasSelector(selectorName)) {
                throw new RepositoryException(JcrI18n.selectorNotUsedInQuery.text(selectorName, iterator.query));
            }
            return getScore();
        }
    }

    protected static class MultiSelectorQueryResultRow extends AbstractRow {

        protected MultiSelectorQueryResultRow( QueryResultRowIterator iterator,
                                               Batch batchAtRow ) {
            super(iterator, batchAtRow);
        }

        @Override
        public Value getValue( String columnName ) throws ItemNotFoundException, RepositoryException {
            String selectorName = iterator.columns.getSelectorNameForColumnName(columnName);
            int nodeIndex = iterator.columns.getSelectorIndex(selectorName);
            if (nodeIndex == -1) {
                throw new RepositoryException(JcrI18n.queryResultsDoNotIncludeColumn.text(columnName, iterator.query));
            }
            CachedNode cachedNode = batchAtRow.getNode(nodeIndex);
            return getValue(columnName, cachedNode, nodeIndex);
        }
    }
}
