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

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.omid.HBaseShims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

@SuppressWarnings("all")
public final class CellUtils {

    private static final Logger LOG = LoggerFactory.getLogger(CellUtils.class);
    static final byte[] SHADOW_CELL_SUFFIX = "\u0080".getBytes(Charsets.UTF_8); // Non printable char (128 ASCII)
    //Prefix starts with 0 to apear before other cells in TransactionVisibilityFilter
    static final byte[] SHADOW_CELL_PREFIX = "\u0000\u0080".getBytes(Charsets.UTF_8);
    static byte[] DELETE_TOMBSTONE = HConstants.EMPTY_BYTE_ARRAY;
    static byte[] LEGACY_DELETE_TOMBSTONE = Bytes.toBytes("__OMID_TOMBSTONE__");
    public static final byte[] FAMILY_DELETE_QUALIFIER = HConstants.EMPTY_BYTE_ARRAY;
    public static final String TRANSACTION_ATTRIBUTE = "__OMID_TRANSACTION__";
    /**/
    public static final String CLIENT_GET_ATTRIBUTE = "__OMID_CLIENT_GET__";
    public static final String LL_ATTRIBUTE = "__OMID_LL__";

    /**
     * Utility interface to get rid of the dependency on HBase server package
     */
    interface CellGetter {
        Result get(Get get) throws IOException;
    }

    /**
     * Returns true if the particular cell passed exists in the datastore.
     * @param row row
     * @param family column family
     * @param qualifier columnn name
     * @param version version
     * @param cellGetter an instance of CellGetter
     * @return true if the cell specified exists. false otherwise
     * @throws IOException
     */
    public static boolean hasCell(byte[] row,
                                  byte[] family,
                                  byte[] qualifier,
                                  long version,
                                  CellGetter cellGetter)
            throws IOException {
        Get get = new Get(row);
        get.addColumn(family, qualifier);
        get.setTimeStamp(version);

        Result result = cellGetter.get(get);

        return result.containsColumn(family, qualifier);
    }

    /**
     * Returns true if the particular cell passed has a corresponding shadow cell in the datastore
     * @param row row
     * @param family column family
     * @param qualifier columnn name
     * @param version version
     * @param cellGetter an instance of CellGetter
     * @return true if it has a shadow cell. false otherwise.
     * @throws IOException
     */
    public static boolean hasShadowCell(byte[] row,
                                        byte[] family,
                                        byte[] qualifier,
                                        long version,
                                        CellGetter cellGetter) throws IOException {
        return hasCell(row, family, addShadowCellSuffixPrefix(qualifier),
                version, cellGetter);
    }

    /**
     * Builds a new qualifier composed of the HBase qualifier passed + the shadow cell suffix.
     * @param qualifierArray the qualifier to be suffixed
     * @param qualOffset the offset where the qualifier starts
     * @param qualLength the qualifier length
     * @return the suffixed qualifier
     */
    public static byte[] addShadowCellSuffixPrefix(byte[] qualifierArray, int qualOffset, int qualLength) {
        byte[] result = new byte[qualLength + SHADOW_CELL_SUFFIX.length + SHADOW_CELL_PREFIX.length];
        System.arraycopy(SHADOW_CELL_PREFIX, 0, result,0 , SHADOW_CELL_PREFIX.length);
        System.arraycopy(qualifierArray, qualOffset, result, SHADOW_CELL_PREFIX.length, qualLength);
        System.arraycopy(SHADOW_CELL_SUFFIX, 0, result, qualLength + SHADOW_CELL_PREFIX.length,
                SHADOW_CELL_SUFFIX.length);
        return result;
    }

    /**
     * Builds a new qualifier composed of the HBase qualifier passed + the shadow cell suffix.
     * Contains a reduced signature to avoid boilerplate code in client side.
     * @param qualifier
     *            the qualifier to be suffixed
     * @return the suffixed qualifier
     */
    public static byte[] addShadowCellSuffixPrefix(byte[] qualifier) {
        return addShadowCellSuffixPrefix(qualifier, 0, qualifier.length);
    }

    /**
     * Builds a new qualifier removing the shadow cell suffix from the
     * passed HBase qualifier.
     * @param qualifier the qualifier to remove the suffix from
     * @param qualOffset the offset where the qualifier starts
     * @param qualLength the qualifier length
     * @return the new qualifier without the suffix
     */
    public static byte[] removeShadowCellSuffixPrefix(byte[] qualifier, int qualOffset, int qualLength) {
        if (endsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_SUFFIX)) {
            if (startsWith(qualifier, qualOffset,qualLength, SHADOW_CELL_PREFIX)) {
                return Arrays.copyOfRange(qualifier,
                        qualOffset + SHADOW_CELL_PREFIX.length,
                        qualOffset + (qualLength - SHADOW_CELL_SUFFIX.length));
            } else {
                //support backward competatbiliy
                return Arrays.copyOfRange(qualifier,
                        qualOffset,qualOffset + (qualLength - SHADOW_CELL_SUFFIX.length));
            }

        }

        throw new IllegalArgumentException(
                "Can't find shadow cell suffix in qualifier "
                        + Bytes.toString(qualifier));
    }

    /**
     * Returns the qualifier length removing the shadow cell suffix and prefix. In case that que suffix is not found,
     * just returns the length of the qualifier passed.
     * @param qualifier the qualifier to remove the suffix from
     * @param qualOffset the offset where the qualifier starts
     * @param qualLength the qualifier length
     * @return the qualifier length without the suffix
     */
    public static int qualifierLengthFromShadowCellQualifier(byte[] qualifier, int qualOffset, int qualLength) {

        if (endsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_SUFFIX)) {
            if (startsWith(qualifier,qualOffset, qualLength, SHADOW_CELL_PREFIX)) {
                return qualLength - SHADOW_CELL_SUFFIX.length - SHADOW_CELL_PREFIX.length;
            } else {
                return qualLength - SHADOW_CELL_SUFFIX.length;
            }
        }
        return qualLength;
    }


    /**
     * Returns the qualifier length removing the shadow cell suffix and prefix. In case that que suffix is not found,
     * just returns the length of the qualifier passed.
     * @param qualifier the qualifier to remove the suffix from
     * @param qualOffset the offset where the qualifier starts
     * @param qualLength the qualifier length
     * @return the qualifier length without the suffix
     */
    public static int qualifierOffsetFromShadowCellQualifier(byte[] qualifier, int qualOffset, int qualLength) {

        if (startsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_PREFIX)) {
            return qualOffset + SHADOW_CELL_PREFIX.length;
        }
        return qualOffset;
    }


    /**
     * Complement to matchingQualifier() methods in HBase's CellUtil.class
     * @param left the cell to compare the qualifier
     * @param qualArray the explicit qualifier array passed
     * @param qualOffset the explicit qualifier offset passed
     * @param qualLen the explicit qualifier length passed
     * @return whether the qualifiers are equal or not
     */
    public static boolean matchingQualifier(final Cell left, final byte[] qualArray, int qualOffset, int qualLen) {
        return Bytes.equals(left.getQualifierArray(), left.getQualifierOffset(), left.getQualifierLength(),
                qualArray, qualOffset, qualLen);
    }

    /**
     * Check that the cell passed meets the requirements for a valid cell identifier with Omid. Basically, users can't:
     * 1) specify a timestamp
     * 2) use a particular suffix in the qualifier
     */
    public static void validateCell(Cell cell, long startTimestamp) {
        // Throw exception if timestamp is set by the user
        if (cell.getTimestamp() != HConstants.LATEST_TIMESTAMP
                && cell.getTimestamp() != startTimestamp) {
            throw new IllegalArgumentException(
                    "Timestamp not allowed in transactional user operations");
        }
        // Throw exception if using a non-allowed qualifier
        if (isShadowCell(cell)) {
            throw new IllegalArgumentException(
                    "Reserved string used in column qualifier");
        }
    }

    /**
     * Returns whether a cell contains a qualifier that is a delete cell
     * column qualifier or not.
     * @param cell the cell to check if contains the delete cell qualifier
     * @return whether the cell passed contains a delete cell qualifier or not
     */
    public static boolean isFamilyDeleteCell(Cell cell) {
        return CellUtil.matchingQualifier(cell, CellUtils.FAMILY_DELETE_QUALIFIER) &&
                CellUtil.matchingValue(cell, HConstants.EMPTY_BYTE_ARRAY);
    }

    /**
     * Returns whether a cell contains a qualifier that is a shadow cell
     * column qualifier or not.
     * @param cell the cell to check if contains the shadow cell qualifier
     * @return whether the cell passed contains a shadow cell qualifier or not
     */
    public static boolean isShadowCell(Cell cell) {
        byte[] qualifier = cell.getQualifierArray();
        int qualOffset = cell.getQualifierOffset();
        int qualLength = cell.getQualifierLength();

        return endsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_SUFFIX);
    }

    private static boolean endsWith(byte[] value, int offset, int length, byte[] suffix) {
        if (length <= suffix.length) {
            return false;
        }

        int suffixOffset = offset + length - suffix.length;
        int result = Bytes.compareTo(value, suffixOffset, suffix.length,
                suffix, 0, suffix.length);
        return result == 0;
    }

    private static boolean startsWith(byte[] value, int offset, int length, byte[] prefix) {
        if (length <= prefix.length) {
            return false;
        }

        int result = Bytes.compareTo(value, offset, prefix.length,
                prefix, 0, prefix.length);
        return result == 0;
    }

    /**
     * Returns if a cell is marked as a tombstone.
     * @param cell the cell to check
     * @return whether the cell is marked as a tombstone or not
     */
    public static boolean isTombstone(Cell cell) {
        return CellUtil.matchingValue(cell, DELETE_TOMBSTONE) ||
                CellUtil.matchingValue(cell, LEGACY_DELETE_TOMBSTONE);
    }


    /**
     * Returns a new shadow cell created from a particular cell.
     * @param cell
     *            the cell to reconstruct the shadow cell from.
     * @param shadowCellValue
     *            the value for the new shadow cell created
     * @return the brand-new shadow cell
     */
    public static Cell buildShadowCellFromCell(Cell cell, byte[] shadowCellValue) {
        byte[] shadowCellQualifier = addShadowCellSuffixPrefix(cell.getQualifierArray(),
                cell.getQualifierOffset(),
                cell.getQualifierLength());
        return new KeyValue(
                cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(),
                cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength(),
                shadowCellQualifier, 0, shadowCellQualifier.length,
                cell.getTimestamp(), KeyValue.Type.codeToType(cell.getTypeByte()),
                shadowCellValue, 0, shadowCellValue.length);
    }

    /**
     * Analyzes a list of cells, associating the corresponding shadow cell if present.
     *
     * @param cells the list of cells to classify
     * @return a sorted map associating each cell with its shadow cell
     */
    public static SortedMap<Cell, Optional<Cell>> mapCellsToShadowCells(List<Cell> cells) {

        // Move CellComparator to HBaseSims for 2.0 support
        // Need to access through CellComparatorImpl.COMPARATOR
        SortedMap<Cell, Optional<Cell>> cellToShadowCellMap
                = new TreeMap<Cell, Optional<Cell>>(HBaseShims.cellComparatorInstance());

        Map<CellId, Cell> cellIdToCellMap = new HashMap<CellId, Cell>();
        Map<CellId, Cell> cellIdToSCCellMap = new HashMap<CellId, Cell>();
        for (Cell cell : cells) {
            if (!isShadowCell(cell)) {
                CellId key = new CellId(cell, false);
                // Get the current cell and compare the values
                Cell storedCell = cellIdToCellMap.get(key);
                if (storedCell != null) {
                    if (CellUtil.matchingValue(cell, storedCell)) {
                        // TODO: Should we check also here the MVCC and swap if its greater???
                        // Values are the same, ignore
                    } else {
                        if (cell.getSequenceId() > storedCell.getSequenceId()) { // Swap values
                            Optional<Cell> previousValue = cellToShadowCellMap.remove(storedCell);
                            Preconditions.checkNotNull(previousValue, "Should contain an Optional<Cell> value");
                            cellIdToCellMap.put(key, cell);
                            cellToShadowCellMap.put(cell, previousValue);
                        } else {
                            LOG.warn("Cell {} with an earlier MVCC found. Ignoring...", cell);
                        }
                    }
                } else {
                    cellIdToCellMap.put(key, cell);
                    Cell sc = cellIdToSCCellMap.get(key);
                    if (sc != null) {
                        cellToShadowCellMap.put(cell, Optional.of(sc));
                    } else {
                        cellToShadowCellMap.put(cell, Optional.<Cell>absent());
                    }
                }
            } else {
                CellId key = new CellId(cell, true);
                Cell savedCell = cellIdToCellMap.get(key);
                if (savedCell != null) {
                    Cell originalCell = savedCell;
                    cellToShadowCellMap.put(originalCell, Optional.of(cell));
                } else {
                    cellIdToSCCellMap.put(key, cell);
                }
            }
        }

        return cellToShadowCellMap;
    }

    private static class CellId {

        private static final int MIN_BITS = 32;

        private final Cell cell;
        private final boolean isShadowCell;

        CellId(Cell cell, boolean isShadowCell) {

            this.cell = cell;
            this.isShadowCell = isShadowCell;

        }

        Cell getCell() {
            return cell;
        }

        boolean isShadowCell() {
            return isShadowCell;
        }

        @Override
        public boolean equals(Object o) {
            if (o == this)
                return true;
            if (!(o instanceof CellId))
                return false;
            CellId otherCellId = (CellId) o;
            Cell otherCell = otherCellId.getCell();

            // Row comparison
            if (!CellUtil.matchingRow(otherCell, cell)) {
                return false;
            }

            // Family comparison
            if (!CellUtil.matchingFamily(otherCell, cell)) {
                return false;
            }

            // Qualifier comparison
            int qualifierLength = cell.getQualifierLength();
            int qualifierOffset = cell.getQualifierOffset();
            int otherQualifierLength = otherCell.getQualifierLength();
            int otherQualifierOffset = otherCell.getQualifierOffset();

            if (isShadowCell()) {
                qualifierLength = qualifierLengthFromShadowCellQualifier(cell.getQualifierArray(),
                        cell.getQualifierOffset(),
                        cell.getQualifierLength());
                qualifierOffset = qualifierOffsetFromShadowCellQualifier(cell.getQualifierArray(), cell.getQualifierOffset(),
                        cell.getQualifierLength());
            }
            if (otherCellId.isShadowCell()) {
                otherQualifierLength = qualifierLengthFromShadowCellQualifier(otherCell.getQualifierArray(),
                        otherCell.getQualifierOffset(),
                        otherCell.getQualifierLength());
                otherQualifierOffset = qualifierOffsetFromShadowCellQualifier(otherCell.getQualifierArray(), otherCell.getQualifierOffset(),
                        otherCell.getQualifierLength());
            }

            if (!Bytes.equals(cell.getQualifierArray(), qualifierOffset, qualifierLength,
                    otherCell.getQualifierArray(), otherQualifierOffset, otherQualifierLength)) {
                return false;
            }

            // Timestamp comparison
            return otherCell.getTimestamp() == cell.getTimestamp();

        }

        @Override
        public int hashCode() {
            Hasher hasher = Hashing.goodFastHash(MIN_BITS).newHasher();
            hasher.putBytes(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
            hasher.putBytes(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength());
            int qualifierLength = cell.getQualifierLength();
            int qualifierOffset = cell.getQualifierOffset();
            if (isShadowCell()) {
                qualifierLength = qualifierLengthFromShadowCellQualifier(cell.getQualifierArray(),
                        cell.getQualifierOffset(),
                        cell.getQualifierLength());
                if (startsWith(cell.getQualifierArray(), cell.getQualifierOffset(),
                        cell.getQualifierLength(), SHADOW_CELL_PREFIX)) {
                    qualifierOffset = qualifierOffset + SHADOW_CELL_PREFIX.length;
                }
            }

            hasher.putBytes(cell.getQualifierArray(),qualifierOffset , qualifierLength);
            hasher.putLong(cell.getTimestamp());
            return hasher.hash().asInt();
        }

        @Override
        public String toString() {
            ToStringHelper helper = Objects.toStringHelper(this);
            helper.add("row", Bytes.toStringBinary(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength()));
            helper.add("family", Bytes.toString(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength()));
            helper.add("is shadow cell?", isShadowCell);
            helper.add("qualifier",
                    Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()));
            if (isShadowCell()) {
                int qualifierLength = qualifierLengthFromShadowCellQualifier(cell.getQualifierArray(),
                        cell.getQualifierOffset(),
                        cell.getQualifierLength());
                byte[] cellWithoutSc = removeShadowCellSuffixPrefix(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength());
                helper.add("qualifier whithout shadow cell suffix", Bytes.toString(cellWithoutSc));
            }
            helper.add("ts", cell.getTimestamp());
            return helper.toString();
        }
    }

}