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

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;

import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RangeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;

import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.QueryInfo;
import org.apache.jackrabbit.spi.QueryResultRow;
import org.apache.jackrabbit.spi.NodeId;
import org.apache.jackrabbit.spi.commons.conversion.NameException;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.value.ValueFormat;
import org.apache.jackrabbit.jcr2spi.ItemManager;
import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager;

/**
 * Implements the {@link javax.jcr.query.RowIterator} interface returned by
 * a {@link javax.jcr.query.QueryResult}.
 */
class RowIteratorImpl implements RowIterator {

    /**
     * The result rows from the SPI implementation.
     */
    private final RangeIterator rows;

    /**
     * The column names.
     */
    private final String[] columnNames;

    /**
     * The <code>NamePathResolver</code> of the user <code>Session</code>.
     */
    private final NamePathResolver resolver;

    /**
     * The JCR value factory.
     */
    private final ValueFactory vFactory;

    /**
     * The item manager.
     */
    private final ItemManager itemMgr;

    /**
     * The hierarchy manager.
     */
    private final HierarchyManager hmgr;

    /**
     * Creates a new <code>RowIteratorImpl</code> that iterates over the result
     * nodes.
     *
     * @param queryInfo the query info.
     * @param resolver  <code>NameResolver</code> of the user
     *                  <code>Session</code>.
     * @param vFactory  the JCR value factory.
     * @param itemMgr   the item manager.
     * @param hmgr      the hierarchy manager.
     */
    RowIteratorImpl(QueryInfo queryInfo, NamePathResolver resolver,
                    ValueFactory vFactory, ItemManager itemMgr,
                    HierarchyManager hmgr) {
        this.rows = queryInfo.getRows();
        this.columnNames = queryInfo.getColumnNames();
        this.resolver = resolver;
        this.vFactory = vFactory;
        this.itemMgr = itemMgr;
        this.hmgr = hmgr;
    }

    //--------------------------------------------------------< RowIterator >---
    /**
     * Returns the next <code>Row</code> in the iteration.
     *
     * @return the next <code>Row</code> in the iteration.
     * @throws NoSuchElementException if iteration has no more <code>Row</code>s.
     * @see RowIterator#nextRow()
     */
    public Row nextRow() throws NoSuchElementException {
        return new RowImpl((QueryResultRow) rows.next());
    }

    //------------------------------------------------------< RangeIterator >---
    /**
     * Skip a number of <code>Row</code>s in this iterator.
     *
     * @param skipNum the non-negative number of <code>Row</code>s to skip
     * @throws NoSuchElementException if skipped past the last <code>Row</code>
     * in this iterator.
     * @see javax.jcr.RangeIterator#skip(long)
     */
    public void skip(long skipNum) throws NoSuchElementException {
        rows.skip(skipNum);
    }

    /**
     * Returns the number of <code>Row</code>s in this iterator.
     *
     * @return the number of <code>Row</code>s in this iterator.
     * @see RangeIterator#getSize()
     */
    public long getSize() {
        return rows.getSize();
    }

    /**
     * Returns the current position within this iterator. The number
     * returned is the 0-based index of the next <code>Row</code> in the iterator,
     * i.e. the one that will be returned on the subsequent <code>next</code> call.
     * <p/>
     * Note that this method does not check if there is a next element,
     * i.e. an empty iterator will always return 0.
     *
     * @return the current position withing this iterator.
     * @see RangeIterator#getPosition()
     */
    public long getPosition() {
        return rows.getPosition();
    }

    /**
     * @throws UnsupportedOperationException always.
     * @see Iterator#remove()
     */
    public void remove() {
        throw new UnsupportedOperationException("remove");
    }

    /**
     * Returns <code>true</code> if the iteration has more <code>Row</code>s.
     * (In other words, returns <code>true</code> if <code>next</code> would
     * return an <code>Row</code> rather than throwing an exception.)
     *
     * @return <code>true</code> if the iterator has more elements.
     * @see Iterator#hasNext()
     */
    public boolean hasNext() {
        return rows.hasNext();
    }

    /**
     * Returns the next <code>Row</code> in the iteration.
     *
     * @return the next <code>Row</code> in the iteration.
     * @throws NoSuchElementException if iteration has no more <code>Row</code>s.
     * @see Iterator#next()
     */
    public Object next() throws NoSuchElementException {
        return nextRow();
    }

    //---------------------< inner class RowImpl >------------------------------
    /**
     * Implements the {@link javax.jcr.query.Row} interface, which represents
     * a row in the query result.
     */
    class RowImpl implements Row {

        /**
         * The underlying <code>QueryResultRow</code>.
         */
        private final QueryResultRow row;

        /**
         * Cached value array for returned by {@link #getValues()}.
         */
        private Value[] values;

        /**
         * Map of select property names. Key: String, Value:
         * Integer, which refers to the array index in {@link #values}.
         */
        private Map<String, Integer> propertyMap;

        /**
         * Creates a new <code>RowImpl</code> instance based on a SPI result
         * row.
         *
         * @param row the underlying query result row
         */
        private RowImpl(QueryResultRow row) {
            this.row = row;
        }

        //------------------------------------------------------------< Row >---
        /**
         * Returns an array of all the values in the same order as the property
         * names (column names) returned by
         * {@link javax.jcr.query.QueryResult#getColumnNames()}.
         *
         * @return a <code>Value</code> array.
         * @throws RepositoryException if an error occurs while retrieving the
         * values from the <code>Node</code>.
         * @see Row#getValues()
         */
        public Value[] getValues() throws RepositoryException {
            if (values == null) {
                QValue[] qVals = row.getValues();
                Value[] tmp = new Value[qVals.length];
                for (int i = 0; i < qVals.length; i++) {
                    if (qVals[i] == null) {
                        tmp[i] = null;
                    } else {
                        tmp[i] = ValueFormat.getJCRValue(
                                qVals[i], resolver, vFactory);
                    }
                }
                values = tmp;
            }
            // return a copy of the array
            Value[] ret = new Value[values.length];
            System.arraycopy(values, 0, ret, 0, values.length);
            return ret;
        }

        /**
         * Returns the value of the indicated  property in this <code>Row</code>.
         * <p/>
         * If <code>propertyName</code> is not among the column names of the
         * query result table, an <code>ItemNotFoundException</code> is thrown.
         *
         * @return a <code>Value</code>
         * @throws ItemNotFoundException if <code>propertyName</code> is not
         *                               among the column names of the query result table.
         * @throws RepositoryException   if <code>propertyName</code> is not a
         *                               valid property name.
         * @see Row#getValue(String)
         */
        public Value getValue(String propertyName) throws ItemNotFoundException, RepositoryException {
            if (propertyMap == null) {
                // create the map first
                Map<String, Integer> tmp = new HashMap<String, Integer>();
                for (int i = 0; i < columnNames.length; i++) {
                    tmp.put(columnNames[i], i);
                }
                propertyMap = tmp;
            }
            try {
                Integer idx = propertyMap.get(propertyName);
                if (idx == null) {
                    throw new ItemNotFoundException(propertyName);
                }
                // make sure values are there
                if (values == null) {
                    getValues();
                }
                return values[idx];
            } catch (NameException e) {
                throw new RepositoryException(e.getMessage(), e);
            }
        }

        /**
         * @see Row#getNode()
         */
        public Node getNode() throws RepositoryException {
            return getNode(row.getNodeId(null));
        }

        /**
         * @see Row#getNode(String)
         */
        public Node getNode(String selectorName) throws RepositoryException {
            return getNode(row.getNodeId(resolver.getQName(selectorName)));
        }

        /**
         * @see Row#getPath()
         */
        public String getPath() throws RepositoryException {
            String path = null;
            Node n = getNode();
            if (n != null) {
                path = n.getPath();
            }
            return path;
        }

        /**
         * @see Row#getPath(String)
         */
        public String getPath(String selectorName) throws RepositoryException {
            String path = null;
            Node n = getNode(selectorName);
            if (n != null) {
                path = n.getPath();
            }
            return path;
        }

        /**
         * @see Row#getScore()
         */
        public double getScore() throws RepositoryException {
            return row.getScore(null);
        }

        /**
         * @see Row#getScore(String)
         */
        public double getScore(String selectorName) throws RepositoryException {
            return row.getScore(resolver.getQName(selectorName));
        }

        /**
         * Returns the node with the given <code>id</code> or <code>null</code>
         * if <code>id</code> is <code>null</code>.
         *
         * @param id a node id or <code>null</code>.
         * @return the node with the given id or <code>null</code>.
         * @throws RepositoryException if an error occurs while retrieving the
         *                             node.
         */
        private Node getNode(NodeId id) throws RepositoryException {
            Node node = null;
            if (id != null) {
                node = (Node) itemMgr.getItem(hmgr.getNodeEntry(id));
            }
            return node;
        }
    }
}
