/*
 * Copyright (c) 2018-2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#ifndef PAS_SEGREGATED_DIRECTORY_H
#define PAS_SEGREGATED_DIRECTORY_H

#include "pas_bitvector.h"
#include "pas_bootstrap_free_heap.h"
#include "pas_compact_atomic_segregated_view.h"
#include "pas_config.h"
#include "pas_immutable_vector.h"
#include "pas_lock.h"
#include "pas_page_sharing_mode.h"
#include "pas_page_sharing_participant.h"
#include "pas_segmented_vector.h"
#include "pas_segregated_directory_first_eligible_kind.h"
#include "pas_segregated_page_config_kind.h"
#include "pas_segmented_vector.h"
#include "pas_segregated_view.h"
#include "pas_versioned_field.h"

PAS_BEGIN_EXTERN_C;

#ifndef pas_heap
#define pas_heap __pas_heap
#endif

struct pas_heap;
struct pas_segregated_heap;
struct pas_segregated_page;
struct pas_segregated_page_config;
struct pas_segregated_directory;
struct pas_segregated_directory_bitvector_segment;
struct pas_segregated_directory_simple_bitvector_segment;
struct pas_segregated_directory_data;
struct pas_page_sharing_pool;
typedef struct pas_heap pas_heap;
typedef struct pas_segregated_heap pas_segregated_heap;
typedef struct pas_segregated_page pas_segregated_page;
typedef struct pas_segregated_page_config pas_segregated_page_config;
typedef struct pas_segregated_directory pas_segregated_directory;
typedef struct pas_segregated_directory_bitvector_segment pas_segregated_directory_bitvector_segment;
typedef struct pas_segregated_directory_simple_bitvector_segment pas_segregated_directory_simple_bitvector_segment;
typedef struct pas_segregated_directory_data pas_segregated_directory_data;
typedef struct pas_page_sharing_pool pas_page_sharing_pool;

struct pas_segregated_directory_bitvector_segment {
    /* Eligible means that the page is available for taking, either to:
       - Allocate in,
       - Decommit, or
       - Commit and then allocate in. */
    unsigned eligible_bits;
    
    /* Empty means that the page has stuff that can be decommitted. However, this bit has no meaning if
       the eligible bit is not set. */
    unsigned empty_bits;
};

/* FIXME: Attempt to introduce code to pack the bitvector if we don't need the empty bits. Currently
   this is not used, but should be. */
struct pas_segregated_directory_simple_bitvector_segment {
    unsigned eligible_bits;
};

#define PAS_SEGREGATED_DIRECTORY_BITVECTOR_SEGMENT_INITIALIZER \
    ((pas_segregated_directory_bitvector_segment){ \
        .eligible_bits = 0, \
        .empty_bits = 0 \
    })

#define PAS_SEGREGATED_DIRECTORY_SIMPLE_BITVECTOR_SEGMENT_INITIALIZER \
    ((pas_segregated_directory_simple_bitvector_segment){ \
        .eligible_bits = 0 \
    })

PAS_DECLARE_SEGMENTED_VECTOR(pas_segregated_directory_segmented_bitvectors,
                             pas_segregated_directory_bitvector_segment,
                             4);
PAS_DECLARE_SEGMENTED_VECTOR(pas_segregated_directory_simple_segmented_bitvectors,
                             pas_segregated_directory_simple_bitvector_segment,
                             16);
PAS_DECLARE_IMMUTABLE_VECTOR(pas_segregated_directory_view_vector,
                             pas_compact_atomic_segregated_view);

PAS_DEFINE_COMPACT_ATOMIC_PTR(pas_segregated_directory_data, pas_segregated_directory_data_ptr);

PAS_DEFINE_COMPACT_TAGGED_ATOMIC_PTR(uintptr_t, pas_segregated_directory_sharing_payload_ptr);

struct pas_segregated_directory {
    pas_compact_atomic_segregated_view first_view;
    pas_segregated_directory_data_ptr data;
    unsigned bits;
    pas_segregated_page_config_kind page_config_kind : 8;
    pas_page_sharing_mode sharing_mode : 8;
    pas_segregated_directory_kind directory_kind : 8;
    bool is_basic_size_directory; /* This field is owned by the segregated_heap. */
};

#define PAS_SEGREGATED_DIRECTORY_BITS_ELIGIBLE_SHIFT       0u
#define PAS_SEGREGATED_DIRECTORY_BITS_EMPTY_SHIFT          1u

#define PAS_SEGREGATED_DIRECTORY_BITS_MISC_SHIFT           2u
#define PAS_SEGREGATED_DIRECTORY_BITS_OTHER_MISC_SHIFT     3u

#define PAS_SEGREGATED_DIRECTORY_BITS_ELIGIBLE_MASK \
    (1u << PAS_SEGREGATED_DIRECTORY_BITS_ELIGIBLE_SHIFT)
#define PAS_SEGREGATED_DIRECTORY_BITS_EMPTY_MASK \
    (1u << PAS_SEGREGATED_DIRECTORY_BITS_EMPTY_SHIFT)

#define PAS_SEGREGATED_DIRECTORY_BITS_VIEW_MASK \
    (PAS_SEGREGATED_DIRECTORY_BITS_ELIGIBLE_MASK | \
     PAS_SEGREGATED_DIRECTORY_BITS_EMPTY_MASK)

#define PAS_SEGREGATED_DIRECTORY_BITS_MISC_MASK \
    (1u << PAS_SEGREGATED_DIRECTORY_BITS_MISC_SHIFT)
#define PAS_SEGREGATED_DIRECTORY_BITS_OTHER_MISC_MASK \
    (1u << PAS_SEGREGATED_DIRECTORY_BITS_OTHER_MISC_SHIFT)

/* This makes the GET_BIT/SET_BIT macros work. */
#define PAS_SEGREGATED_DIRECTORY_BITS_eligible_MASK \
    PAS_SEGREGATED_DIRECTORY_BITS_ELIGIBLE_MASK
#define PAS_SEGREGATED_DIRECTORY_BITS_empty_MASK \
    PAS_SEGREGATED_DIRECTORY_BITS_EMPTY_MASK

struct PAS_ALIGNED(sizeof(pas_versioned_field)) pas_segregated_directory_data {
    pas_versioned_field first_eligible;
    pas_versioned_field last_empty_plus_one; /* Zero means there aren't any. */

    pas_segregated_directory_segmented_bitvectors bitvectors;
    pas_segregated_directory_view_vector views;
    pas_segregated_directory_sharing_payload_ptr sharing_payload;
};

#define PAS_SEGREGATED_DIRECTORY_SHARING_PAYLOAD_IS_INITIALIZED_BIT 1lu

#define PAS_SEGREGATED_DIRECTORY_INITIALIZER(page_config_kind_argument, sharing_mode_argument, directory_kind_argument) \
    ((pas_segregated_directory){ \
        .first_view = PAS_COMPACT_ATOMIC_PTR_INITIALIZER, \
        .data = PAS_COMPACT_ATOMIC_PTR_INITIALIZER, \
        .bits = 0, \
        .page_config_kind = page_config_kind_argument, \
        .sharing_mode = (sharing_mode_argument), \
        .directory_kind = (directory_kind_argument), \
        .is_basic_size_directory = false \
    })

static inline size_t pas_segregated_directory_size(pas_segregated_directory* directory)
{
    pas_segregated_directory_data* data;
    if (!pas_compact_atomic_segregated_view_load(&directory->first_view))
        return 0;
    data = pas_segregated_directory_data_ptr_load(&directory->data);
    if (data)
        return data->views.size + 1;
    return 1;
}

static inline bool pas_segregated_directory_bits_get_by_mask(
    pas_segregated_directory* directory, unsigned mask)
{
    return !!(directory->bits & mask);
}

static inline bool pas_segregated_directory_bits_set_by_mask(
    pas_segregated_directory* directory,
    unsigned mask,
    bool value)
{
    for (;;) {
        unsigned old_bits;
        unsigned new_bits;

        old_bits = directory->bits;
        
        if (value)
            new_bits = old_bits | mask;
        else
            new_bits = old_bits & ~mask;

        if (new_bits == old_bits)
            return false;

        if (pas_compare_and_swap_uint32_weak(&directory->bits, old_bits, new_bits))
            return true;
    }
}

static inline bool pas_segregated_directory_get_misc_bit(pas_segregated_directory* directory)
{
    return pas_segregated_directory_bits_get_by_mask(
        directory, PAS_SEGREGATED_DIRECTORY_BITS_MISC_MASK);
}

static inline bool pas_segregated_directory_set_misc_bit(pas_segregated_directory* directory,
                                                         bool value)
{
    return pas_segregated_directory_bits_set_by_mask(
        directory, PAS_SEGREGATED_DIRECTORY_BITS_MISC_MASK, value);
}

static inline bool pas_segregated_directory_get_other_misc_bit(pas_segregated_directory* directory)
{
    return pas_segregated_directory_bits_get_by_mask(
        directory, PAS_SEGREGATED_DIRECTORY_BITS_OTHER_MISC_MASK);
}

static inline bool pas_segregated_directory_set_other_misc_bit(pas_segregated_directory* directory,
                                                               bool value)
{
    return pas_segregated_directory_bits_set_by_mask(
        directory, PAS_SEGREGATED_DIRECTORY_BITS_OTHER_MISC_MASK, value);
}

static PAS_ALWAYS_INLINE pas_segregated_directory_bitvector_segment
pas_segregated_directory_spoof_inline_segment(pas_segregated_directory* directory)
{
    pas_segregated_directory_bitvector_segment segment;
    unsigned bits;
    
    bits = directory->bits;
    
    segment.eligible_bits = (bits >> PAS_SEGREGATED_DIRECTORY_BITS_ELIGIBLE_SHIFT) & 1u;
    segment.empty_bits = (bits >> PAS_SEGREGATED_DIRECTORY_BITS_EMPTY_SHIFT) & 1u;

    return segment;
}

#define PAS_SEGREGATED_DIRECTORY_GET_BIT(directory, index, bit_name) ({ \
        pas_segregated_directory* _directory; \
        size_t _index; \
        unsigned* _word_ptr; \
        bool _result; \
        \
        _directory = (directory); \
        \
        _index = (index); \
        PAS_TESTING_ASSERT(_index < pas_segregated_directory_size(directory)); \
        \
        if (!_index) { \
            _result = pas_segregated_directory_bits_get_by_mask( \
                _directory, PAS_SEGREGATED_DIRECTORY_BITS_##bit_name##_MASK); \
        } else { \
            \
            --_index; \
            _word_ptr = &pas_segregated_directory_segmented_bitvectors_get_ptr( \
                &pas_segregated_directory_data_ptr_load_non_null(&_directory->data)->bitvectors, \
                PAS_BITVECTOR_WORD_INDEX(_index))->bit_name##_bits; \
            _result = pas_bitvector_get_from_word(*_word_ptr, _index); \
        } \
        _result; \
    })

#define PAS_SEGREGATED_DIRECTORY_SET_BIT(directory, index, bit_name, value) ({ \
        const bool _verbose = false; \
        \
        pas_segregated_directory* _directory; \
        size_t _index; \
        unsigned* _word_ptr; \
        bool _value; \
        bool _result; \
        \
        _directory = (directory); \
        _index = (index); \
        _value = (value); \
        \
        PAS_TESTING_ASSERT(_index < pas_segregated_directory_size(directory)); \
        \
        if (!_index) { \
            _result = pas_segregated_directory_bits_set_by_mask( \
                _directory, PAS_SEGREGATED_DIRECTORY_BITS_##bit_name##_MASK, _value); \
        } else {\
            size_t _adjusted_index; \
            _adjusted_index = _index - 1; \
            _word_ptr = &pas_segregated_directory_segmented_bitvectors_get_ptr( \
                &pas_segregated_directory_data_ptr_load_non_null(&_directory->data)->bitvectors, \
                PAS_BITVECTOR_WORD_INDEX(_adjusted_index))->bit_name##_bits; \
            _result = pas_bitvector_set_atomic_in_word(_word_ptr, _adjusted_index, _value); \
        } \
        \
        if (_verbose && _result) { \
            pas_log("%p[%s, %zu]: set to %s at %s:%d %s\n", \
                    _directory, #bit_name, _index, _value ? "true" : "false", \
                    __FILE__, __LINE__, __PRETTY_FUNCTION__); \
        } \
        \
        _result; \
    })

static inline bool pas_segregated_directory_get_empty_bit(
    pas_segregated_directory* directory,
    size_t page_index)
{
    return PAS_SEGREGATED_DIRECTORY_GET_BIT(directory, page_index, empty);
}

static inline bool pas_segregated_directory_set_empty_bit(
    pas_segregated_directory* directory,
    size_t page_index,
    bool value)
{
    return PAS_SEGREGATED_DIRECTORY_SET_BIT(directory, page_index, empty, value);
}

PAS_API void pas_segregated_directory_construct(
    pas_segregated_directory* directory,
    pas_segregated_page_config_kind page_config_kind,
    pas_page_sharing_mode page_sharing_mode,
    pas_segregated_directory_kind directory_kind);

PAS_API pas_segregated_directory_data*
pas_segregated_directory_get_data_slow(pas_segregated_directory* directory,
                                       pas_lock_hold_mode heap_lock_hold_mode);

static inline pas_segregated_directory_data*
pas_segregated_directory_get_data(pas_segregated_directory* directory,
                                  pas_lock_hold_mode heap_lock_hold_mode)
{
    pas_segregated_directory_data* data;
    data = pas_segregated_directory_data_ptr_load(&directory->data);
    if (data)
        return data;
    return pas_segregated_directory_get_data_slow(directory, heap_lock_hold_mode);
}

static inline bool
pas_segregated_directory_can_do_sharing(pas_segregated_directory* directory)
{
    return pas_page_sharing_mode_does_sharing(directory->sharing_mode);
}

static inline bool
pas_segregated_directory_is_doing_sharing(pas_segregated_directory* directory)
{
    pas_segregated_directory_data* data;
    data = pas_segregated_directory_data_ptr_load(&directory->data);
    return data
        && (pas_segregated_directory_sharing_payload_ptr_load(&data->sharing_payload)
            & PAS_SEGREGATED_DIRECTORY_SHARING_PAYLOAD_IS_INITIALIZED_BIT);
}

static inline pas_page_sharing_participant_payload*
pas_segregated_directory_data_try_get_sharing_payload(pas_segregated_directory_data* data)
{
    uintptr_t value;

    value = pas_segregated_directory_sharing_payload_ptr_load(&data->sharing_payload);

    return (pas_page_sharing_participant_payload*)(
        value & ~PAS_SEGREGATED_DIRECTORY_SHARING_PAYLOAD_IS_INITIALIZED_BIT);
}

PAS_API uint64_t pas_segregated_directory_get_use_epoch(pas_segregated_directory* directory);

PAS_API pas_page_sharing_participant_payload*
pas_segregated_directory_get_sharing_payload(pas_segregated_directory* directory,
                                             pas_lock_hold_mode heap_lock_hold_mode);

static inline void
pas_segregated_directory_start_sharing_if_necessary(pas_segregated_directory* directory,
                                                    pas_lock_hold_mode heap_lock_hold_mode)
{
    pas_segregated_directory_get_sharing_payload(directory, heap_lock_hold_mode);
}

PAS_API void pas_segregated_directory_minimize_first_eligible(
    pas_segregated_directory* directory,
    size_t index);

PAS_API void pas_segregated_directory_update_first_eligible_after_search(
    pas_segregated_directory* directory,
    pas_versioned_field first_eligible,
    size_t new_value);

PAS_API bool pas_segregated_directory_view_did_become_eligible_at_index(
    pas_segregated_directory* directory,
    size_t index);
PAS_API bool pas_segregated_directory_view_did_become_eligible(
    pas_segregated_directory* directory,
    pas_segregated_view view);
PAS_API bool pas_segregated_directory_view_did_become_empty_at_index(
    pas_segregated_directory* directory,
    size_t index);
PAS_API bool pas_segregated_directory_view_did_become_empty(
    pas_segregated_directory* directory,
    pas_segregated_view view);

static inline bool pas_segregated_directory_is_eligible(
    pas_segregated_directory* directory, size_t index)
{
    return PAS_SEGREGATED_DIRECTORY_GET_BIT(directory, index, eligible);
}

static inline bool pas_segregated_directory_is_empty(
    pas_segregated_directory* directory, size_t index)
{
    return PAS_SEGREGATED_DIRECTORY_GET_BIT(directory, index, empty);
}

PAS_API bool pas_segregated_directory_is_committed(
    pas_segregated_directory* directory, size_t index);

PAS_API size_t pas_segregated_directory_num_committed_views(
    pas_segregated_directory* directory);

PAS_API size_t pas_segregated_directory_num_empty_views(
    pas_segregated_directory* directory);

PAS_API size_t pas_segregated_directory_num_empty_granules(
    pas_segregated_directory* directory);

static inline pas_versioned_field pas_segregated_directory_get_first_eligible_impl(
    pas_segregated_directory* directory,
    pas_versioned_field (*read)(pas_versioned_field* field))
{
    pas_segregated_directory_data* data;

    data = pas_segregated_directory_data_ptr_load(&directory->data);
    if (data)
        return read(&data->first_eligible);

    if (directory->bits & PAS_SEGREGATED_DIRECTORY_BITS_ELIGIBLE_MASK)
        return pas_versioned_field_create_with_invalid_version(0);

    return !pas_compact_atomic_segregated_view_is_null(&directory->first_view)
        ? pas_versioned_field_create_with_invalid_version(1)
        : pas_versioned_field_create_with_invalid_version(0);
}

static inline pas_versioned_field pas_segregated_directory_get_first_eligible(
    pas_segregated_directory* directory)
{
    return pas_segregated_directory_get_first_eligible_impl(
        directory, pas_versioned_field_read);
}

static inline pas_versioned_field pas_segregated_directory_get_first_eligible_torn(
    pas_segregated_directory* directory)
{
    return pas_segregated_directory_get_first_eligible_impl(
        directory, pas_versioned_field_read_torn);
}

static inline pas_versioned_field pas_segregated_directory_watch_first_eligible(
    pas_segregated_directory* directory)
{
    return pas_segregated_directory_get_first_eligible_impl(
        directory, pas_versioned_field_read_to_watch);
}

static inline pas_versioned_field pas_segregated_directory_get_last_empty_plus_one_impl(
    pas_segregated_directory* directory,
    pas_versioned_field (*read)(pas_versioned_field* field))
{
    pas_segregated_directory_data* data;

    data = pas_segregated_directory_data_ptr_load(&directory->data);

    if (data)
        return read(&data->last_empty_plus_one);
    
    if (directory->bits & PAS_SEGREGATED_DIRECTORY_BITS_EMPTY_MASK)
        return pas_versioned_field_create_with_invalid_version(1);

    return pas_versioned_field_create_with_invalid_version(0);
}

static inline pas_versioned_field pas_segregated_directory_get_last_empty_plus_one(
    pas_segregated_directory* directory)
{
    return pas_segregated_directory_get_last_empty_plus_one_impl(
        directory, pas_versioned_field_read);
}

static inline uintptr_t pas_segregated_directory_get_last_empty_plus_one_value(
    pas_segregated_directory* directory)
{
    return pas_segregated_directory_get_last_empty_plus_one_impl(
        directory, pas_versioned_field_read_torn).value;
}

static inline pas_versioned_field pas_segregated_directory_watch_last_empty_plus_one(
    pas_segregated_directory* directory)
{
    return pas_segregated_directory_get_last_empty_plus_one_impl(
        directory, pas_versioned_field_read_to_watch);
}

PAS_API void pas_segregated_directory_update_last_empty_plus_one_after_search(
    pas_segregated_directory* directory,
    pas_versioned_field last_empty_plus_one,
    size_t new_value);

static inline pas_versioned_field* pas_segregated_directory_last_empty_plus_one(
    pas_segregated_directory* directory)
{
    pas_segregated_directory_data* data;

    data = pas_segregated_directory_data_ptr_load(&directory->data);
    if (!data)
        return NULL;

    return &data->last_empty_plus_one;
}

static inline pas_segregated_view pas_segregated_directory_get(
    pas_segregated_directory* directory, size_t index)
{
    PAS_ASSERT(index < pas_segregated_directory_size(directory));
    if (!index)
        return pas_compact_atomic_segregated_view_load(&directory->first_view);
    return pas_compact_atomic_segregated_view_load(
        pas_segregated_directory_view_vector_get_ptr(
            &pas_segregated_directory_data_ptr_load_non_null(&directory->data)->views,
            index - 1));
}

/* Must call this requesting to append at index == size. We use this pattern because we expect
   that everything is locked when we do this and we have already constructed the view at least
   somewhat and may have locked in the expectation that it'll be at this index. */
PAS_API void pas_segregated_directory_append(
    pas_segregated_directory* directory, size_t index, pas_segregated_view view);

/* This has a maximally inclusion way of treating memory that multiple views may have dibs on. For
   example, shared page directories and partial views in size directories may report on the same
   bytes of memory. */
PAS_API pas_heap_summary
pas_segregated_directory_compute_summary(pas_segregated_directory* directory);

PAS_END_EXTERN_C;

#endif /* PAS_SEGREGATED_DIRECTORY_H */

