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

import java.io.DataInput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;

import com.google.common.collect.AbstractIterator;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.cql3.CQL3Row;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.filter.IDiskAtomFilter;
import org.apache.cassandra.db.filter.NamesQueryFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.ColumnToCollectionType;
import org.apache.cassandra.io.ISerializer;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.ByteBufferUtil;

public abstract class AbstractCellNameType extends AbstractCType implements CellNameType
{
    private final Comparator<Cell> columnComparator;
    private final Comparator<Cell> columnReverseComparator;
    private final Comparator<OnDiskAtom> onDiskAtomComparator;

    private final ISerializer<CellName> cellSerializer;
    private final ColumnSerializer columnSerializer;
    private final OnDiskAtom.Serializer onDiskAtomSerializer;
    private final IVersionedSerializer<NamesQueryFilter> namesQueryFilterSerializer;
    private final IVersionedSerializer<IDiskAtomFilter> diskAtomFilterSerializer;

    protected AbstractCellNameType(boolean isByteOrderComparable)
    {
        super(isByteOrderComparable);
        columnComparator = new Comparator<Cell>()
        {
            public int compare(Cell c1, Cell c2)
            {
                return AbstractCellNameType.this.compare(c1.name(), c2.name());
            }
        };
        columnReverseComparator = new Comparator<Cell>()
        {
            public int compare(Cell c1, Cell c2)
            {
                return AbstractCellNameType.this.compare(c2.name(), c1.name());
            }
        };
        onDiskAtomComparator = new Comparator<OnDiskAtom>()
        {
            public int compare(OnDiskAtom c1, OnDiskAtom c2)
            {
                int comp = AbstractCellNameType.this.compare(c1.name(), c2.name());
                if (comp != 0)
                    return comp;

                if (c1 instanceof RangeTombstone)
                {
                    if (c2 instanceof RangeTombstone)
                    {
                        RangeTombstone t1 = (RangeTombstone)c1;
                        RangeTombstone t2 = (RangeTombstone)c2;
                        int comp2 = AbstractCellNameType.this.compare(t1.max, t2.max);
                        return comp2 == 0 ? t1.data.compareTo(t2.data) : comp2;
                    }
                    else
                    {
                        return -1;
                    }
                }
                else
                {
                    return c2 instanceof RangeTombstone ? 1 : 0;
                }
            }
        };

        // A trivial wrapped over the composite serializer
        cellSerializer = new ISerializer<CellName>()
        {
            public void serialize(CellName c, DataOutputPlus out) throws IOException
            {
                serializer().serialize(c, out);
            }

            public CellName deserialize(DataInput in) throws IOException
            {
                Composite ct = serializer().deserialize(in);
                if (ct.isEmpty())
                    throw ColumnSerializer.CorruptColumnException.create(in, ByteBufferUtil.EMPTY_BYTE_BUFFER);

                assert ct instanceof CellName : ct;
                return (CellName)ct;
            }

            public long serializedSize(CellName c, TypeSizes type)
            {
                return serializer().serializedSize(c, type);
            }
        };
        columnSerializer = new ColumnSerializer(this);
        onDiskAtomSerializer = new OnDiskAtom.Serializer(this);
        namesQueryFilterSerializer = new NamesQueryFilter.Serializer(this);
        diskAtomFilterSerializer = new IDiskAtomFilter.Serializer(this);
    }

    public Comparator<Cell> columnComparator()
    {
        return columnComparator;
    }

    public Comparator<Cell> columnReverseComparator()
    {
        return columnReverseComparator;
    }

    public Comparator<OnDiskAtom> onDiskAtomComparator()
    {
        return onDiskAtomComparator;
    }

    public ISerializer<CellName> cellSerializer()
    {
        return cellSerializer;
    }

    public ColumnSerializer columnSerializer()
    {
        return columnSerializer;
    }

    public OnDiskAtom.Serializer onDiskAtomSerializer()
    {
        return onDiskAtomSerializer;
    }

    public IVersionedSerializer<NamesQueryFilter> namesQueryFilterSerializer()
    {
        return namesQueryFilterSerializer;
    }

    public IVersionedSerializer<IDiskAtomFilter> diskAtomFilterSerializer()
    {
        return diskAtomFilterSerializer;
    }

    public CellName cellFromByteBuffer(ByteBuffer bytes)
    {
        return (CellName)fromByteBuffer(bytes);
    }

    public CellName create(Composite prefix, ColumnDefinition column, ByteBuffer collectionElement)
    {
        throw new UnsupportedOperationException();
    }

    public CellName rowMarker(Composite prefix)
    {
        throw new UnsupportedOperationException();
    }

    public Composite staticPrefix()
    {
        throw new UnsupportedOperationException();
    }

    public boolean hasCollections()
    {
        return false;
    }

    public boolean supportCollections()
    {
        return false;
    }

    public ColumnToCollectionType collectionType()
    {
        throw new UnsupportedOperationException();
    }

    public CellNameType addOrUpdateCollection(ColumnIdentifier columnName, CollectionType newCollection)
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public Composite make(Object... components)
    {
        return components.length == size() ? makeCellName(components) : super.make(components);
    }

    public CellName makeCellName(Object... components)
    {
        ByteBuffer[] rawComponents = new ByteBuffer[components.length];
        for (int i = 0; i < components.length; i++)
        {
            Object c = components[i];
            if (c instanceof ByteBuffer)
            {
                rawComponents[i] = (ByteBuffer)c;
            }
            else
            {
                AbstractType<?> type = subtype(i);
                // If it's a collection type, we need to find the right collection and use the key comparator (since we're building a cell name)
                if (type instanceof ColumnToCollectionType)
                {
                    assert i > 0;
                    type = ((ColumnToCollectionType)type).defined.get(rawComponents[i-1]).nameComparator();
                }
                rawComponents[i] = ((AbstractType)type).decompose(c);
            }
        }
        return makeCellName(rawComponents);
    }

    protected abstract CellName makeCellName(ByteBuffer[] components);

    protected static CQL3Row.Builder makeDenseCQL3RowBuilder(final long now)
    {
        return new CQL3Row.Builder()
        {
            public CQL3Row.RowIterator group(Iterator<Cell> cells)
            {
                return new DenseRowIterator(cells, now);
            }
        };
    }

    private static class DenseRowIterator extends AbstractIterator<CQL3Row> implements CQL3Row.RowIterator
    {
        private final Iterator<Cell> cells;
        private final long now;

        public DenseRowIterator(Iterator<Cell> cells, long now)
        {
            this.cells = cells;
            this.now = now;
        }

        public CQL3Row getStaticRow()
        {
            // There can't be static columns in dense tables
            return null;
        }

        protected CQL3Row computeNext()
        {
            while (cells.hasNext())
            {
                final Cell cell = cells.next();
                if (!cell.isLive(now))
                    continue;

                return new CQL3Row()
                {
                    public ByteBuffer getClusteringColumn(int i)
                    {
                        return cell.name().get(i);
                    }

                    public Cell getColumn(ColumnIdentifier name)
                    {
                        return cell;
                    }

                    public List<Cell> getCollection(ColumnIdentifier name)
                    {
                        return null;
                    }
                };
            }
            return endOfData();
        }
    }

    protected static CQL3Row.Builder makeSparseCQL3RowBuilder(final CFMetaData cfMetaData, final CellNameType type, final long now)
    {
        return new CQL3Row.Builder()
        {
            public CQL3Row.RowIterator group(Iterator<Cell> cells)
            {
                return new SparseRowIterator(cfMetaData, type, cells, now);
            }
        };
    }

    private static class SparseRowIterator extends AbstractIterator<CQL3Row> implements CQL3Row.RowIterator
    {
        private final CFMetaData cfMetaData;
        private final CellNameType type;
        private final Iterator<Cell> cells;
        private final long now;
        private final CQL3Row staticRow;

        private Cell nextCell;
        private CellName previous;
        private CQL3RowOfSparse currentRow;

        public SparseRowIterator(CFMetaData cfMetaData, CellNameType type, Iterator<Cell> cells, long now)
        {
            this.cfMetaData = cfMetaData;
            this.type = type;
            this.cells = cells;
            this.now = now;
            this.staticRow = hasNextCell() && nextCell.name().isStatic()
                           ? computeNext()
                           : null;
        }

        public CQL3Row getStaticRow()
        {
            return staticRow;
        }

        private boolean hasNextCell()
        {
            if (nextCell != null)
                return true;

            while (cells.hasNext())
            {
                Cell cell = cells.next();
                if (!cell.isLive(now))
                    continue;

                nextCell = cell;
                return true;
            }
            return false;
        }

        protected CQL3Row computeNext()
        {
            while (hasNextCell())
            {
                CQL3Row toReturn = null;
                CellName current = nextCell.name();
                if (currentRow == null || !current.isSameCQL3RowAs(type, previous))
                {
                    toReturn = currentRow;
                    currentRow = new CQL3RowOfSparse(cfMetaData, current);
                }
                currentRow.add(nextCell);
                nextCell = null;
                previous = current;

                if (toReturn != null)
                    return toReturn;
            }
            if (currentRow != null)
            {
                CQL3Row toReturn = currentRow;
                currentRow = null;
                return toReturn;
            }
            return endOfData();
        }
    }

    private static class CQL3RowOfSparse implements CQL3Row
    {
        private final CFMetaData cfMetaData;
        private final CellName cell;
        private Map<ColumnIdentifier, Cell> columns;
        private Map<ColumnIdentifier, List<Cell>> collections;

        CQL3RowOfSparse(CFMetaData metadata, CellName cell)
        {
            this.cfMetaData = metadata;
            this.cell = cell;
        }

        public ByteBuffer getClusteringColumn(int i)
        {
            return cell.get(i);
        }

        void add(Cell cell)
        {
            CellName cellName = cell.name();
            ColumnIdentifier columnName =  cellName.cql3ColumnName(cfMetaData);
            if (cellName.isCollectionCell())
            {
                if (collections == null)
                    collections = new HashMap<>();

                List<Cell> values = collections.get(columnName);
                if (values == null)
                {
                    values = new ArrayList<Cell>();
                    collections.put(columnName, values);
                }
                values.add(cell);
            }
            else
            {
                if (columns == null)
                    columns = new HashMap<>();
                columns.put(columnName, cell);
            }
        }

        public Cell getColumn(ColumnIdentifier name)
        {
            return columns == null ? null : columns.get(name);
        }

        public List<Cell> getCollection(ColumnIdentifier name)
        {
            return collections == null ? null : collections.get(name);
        }
    }
}
