/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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
 *
 *      http://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 <assert.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "block_allocator.h"
#include "debug.h"
#include "transaction.h"

/*
 * BLOCK_ALLOCATOR_QUEUE_LEN:
 * Queue length large enough for a worst case tree update, e.g. update of a tree
 * where each entry needs to be copied then split.
 */
#define BLOCK_ALLOCATOR_QUEUE_LEN \
    (BLOCK_SET_MAX_DEPTH * 2 * BLOCK_SET_MAX_DEPTH * 2)

/**
 * struct block_allocator_queue_entry - pending block allocation set update
 * @block:      block number to free or allocate.
 * @tmp:        is_tmp value passed to block_allocate_etc or block_free_etc.
 * @free:       @block is free.
 * @removed:    queue entry was removed.
 */
struct block_allocator_queue_entry {
    data_block_t block;
    bool tmp;
    bool free;
    bool removed;
};

/**
 * struct block_allocator_queue - queue of pending block allocation set updates
 * @entry:      Ring buffer.
 * @head        Index in @entry of first (oldest) entry in queue.
 * @tail:       Index in @entry where next entry should be inserted.
 * @updating:   %true while updating sets, false otherwise.
 */
struct block_allocator_queue {
    struct block_allocator_queue_entry entry[BLOCK_ALLOCATOR_QUEUE_LEN];
    unsigned int head;
    unsigned int tail;
    bool updating;
};

/**
 * block_allocator_queue_empty - Check if block allocator queue is empty
 * @q:          Queue object.
 *
 * Return: %true if @q is empty, %false otherwise.
 */
static bool block_allocator_queue_empty(struct block_allocator_queue* q) {
    assert(q->head < countof(q->entry));
    assert(q->tail < countof(q->entry));

    return q->head == q->tail;
}

/**
 * block_allocator_queue_count - Get number of entries in block allocator queue
 * @q:          Queue object.
 *
 * Return: number of etnries in @q.
 */
static unsigned int block_allocator_queue_count(
        struct block_allocator_queue* q) {
    return (q->tail + countof(q->entry) - q->head) % countof(q->entry);
}

/**
 * block_allocator_queue_find - Search block allocator queue
 * @q:          Queue object.
 * @block:      Block number to search for.
 *
 * Return: If block is in @q index in @q->entry of matching entry, -1 if @block
 * is not in @q.
 */
static int block_allocator_queue_find(struct block_allocator_queue* q,
                                      data_block_t block) {
    unsigned int i;

    assert(q->head < countof(q->entry));
    assert(q->tail < countof(q->entry));

    for (i = q->head; i != q->tail; i = (i + 1) % countof(q->entry)) {
        if (q->entry[i].removed) {
            continue;
        }
        if (block == q->entry[i].block) {
            return i;
        }
    }
    return -1;
}

/**
 * block_allocator_queue_add_removed - Add a removed entry to block allocator
 * queue
 * @q:          Queue object.
 *
 * Add a removed entry to @q to make it non-empty.
 */
static void block_allocator_queue_add_removed(struct block_allocator_queue* q) {
    assert(block_allocator_queue_empty(q));
    unsigned int new_tail = (q->tail + 1) % countof(q->entry);
    q->entry[q->tail].removed = true;
    q->tail = new_tail;
    pr_write("index %d\n", q->tail);
}

/**
 * block_allocator_queue_add - Add a entry to block allocator queue
 * @q:          Queue object.
 * @block:      block number to free or allocate.
 * @is_tmp:     is_tmp value passed to block_allocate_etc or block_free_etc.
 * @is_free:    @block is free.
 */
static void block_allocator_queue_add(struct block_allocator_queue* q,
                                      data_block_t block,
                                      bool is_tmp,
                                      bool is_free) {
    int ret;
    unsigned int index;
    unsigned int new_tail;

    ret = block_allocator_queue_find(q, block);
    if (ret >= 0) {
        index = ret;
        assert(index < countof(q->entry));
        assert(q->entry[index].tmp == is_tmp);
        assert(q->entry[index].free != is_free);

        q->entry[index].removed = true;
        if (index != q->head || !q->updating) {
            return;
        }
        pr_warn("block %" PRIu64
                ", tmp %d, free %d, removed head during update\n",
                block, is_tmp, is_free);
    }

    new_tail = (q->tail + 1) % countof(q->entry);

    assert(q->head < countof(q->entry));
    assert(q->tail < countof(q->entry));
    assert(new_tail < countof(q->entry));
    assert(new_tail != q->head);

    q->entry[q->tail].block = block;
    assert(q->entry[q->tail].block == block);
    q->entry[q->tail].tmp = is_tmp;
    q->entry[q->tail].free = is_free;
    q->entry[q->tail].removed = false;
    q->tail = new_tail;
    pr_write("block %" PRIu64 ", tmp %d, free %d, index %d (rel %d)\n", block,
             is_tmp, is_free, q->tail, block_allocator_queue_count(q));
}

/**
 * block_allocator_queue_peek_head - Get head block allocator queue entry
 * @q:          Queue object.
 *
 * Return: entry at head of @q.
 */
static struct block_allocator_queue_entry block_allocator_queue_peek_head(
        struct block_allocator_queue* q) {
    assert(!block_allocator_queue_empty(q));

    return q->entry[q->head];
}

/**
 * block_allocator_queue_remove_head - Remove head block allocator queue entry
 * @q:          Queue object.
 * @entry:      Entry that should be at head of @q (used for validation only).
 */
static void block_allocator_queue_remove_head(
        struct block_allocator_queue* q,
        struct block_allocator_queue_entry entry) {
    assert(block_allocator_queue_peek_head(q).block == entry.block);

    pr_write("index %d, count %d\n", q->head, block_allocator_queue_count(q));
    q->head = (q->head + 1) % countof(q->entry);
}

/**
 * block_allocator_queue_find_free_block - Search allocator queue for free block
 * @q:          Queue object.
 * @block:      Block number to search for.
 *
 * Return: First block >= @block that is not in @q as an allocated block.
 */
static data_block_t block_allocator_queue_find_free_block(
        struct block_allocator_queue* q,
        data_block_t block) {
    int index;

    while (true) {
        index = block_allocator_queue_find(q, block);
        if (index < 0) {
            return block;
        }
        if (q->entry[index].free) {
            return block;
        }
        block++;
    }
}

static struct block_allocator_queue block_allocator_queue;

/**
 * find_free_block - Search for a free block
 * @tr:             Transaction object.
 * @min_block_in:   Block number to start search at.
 *
 * Return: Block number that is in all committed free sets and not already
 * allocated by any transaction. To be considered free, a block must be in the
 * current file-system free set AND any checkpointed free sets, if available.
 */
static data_block_t find_free_block(struct transaction* tr,
                                    data_block_t min_block_in) {
    data_block_t block;
    data_block_t min_block = min_block_in;
    struct block_set* set;

    if (!fs_is_writable(tr->fs)) {
        printf("Read-only filesystem tried to allocate a free block\n");
        if (!tr->failed) {
            transaction_fail(tr);
        }
        return 0;
    }

    assert(list_in_list(&tr->node)); /* transaction must be active */

    pr_read("min_block %" PRIu64 "\n", min_block);

    block = min_block;
    do {
        block = block_set_find_next_block(tr, &tr->fs->free, block, true);
        if (tr->failed) {
            return 0;
        }

        /*
         * if block_set_find_next_block() returns 0, there was no available free
         * block after min_block
         */
        if (!block) {
            break;
        }
        assert(block >= min_block);

        /*
         * set min_block to a candidate for an available free block. If no
         * checkpoint or pending allocation contains this block, block will
         * still equal min_block and we will exit the loop
         */
        min_block = block;

        pr_read("check free block %" PRIu64 "\n", block);

        /* check if the block is also free in the checkpoint */
        block = block_set_find_next_block(tr, &tr->fs->checkpoint_free, block,
                                          true);
        if (tr->failed) {
            return 0;
        }
        if (!block) {
            break;
        }
        assert(block >= min_block);

        assert(!list_is_empty(&tr->fs->allocated));
        list_for_every_entry(&tr->fs->allocated, set, struct block_set, node) {
            block = block_set_find_next_block(tr, set, block, false);
            if (tr->failed) {
                return 0;
            }
            assert(block >= min_block);
        };
        block = block_allocator_queue_find_free_block(&block_allocator_queue,
                                                      block);
        assert(block >= min_block);
    } while (block != min_block);

    if (!block) {
        if (LOCAL_TRACE >= TRACE_LEVEL_READ) {
            if (min_block_in) {
                block = find_free_block(tr, 0);
            }
            printf("%s: no space, min_block %" PRIu64
                   ", free block ignoring_min_block %" PRIu64 "\n",
                   __func__, min_block_in, block);

            printf("%s: free\n", __func__);
            block_set_print(tr, &tr->fs->free);
            printf("%s: checkpoint free\n", __func__);
            block_set_print(tr, &tr->fs->checkpoint_free);
            list_for_every_entry(&tr->fs->allocated, set, struct block_set,
                                 node) {
#if TLOG_LVL >= TLOG_LVL_DEBUG
                printf("%s: allocated %p\n", __func__, set);
#endif
                block_set_print(tr, set);
            }
            if (tr->new_free_set) {
                printf("%s: new free\n", __func__);
                block_set_print(tr, tr->new_free_set);
            }
        }

        return 0;
    }

    pr_read("found free block %" PRIu64 "\n", block);

    return block;
}

/**
 * block_allocate_etc - Allocate a block
 * @tr:         Transaction object.
 * @is_tmp:     %true if allocated block should be automatically freed when
 *              transaction completes, %false if allocated block should be added
 *              to free set when transaction completes.
 *
 * Find a free block and add queue a set update.
 *
 * Return: Allocated block number.
 */
data_block_t block_allocate_etc(struct transaction* tr, bool is_tmp) {
    data_block_t block;
    data_block_t min_block;
    bool update_sets;

    if (tr->failed) {
        pr_warn("transaction failed, abort\n");

        return 0;
    }
    assert(transaction_is_active(tr));

    /* TODO: group allocations by set */
    update_sets = block_allocator_queue_empty(&block_allocator_queue);
    if (update_sets) {
        tr->last_tmp_free_block = tr->fs->dev->block_count / 4 * 3;
        tr->last_free_block = 0;
    }
    min_block = is_tmp ? tr->last_tmp_free_block : tr->last_free_block;

    block = find_free_block(tr, min_block);
    if (!block) {
        block = find_free_block(tr, 0);
        if (!block) {
            if (!tr->failed) {
                pr_err("no space\n");
                transaction_fail(tr);
            }
            return 0;
        }
    }

    block_allocator_queue_add(&block_allocator_queue, block, is_tmp, false);
    full_assert(find_free_block(tr, block) != block);
    if (update_sets) {
        block_allocator_process_queue(tr);
    }

    full_assert(tr->failed || find_free_block(tr, block) != block);
    if (tr->failed) {
        return 0;
    }

    return block;
}

/**
 * block_allocator_add_allocated - Update block sets with new allocated block
 * @tr:         Transaction object.
 * @block:      Block that was allocated.
 * @is_tmp:     %true if allocated block should be automatically freed when
 *              transaction completes, %false if allocated block should be added
 *              to free set when transaction completes.
 *
 * Add @block to the allocated set selected by @is_tmp. If called while
 * completing the transaction update the new free set directly if needed.
 */
static void block_allocator_add_allocated(struct transaction* tr,
                                          data_block_t block,
                                          bool is_tmp) {
    if (is_tmp) {
        pr_write("add %" PRIu64 " to tmp_allocated\n", block);

        block_set_add_block(tr, &tr->tmp_allocated, block);
        tr->last_tmp_free_block = block + 1;
    } else {
        pr_write("add %" PRIu64 " to allocated\n", block);

        block_set_add_block(tr, &tr->allocated, block);

        if (block < tr->min_free_block) {
            pr_write("remove %" PRIu64 " from new_free_set\n", block);

            assert(tr->new_free_set);
            block_set_remove_block(tr, tr->new_free_set, block);
            tr->last_free_block = block + 1;
        }
    }
}

/**
 * block_free_etc - Free a block
 * @tr:         Transaction object.
 * @block:      Block that should be freed.
 * @is_tmp:     Must match is_tmp value passed to block_allocate_etc (always
 *              %false if @block was not allocated by this transaction).
 */
void block_free_etc(struct transaction* tr, data_block_t block, bool is_tmp) {
    bool update_sets = block_allocator_queue_empty(&block_allocator_queue);

    assert(block_is_clean(tr->fs->dev, block));

    block_allocator_queue_add(&block_allocator_queue, block, is_tmp, true);
    if (update_sets) {
        block_allocator_process_queue(tr);
    }
}

/**
 * block_allocator_add_free - Update block sets with new free block
 * @tr:         Transaction object.
 * @block:      Block that should be freed.
 * @is_tmp:     Must match is_tmp value passed to block_allocate_etc (always
 *              %false if @block was not allocated by this transaction).
 *
 * If @block was allocated in this transaction, remove it from the allocated set
 * (selected by @is_tmp). Otherwise add it to the set of blocks to remove when
 * transaction completes. If called while completing the transaction update the
 * new free set directly if needed.
 */
static void block_allocator_add_free(struct transaction* tr,
                                     data_block_t block,
                                     bool is_tmp) {
    assert(block_is_clean(tr->fs->dev, block));
    if (is_tmp) {
        assert(!block_set_block_in_set(tr, &tr->allocated, block));
        assert(!block_set_block_in_set(tr, &tr->freed, block));

        pr_write("remove %" PRIu64 " from tmp_allocated\n", block);

        block_set_remove_block(tr, &tr->tmp_allocated, block);

        return;
    }

    assert(!block_set_block_in_set(tr, &tr->tmp_allocated, block));
    if (block_set_block_in_set(tr, &tr->allocated, block)) {
        pr_write("remove %" PRIu64 " from allocated\n", block);

        block_set_remove_block(tr, &tr->allocated, block);

        if (block < tr->min_free_block) {
            pr_write("add %" PRIu64 " to new_free_root\n", block);

            assert(tr->new_free_set);
            block_set_add_block(tr, tr->new_free_set, block);
        }
    } else {
        if (block < tr->min_free_block) {
            pr_write("add %" PRIu64 " to new_free_root\n", block);

            assert(tr->new_free_set);
            block_set_add_block(tr, tr->new_free_set, block);
        } else {
            pr_write("add %" PRIu64 " to freed\n", block);

            block_set_add_block(tr, &tr->freed, block);
        }
    }
}

/**
 * block_allocator_suspend_set_updates - Prevent set updates
 * @tr:         Transaction object.
 */
void block_allocator_suspend_set_updates(struct transaction* tr) {
    block_allocator_queue_add_removed(&block_allocator_queue);
}

/**
 * block_allocator_allocation_queued - Check for queued block allocation
 * @tr:         Transaction object.
 * @block:      Block to serach for.
 * @is_tmp:     is_tmp value passed to block_allocate_etc.
 *
 * Return: %true if a block matching @block, @is_tmp and !free is in the
 * block allocator queue, %false otherwise.
 */
bool block_allocator_allocation_queued(struct transaction* tr,
                                       data_block_t block,
                                       bool is_tmp) {
    int index;
    index = block_allocator_queue_find(&block_allocator_queue, block);
    return index >= 0 && block_allocator_queue.entry[index].tmp == is_tmp &&
           !block_allocator_queue.entry[index].free;
}

/**
 * block_allocator_process_queue - Process all block allocator queue entries
 * @tr:         Transaction object.
 */
void block_allocator_process_queue(struct transaction* tr) {
    struct block_allocator_queue_entry entry;
    int loop_limit =
            BLOCK_SET_MAX_DEPTH * BLOCK_SET_MAX_DEPTH * BLOCK_SET_MAX_DEPTH +
            tr->fs->dev->block_count;

    assert(!block_allocator_queue.updating);

    block_allocator_queue.updating = true;
    while (!block_allocator_queue_empty(&block_allocator_queue)) {
        assert(loop_limit-- > 0);
        entry = block_allocator_queue_peek_head(&block_allocator_queue);
        pr_write("block %" PRIu64 ", tmp %d, free %d, removed %d\n",
                 (data_block_t)entry.block, entry.tmp, entry.free,
                 entry.removed);
        if (entry.removed) {
        } else if (entry.free) {
            block_allocator_add_free(tr, entry.block, entry.tmp);
        } else {
            block_allocator_add_allocated(tr, entry.block, entry.tmp);
        }
        block_allocator_queue_remove_head(&block_allocator_queue, entry);
    }
    block_allocator_queue.updating = false;
}