# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

from math import ceil, log2


class Buckets:
    # No method of this class is protected by locks because instances of this
    # class are only used in methods that are protected by locks themselves.

    def __init__(self):
        self._counts = [0]

        # The term index refers to the number of the exponential histogram bucket
        # used to determine its boundaries. The lower boundary of a bucket is
        # determined by base ** index and the upper boundary of a bucket is
        # determined by base ** (index + 1). index values are signedto account
        # for values less than or equal to 1.

        # self._index_* will all have values equal to a certain index that is
        # determined by the corresponding mapping _map_to_index function and
        # the value of the index depends on the value passed to _map_to_index.

        # Index of the 0th position in self._counts: self._counts[0] is the
        # count in the bucket with index self.__index_base.
        self.__index_base = 0

        # self.__index_start is the smallest index value represented in
        # self._counts.
        self.__index_start = 0

        # self.__index_start is the largest index value represented in
        # self._counts.
        self.__index_end = 0

    @property
    def index_start(self) -> int:
        return self.__index_start

    @index_start.setter
    def index_start(self, value: int) -> None:
        self.__index_start = value

    @property
    def index_end(self) -> int:
        return self.__index_end

    @index_end.setter
    def index_end(self, value: int) -> None:
        self.__index_end = value

    @property
    def index_base(self) -> int:
        return self.__index_base

    @index_base.setter
    def index_base(self, value: int) -> None:
        self.__index_base = value

    @property
    def counts(self):
        return self._counts

    def get_offset_counts(self):
        bias = self.__index_base - self.__index_start
        return self._counts[-bias:] + self._counts[:-bias]

    def grow(self, needed: int, max_size: int) -> None:
        size = len(self._counts)
        bias = self.__index_base - self.__index_start
        old_positive_limit = size - bias

        # 2 ** ceil(log2(needed)) finds the smallest power of two that is larger
        # or equal than needed:
        # 2 ** ceil(log2(1)) == 1
        # 2 ** ceil(log2(2)) == 2
        # 2 ** ceil(log2(3)) == 4
        # 2 ** ceil(log2(4)) == 4
        # 2 ** ceil(log2(5)) == 8
        # 2 ** ceil(log2(6)) == 8
        # 2 ** ceil(log2(7)) == 8
        # 2 ** ceil(log2(8)) == 8
        new_size = min(2 ** ceil(log2(needed)), max_size)

        new_positive_limit = new_size - bias

        tmp = [0] * new_size
        tmp[new_positive_limit:] = self._counts[old_positive_limit:]
        tmp[0:old_positive_limit] = self._counts[0:old_positive_limit]
        self._counts = tmp

    @property
    def offset(self) -> int:
        return self.__index_start

    def __len__(self) -> int:
        if len(self._counts) == 0:
            return 0

        if self.__index_end == self.__index_start and self[0] == 0:
            return 0

        return self.__index_end - self.__index_start + 1

    def __getitem__(self, key: int) -> int:
        bias = self.__index_base - self.__index_start

        if key < bias:
            key += len(self._counts)

        key -= bias

        return self._counts[key]

    def downscale(self, amount: int) -> None:
        """
        Rotates, then collapses 2 ** amount to 1 buckets.
        """

        bias = self.__index_base - self.__index_start

        if bias != 0:
            self.__index_base = self.__index_start

            # [0, 1, 2, 3, 4] Original backing array

            self._counts = self._counts[::-1]
            # [4, 3, 2, 1, 0]

            self._counts = (
                self._counts[:bias][::-1] + self._counts[bias:][::-1]
            )
            # [3, 4, 0, 1, 2] This is a rotation of the backing array.

        size = 1 + self.__index_end - self.__index_start
        each = 1 << amount
        inpos = 0
        outpos = 0

        pos = self.__index_start

        while pos <= self.__index_end:
            mod = pos % each
            if mod < 0:
                mod += each

            index = mod

            while index < each and inpos < size:
                if outpos != inpos:
                    self._counts[outpos] += self._counts[inpos]
                    self._counts[inpos] = 0

                inpos += 1
                pos += 1
                index += 1

            outpos += 1

        self.__index_start >>= amount
        self.__index_end >>= amount
        self.__index_base = self.__index_start

    def increment_bucket(self, bucket_index: int, increment: int = 1) -> None:
        self._counts[bucket_index] += increment

    def copy_empty(self) -> "Buckets":
        copy = Buckets()

        # pylint: disable=no-member
        # pylint: disable=protected-access
        # pylint: disable=attribute-defined-outside-init
        # pylint: disable=invalid-name
        copy._Buckets__index_base = self._Buckets__index_base  # type: ignore[reportArgumentType]
        copy._Buckets__index_start = self._Buckets__index_start  # type: ignore[reportArgumentType]
        copy._Buckets__index_end = self._Buckets__index_end  # type: ignore[reportArgumentType]
        copy._counts = [0 for _ in self._counts]

        return copy
