/*
 * 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.jackrabbit.oak.segment.file;

import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCStatus.ESTIMATION;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCStatus.IDLE;

import java.io.IOException;
import java.util.List;
import java.util.function.Supplier;

import org.apache.jackrabbit.guava.common.base.Predicate;
import org.apache.jackrabbit.oak.segment.Revisions;
import org.apache.jackrabbit.oak.segment.SegmentCache;
import org.apache.jackrabbit.oak.segment.SegmentReader;
import org.apache.jackrabbit.oak.segment.SegmentTracker;
import org.apache.jackrabbit.oak.segment.SegmentWriterFactory;
import org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions;
import org.apache.jackrabbit.oak.segment.file.cancel.Canceller;
import org.apache.jackrabbit.oak.segment.file.tar.GCGeneration;
import org.apache.jackrabbit.oak.segment.file.tar.TarFiles;
import org.apache.jackrabbit.oak.spi.blob.BlobStore;

abstract class AbstractGarbageCollectionStrategy implements GarbageCollectionStrategy {

    abstract EstimationStrategy getFullEstimationStrategy();

    abstract EstimationStrategy getTailEstimationStrategy();

    abstract CompactionStrategy getFullCompactionStrategy();

    abstract CompactionStrategy getTailCompactionStrategy();

    abstract CleanupStrategy getCleanupStrategy();

    @Override
    public void collectGarbage(Context context) throws IOException {
        switch (context.getGCOptions().getGCType()) {
            case FULL:
                collectFullGarbage(context);
                break;
            case TAIL:
                collectTailGarbage(context);
                break;
            default:
                throw new IllegalStateException("Invalid GC type");
        }
    }

    @Override
    public void collectFullGarbage(Context context) throws IOException {
        run(context, getFullEstimationStrategy(), getFullCompactionStrategy());
    }

    @Override
    public void collectTailGarbage(Context context) throws IOException {
        run(context, getTailEstimationStrategy(), getTailCompactionStrategy());
    }

    @Override
    public CompactionResult compactFull(Context context) throws IOException {
        return getFullCompactionStrategy().compact(newCompactionStrategyContext(context));
    }

    @Override
    public CompactionResult compactTail(Context context) throws IOException {
        return getTailCompactionStrategy().compact(newCompactionStrategyContext(context));
    }

    @Override
    public List<String> cleanup(Context context) throws IOException {
        return cleanup(context, CompactionResult.skipped(
            context.getLastCompactionType(),
            getGcGeneration(context),
            context.getGCOptions(),
            context.getRevisions().getHead(),
            context.getGCCount()
        ));
    }

    void run(Context context, EstimationStrategy estimationStrategy, CompactionStrategy compactionStrategy) throws IOException {
        try {
            context.getGCListener().info("started");

            long dt = System.currentTimeMillis() - context.getLastSuccessfulGC();

            if (dt < context.getGCBackOff()) {
                context.getGCListener().skipped("skipping garbage collection as it already ran less than {} hours ago ({} s).", context.getGCBackOff() / 3600000, dt / 1000);
                return;
            }

            boolean sufficientEstimatedGain = true;
            if (context.getGCOptions().isEstimationDisabled()) {
                context.getGCListener().info("estimation skipped because it was explicitly disabled");
            } else if (context.getGCOptions().isPaused()) {
                context.getGCListener().info("estimation skipped because compaction is paused");
            } else {
                context.getGCListener().info("estimation started");
                context.getGCListener().updateStatus(ESTIMATION.message());

                PrintableStopwatch watch = PrintableStopwatch.createStarted();
                EstimationResult estimation = estimationStrategy.estimate(newEstimationStrategyContext(context));
                sufficientEstimatedGain = estimation.isGcNeeded();
                String gcLog = estimation.getGcLog();
                if (sufficientEstimatedGain) {
                    context.getGCListener().info("estimation completed in {}. {}", watch, gcLog);
                } else {
                    context.getGCListener().skipped("estimation completed in {}. {}", watch, gcLog);
                }
            }

            if (sufficientEstimatedGain) {
                try (GCMemoryBarrier ignored = new GCMemoryBarrier(context.getSufficientMemory(), context.getGCListener(), context.getGCOptions())) {
                    if (context.getGCOptions().isPaused()) {
                        context.getGCListener().skipped("compaction paused");
                    } else if (!context.getSufficientMemory().get()) {
                        context.getGCListener().skipped("compaction skipped. Not enough memory");
                    } else {
                        CompactionResult compactionResult = compactionStrategy.compact(newCompactionStrategyContext(context));
                        if (compactionResult.isSuccess()) {
                            context.getSuccessfulGarbageCollectionListener().onSuccessfulGarbageCollection();
                        } else {
                            context.getGCListener().info("cleaning up after failed compaction");
                        }
                        context.getFileReaper().add(cleanup(context, compactionResult));
                    }
                }
            }
        } finally {
            context.getCompactionMonitor().finished();
            context.getGCListener().updateStatus(IDLE.message());
        }
    }

    private GCGeneration getGcGeneration(Context context) {
        return context.getRevisions().getHead().getSegmentId().getGcGeneration();
    }

    public List<String> cleanup(Context context, CompactionResult compactionResult) throws IOException {
        return getCleanupStrategy().cleanup(newCleanupStrategyContext(context, compactionResult));
    }

    private EstimationStrategy.Context newEstimationStrategyContext(Context context) {
        return new EstimationStrategy.Context() {

            @Override
            public long getSizeDelta() {
                return context.getGCOptions().getGcSizeDeltaEstimation();
            }

            @Override
            public long getCurrentSize() {
                return context.getTarFiles().size();
            }

            @Override
            public GCJournal getGCJournal() {
                return context.getGCJournal();
            }

        };
    }

    private static CompactionStrategy.Context newCompactionStrategyContext(Context context) {
        return new CompactionStrategy.Context() {

            @Override
            public SegmentTracker getSegmentTracker() {
                return context.getSegmentTracker();
            }

            @Override
            public GCListener getGCListener() {
                return context.getGCListener();
            }

            @Override
            public GCJournal getGCJournal() {
                return context.getGCJournal();
            }

            @Override
            public SegmentGCOptions getGCOptions() {
                return context.getGCOptions();
            }

            @Override
            public GCNodeWriteMonitor getCompactionMonitor() {
                return context.getCompactionMonitor();
            }

            @Override
            public SegmentReader getSegmentReader() {
                return context.getSegmentReader();
            }

            @Override
            public SegmentWriterFactory getSegmentWriterFactory() {
                return context.getSegmentWriterFactory();
            }

            @Override
            public Revisions getRevisions() {
                return context.getRevisions();
            }

            @Override
            public TarFiles getTarFiles() {
                return context.getTarFiles();
            }

            @Override
            public BlobStore getBlobStore() {
                return context.getBlobStore();
            }

            @Override
            public Canceller getHardCanceller() {
                return context.getCanceller();
            }

            @Override
            public Canceller getSoftCanceller() {
                return Canceller.newCanceller();
            }

            @Override
            public Supplier<Canceller> getStateSaveTriggerSupplier() {
                return Canceller::newCanceller;
            }

            @Override
            public int getGCCount() {
                return context.getGCCount();
            }

            @Override
            public SuccessfulCompactionListener getSuccessfulCompactionListener() {
                return context.getSuccessfulCompactionListener();
            }

            @Override
            public Flusher getFlusher() {
                return context.getFlusher();
            }

        };
    }

    private CleanupStrategy.Context newCleanupStrategyContext(Context context, CompactionResult compactionResult) {
        return new CleanupStrategy.Context() {

            @Override
            public GCListener getGCListener() {
                return context.getGCListener();
            }

            @Override
            public SegmentCache getSegmentCache() {
                return context.getSegmentCache();
            }

            @Override
            public SegmentTracker getSegmentTracker() {
                return context.getSegmentTracker();
            }

            @Override
            public FileStoreStats getFileStoreStats() {
                return context.getFileStoreStats();
            }

            @Override
            public GCNodeWriteMonitor getCompactionMonitor() {
                return context.getCompactionMonitor();
            }

            @Override
            public GCJournal getGCJournal() {
                return compactionResult.requiresGCJournalEntry() ? context.getGCJournal() : null;
            }

            @Override
            public Predicate<GCGeneration> getReclaimer() {
                return compactionResult.reclaimer();
            }

            @Override
            public TarFiles getTarFiles() {
                return context.getTarFiles();
            }

            @Override
            public Revisions getRevisions() {
                return context.getRevisions();
            }

            @Override
            public String getCompactedRootId() {
                return compactionResult.getCompactedRootId().toString10();
            }

            @Override
            public String getSegmentEvictionReason() {
                return compactionResult.gcInfo();
            }

        };
    }

}
