/**
 * Copyright 2009 The Apache Software Foundation 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.hadoop.hbase.regionserver.tableindexed;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HRegionInfo;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTableInterface;
import org.apache.hadoop.hbase.client.HTablePool;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.tableindexed.IndexSpecification;
import org.apache.hadoop.hbase.client.tableindexed.IndexedTableDescriptor;
import org.apache.hadoop.hbase.regionserver.FlushRequester;
import org.apache.hadoop.hbase.regionserver.OperationStatus;
import org.apache.hadoop.hbase.regionserver.transactional.TransactionalRegion;
import org.apache.hadoop.hbase.regionserver.wal.HLog;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.GetUtil;
import org.apache.hadoop.hbase.util.Pair;

public class IndexedRegion extends TransactionalRegion {

  private static final Log LOG = LogFactory.getLog(IndexedRegion.class);

  @SuppressWarnings("unused")
private final Configuration conf;
  private final IndexedTableDescriptor indexTableDescriptor;
  private final HTablePool tablePool;

  @SuppressWarnings("deprecation")
public IndexedRegion(final Path basedir, final HLog log, final FileSystem fs,
      final Configuration conf, final HRegionInfo regionInfo,
      final FlushRequester flushListener) throws IOException {
    super(basedir, log, fs, conf, regionInfo, flushListener);
    this.indexTableDescriptor = new IndexedTableDescriptor(
        regionInfo.getTableDesc());
    this.conf = conf;
    this.tablePool = new HTablePool();
  }

  @SuppressWarnings("deprecation")
private HTableInterface getIndexTable(final IndexSpecification index)
      throws IOException {
    return tablePool.getTable(index.getIndexedTableName(super.getRegionInfo()
        .getTableDesc().getName()));
  }

  @SuppressWarnings("deprecation")
private void putTable(final HTableInterface t) {
    if (t == null) {
      return;
    }
    try {
		tablePool.putTable(t);
	} catch (IOException e) {
		e.printStackTrace();
	}
  }

  private Collection<IndexSpecification> getIndexes() {
    return indexTableDescriptor.getIndexes();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void put(final Put put, final Integer lockId, final boolean writeToWAL)
      throws IOException {
    updateIndexes(put, lockId); // Do this first because will want to see the
                                // old row
    super.put(put, lockId, writeToWAL);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public OperationStatus[] put(Pair<Put, Integer>[] putsAndLocks)
      throws IOException {
    for (Pair<Put, Integer> pair : putsAndLocks) {
      updateIndexes(pair.getFirst(), pair.getSecond());
    }
    return super.put(putsAndLocks);
  }

  private void updateIndexes(final Put put, final Integer lockId)
      throws IOException {
    List<IndexSpecification> indexesToUpdate = new LinkedList<IndexSpecification>();

    // Find the indexes we need to update
    for (IndexSpecification index : getIndexes()) {
      if (possiblyAppliesToIndex(index, put)) {
        indexesToUpdate.add(index);
      }
    }

    if (indexesToUpdate.size() == 0) {
      return;
    }

    NavigableSet<byte[]> neededColumns = getColumnsForIndexes(indexesToUpdate);
    NavigableMap<byte[], byte[]> newColumnValues = getColumnsFromPut(put);

    Get oldGet = new Get(put.getRow());
    for (byte[] neededCol : neededColumns) {
//      oldGet.addColumn(neededCol);
      GetUtil.addColumn(oldGet, neededCol);
    }

    Result oldResult = super.get(oldGet, lockId);

    // Add the old values to the new if they are not there
    if (oldResult != null && oldResult.raw() != null) {
      for (KeyValue oldKV : oldResult.raw()) {
        byte[] column = KeyValue.makeColumn(oldKV.getFamily(),
            oldKV.getQualifier());
        if (!newColumnValues.containsKey(column)) {
          newColumnValues.put(column, oldKV.getValue());
        }
      }
    }

    Iterator<IndexSpecification> indexIterator = indexesToUpdate.iterator();
    while (indexIterator.hasNext()) {
      IndexSpecification indexSpec = indexIterator.next();
      if (!IndexMaintenanceUtils.doesApplyToIndex(indexSpec, newColumnValues)) {
        indexIterator.remove();
      }
    }

    SortedMap<byte[], byte[]> oldColumnValues = convertToValueMap(oldResult);

    for (IndexSpecification indexSpec : indexesToUpdate) {
      updateIndex(indexSpec, put, newColumnValues, oldColumnValues);
    }
  }

  // FIXME: This call takes place in an RPC, and requires an RPC. This makes for
  // a likely deadlock if the number of RPCs we are trying to serve is >= the
  // number of handler threads.
  private void updateIndex(final IndexSpecification indexSpec, final Put put,
      final NavigableMap<byte[], byte[]> newColumnValues,
      final SortedMap<byte[], byte[]> oldColumnValues) throws IOException {
    Delete indexDelete = makeDeleteToRemoveOldIndexEntry(indexSpec,
        put.getRow(), oldColumnValues);
    Put indexPut = makeIndexUpdate(indexSpec, put.getRow(), newColumnValues);

    HTableInterface indexTable = getIndexTable(indexSpec);
    try {
      if (indexDelete != null
          && !Bytes.equals(indexDelete.getRow(), indexPut.getRow())) {
        // Only do the delete if the row changed. This way we save the put after
        // delete issues in HBASE-2256
        LOG.debug("Deleting old index row ["
            + Bytes.toString(indexDelete.getRow()) + "]. New row is ["
            + Bytes.toString(indexPut.getRow()) + "].");
        indexTable.delete(indexDelete);
      } else if (indexDelete != null) {
        LOG.debug("Skipping deleting index row ["
            + Bytes.toString(indexDelete.getRow())
            + "] because it has not changed.");
      }
      indexTable.put(indexPut);
    } finally {
      putTable(indexTable);
    }
  }

  /** Return the columns needed for the update. */
  private NavigableSet<byte[]> getColumnsForIndexes(
      final Collection<IndexSpecification> indexes) {
    NavigableSet<byte[]> neededColumns = new TreeSet<byte[]>(
        Bytes.BYTES_COMPARATOR);
    for (IndexSpecification indexSpec : indexes) {
      for (byte[] col : indexSpec.getAllColumns()) {
        neededColumns.add(col);
      }
    }
    return neededColumns;
  }

  private Delete makeDeleteToRemoveOldIndexEntry(
      final IndexSpecification indexSpec, final byte[] row,
      final SortedMap<byte[], byte[]> oldColumnValues) throws IOException {
    for (byte[] indexedCol : indexSpec.getIndexedColumns()) {
      if (!oldColumnValues.containsKey(indexedCol)) {
        LOG.debug("Index [" + indexSpec.getIndexId()
            + "] not trying to remove old entry for row ["
            + Bytes.toString(row) + "] because col ["
            + Bytes.toString(indexedCol) + "] is missing");
        return null;
      }
    }

    byte[] oldIndexRow = indexSpec.getKeyGenerator().createIndexKey(row,
        oldColumnValues);
    LOG.debug("Index [" + indexSpec.getIndexId() + "] removing old entry ["
        + Bytes.toString(oldIndexRow) + "]");
    return new Delete(oldIndexRow);
  }

  private NavigableMap<byte[], byte[]> getColumnsFromPut(final Put put) {
    NavigableMap<byte[], byte[]> columnValues = new TreeMap<byte[], byte[]>(
        Bytes.BYTES_COMPARATOR);
    for (List<KeyValue> familyPuts : put.getFamilyMap().values()) {
      for (KeyValue kv : familyPuts) {
        columnValues.put(
            KeyValue.makeColumn(kv.getFamily(), kv.getQualifier()),
            kv.getValue());
      }
    }
    return columnValues;
  }

  /**
   * Ask if this put *could* apply to the index. It may actually apply if some
   * of the columns needed are missing.
   * 
   * @param indexSpec
   * @param put
   * @return true if possibly apply.
   */
  private boolean possiblyAppliesToIndex(final IndexSpecification indexSpec,
      final Put put) {
    for (List<KeyValue> familyPuts : put.getFamilyMap().values()) {
      for (KeyValue kv : familyPuts) {
        if (indexSpec.containsColumn(KeyValue.makeColumn(kv.getFamily(),
            kv.getQualifier()))) {
          return true;
        }
      }
    }
    return false;
  }

  private Put makeIndexUpdate(final IndexSpecification indexSpec,
      final byte[] row, final SortedMap<byte[], byte[]> columnValues)
      throws IOException {
    Put indexUpdate = IndexMaintenanceUtils.createIndexUpdate(indexSpec, row,
        columnValues);
    LOG.debug("Index [" + indexSpec.getIndexId() + "] adding new entry ["
        + Bytes.toString(indexUpdate.getRow()) + "] for row ["
        + Bytes.toString(row) + "]");

    return indexUpdate;

  }

  // FIXME we can be smarter about this and avoid the base gets and index
  // maintenance in many cases.
  @Override
  public void delete(final Delete delete, final Integer lockid,
      final boolean writeToWAL) throws IOException {
    // First look at the current (to be the old) state.
    SortedMap<byte[], byte[]> oldColumnValues = null;
    if (!getIndexes().isEmpty()) {
      // Need all columns
      NavigableSet<byte[]> neededColumns = getColumnsForIndexes(getIndexes());

      Get get = new Get(delete.getRow());
      for (byte[] col : neededColumns) {
//        get.addColumn(col);
        GetUtil.addColumn(get, col);
      }

      Result oldRow = super.get(get, lockid);
      oldColumnValues = convertToValueMap(oldRow);
    }

    super.delete(delete, lockid, writeToWAL);

    if (!getIndexes().isEmpty()) {
      Get get = new Get(delete.getRow());

      // Rebuild index if there is still a version visible.
      Result currentRow = super.get(get, lockid);
      SortedMap<byte[], byte[]> currentColumnValues = convertToValueMap(currentRow);

      for (IndexSpecification indexSpec : getIndexes()) {
        Delete indexDelete = null;
        if (IndexMaintenanceUtils.doesApplyToIndex(indexSpec, oldColumnValues)) {
          indexDelete = makeDeleteToRemoveOldIndexEntry(indexSpec,
              delete.getRow(), oldColumnValues);
        }
        Put indexPut = null;
        if (IndexMaintenanceUtils.doesApplyToIndex(indexSpec,
            currentColumnValues)) {
          indexPut = makeIndexUpdate(indexSpec, delete.getRow(),
              currentColumnValues);
        }
        if (indexPut == null && indexDelete == null) {
          continue;
        }

        HTableInterface indexTable = getIndexTable(indexSpec);
        try {
          if (indexDelete != null
              && (indexPut == null || !Bytes.equals(indexDelete.getRow(),
                  indexPut.getRow()))) {
            // Only do the delete if the row changed. This way we save the put
            // after delete issues in HBASE-2256
            LOG.debug("Deleting old index row ["
                + Bytes.toString(indexDelete.getRow()) + "].");
            indexTable.delete(indexDelete);
          } else if (indexDelete != null) {
            LOG.debug("Skipping deleting index row ["
                + Bytes.toString(indexDelete.getRow())
                + "] because it has not changed.");

            for (byte[] indexCol : indexSpec.getAdditionalColumns()) {
              byte[][] parsed = KeyValue.parseColumn(indexCol);
              List<KeyValue> famDeletes = delete.getFamilyMap().get(parsed[0]);
              if (famDeletes != null) {
                for (KeyValue kv : famDeletes) {
                  if (Bytes.equals(indexCol,
                      KeyValue.makeColumn(kv.getFamily(), kv.getQualifier()))) {
                    LOG.debug("Need to delete this specific column: "
                        + Bytes.toString(KeyValue.makeColumn(kv.getFamily(),
                            kv.getQualifier())));
                    Delete columnDelete = new Delete(indexDelete.getRow());
                    columnDelete.deleteColumns(kv.getFamily(),
                        kv.getQualifier());
                    indexTable.delete(columnDelete);
                  }
                }

              }
            }
          }

          if (indexPut != null) {
            indexTable.put(indexPut);
          }
        } finally {
          putTable(indexTable);
        }
      }
    }

  }

  private SortedMap<byte[], byte[]> convertToValueMap(final Result result) {
    SortedMap<byte[], byte[]> currentColumnValues = new TreeMap<byte[], byte[]>(
        Bytes.BYTES_COMPARATOR);

    if (result == null || result.raw() == null) {
      return currentColumnValues;
    }
    List<KeyValue> list = result.list();
    if (list != null) {
      for (KeyValue kv : result.list()) {
        currentColumnValues.put(
            KeyValue.makeColumn(kv.getFamily(), kv.getQualifier()),
            kv.getValue());
      }
    }
    return currentColumnValues;
  }
}