/*
 * 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.cassandra.db.filter;

import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.cassandra.db.marshal.CollectionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.composites.CellName;
import org.apache.cassandra.db.composites.Composite;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.CompositeType;

/**
 * Extends a column filter (IFilter) to include a number of IndexExpression.
 */
public abstract class ExtendedFilter
{
    private static final Logger logger = LoggerFactory.getLogger(ExtendedFilter.class);

    public final ColumnFamilyStore cfs;
    public final long timestamp;
    public final DataRange dataRange;
    private final int maxResults;
    private final boolean countCQL3Rows;
    private volatile int currentLimit;

    public static ExtendedFilter create(ColumnFamilyStore cfs,
                                        DataRange dataRange,
                                        List<IndexExpression> clause,
                                        int maxResults,
                                        boolean countCQL3Rows,
                                        long timestamp)
    {
        if (clause == null || clause.isEmpty())
            return new EmptyClauseFilter(cfs, dataRange, maxResults, countCQL3Rows, timestamp);

        return new WithClauses(cfs, dataRange, clause, maxResults, countCQL3Rows, timestamp);
    }

    protected ExtendedFilter(ColumnFamilyStore cfs, DataRange dataRange, int maxResults, boolean countCQL3Rows, long timestamp)
    {
        assert cfs != null;
        assert dataRange != null;
        this.cfs = cfs;
        this.dataRange = dataRange;
        this.maxResults = maxResults;
        this.timestamp = timestamp;
        this.countCQL3Rows = countCQL3Rows;
        this.currentLimit = maxResults;
        if (countCQL3Rows)
            dataRange.updateColumnsLimit(maxResults);
    }

    public int maxRows()
    {
        return countCQL3Rows ? Integer.MAX_VALUE : maxResults;
    }

    public int maxColumns()
    {
        return countCQL3Rows ? maxResults : Integer.MAX_VALUE;
    }

    public int currentLimit()
    {
        return currentLimit;
    }

    public IDiskAtomFilter columnFilter(ByteBuffer key)
    {
        return dataRange.columnFilter(key);
    }

    public int lastCounted(ColumnFamily data)
    {
        return dataRange.getLiveCount(data, timestamp);
    }

    public void updateFilter(int currentColumnsCount)
    {
        if (!countCQL3Rows)
            return;

        currentLimit = maxResults - currentColumnsCount;
        // We propagate that limit to the underlying filter so each internal query don't
        // fetch more than we needs it to.
        dataRange.updateColumnsLimit(currentLimit);
    }

    public abstract List<IndexExpression> getClause();

    /**
     * Returns a filter to query the columns from the clause that the initial slice filter may not have caught.
     * @param data the data retrieve by the initial filter
     * @return a filter or null if there can't be any columns we missed with our initial filter (typically if it was a names query, or a slice of the entire row)
     */
    public abstract IDiskAtomFilter getExtraFilter(DecoratedKey key, ColumnFamily data);

    /**
     * @return data pruned down to the columns originally asked for
     */
    public abstract ColumnFamily prune(DecoratedKey key, ColumnFamily data);

    /**
     * @return true if the provided data satisfies all the expressions from
     * the clause of this filter.
     */
    public abstract boolean isSatisfiedBy(DecoratedKey rowKey, ColumnFamily data, Composite prefix, ByteBuffer collectionElement);

    public static boolean satisfies(int comparison, IndexExpression.Operator op)
    {
        switch (op)
        {
            case EQ:
                return comparison == 0;
            case GTE:
                return comparison >= 0;
            case GT:
                return comparison > 0;
            case LTE:
                return comparison <= 0;
            case LT:
                return comparison < 0;
            default:
                throw new IllegalStateException();
        }
    }

    public static class WithClauses extends ExtendedFilter
    {
        private final List<IndexExpression> clause;
        private final IDiskAtomFilter optimizedFilter;

        public WithClauses(ColumnFamilyStore cfs,
                           DataRange range,
                           List<IndexExpression> clause,
                           int maxResults,
                           boolean countCQL3Rows,
                           long timestamp)
        {
            super(cfs, range, maxResults, countCQL3Rows, timestamp);
            assert clause != null;
            this.clause = clause;
            this.optimizedFilter = computeOptimizedFilter();
        }

        /*
         * Potentially optimize the column filter if we have a change to make it catch all clauses
         * right away.
         */
        private IDiskAtomFilter computeOptimizedFilter()
        {
            /*
             * We shouldn't do the "optimization" for composites as the index names are not valid column names 
             * (which the rest of the method assumes). Said optimization is not useful for composites anyway.
             * We also don't want to do for paging ranges as the actual filter depends on the row key (it would
             * probably be possible to make it work but we won't really use it so we don't bother).
             */
            if (cfs.getComparator().isCompound() || dataRange instanceof DataRange.Paging)
                return null;

            IDiskAtomFilter filter = dataRange.columnFilter(null); // ok since not a paging range
            if (filter instanceof SliceQueryFilter)
            {
                // if we have a high chance of getting all the columns in a single index slice (and it's not too costly), do that.
                // otherwise, the extraFilter (lazily created) will fetch by name the columns referenced by the additional expressions.
                if (cfs.getMaxRowSize() < DatabaseDescriptor.getColumnIndexSize())
                {
                    logger.trace("Expanding slice filter to entire row to cover additional expressions");
                    return new SliceQueryFilter(ColumnSlice.ALL_COLUMNS_ARRAY, ((SliceQueryFilter)filter).reversed, Integer.MAX_VALUE);
                }
            }
            else
            {
                logger.trace("adding columns to original Filter to cover additional expressions");
                assert filter instanceof NamesQueryFilter;
                if (!clause.isEmpty())
                {
                    SortedSet<CellName> columns = new TreeSet<CellName>(cfs.getComparator());
                    for (IndexExpression expr : clause)
                        columns.add(cfs.getComparator().cellFromByteBuffer(expr.column));
                    columns.addAll(((NamesQueryFilter) filter).columns);
                    return ((NamesQueryFilter) filter).withUpdatedColumns(columns);
                }
            }
            return null;
        }

        @Override
        public IDiskAtomFilter columnFilter(ByteBuffer key)
        {
            return optimizedFilter == null ? dataRange.columnFilter(key) : optimizedFilter;
        }

        public List<IndexExpression> getClause()
        {
            return clause;
        }

        /*
         * We may need an extra query only if the original query wasn't selecting the row entirely.
         * Furthermore, we only need the extra query if we haven't yet got all the expressions from the clause.
         */
        private boolean needsExtraQuery(ByteBuffer rowKey, ColumnFamily data)
        {
            IDiskAtomFilter filter = columnFilter(rowKey);
            if (filter instanceof SliceQueryFilter && DataRange.isFullRowSlice((SliceQueryFilter)filter))
                return false;

            for (IndexExpression expr : clause)
            {
                if (data.getColumn(data.getComparator().cellFromByteBuffer(expr.column)) == null)
                {
                    logger.debug("adding extraFilter to cover additional expressions");
                    return true;
                }
            }
            return false;
        }

        public IDiskAtomFilter getExtraFilter(DecoratedKey rowKey, ColumnFamily data)
        {
            /*
             * This method assumes the IndexExpression names are valid column names, which is not the
             * case with composites. This is ok for now however since:
             * 1) CompositeSearcher doesn't use it.
             * 2) We don't yet allow non-indexed range slice with filters in CQL3 (i.e. this will never be
             * called by CFS.filter() for composites).
             */
            assert !(cfs.getComparator().isCompound()) : "Sequential scan with filters is not supported (if you just created an index, you "
                                                         + "need to wait for the creation to be propagated to all nodes before querying it)";

            if (!needsExtraQuery(rowKey.getKey(), data))
                return null;

            // Note: for counters we must be careful to not add a column that was already there (to avoid overcount). That is
            // why we do the dance of avoiding to query any column we already have (it's also more efficient anyway)
            SortedSet<CellName> columns = new TreeSet<CellName>(cfs.getComparator());
            for (IndexExpression expr : clause)
            {
                CellName name = data.getComparator().cellFromByteBuffer(expr.column);
                if (data.getColumn(name) == null)
                    columns.add(name);
            }
            assert !columns.isEmpty();
            return new NamesQueryFilter(columns);
        }

        public ColumnFamily prune(DecoratedKey rowKey, ColumnFamily data)
        {
            if (optimizedFilter == null)
                return data;

            ColumnFamily pruned = data.cloneMeShallow();
            IDiskAtomFilter filter = dataRange.columnFilter(rowKey.getKey());
            Iterator<Cell> iter = filter.getColumnIterator(data);
            filter.collectReducedColumns(pruned, QueryFilter.gatherTombstones(pruned, iter), cfs.gcBefore(timestamp), timestamp);
            return pruned;
        }

        public boolean isSatisfiedBy(DecoratedKey rowKey, ColumnFamily data, Composite prefix, ByteBuffer collectionElement)
        {
            for (IndexExpression expression : clause)
            {
                ColumnDefinition def = data.metadata().getColumnDefinition(expression.column);
                ByteBuffer dataValue = null;
                AbstractType<?> validator = null;
                if (def == null)
                {
                    // This can't happen with CQL3 as this should be rejected upfront. For thrift however,
                    // cell name are not predefined. But that means the cell name correspond to an internal one.
                    Cell cell = data.getColumn(data.getComparator().cellFromByteBuffer(expression.column));
                    if (cell != null)
                    {
                        dataValue = cell.value();
                        validator = data.metadata().getDefaultValidator();
                    }
                }
                else
                {
                    if (def.type.isCollection())
                    {
                        if (!collectionSatisfies(def, data, prefix, expression, collectionElement))
                            return false;
                        continue;
                    }

                    dataValue = extractDataValue(def, rowKey.getKey(), data, prefix);
                    validator = def.type;
                }

                if (dataValue == null)
                    return false;

                int v = validator.compare(dataValue, expression.value);
                if (!satisfies(v, expression.operator))
                    return false;
            }
            return true;
        }

        private static boolean collectionSatisfies(ColumnDefinition def, ColumnFamily data, Composite prefix, IndexExpression expr, ByteBuffer collectionElement)
        {
            assert def.type.isCollection();

            CollectionType type = (CollectionType)def.type;
            switch (type.kind)
            {
                case LIST:
                    assert collectionElement != null;
                    return type.valueComparator().compare(data.getColumn(data.getComparator().create(prefix, def, collectionElement)).value(), expr.value) == 0;
                case SET:
                    return data.getColumn(data.getComparator().create(prefix, def, expr.value)) != null;
                case MAP:
                    if (expr.operator == IndexExpression.Operator.CONTAINS_KEY)
                    {
                        return data.getColumn(data.getComparator().create(prefix, def, expr.value)) != null;
                    }
                    else
                    {
                        assert collectionElement != null;
                        return type.valueComparator().compare(data.getColumn(data.getComparator().create(prefix, def, collectionElement)).value(), expr.value) == 0;
                    }
            }
            throw new AssertionError();
        }

        private ByteBuffer extractDataValue(ColumnDefinition def, ByteBuffer rowKey, ColumnFamily data, Composite prefix)
        {
            switch (def.kind)
            {
                case PARTITION_KEY:
                    return def.isOnAllComponents()
                         ? rowKey
                         : ((CompositeType)data.metadata().getKeyValidator()).split(rowKey)[def.position()];
                case CLUSTERING_COLUMN:
                    return prefix.get(def.position());
                case REGULAR:
                    CellName cname = prefix == null
                                   ? data.getComparator().cellFromByteBuffer(def.name.bytes)
                                   : data.getComparator().create(prefix, def);

                    Cell cell = data.getColumn(cname);
                    return cell == null ? null : cell.value();
                case COMPACT_VALUE:
                    assert data.getColumnCount() == 1;
                    return data.getSortedColumns().iterator().next().value();
            }
            throw new AssertionError();
        }
    }

    private static class EmptyClauseFilter extends ExtendedFilter
    {
        public EmptyClauseFilter(ColumnFamilyStore cfs, DataRange range, int maxResults, boolean countCQL3Rows, long timestamp)
        {
            super(cfs, range, maxResults, countCQL3Rows, timestamp);
        }

        public List<IndexExpression> getClause()
        {
            return Collections.<IndexExpression>emptyList();
        }

        public IDiskAtomFilter getExtraFilter(DecoratedKey key, ColumnFamily data)
        {
            return null;
        }

        public ColumnFamily prune(DecoratedKey rowKey, ColumnFamily data)
        {
            return data;
        }

        public boolean isSatisfiedBy(DecoratedKey rowKey, ColumnFamily data, Composite prefix, ByteBuffer collectionElement)
        {
            return true;
        }
    }
}
