/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "util/heap.h"

#include "util/assert.h"
#include "util/math.h"
#include "util/logging.h"

#include <inttypes.h>
#include <stddef.h>
#include <string.h>
#include <stdio.h>

/* The following defines a type that is the size in bytes of the     */
/* desired alignment of each datr fragment.                          */
typedef unsigned long Alignment_t;

/* The following defines the byte boundary size that has been        */
/* specified size if the alignment data.                             */
#define ALIGNMENT_SIZE          sizeof(Alignment_t)

/* The following structure is used to allign data fragments on a     */
/* specified memory boundary.                                        */
typedef union _tagAlignmentStruct_t
{
  Alignment_t   AlignmentValue;
  unsigned char ByteValue;
} AlignmentStruct_t;

/* The following defines the size in bytes of a data fragment that is*/
/* considered a large value.  Allocations that are equal to and      */
/* larger than this value will be allocated from the end of the      */
/* buffer.                                                           */
#define LARGE_SIZE              (256/ALIGNMENT_SIZE)

//! The maximum size of the heap as a number of ALIGNMENT_SIZE
#define SEGMENT_SIZE_MAX      (0x7FFF)

/* The following defines the minimum size (in alignment units) of a  */
/* fragment that is considered useful.  The value is used when trying*/
/* to determine if a fragment that is larger than the requested size */
/* can be broken into 2 framents leaving a fragment that is of the   */
/* requested size and one that is at least as larger as the          */
/* MINIMUM_MEMORY_SIZE.                                              */
#define MINIMUM_MEMORY_SIZE     1

/* The following defines the structure that describes a memory       */
/* fragment.                                                         */
typedef struct _tagHeapInfo_t
{
  //! Size of the preceding segment, measured in units of ALIGNMENT_SIZE, including the size of this beginer
  uint16_t PrevSize;

  //! Whether or not this block is currently allocated (vs being free).
  bool is_allocated:1;

  //! Size of this segment, measured in units of ALIGNMENT_SIZE, including this size of this beginer
  uint16_t Size:15;

#ifdef MALLOC_INSTRUMENTATION
  uintptr_t pc; //<! The address that called malloc.
#endif

  //! This is the actual buffer that's returned to the caller. We use this struct to make
  //! sure the buffer we return is appropriately aligned.
  AlignmentStruct_t Data;
} HeapInfo_t;


//! The size of a block in units of Alignment_t, including the beginer and including _x words of data.
#define HEAP_INFO_BLOCK_SIZE(_x) ((offsetof(HeapInfo_t, Data) / ALIGNMENT_SIZE) + (_x))

//! Convert a pointer to the Data member to a pointer to the HeapInfo_t beginer
#define HEAP_INFO_FOR_PTR(ptr) (HeapInfo_t *)(((Alignment_t *)ptr) - HEAP_INFO_BLOCK_SIZE(0))

_Static_assert((offsetof(HeapInfo_t, Data) % ALIGNMENT_SIZE) == 0, "Heap not properly aligned.");

//! Heap is assumed corrupt if expr does not evaluate true
#define HEAP_ASSERT_SANE(heap, expr, log_addr) \
            if (!(expr)) { prv_handle_corruption(heap, log_addr); }

//! Lock the heap, using whatever behaviour the heap has configured using heap_set_lock_impl
static void heap_lock(Heap* heap) {
  if (heap->lock_impl.lock_function) {
    heap->lock_impl.lock_function(heap->lock_impl.lock_context);
  }
}

//! Unlock the heap, using whatever behaviour the heap has configured using heap_set_lock_impl
static void heap_unlock(Heap* heap) {
  if (heap->lock_impl.unlock_function) {
    heap->lock_impl.unlock_function(heap->lock_impl.lock_context);
  }

  // Handle any heap corruption that may have been detected while the heap was locked.
  if (heap->corrupt_block != NULL) {
    heap->corruption_handler(heap->corrupt_block);
    heap->corrupt_block = NULL;
  }
}

static void prv_handle_corruption(Heap * const heap, void *ptr) {
  if (heap->corruption_handler) {
    heap->corrupt_block = ptr;
    return;
  }
  UTIL_ASSERT(0); // Error: Heap corrupt around <ptr>
}

static HeapInfo_t *find_segment(Heap* const heap, unsigned long n_units);
static HeapInfo_t *allocate_block(Heap* const heap, unsigned long n_units, HeapInfo_t* heap_info_ptr);

//! Advance the block pointer to the next block.
static HeapInfo_t* get_next_block(Heap * const heap, HeapInfo_t* block) {
  HEAP_ASSERT_SANE(heap, block->Size != 0, block);
  return (HeapInfo_t *)(((Alignment_t *)block) + block->Size);
}

//! Move the block pointer back to the previous block.
static HeapInfo_t* get_previous_block(Heap * const heap, HeapInfo_t* block) {
  HEAP_ASSERT_SANE(heap, block->PrevSize != 0, block);
  return (HeapInfo_t *)(((Alignment_t *)block) - block->PrevSize);
}

static void prv_calc_totals(Heap* const heap, unsigned int *used, unsigned int *free, unsigned int *max_free) {
  HeapInfo_t    *heap_info_ptr;
  uint16_t      free_segments;
  uint16_t      alloc_segments;

  /* Initialize the return values.                                  */
  *used         = 0;
  *free         = 0;
  *max_free      = 0;
  free_segments  = 0;
  alloc_segments = 0;
  heap_info_ptr   = heap->begin;

  do {
    /* Check to see if the current fragment is marked as free.     */
    if(heap_info_ptr->is_allocated) {
      alloc_segments++;

      *used += heap_info_ptr->Size * ALIGNMENT_SIZE;
    } else {
      free_segments++;

      /* Accumulate the total number of free Bytes.               */
      *free += heap_info_ptr->Size * ALIGNMENT_SIZE;

      /* Check to see if the current fragment is larger that any  */
      /* we have seen and update the Max Value if it is larger.   */
      if(heap_info_ptr->Size > *max_free) {
        *max_free = heap_info_ptr->Size * ALIGNMENT_SIZE;
      }
    }

    /* Adjust the pointer to the next entry.                       */
    heap_info_ptr = get_next_block(heap, heap_info_ptr);

  } while (heap_info_ptr < heap->end);

  UTIL_ASSERT(heap_info_ptr == heap->end);

  char format_str[80];
  snprintf(format_str, sizeof(format_str), "alloc: %u (%u bytes), free: %u (%u bytes)",
           alloc_segments, *used, free_segments, *free);
  util_dbgserial_str(format_str);
}

void heap_calc_totals(Heap* const heap, unsigned int *used, unsigned int *free, unsigned int *max_free) {
  UTIL_ASSERT(heap);

  /* Verify that the parameters that were passed in appear valid.      */
  if((used) && (free) && (max_free) && (heap->begin)) {
    heap_lock(heap);
    prv_calc_totals(heap, used, free, max_free);
    heap_unlock(heap);
  }
}

void heap_init(Heap* const heap, void* start, void* end, bool fuzz_on_free) {
  UTIL_ASSERT(start && end);

  // Align the pointer by advancing it to the next boundary.
  start = (void*)((((uintptr_t) start) + (sizeof(Alignment_t) - 1)) & ~(sizeof(Alignment_t) - 1));
  end = (void*) (((uintptr_t) end) & ~(sizeof(Alignment_t) - 1));

  // Calculate the size of the heap in alignment units.
  uint32_t heap_size = ((uintptr_t)end - (uintptr_t)start) / ALIGNMENT_SIZE;
  // If we have more space than we can use, just limit it to the usable space. This limit is caused
  // by the width of .Size and .PrevSize in HeapInfo_t
  heap_size = MIN(SEGMENT_SIZE_MAX, heap_size);
  end = ((char*) start) + (heap_size * ALIGNMENT_SIZE);

  memset(start, 0, heap_size * ALIGNMENT_SIZE);

  *heap = (Heap) {
    .begin = start,
    .end = end,
    .fuzz_on_free = fuzz_on_free
  };

  *(heap->begin) = (HeapInfo_t) {
    .PrevSize = heap_size,
    .is_allocated = false,
    .Size = heap_size
  };
}

void heap_set_lock_impl(Heap *heap, HeapLockImpl lock_impl) {
  heap->lock_impl = lock_impl;
}

void heap_set_double_free_handler(Heap *heap, DoubleFreeHandler double_free_handler) {
  heap->double_free_handler = double_free_handler;
}

void heap_set_corruption_handler(Heap *heap, CorruptionHandler corruption_handler) {
  heap->corruption_handler = corruption_handler;
}

void *heap_malloc(Heap* const heap, unsigned long nbytes, uintptr_t client_pc) {
  // Check to make sure the heap we have is initialized.
  UTIL_ASSERT(heap->begin);

  /* Convert the requested memory allocation in bytes to alignment     */
  /* size.                                                             */
  unsigned long allocation_size = (nbytes + (ALIGNMENT_SIZE - 1)) / ALIGNMENT_SIZE;

  /* Add the beginer size to the requested size.                        */
  allocation_size += HEAP_INFO_BLOCK_SIZE(0);

  /* Verify that the requested size is valid                           */
  if ((allocation_size < HEAP_INFO_BLOCK_SIZE(1)) || (allocation_size >= SEGMENT_SIZE_MAX)) {
    return NULL;
  }

  HeapInfo_t* allocated_block;

  heap_lock(heap);
  {
    HeapInfo_t* free_block = find_segment(heap, allocation_size);
    allocated_block = allocate_block(heap, allocation_size, free_block);

    if (allocated_block != NULL) {
      // We've allocated a new block, update our metrics

#ifdef MALLOC_INSTRUMENTATION
      allocated_block->pc = client_pc;
#endif

      heap->current_size += allocated_block->Size * ALIGNMENT_SIZE;
      if (heap->current_size > heap->high_water_mark) {
        heap->high_water_mark = heap->current_size;
      }
    }
  }
  heap_unlock(heap);

  if (allocated_block) {
    return &allocated_block->Data;
  }
  return NULL;
}

void heap_free(Heap* const heap, void *ptr, uintptr_t client_pc) {
  UTIL_ASSERT(heap->begin);

  if (!ptr) {
    // free(0) is a no-op, just bail
    return;
  }

  UTIL_ASSERT(heap_contains_address(heap, ptr)); // <ptr> not in range (heap->begin, heap->end)

  heap_lock(heap);
  {
    /* Get a pointer to the Heap Info.                             */
    HeapInfo_t *heap_info_ptr = HEAP_INFO_FOR_PTR(ptr);

    /* Verify that this segment is allocated.                      */
    if (!heap_info_ptr->is_allocated) {
      // If not, try to the call the handler if present. If there's no handler, just explode.
      // If there is a handler, let them know this happened and then just no-op and fast return.
      if (heap->double_free_handler) {
        heap_unlock(heap);

        heap->double_free_handler(ptr);

        return;
      }

      UTIL_ASSERT(0); // heap_free on invalid pointer <ptr>
    }

    /* Mask out the allocation bit of the segment to be freed.  */
    /* This will make calculations in this block easier.        */
    heap_info_ptr->is_allocated = false;

#ifndef RELEASE
    if (heap->fuzz_on_free) {
      memset(ptr, 0xBD,
             (heap_info_ptr->Size - HEAP_INFO_BLOCK_SIZE(0)) * ALIGNMENT_SIZE);
    }
#endif

    // Update metrics
#ifdef MALLOC_INSTRUMENTATION
    heap_info_ptr->pc = client_pc;
#endif
    heap->current_size -= heap_info_ptr->Size * ALIGNMENT_SIZE;

    /* If the segment to be freed is at the begin of the heap,   */
    /* then we do not have to merge or update any sizes of the  */
    /* previous segment.  This will also handle the case where  */
    /* the entire heap has been allocated to one segment.       */
    if(heap_info_ptr != heap->begin) {
      /* Calculate the pointer to the previous segment.        */
      HeapInfo_t *previous_block = get_previous_block(heap, heap_info_ptr);

      HEAP_ASSERT_SANE(heap, previous_block->Size == heap_info_ptr->PrevSize, heap_info_ptr);

      /* Check to see if the previous segment can be combined. */
      if(!previous_block->is_allocated) {
        /* Add the segment to be freed to the new beginer.     */
        previous_block->Size += heap_info_ptr->Size;

        /* Set the pointer to the beginning of the previous   */
        /* free segment.                                      */
        heap_info_ptr = previous_block;
      }
    }

    /* Calculate the pointer to the next segment.               */
    HeapInfo_t *next_block = get_next_block(heap, heap_info_ptr);

    /* If we are pointing at the end of the heap, then use the  */
    /* begin as the next segment.                                */
    if(next_block == heap->end) {
      /* We can't combine the begin with the end, so just      */
      /* update the PrevSize field.                            */
      heap->begin->PrevSize = heap_info_ptr->Size;
    }
    else {
      HEAP_ASSERT_SANE(heap, next_block->PrevSize == (HEAP_INFO_FOR_PTR(ptr))->Size, next_block);

      /* We are not pointing to the end of the heap, so if the */
      /* next segment is allocated then update the PrevSize    */
      /* field.                                                */
      if (next_block->is_allocated) {
        next_block->PrevSize = heap_info_ptr->Size;
      } else {
        /* The next segment is free, so merge it with the     */
        /* current segment.                                   */
        heap_info_ptr->Size += next_block->Size;

        /* Since we merged the next segment, we have to update*/
        /* the next next segment's PrevSize field.            */
        HeapInfo_t *next_next_block = get_next_block(heap, heap_info_ptr);

        /* If we are pointing at the end of the heap, then use*/
        /* the begin as the next next segment.                 */
        if(next_next_block == heap->end) {
          heap->begin->PrevSize = heap_info_ptr->Size;
        }
        else {
          next_next_block->PrevSize = heap_info_ptr->Size;
        }
      }
    }
  }
  heap_unlock(heap);
}

bool heap_is_allocated(Heap* const heap, void* ptr) {
  bool rc = false;
  if (!heap_contains_address(heap, ptr)) {
    return rc;
  }

  HeapInfo_t *heap_info_ptr = heap->begin;

  // Iterate through the heap to see if this pointer is still allocated.
  heap_lock(heap);
  while (heap_info_ptr < heap->end) {
    if (heap_info_ptr == HEAP_INFO_FOR_PTR(ptr)) {
      rc = heap_info_ptr->is_allocated;
      break;
    }
    heap_info_ptr = get_next_block(heap, heap_info_ptr);
  }
  heap_unlock(heap);

  return rc;
}

bool heap_contains_address(Heap* const heap, void* ptr) {
  return (ptr >= (void*) heap->begin && ptr < (void*) heap->end);
}

size_t heap_size(const Heap *heap) {
  return ((char*) heap->end) - ((char*) heap->begin);
}

static void prv_sanity_check_block(Heap * const heap, HeapInfo_t *block) {
  HeapInfo_t* prev_block = get_previous_block(heap, block);
  HEAP_ASSERT_SANE(heap,
      prev_block <= heap->begin || prev_block->Size == block->PrevSize, block);
  HeapInfo_t* next_block = get_next_block(heap, block);
  HEAP_ASSERT_SANE(heap, next_block >= heap->end || next_block->PrevSize == block->Size, block);
}

//! Finds a segment where data of the size n_units  will fit.
//!     @param n_units number of ALIGNMENT_SIZE units this segment requires.
static HeapInfo_t *find_segment(Heap* const heap, unsigned long n_units) {
  HeapInfo_t *heap_info_ptr = NULL;
  /* If we are allocating a large segment, then start at the  */
  /* end of the heap.  Otherwise, start at the beginning of   */
  /* the heap.  If there is only one segment in the heap, then*/
  /* heap->begin will be used either way.                      */
  if (n_units >= LARGE_SIZE) {
    heap_info_ptr = (HeapInfo_t *)(((Alignment_t *)heap->end) - heap->begin->PrevSize);
  } else {
    heap_info_ptr = heap->begin;
  }

  /* Loop until we have walked the entire list.               */
  while (((n_units < LARGE_SIZE) || (heap_info_ptr > heap->begin)) && (heap_info_ptr < heap->end)) {

    prv_sanity_check_block(heap, heap_info_ptr);

    /* Check to see if the current entry is free and is large*/
    /* enough to hold the data being requested.              */
    if (heap_info_ptr->is_allocated || (heap_info_ptr->Size < n_units)) {
      /* If the requested size is larger than the limit then*/
      /* search backwards for an available buffer, else go  */
      /* forward.  This will hopefully help to reduce       */
      /* fragmentataion problems.                           */
      if (n_units >= LARGE_SIZE) {
        heap_info_ptr = get_previous_block(heap, heap_info_ptr);
      } else {
        heap_info_ptr = get_next_block(heap, heap_info_ptr);
      }
    } else {
      break;
    }
  }

  // make sure the space we found is within the bounds of the heap
  UTIL_ASSERT((heap_info_ptr >= heap->begin) &&
      (heap_info_ptr <= heap->end));

  return heap_info_ptr;
}

//! Split a block into two smaller blocks, returning a pointer to the new second block.
//! The first block will be available at the same location as before, but with a smaller size.
//! Assumes the block is big enough to be split and is unallocated.
//! @param first_part_size the size of the new block, in ALIGNMENT_SIZE units, including the beginer
static HeapInfo_t* split_block(Heap *heap, HeapInfo_t* block, size_t first_part_size) {
  HeapInfo_t* second_block = (HeapInfo_t*) (((Alignment_t*) block) + first_part_size);

  second_block->PrevSize = first_part_size;
  second_block->is_allocated = false;
  second_block->Size = block->Size - first_part_size;

  block->Size = first_part_size;

  /* Calculate the pointer to the next segment.         */
  HeapInfo_t* next_next_block = get_next_block(heap, second_block);

  /* Check for a wrap condition and update the next     */
  /* segment's PrevSize field.                          */
  if (next_next_block == heap->end) {
    heap->begin->PrevSize = second_block->Size;
  } else {
    next_next_block->PrevSize = second_block->Size;
  }

  return second_block;
}

//! Allocated the block in the given HeapInfo_t segment.
//!     @param n_units number of ALIGNMENT_SIZE units this segment requires (including space for the beginer).
//!     @param heap_info_ptr the segment where the block should be allocated.
static HeapInfo_t *allocate_block(Heap* const heap, unsigned long n_units, HeapInfo_t* heap_info_ptr) {
  // Make sure we can use all or part of this block for this allocation.
  if (heap_info_ptr == heap->end || heap_info_ptr->is_allocated || heap_info_ptr->Size < n_units) {
    return NULL;
  }

  /* Check to see if we need to split this into two        */
  /* entries.                                              */
  /* * NOTE * If there is not enough room to make another  */
  /*          entry then we will not adjust the size of    */
  /*          this entry to match the amount requested.    */
  if (heap_info_ptr->Size < (n_units + HEAP_INFO_BLOCK_SIZE(MINIMUM_MEMORY_SIZE))) {
    // Nope! Just use the whole block.
    heap_info_ptr->is_allocated = true;
    return heap_info_ptr;
  }

  /* If this is a large segment allocation, then split  */
  /* the segment so that the free segment is at the     */
  /* beginning.                                         */
  if (n_units >= LARGE_SIZE) {
    HeapInfo_t *second_block = split_block(heap, heap_info_ptr, heap_info_ptr->Size - n_units);
    second_block->is_allocated = true;
    return second_block;
  }

  split_block(heap, heap_info_ptr, n_units);
  heap_info_ptr->is_allocated = true;
  return heap_info_ptr;
}

// A very naive realloc implementation
void* heap_realloc(Heap* const heap, void *ptr, unsigned long nbytes, uintptr_t client_pc) {
#if !defined(MALLOC_INSTRUMENTATION)
  client_pc = 0;
#endif
  // Get a pointer to the Heap Info.
  void *new_ptr = heap_malloc(heap, nbytes, client_pc);
  if (new_ptr && ptr) {
    // Copy over old data.
    const HeapInfo_t *heap_info_ptr = HEAP_INFO_FOR_PTR(ptr);
    const uint16_t original_size = heap_info_ptr->Size * ALIGNMENT_SIZE;
    memcpy(new_ptr, ptr, MIN(nbytes, original_size));
    heap_free(heap, ptr, client_pc);
  }
  return new_ptr;
}

void* heap_zalloc(Heap* const heap, size_t size, uintptr_t client_pc) {
  void *ptr = heap_malloc(heap, size, client_pc);
  if (ptr) {
    memset(ptr, 0, size);
  }
  return ptr;
}

void* heap_calloc(Heap* const heap, size_t count, size_t size, uintptr_t client_pc) {
  return heap_zalloc(heap, count * size, client_pc);
}

uint32_t heap_get_minimum_headroom(Heap *heap) {
  return (heap_size(heap) - heap->high_water_mark);
}

// Serial Commands
///////////////////////////////////////////////////////////
#ifdef MALLOC_INSTRUMENTATION
void heap_dump_malloc_instrumentation_to_dbgserial(Heap *heap) {
  char buffer[80];
  unsigned long num_free_blocks = 0;
  unsigned long num_free_bytes = 0;
  unsigned long num_alloc_blocks = 0;
  unsigned long num_alloc_bytes = 0;
  unsigned long largest_free = 0;

  // The output in this function is parsed by tools/parse_dump_malloc.py, so don't change it
  // without updating that file as well.

  HeapInfo_t* heap_iter = heap->begin;

  heap_lock(heap);
  void* pc;
  char* type;
  while (heap_iter < heap->end) {
    unsigned long block_size = (long)(heap_iter->Size) * ALIGNMENT_SIZE;

    if (heap_iter->is_allocated) {
      pc = (void *)heap_iter->pc;
      type = "";
      num_alloc_blocks++;
      num_alloc_bytes += block_size;
    } else {
      pc = NULL;
      type = "FREE";
      num_free_blocks++;
      num_free_bytes += block_size;
      largest_free = MAX(largest_free, block_size);
    }

    snprintf(buffer, sizeof(buffer), "PC:0x%08lX Addr:0x%08lX Bytes:%-8lu %s",
                         (long)pc, (long)&heap_iter->Data, block_size, type);
    util_dbgserial_str(buffer);
    heap_iter = get_next_block(heap, heap_iter);
  }

  snprintf(buffer, sizeof(buffer), "Heap start %p", heap->begin);
  util_dbgserial_str(buffer);

  snprintf(buffer, sizeof(buffer), "Heap end %p", heap->end);
  util_dbgserial_str(buffer);

  snprintf(buffer, sizeof(buffer), "Heap total size %zu", heap_size(heap));
  util_dbgserial_str(buffer);

  snprintf(buffer, sizeof(buffer), "Heap allocated %u", heap->current_size);
  util_dbgserial_str(buffer);

  snprintf(buffer, sizeof(buffer), "Heap high water mark %u", heap->high_water_mark);
  util_dbgserial_str(buffer);

  snprintf(buffer, sizeof(buffer), "Heap free blocks: %lu bytes, %lu blocks", num_free_bytes,
           num_free_blocks);
  util_dbgserial_str(buffer);

  snprintf(buffer, sizeof(buffer), "Heap alloc blocks: %lu bytes, %lu blocks", num_alloc_bytes,
           num_alloc_blocks);
  util_dbgserial_str(buffer);

  snprintf(buffer, sizeof(buffer), "Heap largest free block: %lu", largest_free);
  util_dbgserial_str(buffer);

  heap_unlock(heap);

}
#endif
