/*
 * 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
     *
     * @param results
     *            next batch of {@link KeyValue}s
     * @throws IOException 
     */
    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();
    }

}