/*
 * 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.phoenix.schema.stats;

import static org.apache.phoenix.query.QueryServices.COMMIT_STATS_ASYNC;
import static org.apache.phoenix.query.QueryServicesOptions.DEFAULT_COMMIT_STATS_ASYNC;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.Mutation;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
import org.apache.hadoop.hbase.regionserver.InternalScanner;
import org.apache.hadoop.hbase.regionserver.Region;
import org.apache.hadoop.hbase.regionserver.ScannerContext;
import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The scanner that does the scanning to collect the stats during major
 * compaction.{@link DefaultStatisticsCollector}
 */
public class StatisticsScanner implements InternalScanner {
  private static final Logger LOGGER = LoggerFactory.getLogger(StatisticsScanner.class);
  private InternalScanner delegate;
  private StatisticsWriter statsWriter;
  private Region region;
  private StatisticsCollector tracker;
  private ImmutableBytesPtr family;
  private final Configuration config;
  private final RegionCoprocessorEnvironment env;

  public StatisticsScanner(StatisticsCollector tracker, StatisticsWriter stats,
    RegionCoprocessorEnvironment env, InternalScanner delegate, ImmutableBytesPtr family) {
    this.tracker = tracker;
    this.statsWriter = stats;
    this.delegate = delegate;
    this.region = env.getRegion();
    this.env = env;
    this.family = family;
    this.config = env.getConfiguration();
    StatisticsCollectionRunTracker.getInstance(config).addCompactingRegion(region.getRegionInfo());
  }

  @Override
  public boolean next(List<Cell> result) throws IOException {
    boolean ret = delegate.next(result);
    updateStats(result);
    return ret;
  }

  @Override
  public boolean next(List<Cell> result, ScannerContext scannerContext) throws IOException {
    return next(result);
  }

  /**
   * Update the current statistics based on the lastest batch of key-values from the underlying
   * scanner next batch of {@link KeyValue}s
   */
  private void updateStats(final List<Cell> results) throws IOException {
    if (!results.isEmpty()) {
      tracker.collectStatistics(results);
    }
  }

  @Override
  public void close() throws IOException {
    boolean async = getConfig().getBoolean(COMMIT_STATS_ASYNC, DEFAULT_COMMIT_STATS_ASYNC);
    StatisticsCollectionRunTracker collectionTracker = getStatsCollectionRunTracker(config);
    StatisticsScannerCallable callable = createCallable();
    if (isConnectionClosed()) {
      LOGGER.debug("Not updating table statistics because the server is stopping/stopped");
      return;
    }
    if (!async) {
      callable.call();
    } else {
      collectionTracker.runTask(callable);
    }
  }

  // VisibleForTesting
  StatisticsCollectionRunTracker getStatsCollectionRunTracker(Configuration c) {
    return StatisticsCollectionRunTracker.getInstance(c);
  }

  Configuration getConfig() {
    return config;
  }

  StatisticsWriter getStatisticsWriter() {
    return statsWriter;
  }

  Region getRegion() {
    return region;
  }

  Connection getConnection() {
    return env.getConnection();
  }

  StatisticsScannerCallable createCallable() {
    return new StatisticsScannerCallable();
  }

  StatisticsCollector getTracker() {
    return tracker;
  }

  InternalScanner getDelegate() {
    return delegate;
  }

  class StatisticsScannerCallable implements Callable<Void> {
    @Override
    public Void call() throws IOException {
      IOException toThrow = null;
      StatisticsCollectionRunTracker collectionTracker = getStatsCollectionRunTracker(config);
      final RegionInfo regionInfo = getRegion().getRegionInfo();
      try {
        // update the statistics table
        // Just verify if this if fine
        ArrayList<Mutation> mutations = new ArrayList<Mutation>();

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Deleting the stats for the region " + regionInfo.getRegionNameAsString()
            + " as part of major compaction");
        }
        getStatisticsWriter().deleteStatsForRegion(region, tracker, family, mutations);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Adding new stats for the region " + regionInfo.getRegionNameAsString()
            + " as part of major compaction");
        }
        getStatisticsWriter().addStats(tracker, family, mutations, tracker.getGuidePostDepth());
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Committing new stats for the region " + regionInfo.getRegionNameAsString()
            + " as part of major compaction");
        }
        getStatisticsWriter().commitStats(mutations, tracker);
      } catch (IOException e) {
        if (isConnectionClosed()) {
          LOGGER.debug("Ignoring error updating statistics because region is closing/closed");
        } else {
          LOGGER.error("Failed to update statistics table!", e);
          toThrow = e;
        }
      } finally {
        try {
          collectionTracker.removeCompactingRegion(regionInfo);
          getStatisticsWriter().close();// close the writer
          getTracker().close();// close the tracker
        } catch (IOException e) {
          if (toThrow == null) toThrow = e;
          LOGGER.error("Error while closing the stats table", e);
        } finally {
          // close the delegate scanner
          try {
            getDelegate().close();
          } catch (IOException e) {
            if (toThrow == null) toThrow = e;
            LOGGER.error("Error while closing the scanner", e);
          } finally {
            if (toThrow != null) {
              throw toThrow;
            }
          }
        }
      }
      return null;
    }
  }

  private boolean isConnectionClosed() {
    return getConnection() == null || getConnection().isClosed() || getConnection().isAborted();
  }

}
