/*
 *
 *  Copyright 2016 Vladimir Bukhtoyarov
 *
 *    Licensed 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 com.github.rollingmetrics.histogram.accumulator;

import com.github.rollingmetrics.histogram.util.HistogramUtil;
import com.github.rollingmetrics.util.ResilientExecutionUtil;
import com.github.rollingmetrics.util.Clock;
import com.codahale.metrics.Snapshot;
import com.github.rollingmetrics.histogram.util.Printer;
import com.github.rollingmetrics.histogram.util.HistogramUtil;
import com.github.rollingmetrics.util.Clock;
import org.HdrHistogram.Histogram;
import org.HdrHistogram.Recorder;

import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;

public class ResetByChunksAccumulator implements Accumulator {

    private final Executor backgroundExecutor;
    private final long intervalBetweenResettingMillis;
    private final long creationTimestamp;
    private final ArchivedHistogram[] archive;
    private final boolean historySupported;
    private final Clock clock;
    private final Histogram temporarySnapshotHistogram;

    private final Phase left;
    private final Phase right;
    private final Phase[] phases;
    private final AtomicReference<Phase> currentPhaseRef;

    public ResetByChunksAccumulator(Supplier<Recorder> recorderSupplier, int numberHistoryChunks, long intervalBetweenResettingMillis, Clock clock, Executor backgroundExecutor) {
        this.intervalBetweenResettingMillis = intervalBetweenResettingMillis;
        this.clock = clock;
        this.creationTimestamp = clock.currentTimeMillis();
        this.backgroundExecutor = backgroundExecutor;

        this.left = new Phase(recorderSupplier, creationTimestamp + intervalBetweenResettingMillis);
        this.right = new Phase(recorderSupplier, Long.MAX_VALUE);
        this.phases = new Phase[] {left, right};
        this.currentPhaseRef = new AtomicReference<>(left);


        this.historySupported = numberHistoryChunks > 0;
        if (historySupported) {
            this.archive = new ArchivedHistogram[numberHistoryChunks];
            for (int i = 0; i < numberHistoryChunks; i++) {
                Histogram archivedHistogram = HistogramUtil.createNonConcurrentCopy(left.intervalHistogram);
                this.archive[i] = new ArchivedHistogram(archivedHistogram, Long.MIN_VALUE);
            }
        } else {
            this.archive = null;
        }

        this.temporarySnapshotHistogram = HistogramUtil.createNonConcurrentCopy(left.intervalHistogram);
    }

    @Override
    public void recordSingleValueWithExpectedInterval(long value, long expectedIntervalBetweenValueSamples) {
        long currentTimeMillis = clock.currentTimeMillis();
        Phase currentPhase = currentPhaseRef.get();
        if (currentTimeMillis < currentPhase.proposedInvalidationTimestamp) {
            currentPhase.recorder.recordValueWithExpectedInterval(value, expectedIntervalBetweenValueSamples);
            return;
        }

        Phase nextPhase = currentPhase == left ? right : left;
        nextPhase.recorder.recordValueWithExpectedInterval(value, expectedIntervalBetweenValueSamples);

        if (!currentPhaseRef.compareAndSet(currentPhase, nextPhase)) {
            // another writer achieved progress and must submit rotation task to backgroundExecutor
            return;
        }

        // Current thread is responsible to rotate phases.
        Runnable phaseRotation = () -> rotate(currentTimeMillis, currentPhase, nextPhase);
        ResilientExecutionUtil.getInstance().execute(backgroundExecutor, phaseRotation);
    }

    private synchronized void rotate(long currentTimeMillis, Phase currentPhase, Phase nextPhase) {
        try {
            currentPhase.intervalHistogram = currentPhase.recorder.getIntervalHistogram(currentPhase.intervalHistogram);
            HistogramUtil.addSecondToFirst(currentPhase.totalsHistogram, currentPhase.intervalHistogram);
            if (historySupported) {
                // move values from recorder to correspondent archived histogram
                long currentPhaseNumber = (currentPhase.proposedInvalidationTimestamp - creationTimestamp) / intervalBetweenResettingMillis;
                int correspondentArchiveIndex = (int) (currentPhaseNumber - 1) % archive.length;
                ArchivedHistogram correspondentArchivedHistogram = archive[correspondentArchiveIndex];
                HistogramUtil.reset(correspondentArchivedHistogram.histogram);
                HistogramUtil.addSecondToFirst(correspondentArchivedHistogram.histogram, currentPhase.totalsHistogram);
                correspondentArchivedHistogram.proposedInvalidationTimestamp = currentPhase.proposedInvalidationTimestamp + archive.length * intervalBetweenResettingMillis;
            }
            HistogramUtil.reset(currentPhase.totalsHistogram);
        } finally {
            long millisSinceCreation = currentTimeMillis - creationTimestamp;
            long intervalsSinceCreation = millisSinceCreation / intervalBetweenResettingMillis;
            currentPhase.proposedInvalidationTimestamp = Long.MAX_VALUE;
            nextPhase.proposedInvalidationTimestamp = creationTimestamp + (intervalsSinceCreation + 1) * intervalBetweenResettingMillis;
        }
    }

    @Override
    public final synchronized Snapshot getSnapshot(Function<Histogram, Snapshot> snapshotTaker) {
        HistogramUtil.reset(temporarySnapshotHistogram);
        long currentTimeMillis = clock.currentTimeMillis();

        for (Phase phase : phases) {
            if (phase.isNeedToBeReportedToSnapshot(currentTimeMillis)) {
                phase.intervalHistogram = phase.recorder.getIntervalHistogram(phase.intervalHistogram);
                HistogramUtil.addSecondToFirst(phase.totalsHistogram, phase.intervalHistogram);
                HistogramUtil.addSecondToFirst(temporarySnapshotHistogram, phase.totalsHistogram);
            }
        }
        if (historySupported) {
            for (ArchivedHistogram archivedHistogram : archive) {
                if (archivedHistogram.proposedInvalidationTimestamp > currentTimeMillis) {
                    HistogramUtil.addSecondToFirst(temporarySnapshotHistogram, archivedHistogram.histogram);
                }
            }
        }

        return HistogramUtil.getSnapshot(temporarySnapshotHistogram, snapshotTaker);
    }

    @Override
    public int getEstimatedFootprintInBytes() {
        // each histogram has equivalent pessimistic estimation
        int oneHistogramPessimisticFootprint = temporarySnapshotHistogram.getEstimatedFootprintInBytes();

        // 4 - two recorders with two histogram
        // 2 - two histogram for storing accumulated values from current phase
        // 1 - temporary histogram used for snapshot extracting
        return oneHistogramPessimisticFootprint * ((archive != null? archive.length : 0) + 4 + 2 + 1);
    }

    private final class ArchivedHistogram {

        private final Histogram histogram;
        private volatile long proposedInvalidationTimestamp;

        public ArchivedHistogram(Histogram histogram, long proposedInvalidationTimestamp) {
            this.histogram = histogram;
            this.proposedInvalidationTimestamp = proposedInvalidationTimestamp;
        }

        @Override
        public String toString() {
            return "ArchivedHistogram{" +
                    "\n, proposedInvalidationTimestamp=" + proposedInvalidationTimestamp +
                    "\n, histogram=" + Printer.histogramToString(histogram) +
                    "\n}";
        }
    }

    private final class Phase {

        final Recorder recorder;
        final Histogram totalsHistogram;
        Histogram intervalHistogram;
        volatile long proposedInvalidationTimestamp;

        Phase(Supplier<Recorder> recorderSupplier, long proposedInvalidationTimestamp) {
            this.recorder = recorderSupplier.get();
            this.intervalHistogram = recorder.getIntervalHistogram();
            this.totalsHistogram = intervalHistogram.copy();
            this.proposedInvalidationTimestamp = proposedInvalidationTimestamp;
        }

        @Override
        public String toString() {
            return "Phase{" +
                    "\n, proposedInvalidationTimestamp=" + proposedInvalidationTimestamp +
                    "\n, totalsHistogram=" + (totalsHistogram != null? Printer.histogramToString(totalsHistogram): "null") +
                    "\n, intervalHistogram=" + Printer.histogramToString(intervalHistogram) +
                    "\n}";
        }

        boolean isNeedToBeReportedToSnapshot(long currentTimeMillis) {
            long proposedInvalidationTimestampLocal = proposedInvalidationTimestamp;
            if (proposedInvalidationTimestampLocal > currentTimeMillis) {
                return true;
            }
            if (!historySupported) {
                return false;
            }
            long correspondentChunkProposedInvalidationTimestamp = proposedInvalidationTimestampLocal + archive.length * intervalBetweenResettingMillis;
            return correspondentChunkProposedInvalidationTimestamp > currentTimeMillis;
        }
    }

    @Override
    public String toString() {
        return "ResetByChunksAccumulator{" +
                "\nintervalBetweenResettingMillis=" + intervalBetweenResettingMillis +
                ",\n creationTimestamp=" + creationTimestamp +
                (!historySupported ? "" : ",\n archive=" + Printer.printArray(archive, "chunk")) +
                ",\n clock=" + clock +
                ",\n left=" + left +
                ",\n right=" + right +
                ",\n currentPhase=" + (currentPhaseRef.get() == left? "left": "right") +
                ",\n temporarySnapshotHistogram=" + Printer.histogramToString(temporarySnapshotHistogram)  +
                '}';
    }

}