#include <stdarg.h>
#define _DEFAULT_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <assert.h>

#include "mem_internals.h"
#include "mem.h"
#include "util.h"

void debug_block(struct block_header* b, const char* fmt, ... );
void debug(const char* fmt, ... );

extern inline block_size size_from_capacity( block_capacity cap );
extern inline block_capacity capacity_from_size( block_size sz );

static bool            block_is_big_enough( size_t query, struct block_header* block ) { return block->capacity.bytes >= query; }
static size_t          pages_count   ( size_t mem )                      { return mem / getpagesize() + ((mem % getpagesize()) > 0); }
static size_t          round_pages   ( size_t mem )                      { return getpagesize() * pages_count( mem ) ; }

static void block_init( void* restrict addr, block_size block_sz, void* restrict next ) {
  *((struct block_header*)addr) = (struct block_header) {
    .next = next,
    .capacity = capacity_from_size(block_sz),
    .is_free = true
  };
}

static size_t region_actual_size( size_t query ) { return size_max( round_pages( query ), REGION_MIN_SIZE ); }

extern inline bool region_is_invalid( const struct region* r );

void* map_pages(void const* addr, size_t length, int additional_flags) {
  return mmap( (void*) addr, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | additional_flags , 0, 0 );
}

/*  аллоцировать регион памяти и инициализировать его блоком */
static struct region alloc_region(void const* addr, size_t query) {
  struct region allocated_region;
  allocated_region.addr = map_pages(addr, query, MAP_FIXED_NOREPLACE );

  size_t region_size = region_actual_size(query);

  if (allocated_region.addr == MAP_FAILED || allocated_region.addr == NULL) {
      allocated_region = (struct region) { map_pages(addr, query, 0), region_size, false };
  } else {
      allocated_region = (struct region) { allocated_region.addr, region_size, true };
  }

  block_init(allocated_region.addr, (block_size) { region_size }, NULL);

  return allocated_region;
}

static void* block_after( struct block_header const* block )         ;

void* heap_init( size_t initial ) {
  const struct region region = alloc_region( HEAP_START, initial );
  if ( region_is_invalid(&region) ) return NULL;

  return region.addr;
}

#define BLOCK_MIN_CAPACITY 24

/*  --- Разделение блоков (если найденный свободный блок слишком большой )--- */

static bool block_splittable(struct block_header* restrict block, size_t query) {
  return block-> is_free && query + offsetof( struct block_header, contents ) + BLOCK_MIN_CAPACITY <= block->capacity.bytes;
}

static bool split_if_too_big(struct block_header* block, size_t query) {
  if (block_splittable(block, query)) {
    const block_size new_size = { .bytes = block->capacity.bytes - (query + offsetof(struct block_header, contents)) };
    block->capacity.bytes = query;
    block_init(block_after(block), new_size, block->next);
    block->next = block_after(block);

    return  true;
  }

  return false;
}

/*  --- Слияние соседних свободных блоков --- */

static void* block_after(struct block_header const* block) {
  return  (void*) (block->contents + block->capacity.bytes);
}
static bool blocks_continuous (struct block_header const* fst,
                               struct block_header const* snd ) {
  return (void*)snd == block_after(fst);
}

static bool mergeable(struct block_header const* restrict fst, struct block_header const* restrict snd) {
  return fst->is_free && snd->is_free && blocks_continuous( fst, snd ) ;
}

static bool try_merge_with_next(struct block_header* block) {
  if (block->next && mergeable(block, block->next)) {
    block->capacity.bytes = block->capacity.bytes + size_from_capacity(block->next->capacity).bytes;
    block->next = block->next->next;

    return true;
  }

  return false;
}

/*  --- ... ecли размера кучи хватает --- */

struct block_search_result {
  enum {BSR_FOUND_GOOD_BLOCK, BSR_REACHED_END_NOT_FOUND, BSR_CORRUPTED} type;
  struct block_header* block;
};

static struct block_search_result find_good_or_last (struct block_header* restrict block, size_t sz) {
  struct block_header* current_block;
  
  while (block != NULL) {
    while (try_merge_with_next(block));
    if (block->is_free && block_is_big_enough(sz, block)) {
      return (struct block_search_result){ .type = BSR_FOUND_GOOD_BLOCK, .block = block };
    }
    current_block = block;
    block = block->next;
  }

  return (struct block_search_result) { .type=BSR_REACHED_END_NOT_FOUND, .block=current_block};
}

/*  Попробовать выделить память в куче начиная с блока `block` не пытаясь расширить кучу
 Можно переиспользовать как только кучу расширили. */
static struct block_search_result try_memalloc_existing ( size_t query, struct block_header* block ) {
  struct block_search_result result = find_good_or_last(block, query);

  if (result.type == BSR_FOUND_GOOD_BLOCK) {
    split_if_too_big(result.block, query);
    result.block->is_free = false;
  }

  return result;
}

static struct block_header *grow_heap(struct block_header* restrict last, size_t query) {
  const struct region new_region = alloc_region(block_after(last), query);
  last->next = new_region.addr;

  if(try_merge_with_next(last)) {
    return last;
  } else {
    return last->next;
  }
}

/*  Реализует основную логику malloc и возвращает заголовок выделенного блока */

static struct block_header* memalloc(size_t query, struct block_header* heap_start) {
  struct block_search_result result = try_memalloc_existing(query, heap_start);

  if (result.type != BSR_FOUND_GOOD_BLOCK) {
    result = try_memalloc_existing(query, grow_heap(result.block, query));
  }

  return result.block;
}

void* _malloc(size_t query) {
  struct block_header* const addr = memalloc(query, (struct block_header*) HEAP_START);
  if (addr) {
    addr->is_free = false;
    return addr->contents;
  } else {
    return NULL;
  }
}

static struct block_header* block_get_header(void* contents) {
  return (struct block_header*) (((uint8_t*)contents)-offsetof(struct block_header, contents));
}

void _free( void* mem ) {
  if (!mem) return;
  struct block_header* header = block_get_header(mem);
  header->is_free = true;
  while(try_merge_with_next(header));
}