/*
 * Copyright 2014-2020 Real Logic Limited.
 *
 * 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
 *
 * https://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.agrona.concurrent.status;

import org.agrona.DirectBuffer;
import org.agrona.LangUtil;
import org.agrona.MutableDirectBuffer;
import org.agrona.collections.IntArrayList;
import org.agrona.concurrent.AtomicBuffer;
import org.agrona.concurrent.EpochClock;
import org.agrona.concurrent.UnsafeBuffer;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;

import static org.agrona.BitUtil.SIZE_OF_INT;

/**
 * Manages the allocation and freeing of counters that are normally stored in a memory-mapped file.
 * <p>
 * This class in not threadsafe. Counters should be centrally managed.
 * <p>
 * <b>Values Buffer</b>
 * <pre>
 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                        Counter Value                          |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                     120 bytes of padding                     ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *  |                   Repeats to end of buffer                   ...
 *  |                                                               |
 * ...                                                              |
 *  +---------------------------------------------------------------+
 * </pre>
 * <p>
 * <b>Meta Data Buffer</b>
 * <pre>
 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                        Record State                           |
 *  +---------------------------------------------------------------+
 *  |                          Type Id                              |
 *  +---------------------------------------------------------------+
 *  |                   Free-for-reuse Deadline                     |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                      112 bytes for key                       ...
 * ...                                                              |
 *  +-+-------------------------------------------------------------+
 *  |R|                      Label Length                           |
 *  +-+-------------------------------------------------------------+
 *  |                     380 bytes of Label                       ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *  |                   Repeats to end of buffer                   ...
 *  |                                                               |
 * ...                                                              |
 *  +---------------------------------------------------------------+
 * </pre>
 */
public class CountersManager extends CountersReader
{
    /**
     * Default type id of a counter when none is supplied.
     */
    public static final int DEFAULT_TYPE_ID = 0;

    private final long freeToReuseTimeoutMs;
    private int idHighWaterMark = -1;
    private final IntArrayList freeList = new IntArrayList();
    private final EpochClock epochClock;

    /**
     * Create a new counter buffer manager over two buffers.
     *
     * @param metaDataBuffer       containing the types, keys, and labels for the counters.
     * @param valuesBuffer         containing the values of the counters themselves.
     * @param labelCharset         for the label encoding.
     * @param epochClock           to use for determining time for keep counter from being reused after being freed.
     * @param freeToReuseTimeoutMs timeout (in milliseconds) to keep counter from being reused after being freed.
     */
    public CountersManager(
        final AtomicBuffer metaDataBuffer,
        final AtomicBuffer valuesBuffer,
        final Charset labelCharset,
        final EpochClock epochClock,
        final long freeToReuseTimeoutMs)
    {
        super(metaDataBuffer, valuesBuffer, labelCharset);

        valuesBuffer.verifyAlignment();
        this.epochClock = epochClock;
        this.freeToReuseTimeoutMs = freeToReuseTimeoutMs;

        if (metaDataBuffer.capacity() < (valuesBuffer.capacity() * 2))
        {
            throw new IllegalArgumentException("metadata buffer not sufficiently large");
        }
    }

    /**
     * Create a new counter buffer manager over two buffers.
     *
     * @param metaDataBuffer containing the types, keys, and labels for the counters.
     * @param valuesBuffer   containing the values of the counters themselves.
     */
    public CountersManager(final AtomicBuffer metaDataBuffer, final AtomicBuffer valuesBuffer)
    {
        super(metaDataBuffer, valuesBuffer);

        valuesBuffer.verifyAlignment();
        this.epochClock = () -> 0;
        this.freeToReuseTimeoutMs = 0;

        if (metaDataBuffer.capacity() < (valuesBuffer.capacity() * 2))
        {
            throw new IllegalArgumentException("metadata buffer not sufficiently large");
        }
    }

    /**
     * Create a new counter buffer manager over two buffers.
     *
     * @param metaDataBuffer containing the types, keys, and labels for the counters.
     * @param valuesBuffer   containing the values of the counters themselves.
     * @param labelCharset   for the label encoding.
     */
    public CountersManager(
        final AtomicBuffer metaDataBuffer, final AtomicBuffer valuesBuffer, final Charset labelCharset)
    {
        this(metaDataBuffer, valuesBuffer, labelCharset, () -> 0, 0);
    }

    /**
     * Allocate a new counter with a given label with a default type of {@link #DEFAULT_TYPE_ID}.
     *
     * @param label to describe the counter.
     * @return the id allocated for the counter.
     */
    public int allocate(final String label)
    {
        return allocate(label, DEFAULT_TYPE_ID);
    }

    /**
     * Allocate a new counter with a given label and type.
     *
     * @param label  to describe the counter.
     * @param typeId for the type of counter.
     * @return the id allocated for the counter.
     */
    public int allocate(final String label, final int typeId)
    {
        final int counterId = nextCounterId();
        checkCountersCapacity(counterId);

        final int recordOffset = metaDataOffset(counterId);
        checkMetaDataCapacity(recordOffset);

        try
        {
            metaDataBuffer.putInt(recordOffset + TYPE_ID_OFFSET, typeId);
            metaDataBuffer.putLong(recordOffset + FREE_FOR_REUSE_DEADLINE_OFFSET, NOT_FREE_TO_REUSE);
            putLabel(recordOffset, label);

            metaDataBuffer.putIntOrdered(recordOffset, RECORD_ALLOCATED);
        }
        catch (final Exception ex)
        {
            freeList.pushInt(counterId);
            LangUtil.rethrowUnchecked(ex);
        }

        return counterId;
    }

    /**
     * Allocate a new counter with a given label.
     * <p>
     * The key function will be called with a buffer with the exact length of available key space
     * in the record for the user to store what they want for the key. No offset is required.
     *
     * @param label   to describe the counter.
     * @param typeId  for the type of counter.
     * @param keyFunc for setting the key value for the counter.
     * @return the id allocated for the counter.
     */
    public int allocate(final String label, final int typeId, final Consumer<MutableDirectBuffer> keyFunc)
    {
        final int counterId = nextCounterId();
        checkCountersCapacity(counterId);

        final int recordOffset = metaDataOffset(counterId);
        checkMetaDataCapacity(recordOffset);

        try
        {
            metaDataBuffer.putInt(recordOffset + TYPE_ID_OFFSET, typeId);
            keyFunc.accept(new UnsafeBuffer(metaDataBuffer, recordOffset + KEY_OFFSET, MAX_KEY_LENGTH));
            metaDataBuffer.putLong(recordOffset + FREE_FOR_REUSE_DEADLINE_OFFSET, NOT_FREE_TO_REUSE);
            putLabel(recordOffset, label);

            metaDataBuffer.putIntOrdered(recordOffset, RECORD_ALLOCATED);
        }
        catch (final Exception ex)
        {
            freeList.pushInt(counterId);
            LangUtil.rethrowUnchecked(ex);
        }

        return counterId;
    }

    /**
     * Allocate a counter with the minimum of allocation by allowing the label an key to be provided and copied.
     * <p>
     * If the keyBuffer is null then a copy of the key is not attempted.
     *
     * @param typeId      for the counter.
     * @param keyBuffer   containing the optional key for the counter.
     * @param keyOffset   within the keyBuffer at which the key begins.
     * @param keyLength   of the key in the keyBuffer.
     * @param labelBuffer containing the mandatory label for the counter.
     * @param labelOffset within the labelBuffer at which the label begins.
     * @param labelLength of the label in the labelBuffer.
     * @return the id allocated for the counter.
     */
    public int allocate(
        final int typeId,
        final DirectBuffer keyBuffer,
        final int keyOffset,
        final int keyLength,
        final DirectBuffer labelBuffer,
        final int labelOffset,
        final int labelLength)
    {
        final int counterId = nextCounterId();
        checkCountersCapacity(counterId);

        final int recordOffset = metaDataOffset(counterId);
        checkMetaDataCapacity(recordOffset);

        try
        {
            metaDataBuffer.putInt(recordOffset + TYPE_ID_OFFSET, typeId);
            metaDataBuffer.putLong(recordOffset + FREE_FOR_REUSE_DEADLINE_OFFSET, NOT_FREE_TO_REUSE);

            if (null != keyBuffer)
            {
                final int length = Math.min(keyLength, MAX_KEY_LENGTH);
                metaDataBuffer.putBytes(recordOffset + KEY_OFFSET, keyBuffer, keyOffset, length);
            }

            final int length = Math.min(labelLength, MAX_LABEL_LENGTH);
            metaDataBuffer.putInt(recordOffset + LABEL_OFFSET, length);
            metaDataBuffer.putBytes(recordOffset + LABEL_OFFSET + SIZE_OF_INT, labelBuffer, labelOffset, length);

            metaDataBuffer.putIntOrdered(recordOffset, RECORD_ALLOCATED);
        }
        catch (final Exception ex)
        {
            freeList.pushInt(counterId);
            LangUtil.rethrowUnchecked(ex);
        }

        return counterId;
    }

    /**
     * Allocate a counter record and wrap it with a new {@link AtomicCounter} for use with a default type
     * of {@link #DEFAULT_TYPE_ID}.
     *
     * @param label to describe the counter.
     * @return a newly allocated {@link AtomicCounter}
     */
    public AtomicCounter newCounter(final String label)
    {
        return new AtomicCounter(valuesBuffer, allocate(label), this);
    }

    /**
     * Allocate a counter record and wrap it with a new {@link AtomicCounter} for use.
     *
     * @param label  to describe the counter.
     * @param typeId for the type of counter.
     * @return a newly allocated {@link AtomicCounter}
     */
    public AtomicCounter newCounter(final String label, final int typeId)
    {
        return new AtomicCounter(valuesBuffer, allocate(label, typeId), this);
    }

    /**
     * Allocate a counter record and wrap it with a new {@link AtomicCounter} for use.
     *
     * @param label   to describe the counter.
     * @param typeId  for the type of counter.
     * @param keyFunc for setting the key value for the counter.
     * @return a newly allocated {@link AtomicCounter}
     */
    public AtomicCounter newCounter(final String label, final int typeId, final Consumer<MutableDirectBuffer> keyFunc)
    {
        return new AtomicCounter(valuesBuffer, allocate(label, typeId, keyFunc), this);
    }

    /**
     * Allocate a counter record and wrap it with a new {@link AtomicCounter} for use.
     * <p>
     * If the keyBuffer is null then a copy of the key is not attempted.
     *
     * @param typeId      for the counter.
     * @param keyBuffer   containing the optional key for the counter.
     * @param keyOffset   within the keyBuffer at which the key begins.
     * @param keyLength   of the key in the keyBuffer.
     * @param labelBuffer containing the mandatory label for the counter.
     * @param labelOffset within the labelBuffer at which the label begins.
     * @param labelLength of the label in the labelBuffer.
     * @return the id allocated for the counter.
     */
    public AtomicCounter newCounter(
        final int typeId,
        final DirectBuffer keyBuffer,
        final int keyOffset,
        final int keyLength,
        final DirectBuffer labelBuffer,
        final int labelOffset,
        final int labelLength)
    {
        return new AtomicCounter(
            valuesBuffer,
            allocate(typeId, keyBuffer, keyOffset, keyLength, labelBuffer, labelOffset, labelLength),
            this);
    }

    /**
     * Free the counter identified by counterId.
     *
     * @param counterId the counter to freed
     */
    public void free(final int counterId)
    {
        final int recordOffset = metaDataOffset(counterId);

        metaDataBuffer.putIntOrdered(recordOffset, RECORD_RECLAIMED);
        metaDataBuffer.setMemory(recordOffset + KEY_OFFSET, MAX_KEY_LENGTH, (byte)0);
        metaDataBuffer.putLong(
            recordOffset + FREE_FOR_REUSE_DEADLINE_OFFSET, epochClock.time() + freeToReuseTimeoutMs);
        freeList.addInt(counterId);
    }

    /**
     * Set an {@link AtomicCounter} value based on counterId.
     *
     * @param counterId to be set.
     * @param value     to set for the counter.
     */
    public void setCounterValue(final int counterId, final long value)
    {
        valuesBuffer.putLongOrdered(counterOffset(counterId), value);
    }

    /**
     * Set an {@link AtomicCounter} label based on counterId.
     *
     * @param counterId to be set.
     * @param label     to set for the counter.
     */
    public void setCounterLabel(final int counterId, final String label)
    {
        putLabel(metaDataOffset(counterId), label);
    }

    /**
     * Set an {@link AtomicCounter} key based on counterId, using a consumer callback to update the key metadata buffer.
     *
     * @param counterId to be set.
     * @param keyFunc   callback used to set the key.
     */
    public void setCounterKey(final int counterId, final Consumer<MutableDirectBuffer> keyFunc)
    {
        keyFunc.accept(new UnsafeBuffer(metaDataBuffer, metaDataOffset(counterId) + KEY_OFFSET, MAX_KEY_LENGTH));
    }

    /**
     * Set an {@link AtomicCounter} key based on counterId, copying the key metadata from the supplied buffer.
     *
     * @param id        to be set
     * @param keyBuffer containing the updated key
     * @param offset    offset into buffer
     * @param length    length of data to copy
     */
    public void setCounterKey(final int id, final DirectBuffer keyBuffer, final int offset, final int length)
    {
        if (length > MAX_KEY_LENGTH)
        {
            throw new IllegalArgumentException("Supplied key is too long: " + length + ", max: " + MAX_KEY_LENGTH);
        }

        metaDataBuffer.putBytes(metaDataOffset(id) + KEY_OFFSET, keyBuffer, offset, length);
    }

    /**
     * Set an {@link AtomicCounter} label based on counterId.
     *
     * @param counterId to be set.
     * @param label     to set for the counter.
     */
    public void appendToLabel(final int counterId, final String label)
    {
        appendLabel(metaDataOffset(counterId), label);
    }

    private int nextCounterId()
    {
        final long nowMs = epochClock.time();

        for (int i = 0, size = freeList.size(); i < size; i++)
        {
            final int counterId = freeList.getInt(i);

            final long deadlineMs = metaDataBuffer.getLongVolatile(
                metaDataOffset(counterId) + FREE_FOR_REUSE_DEADLINE_OFFSET);

            if (nowMs >= deadlineMs)
            {
                freeList.remove(i);
                valuesBuffer.putLongOrdered(counterOffset(counterId), 0L);

                return counterId;
            }
        }

        return ++idHighWaterMark;
    }

    private void putLabel(final int recordOffset, final String label)
    {
        if (StandardCharsets.US_ASCII == labelCharset)
        {
            final int length = metaDataBuffer.putStringWithoutLengthAscii(
                recordOffset + LABEL_OFFSET + SIZE_OF_INT, label, 0, MAX_LABEL_LENGTH);
            metaDataBuffer.putIntOrdered(recordOffset + LABEL_OFFSET, length);
        }
        else
        {
            final byte[] bytes = label.getBytes(labelCharset);
            final int length = Math.min(bytes.length, MAX_LABEL_LENGTH);

            metaDataBuffer.putBytes(recordOffset + LABEL_OFFSET + SIZE_OF_INT, bytes, 0, length);
            metaDataBuffer.putIntOrdered(recordOffset + LABEL_OFFSET, length);
        }
    }

    private void appendLabel(final int recordOffset, final String suffix)
    {
        final int existingLength = metaDataBuffer.getInt(recordOffset + LABEL_OFFSET);
        final int maxSuffixLength = MAX_LABEL_LENGTH - existingLength;

        if (StandardCharsets.US_ASCII == labelCharset)
        {
            final int suffixLength = metaDataBuffer.putStringWithoutLengthAscii(
                recordOffset + LABEL_OFFSET + SIZE_OF_INT + existingLength, suffix, 0, maxSuffixLength);

            metaDataBuffer.putIntOrdered(recordOffset + LABEL_OFFSET, existingLength + suffixLength);
        }
        else
        {
            final byte[] suffixBytes = suffix.getBytes(labelCharset);
            final int suffixLength = Math.min(suffixBytes.length, maxSuffixLength);

            metaDataBuffer.putBytes(
                recordOffset + LABEL_OFFSET + SIZE_OF_INT + existingLength, suffixBytes, 0, suffixLength);
            metaDataBuffer.putIntOrdered(recordOffset + LABEL_OFFSET, existingLength + suffixLength);
        }
    }

    private void checkCountersCapacity(final int counterId)
    {
        if ((counterOffset(counterId) + COUNTER_LENGTH) > valuesBuffer.capacity())
        {
            throw new IllegalStateException("unable to allocate counter, values buffer is full");
        }
    }

    private void checkMetaDataCapacity(final int recordOffset)
    {
        if ((recordOffset + METADATA_LENGTH) > metaDataBuffer.capacity())
        {
            throw new IllegalStateException("unable to allocate counter, metadata buffer is full");
        }
    }
}