// Copyright 2024 The Pigweed Authors
//
// 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.

#include "pw_multibuf/simple_allocator.h"

#include <algorithm>
#include <mutex>

#include "pw_assert/check.h"
#include "pw_bytes/alignment.h"

namespace pw::multibuf {
namespace internal {

LinkedRegionTracker::~LinkedRegionTracker() {
  // The ``LinkedRegionTracker`` *must* be removed from the parent allocator's
  // region list prior to being destroyed, as doing so requires holding a lock.
  //
  // The destructor is only called via ``Destroy()``'s invocation of
  // ``metadata_alloc_ref.Delete(this);``
  PW_DCHECK(unlisted());
}

void LinkedRegionTracker::Destroy() {
  SimpleAllocator::AvailableMemorySize available;
  {
    // N.B.: this lock *must* go out of scope before the call to
    // ``Delete(this)`` below in order to prevent referencing the ``parent_``
    // field after this tracker has been destroyed.
    std::lock_guard lock(parent_.lock_);
    unlist();
    available = parent_.GetAvailableMemorySize();
  }
  parent_.MoreMemoryAvailable(available.total, available.contiguous);
  parent_.metadata_alloc_.Delete(this);
}

void* LinkedRegionTracker::AllocateChunkClass() {
  return parent_.metadata_alloc_.Allocate(allocator::Layout::Of<Chunk>());
}

void LinkedRegionTracker::DeallocateChunkClass(void* ptr) {
  return parent_.metadata_alloc_.Deallocate(ptr);
}

}  // namespace internal

// PW_CHECK doesn't like %, so...
static bool IsAlignedSize(size_t num, size_t alignment) {
  return num % alignment == 0;
}

SimpleAllocator::SimpleAllocator(ByteSpan data_area,
                                 pw::allocator::Allocator& metadata_alloc,
                                 size_t alignment)
    : metadata_alloc_(metadata_alloc),
      data_area_(data_area),
      alignment_(alignment) {
  PW_CHECK(IsAlignedAs(data_area_.data(), alignment));
  PW_CHECK(IsAlignedSize(data_area_.size(), alignment));
}

pw::Result<MultiBuf> SimpleAllocator::DoAllocate(
    size_t min_size,
    size_t desired_size,
    ContiguityRequirement contiguity_requirement) {
  if (min_size > data_area_.size()) {
    return Status::OutOfRange();
  }
  // NB: std::lock_guard is not used here in order to release the lock
  // prior to destroying ``buf`` below.
  lock_.lock();
  auto available_memory_size = GetAvailableMemorySize();
  size_t available = (contiguity_requirement == kNeedsContiguous)
                         ? available_memory_size.contiguous
                         : available_memory_size.total;
  if (available < min_size) {
    lock_.unlock();
    return Status::ResourceExhausted();
  }
  // All regions should be aligned, so `available` should be aligned.
  PW_CHECK(IsAlignedSize(available, alignment_));
  size_t goal_size = std::min(desired_size, available);
  if (goal_size == 0) {
    lock_.unlock();
    return MultiBuf();
  }
  if (contiguity_requirement == kNeedsContiguous) {
    auto out = InternalAllocateContiguous(goal_size);
    lock_.unlock();
    return out;
  }

  MultiBuf buf;
  Status status;
  const size_t unaligned = goal_size % alignment_;
  const size_t extra_for_alignment = unaligned ? alignment_ - unaligned : 0;
  // There's no danger of increasing the goal here to be more than `available`
  // because `available` is guaranteed to be aligned.
  size_t remaining_goal = goal_size + extra_for_alignment;
  ForEachFreeBlock(
      [this, &buf, &status, extra_for_alignment, remaining_goal](
          const FreeBlock& block) PW_EXCLUSIVE_LOCKS_REQUIRED(lock_) mutable {
        PW_CHECK(IsAlignedAs(block.span.data(), alignment_));
        size_t chunk_size = std::min(block.span.size(), remaining_goal);
        pw::Result<OwnedChunk> chunk =
            InsertRegion({block.iter, ByteSpan(block.span.data(), chunk_size)});
        if (!chunk.ok()) {
          status = chunk.status();
          return ControlFlow::Break;
        }
        remaining_goal -= chunk->size();
        if (remaining_goal == 0) {
          if (extra_for_alignment) {
            // If we had to adjust the goal for alignment, trim the chunk
            // now. This will keep the regions aligned in size even though
            // the chunk isn't.
            (*chunk)->Truncate(chunk->size() - extra_for_alignment);
          }
          buf.PushFrontChunk(std::move(*chunk));
          return ControlFlow::Break;
        }
        buf.PushFrontChunk(std::move(*chunk));
        return ControlFlow::Continue;
      });
  // Lock must be released prior to possibly free'ing the `buf` in the case
  // where `!status.ok()`. This is necessary so that the destructing chunks
  // can free their regions.
  lock_.unlock();
  if (!status.ok()) {
    return status;
  }
  return buf;
}

pw::Result<MultiBuf> SimpleAllocator::InternalAllocateContiguous(size_t size) {
  pw::Result<MultiBuf> buf = Status::ResourceExhausted();
  const size_t aligned_size = (size + alignment_ - 1) / alignment_ * alignment_;
  ForEachFreeBlock(
      [this, &buf, size, aligned_size](const FreeBlock& block)
          PW_EXCLUSIVE_LOCKS_REQUIRED(lock_) {
            if (block.span.size() >= aligned_size) {
              PW_CHECK(IsAlignedAs(block.span.data(), alignment_));
              ByteSpan buf_span(block.span.data(), aligned_size);
              buf = InsertRegion({block.iter, buf_span})
                        .transform([size](OwnedChunk&& owned_chunk) {
                          owned_chunk->Truncate(size);
                          return MultiBuf::FromChunk(std::move(owned_chunk));
                        });
              return ControlFlow::Break;
            }
            return ControlFlow::Continue;
          });
  return buf;
}

pw::Result<OwnedChunk> SimpleAllocator::InsertRegion(const FreeBlock& block) {
  internal::LinkedRegionTracker* new_region =
      metadata_alloc_.New<internal::LinkedRegionTracker>(*this, block.span);
  if (new_region == nullptr) {
    return Status::OutOfRange();
  }
  std::optional<OwnedChunk> chunk = new_region->CreateFirstChunk();
  if (!chunk.has_value()) {
    metadata_alloc_.Delete(new_region);
    return Status::OutOfRange();
  }
  regions_.insert_after(block.iter, *new_region);
  return std::move(*chunk);
}

SimpleAllocator::AvailableMemorySize SimpleAllocator::GetAvailableMemorySize() {
  size_t total = 0;
  size_t max_contiguous = 0;
  ForEachFreeBlock([&total, &max_contiguous](const FreeBlock& block) {
    total += block.span.size();
    if (block.span.size() > max_contiguous) {
      max_contiguous = block.span.size();
    }
    return ControlFlow::Continue;
  });

  AvailableMemorySize available;
  available.total = total;
  available.contiguous = max_contiguous;
  return available;
}

}  // namespace pw::multibuf
