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

import java.util.AbstractCollection;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;

import com.google.common.base.Function;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.Iterables;

import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.db.composites.CellName;
import org.apache.cassandra.db.composites.Composite;
import org.apache.cassandra.db.filter.ColumnSlice;
import org.apache.cassandra.utils.memory.AbstractAllocator;

/**
 * A ColumnFamily backed by an array.
 * This implementation is not synchronized and should only be used when
 * thread-safety is not required. This implementation makes sense when the
 * main operations performed are iterating over the cells and adding cells
 * (especially if insertion is in sorted order).
 */
public class ArrayBackedSortedColumns extends ColumnFamily
{
    private static final Cell[] EMPTY_ARRAY = new Cell[0];
    private static final int MINIMAL_CAPACITY = 10;

    private final boolean reversed;

    private DeletionInfo deletionInfo;
    private Cell[] cells;
    private int size;
    private int sortedSize;
    private volatile boolean isSorted;

    public static final ColumnFamily.Factory<ArrayBackedSortedColumns> factory = new Factory<ArrayBackedSortedColumns>()
    {
        public ArrayBackedSortedColumns create(CFMetaData metadata, boolean insertReversed, int initialCapacity)
        {
            return new ArrayBackedSortedColumns(metadata, insertReversed, initialCapacity == 0 ? EMPTY_ARRAY : new Cell[initialCapacity], 0, 0);
        }
    };

    private ArrayBackedSortedColumns(CFMetaData metadata, boolean reversed, Cell[] cells, int size, int sortedSize)
    {
        super(metadata);
        this.reversed = reversed;
        this.deletionInfo = DeletionInfo.live();
        this.cells = cells;
        this.size = size;
        this.sortedSize = sortedSize;
        this.isSorted = size == sortedSize;
    }

    private ArrayBackedSortedColumns(ArrayBackedSortedColumns original)
    {
        super(original.metadata);
        this.reversed = original.reversed;
        this.deletionInfo = DeletionInfo.live(); // this is INTENTIONALLY not set to original.deletionInfo.
        this.cells = Arrays.copyOf(original.cells, original.size);
        this.size = original.size;
        this.sortedSize = original.sortedSize;
        this.isSorted = original.isSorted;
    }

    public static ArrayBackedSortedColumns localCopy(ColumnFamily original, AbstractAllocator allocator)
    {
        ArrayBackedSortedColumns copy = new ArrayBackedSortedColumns(original.metadata, false, new Cell[original.getColumnCount()], 0, 0);
        for (Cell cell : original)
            copy.internalAdd(cell.localCopy(original.metadata, allocator));
        copy.sortedSize = copy.size; // internalAdd doesn't update sortedSize.
        copy.delete(original);
        return copy;
    }

    public ColumnFamily.Factory getFactory()
    {
        return factory;
    }

    public ColumnFamily cloneMe()
    {
        return new ArrayBackedSortedColumns(this);
    }

    public boolean isInsertReversed()
    {
        return reversed;
    }

    private Comparator<Composite> internalComparator()
    {
        return reversed ? getComparator().reverseComparator() : getComparator();
    }

    private void maybeSortCells()
    {
        if (!isSorted)
            sortCells();
    }

    /**
     * synchronized so that concurrent (read-only) accessors don't mess the internal state.
     */
    private synchronized void sortCells()
    {
        if (isSorted)
            return; // Just sorted by a previous call

        Comparator<Cell> comparator = reversed
                                    ? getComparator().columnReverseComparator()
                                    : getComparator().columnComparator();

        // Sort the unsorted segment - will still potentially contain duplicate (non-reconciled) cells
        Arrays.sort(cells, sortedSize, size, comparator);

        // Determine the merge start position for that segment
        int pos = binarySearch(0, sortedSize, cells[sortedSize].name(), internalComparator());
        if (pos < 0)
            pos = -pos - 1;

        // Copy [pos, lastSortedCellIndex] cells into a separate array
        Cell[] leftCopy = pos == sortedSize
                        ? EMPTY_ARRAY
                        : Arrays.copyOfRange(cells, pos, sortedSize);

        // Store the beginning (inclusive) and the end (exclusive) indexes of the right segment
        int rightStart = sortedSize;
        int rightEnd = size;

        // 'Trim' the sizes to what's left without the leftCopy
        size = sortedSize = pos;

        // Merge the cells from both segments. When adding from the left segment we can rely on it not having any
        // duplicate cells, and thus omit the comparison with the previously entered cell - we'll never need to reconcile.
        int l = 0, r = rightStart;
        while (l < leftCopy.length && r < rightEnd)
        {
            int cmp = comparator.compare(leftCopy[l], cells[r]);
            if (cmp < 0)
                append(leftCopy[l++]);
            else if (cmp == 0)
                append(leftCopy[l++].reconcile(cells[r++]));
            else
                appendOrReconcile(cells[r++]);
        }
        while (l < leftCopy.length)
            append(leftCopy[l++]);
        while (r < rightEnd)
            appendOrReconcile(cells[r++]);

        // Nullify the remainder of the array (in case we had duplicate cells that got reconciled)
        for (int i = size; i < rightEnd; i++)
            cells[i] = null;

        // Fully sorted at this point
        isSorted = true;
    }

    private void appendOrReconcile(Cell cell)
    {
        if (size > 0 && cells[size - 1].name().equals(cell.name()))
            reconcileWith(size - 1, cell);
        else
            append(cell);
    }

    private void append(Cell cell)
    {
        cells[size] = cell;
        size++;
        sortedSize++;
    }

    public Cell getColumn(CellName name)
    {
        maybeSortCells();
        int pos = binarySearch(name);
        return pos >= 0 ? cells[pos] : null;
    }

    /**
      * Adds a cell, assuming that:
      * - it's non-gc-able (if a tombstone) or not a tombstone
      * - it has a more recent timestamp than any partition/range tombstone shadowing it
      * - it sorts *strictly after* the current-last cell in the array.
      */
    public void maybeAppendColumn(Cell cell, DeletionInfo.InOrderTester tester, int gcBefore)
    {
        if (cell.getLocalDeletionTime() >= gcBefore && !tester.isDeleted(cell))
        {
            internalAdd(cell);
            sortedSize++;
        }
    }

    public void addColumn(Cell cell)
    {
        if (size == 0)
        {
            internalAdd(cell);
            sortedSize++;
            return;
        }

        if (!isSorted)
        {
            internalAdd(cell);
            return;
        }

        int c = internalComparator().compare(cells[size - 1].name(), cell.name());
        if (c < 0)
        {
            // Append to the end
            internalAdd(cell);
            sortedSize++;
        }
        else if (c == 0)
        {
            // Resolve against the last cell
            reconcileWith(size - 1, cell);
        }
        else
        {
            int pos = binarySearch(cell.name());
            if (pos >= 0) // Reconcile with an existing cell
            {
                reconcileWith(pos, cell);
            }
            else
            {
                internalAdd(cell); // Append to the end, making cells unsorted from now on
                isSorted = false;
            }
        }
    }

    public void addAll(ColumnFamily other)
    {
        delete(other.deletionInfo());

        if (!other.hasColumns())
            return;

        // In reality, with ABSC being the only remaining container (aside from ABTC), other will aways be ABSC.
        if (size == 0 && other instanceof ArrayBackedSortedColumns)
        {
            fastAddAll((ArrayBackedSortedColumns) other);
        }
        else
        {
            Iterator<Cell> iterator = reversed ? other.reverseIterator() : other.iterator();
            while (iterator.hasNext())
                addColumn(iterator.next());
        }
    }

    // Fast path, when this ABSC is empty.
    private void fastAddAll(ArrayBackedSortedColumns other)
    {
        if (other.isInsertReversed() == isInsertReversed())
        {
            cells = Arrays.copyOf(other.cells, other.cells.length);
            size = other.size;
            sortedSize = other.sortedSize;
            isSorted = other.isSorted;
        }
        else
        {
            if (cells.length < other.getColumnCount())
                cells = new Cell[Math.max(MINIMAL_CAPACITY, other.getColumnCount())];
            Iterator<Cell> iterator = reversed ? other.reverseIterator() : other.iterator();
            while (iterator.hasNext())
                cells[size++] = iterator.next();
            sortedSize = size;
            isSorted = true;
        }
    }

    /**
     * Add a cell to the array, 'resizing' it first if necessary (if it doesn't fit).
     */
    private void internalAdd(Cell cell)
    {
        if (cells.length == size)
            cells = Arrays.copyOf(cells, Math.max(MINIMAL_CAPACITY, size * 3 / 2 + 1));
        cells[size++] = cell;
    }

    /**
     * Remove the cell at a given index, shifting the rest of the array to the left if needed.
     * Please note that we mostly remove from the end, so the shifting should be rare.
     */
    private void internalRemove(int index)
    {
        int moving = size - index - 1;
        if (moving > 0)
            System.arraycopy(cells, index + 1, cells, index, moving);
        cells[--size] = null;
    }

    /**
     * Reconcile with a cell at position i.
     * Assume that i is a valid position.
     */
    private void reconcileWith(int i, Cell cell)
    {
        cells[i] = cell.reconcile(cells[i]);
    }

    private int binarySearch(CellName name)
    {
        return binarySearch(0, size, name, internalComparator());
    }

    /**
     * Simple binary search for a given cell name.
     * The return value has the exact same meaning that the one of Collections.binarySearch().
     * (We don't use Collections.binarySearch() directly because it would require us to create
     * a fake Cell (as well as an Cell comparator) to do the search, which is ugly.
     */
    private int binarySearch(int fromIndex, int toIndex, Composite name, Comparator<Composite> comparator)
    {
        int low = fromIndex;
        int mid = toIndex;
        int high = mid - 1;
        int result = -1;
        while (low <= high)
        {
            mid = (low + high) >> 1;
            if ((result = comparator.compare(name, cells[mid].name())) > 0)
                low = mid + 1;
            else if (result == 0)
                return mid;
            else
                high = mid - 1;
        }
        return -mid - (result < 0 ? 1 : 2);
    }

    public Collection<Cell> getSortedColumns()
    {
        return new CellCollection(reversed);
    }

    public Collection<Cell> getReverseSortedColumns()
    {
        return new CellCollection(!reversed);
    }

    public int getColumnCount()
    {
        maybeSortCells();
        return size;
    }

    public boolean hasColumns()
    {
        return size > 0;
    }

    public void clear()
    {
        setDeletionInfo(DeletionInfo.live());
        for (int i = 0; i < size; i++)
            cells[i] = null;
        size = sortedSize = 0;
        isSorted = true;
    }

    public DeletionInfo deletionInfo()
    {
        return deletionInfo;
    }

    public void delete(DeletionTime delTime)
    {
        deletionInfo.add(delTime);
    }

    public void delete(DeletionInfo newInfo)
    {
        deletionInfo.add(newInfo);
    }

    protected void delete(RangeTombstone tombstone)
    {
        deletionInfo.add(tombstone, getComparator());
    }

    public void setDeletionInfo(DeletionInfo newInfo)
    {
        deletionInfo = newInfo;
    }

    /**
     * Purges any tombstones with a local deletion time before gcBefore.
     * @param gcBefore a timestamp (in seconds) before which tombstones should be purged
     */
    public void purgeTombstones(int gcBefore)
    {
        deletionInfo.purge(gcBefore);
    }

    public Iterable<CellName> getColumnNames()
    {
        return Iterables.transform(new CellCollection(false), new Function<Cell, CellName>()
        {
            public CellName apply(Cell cell)
            {
                return cell.name();
            }
        });
    }

    public Iterator<Cell> iterator(ColumnSlice[] slices)
    {
        maybeSortCells();
        return slices.length == 1
             ? slice(slices[0], reversed, null)
             : new SlicesIterator(slices, reversed);
    }

    public Iterator<Cell> reverseIterator(ColumnSlice[] slices)
    {
        maybeSortCells();
        return slices.length == 1
             ? slice(slices[0], !reversed, null)
             : new SlicesIterator(slices, !reversed);
    }

    private class SlicesIterator extends AbstractIterator<Cell>
    {
        private final ColumnSlice[] slices;
        private final boolean invert;

        private int idx = 0;
        private int previousSliceEnd;
        private Iterator<Cell> currentSlice;

        public SlicesIterator(ColumnSlice[] slices, boolean invert)
        {
            this.slices = slices;
            this.invert = invert;
            previousSliceEnd = invert ? size : 0;
        }

        protected Cell computeNext()
        {
            if (currentSlice == null)
            {
                if (idx >= slices.length)
                    return endOfData();
                currentSlice = slice(slices[idx++], invert, this);
            }

            if (currentSlice.hasNext())
                return currentSlice.next();

            currentSlice = null;
            return computeNext();
        }
    }

    /**
     * @return a sub-range of our cells as an Iterator, between the provided composites (inclusive)
     *
     * @param slice  The slice with the inclusive start and finish bounds
     * @param invert If the sort order of our collection is opposite to the desired sort order of the result;
     *               this results in swapping the start/finish (since they are provided based on the desired
     *               sort order, not our sort order), to normalise to our sort order, and a backwards iterator is returned
     * @param iter   If this slice is part of a multi-slice, the iterator will be updated to ensure cells are visited only once
     */
    private Iterator<Cell> slice(ColumnSlice slice, boolean invert, SlicesIterator iter)
    {
        Composite start = invert ? slice.finish : slice.start;
        Composite finish = invert ? slice.start : slice.finish;

        int lowerBound = 0, upperBound = size;
        if (iter != null)
        {
            if (invert)
                upperBound = iter.previousSliceEnd;
            else
                lowerBound = iter.previousSliceEnd;
        }

        if (!start.isEmpty())
        {
            lowerBound = binarySearch(lowerBound, upperBound, start, internalComparator());
            if (lowerBound < 0)
                lowerBound = -lowerBound - 1;
        }

        if (!finish.isEmpty())
        {
            upperBound = binarySearch(lowerBound, upperBound, finish, internalComparator());
            upperBound = upperBound < 0
                       ? -upperBound - 1
                       : upperBound + 1; // upperBound is exclusive for the iterators
        }

        // If we're going backwards (wrt our sort order) we store the startIdx and use it as our upper bound next round
        if (iter != null)
            iter.previousSliceEnd = invert ? lowerBound : upperBound;

        return invert
             ? new BackwardsCellIterator(lowerBound, upperBound)
             : new ForwardsCellIterator(lowerBound, upperBound);
    }

    private final class BackwardsCellIterator implements Iterator<Cell>
    {
        private int idx, end;
        private boolean shouldCallNext = true;

        // lowerBound inclusive, upperBound exclusive
        private BackwardsCellIterator(int lowerBound, int upperBound)
        {
            idx = upperBound - 1;
            end = lowerBound - 1;
        }

        public boolean hasNext()
        {
            return idx > end;
        }

        public Cell next()
        {
            shouldCallNext = false;
            return cells[idx--];
        }

        public void remove()
        {
            if (shouldCallNext)
                throw new IllegalStateException();
            shouldCallNext = true;
            internalRemove(idx + 1);
            sortedSize--;
        }
    }

    private final class ForwardsCellIterator implements Iterator<Cell>
    {
        private int idx, end;
        private boolean shouldCallNext = true;

        // lowerBound inclusive, upperBound exclusive
        private ForwardsCellIterator(int lowerBound, int upperBound)
        {
            idx = lowerBound;
            end = upperBound;
        }

        public boolean hasNext()
        {
            return idx < end;
        }

        public Cell next()
        {
            shouldCallNext = false;
            return cells[idx++];
        }

        public void remove()
        {
            if (shouldCallNext)
                throw new IllegalStateException();
            shouldCallNext = true;
            internalRemove(--idx);
            sortedSize--;
            end--;
        }
    }

    private final class CellCollection extends AbstractCollection<Cell>
    {
        private final boolean invert;

        private CellCollection(boolean invert)
        {
            this.invert = invert;
        }

        public int size()
        {
            return getColumnCount();
        }

        public Iterator<Cell> iterator()
        {
            maybeSortCells();
            return invert
                 ? new BackwardsCellIterator(0, size)
                 : new ForwardsCellIterator(0, size);
        }
    }
}
