/*
 * 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 org.apache.jackrabbit.oak.spi.gc.GCMonitor;
import org.jetbrains.annotations.NotNull;

/**
 * Monitors the compaction cycle and keeps a compacted nodes counter, in order
 * to provide a best effort progress log based on extrapolating the previous
 * size and node count and current size to deduce current node count.
 */
public class GCNodeWriteMonitor {
    public static final GCNodeWriteMonitor EMPTY = new GCNodeWriteMonitor(
            -1, GCMonitor.EMPTY);

    /**
     * Number of nodes the monitor will log a message, -1 to disable
     */
    private final long gcProgressLog;

    private final GCMonitor gcMonitor;

    /**
     * Start timestamp of compaction (reset at each {@code init()} call).
     */
    private long start = 0;

    /**
     * Estimated nodes to compact per cycle (reset at each {@code init()} call).
     */
    private long estimated = -1;

    /**
     * Number of compacted nodes
     */
    private long nodes;

    /**
     * Number of compacted properties
     */
    private long properties;

    /**
     * Number of compacted binaries
     */
    private long binaries;

    private boolean running = false;

    public GCNodeWriteMonitor(long gcProgressLog, @NotNull GCMonitor gcMonitor) {
        this.gcProgressLog = gcProgressLog;
        this.gcMonitor = gcMonitor;
    }

    /**
     * @param prevSize
     *            size from latest successful compaction
     * @param prevCompactedNodes
     *            number of nodes compacted during latest compaction operation
     * @param currentSize
     *            current repository size
     */
    public synchronized void init(long prevSize, long prevCompactedNodes, long currentSize) {
        if (prevCompactedNodes > 0) {
            estimated = (long) (((double) currentSize / prevSize) * prevCompactedNodes);
            gcMonitor.info(
                "estimated number of nodes to compact is {}, based on {} nodes compacted to {} bytes "
                    + "on disk in previous compaction and current size of {} bytes on disk.",
                estimated, prevCompactedNodes, prevSize, currentSize);
        } else {
            gcMonitor.info("unable to estimate number of nodes for compaction, missing gc history.");
        }
        nodes = 0;
        start = System.currentTimeMillis();
        running = true;
    }

    public synchronized void onNode() {
        nodes++;
        if (gcProgressLog > 0 && nodes % gcProgressLog == 0) {
            gcMonitor.info("compacted {} nodes, {} properties, {} binaries in {} ms. {}",
                nodes, properties, binaries, System.currentTimeMillis() - start, getPercentageDone());
        }
    }

    public synchronized void onProperty() {
        properties++;
    }

    public synchronized void onBinary() {
        binaries++;
    }

    public synchronized void finished() {
        running = false;
    }

    /**
     * Compacted nodes in current cycle
     */
    public synchronized long getCompactedNodes() {
        return nodes;
    }

    /**
     * Estimated nodes to compact in current cycle. Can be {@code -1} if the
     * estimation could not be performed.
     */
    public synchronized long getEstimatedTotal() {
        return estimated;
    }

    @NotNull
    private String getPercentageDone() {
        return estimated > 0
            ? getEstimatedPercentage() + "% complete."
            : "";
    }

    /**
     * Estimated completion percentage. Can be {@code -1} if the estimation
     * could not be performed.
     */
    public synchronized int getEstimatedPercentage() {
        if (estimated > 0) {
            if (!running) {
                return 100;
            } else {
                return Math.min((int) (100 * ((double) nodes / estimated)), 99);
            }
        }
        return -1;
    }

    public synchronized boolean isCompactionRunning() {
        return running;
    }

    public long getGcProgressLog() {
        return gcProgressLog;
    }
}
