#pragma once

#include "../ds/dllist.h"
#include "../ds/helpers.h"
#include "sizeclass.h"

namespace snmalloc
{
  class Slab;

  struct SlabLink
  {
    SlabLink* prev;
    SlabLink* next;

    Slab* get_slab()
    {
      return pointer_cast<Slab>(address_cast(this) & SLAB_MASK);
    }
  };

  using SlabList = DLList<SlabLink>;

  static_assert(
    sizeof(SlabLink) <= MIN_ALLOC_SIZE,
    "Need to be able to pack a SlabLink into any free small alloc");

  // The Metaslab represent the status of a single slab.
  // This can be either a short or a standard slab.
  class Metaslab
  {
  public:
    // How many entries are not in the free list of  slab.
    uint16_t used = 0;

    // How many entries have been allocated from this slab.
    uint16_t allocated;

    // Index of first entry in this slab that forms the free
    // list.  The list entries are stored as the first pointer
    // in each unused object. The terminator is a pointer or
    // offset into the block with the bottom bit set.  This means
    //  I.e.
    //    * an empty list has a head of 1.
    //    * a one element list has an head contains an offset to this
    //       this block, and then contains a pointer with the bottom
    //       bit set.
    Mod<SLAB_SIZE, uint16_t> head;

    // When a slab has free space it will be on the has space list for
    // that size class.  We use an empty block in this slab to be the
    // doubly linked node into that size class's free list.
    Mod<SLAB_SIZE, uint16_t> link;

    uint8_t sizeclass;
    // Initially zero to encode the superslabs relative list of slabs.
    uint8_t next = 0;

    void add_use()
    {
      used++;
    }

    void sub_use()
    {
      used--;
    }

    void set_unused()
    {
      used = 0;
    }

    bool is_unused()
    {
      return used == 0;
    }

    bool is_full()
    {
      return link == 1;
    }

    void set_full()
    {
      assert(head == 1);
      assert(link != 1);
      link = 1;
    }

    SlabLink* get_link(Slab* slab)
    {
      return reinterpret_cast<SlabLink*>(pointer_offset(slab, link));
    }

    /// Value used to check for corruptions in a block
    static constexpr size_t POISON =
      static_cast<size_t>(bits::is64() ? 0xDEADBEEFDEADBEEF : 0xDEADBEEF);

    /// Store next pointer in a block. In Debug using magic value to detect some
    /// simple corruptions.
    static void store_next(void* p, void* head)
    {
#ifndef CHECK_CLIENT
      *static_cast<void**>(p) = head;
#else
      *static_cast<void**>(p) = head;
      *(static_cast<uintptr_t*>(p) + 1) = address_cast(head) ^ POISON;
#endif
    }

    /// Accessor function for the next pointer in a block.
    /// In Debug checks for simple corruptions.
    static void* follow_next(void* node)
    {
#ifdef CHECK_CLIENT
      uintptr_t next = *static_cast<uintptr_t*>(node);
      uintptr_t chk = *(static_cast<uintptr_t*>(node) + 1);
      if ((next ^ chk) != POISON)
        error("Detected memory corruption.  Use-after-free.");
#endif
      return *static_cast<void**>(node);
    }

    bool valid_head(bool is_short)
    {
      size_t size = sizeclass_to_size(sizeclass);
      size_t slab_start = get_initial_offset(sizeclass, is_short);
      size_t all_high_bits = ~static_cast<size_t>(1);

      size_t head_start =
        remove_cache_friendly_offset(head & all_high_bits, sizeclass);

      return ((head_start - slab_start) % size) == 0;
    }

    /**
     * Check bump-free-list-segment for cycles
     *
     * Using
     * https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_Tortoise_and_Hare
     * We don't expect a cycle, so worst case is only followed by a crash, so
     * slow doesn't mater.
     **/
    size_t debug_slab_acyclic_free_list(Slab* slab)
    {
#ifndef NDEBUG
      size_t length = 0;
      void* curr = pointer_offset(slab, head);
      void* curr_slow = pointer_offset(slab, head);
      bool both = false;
      while ((reinterpret_cast<size_t>(curr) & 1) == 0)
      {
        curr = follow_next(curr);
        if (both)
        {
          curr_slow = follow_next(curr_slow);
        }

        if (curr == curr_slow)
        {
          error("Free list contains a cycle, typically indicates double free.");
        }

        both = !both;
        length++;
      }
      return length;
#else
      UNUSED(slab);
      return 0;
#endif
    }

    void debug_slab_invariant(bool is_short, Slab* slab)
    {
#if !defined(NDEBUG) && !defined(SNMALLOC_CHEAP_CHECKS)
      size_t size = sizeclass_to_size(sizeclass);
      size_t offset = get_initial_offset(sizeclass, is_short);

      if (is_unused())
        return;

      size_t accounted_for = used * size + offset;

      if (is_full())
      {
        // All the blocks must be used.
        assert(SLAB_SIZE == accounted_for);
        // There is no free list to validate
        // 'link' value is not important if full.
        return;
      }

      // Block is not full
      assert(SLAB_SIZE > accounted_for);

      size_t length = debug_slab_acyclic_free_list(slab);
      UNUSED(length);
      // Walk bump-free-list-segment accounting for unused space
      void* curr = pointer_offset(slab, head);
      while ((reinterpret_cast<size_t>(curr) & 1) == 0)
      {
        // Check we are looking at a correctly aligned block
        void* start = curr;
        assert(
          ((address_cast(start) - address_cast(slab) - offset) % size) == 0);

        // Account for free elements in free list
        accounted_for += size;
        assert(SLAB_SIZE >= accounted_for);
        // We should never reach the link node in the free list.
        assert(curr != pointer_offset(slab, link));

        // Iterate bump/free list segment
        curr = follow_next(curr);
      }

      auto bumpptr = (allocated * size) + offset;
      // Check we haven't allocaated more than gits in a slab
      assert(bumpptr <= SLAB_SIZE);

      // Account for to be bump allocated space
      accounted_for += SLAB_SIZE - bumpptr;

      if (bumpptr != SLAB_SIZE)
      {
        // The link should be the first allocation as we
        // haven't completely filled this block at any point.
        assert(link == get_initial_offset(sizeclass, is_short));
      }

      assert(!is_full());
      // Add the link node.
      accounted_for += size;

      // All space accounted for
      assert(SLAB_SIZE == accounted_for);
#else
      UNUSED(slab);
      UNUSED(is_short);
#endif
    }
  };
} // namespace snmalloc
