// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)

#include <stdexcept>
#include "blockstore_impl.h"
#include "blockstore_internal.h"
#include "str_util.h"
#include "crc32c.h"

#define INIT_META_EMPTY 0
#define INIT_META_READING 1
#define INIT_META_READ_DONE 2
#define INIT_META_WRITING 3

#define GET_SQE() \
    sqe = bs->get_sqe();\
    if (!sqe)\
        throw std::runtime_error("io_uring is full during initialization");\
    data = ((ring_data_t*)sqe->user_data)

blockstore_init_meta::blockstore_init_meta(blockstore_impl_t *bs)
{
    this->bs = bs;
}

void blockstore_init_meta::handle_event(ring_data_t *data, int buf_num)
{
    if (data->res < 0)
    {
        throw std::runtime_error(
            std::string("read metadata failed at offset ") + std::to_string(buf_num >= 0 ? bufs[buf_num].offset : last_read_offset) +
            std::string(": ") + strerror(-data->res)
        );
    }
    if (buf_num >= 0)
    {
        bufs[buf_num].state = (bufs[buf_num].state == INIT_META_READING
            ? INIT_META_READ_DONE
            : INIT_META_EMPTY);
    }
    submitted--;
    bs->ringloop->wakeup();
}

int blockstore_init_meta::loop()
{
    if (wait_state == 1)      goto resume_1;
    else if (wait_state == 2) goto resume_2;
    else if (wait_state == 3) goto resume_3;
    else if (wait_state == 4) goto resume_4;
    else if (wait_state == 5) goto resume_5;
    else if (wait_state == 6) goto resume_6;
    else if (wait_state == 7) goto resume_7;
    else if (wait_state == 8) goto resume_8;
    else if (wait_state == 9) goto resume_9;
    metadata_buffer = memalign(MEM_ALIGNMENT, 2*bs->metadata_buf_size);
    if (!metadata_buffer)
        throw std::runtime_error("Failed to allocate metadata read buffer");
    // Read metadata superblock
    GET_SQE();
    last_read_offset = 0;
    data->iov = { bs->meta_superblock, (size_t)bs->dsk.meta_block_size };
    data->callback = [this](ring_data_t *data) { handle_event(data, -1); };
    io_uring_prep_readv(sqe, bs->dsk.meta_fd, &data->iov, 1, bs->dsk.meta_offset);
    bs->ringloop->submit();
    submitted++;
resume_1:
    if (submitted > 0)
    {
        wait_state = 1;
        return 1;
    }
    if (is_zero((uint64_t*)bs->meta_superblock, bs->dsk.meta_block_size))
    {
        {
            blockstore_meta_header_v3_t *hdr = (blockstore_meta_header_v3_t *)bs->meta_superblock;
            hdr->zero = 0;
            hdr->magic = BLOCKSTORE_META_MAGIC_V1;
            hdr->version = bs->dsk.meta_format;
            hdr->meta_block_size = bs->dsk.meta_block_size;
            hdr->data_block_size = bs->dsk.data_block_size;
            hdr->bitmap_granularity = bs->dsk.bitmap_granularity;
            if (bs->dsk.meta_format >= BLOCKSTORE_META_FORMAT_V2)
            {
                hdr->data_csum_type = bs->dsk.data_csum_type;
                hdr->csum_block_size = bs->dsk.csum_block_size;
            }
            if (bs->dsk.meta_format >= BLOCKSTORE_META_FORMAT_HEAP)
            {
                hdr->meta_area_size = bs->dsk.meta_area_size;
            }
            hdr->set_crc32c();
        }
        if (bs->readonly)
        {
            printf("Skipping metadata initialization because blockstore is readonly\n");
        }
        else
        {
            printf("Initializing metadata area\n");
            GET_SQE();
            last_read_offset = 0;
            data->iov = (struct iovec){ bs->meta_superblock, (size_t)bs->dsk.meta_block_size };
            data->callback = [this](ring_data_t *data) { handle_event(data, -1); };
            io_uring_prep_writev(sqe, bs->dsk.meta_fd, &data->iov, 1, bs->dsk.meta_offset);
            bs->ringloop->submit();
            submitted++;
        resume_2:
            if (submitted > 0)
            {
                wait_state = 2;
                return 1;
            }
            zero_on_init = true;
        }
    }
    else
    {
        blockstore_meta_header_v3_t *hdr = (blockstore_meta_header_v3_t *)bs->meta_superblock;
        if (hdr->zero != 0 || hdr->magic != BLOCKSTORE_META_MAGIC_V1 || hdr->version < BLOCKSTORE_META_FORMAT_V1)
        {
            printf(
                "Metadata is corrupt or too old (pre-0.6.x).\n"
                " If this is a new OSD, please zero out the metadata area before starting it.\n"
                " If you need to upgrade from 0.5.x, convert metadata with vitastor-disk.\n"
            );
            exit(1);
        }
        if (hdr->version != BLOCKSTORE_META_FORMAT_HEAP)
        {
            printf(
                "OSD is started with meta_format 3, but actually stored format is %ju on disk."
                " Please update the OSD superblock or startup options.\n",
                hdr->version
            );
            exit(1);
        }
        if (hdr->meta_block_size != bs->dsk.meta_block_size ||
            hdr->data_block_size != bs->dsk.data_block_size ||
            hdr->bitmap_granularity != bs->dsk.bitmap_granularity ||
            hdr->data_csum_type != bs->dsk.data_csum_type ||
            hdr->csum_block_size != bs->dsk.csum_block_size ||
            hdr->meta_area_size != bs->dsk.meta_area_size)
        {
            printf(
                "Configuration stored in metadata superblock"
                " (meta_block_size=%u, data_block_size=%u, bitmap_granularity=%u, data_csum_type=%u, csum_block_size=%u, meta_area_size=%ju)"
                " differs from OSD configuration (%ju/%ju/%u, %u/%u, %ju).\n",
                hdr->meta_block_size, hdr->data_block_size, hdr->bitmap_granularity,
                hdr->data_csum_type, hdr->csum_block_size, hdr->meta_area_size,
                bs->dsk.meta_block_size, bs->dsk.data_block_size, bs->dsk.bitmap_granularity,
                bs->dsk.data_csum_type, bs->dsk.csum_block_size, bs->dsk.meta_area_size
            );
            exit(1);
        }
    }
    bs->heap->start_load(((blockstore_meta_header_v3_t *)bs->meta_superblock)->completed_lsn);
    if (bs->dsk.inmemory_journal)
    {
        // Read buffer area
        printf("Reading buffered data\n");
        md_offset = 0;
        while (md_offset < bs->dsk.journal_len)
        {
            GET_SQE();
            data->iov = (iovec){
                bs->buffer_area + md_offset,
                (size_t)(bs->dsk.journal_len - md_offset < bs->metadata_buf_size ? bs->dsk.journal_len - md_offset : bs->metadata_buf_size),
            };
            data->callback = [this](ring_data_t *data) { handle_event(data, -1); };
            io_uring_prep_readv(sqe, bs->dsk.journal_fd, &data->iov, 1, bs->dsk.journal_offset + md_offset);
            md_offset += data->iov.iov_len;
            submitted++;
            bs->ringloop->submit();
resume_3:
            if (submitted > 0)
            {
                wait_state = 3;
                return 1;
            }
        }
    }
    printf("Reading blockstore heap metadata\n");
    // Skip superblock
    md_offset = bs->dsk.meta_block_size;
    next_offset = md_offset;
    // Read the rest of the metadata
resume_4:
    if (next_offset < bs->dsk.meta_area_size && submitted == 0)
    {
        // Submit one read
        for (int i = 0; i < 2; i++)
        {
            if (!bufs[i].state)
            {
                bufs[i].buf = (uint8_t*)metadata_buffer + i*bs->metadata_buf_size;
                bufs[i].offset = next_offset;
                bufs[i].size = bs->dsk.meta_area_size-next_offset > bs->metadata_buf_size
                    ? bs->metadata_buf_size : bs->dsk.meta_area_size-next_offset;
                bufs[i].state = INIT_META_READING;
                submitted++;
                next_offset += bufs[i].size;
                GET_SQE();
                assert(bufs[i].size <= 0x7fffffff);
                data->iov = { bufs[i].buf, (size_t)bufs[i].size };
                data->callback = [this, i](ring_data_t *data) { handle_event(data, i); };
                if (!zero_on_init)
                    io_uring_prep_readv(sqe, bs->dsk.meta_fd, &data->iov, 1, bs->dsk.meta_offset + bufs[i].offset);
                else
                {
                    // Fill metadata with empty block pattern
                    memset(bufs[i].buf, 0, bufs[i].size);
                    for (uint64_t o = 0; o < bufs[i].size; o += bs->dsk.meta_block_size)
                        bs->heap->fill_block_empty_space(bufs[i].buf + o, 0);
                    io_uring_prep_writev(sqe, bs->dsk.meta_fd, &data->iov, 1, bs->dsk.meta_offset + bufs[i].offset);
                }
                bs->ringloop->submit();
                break;
            }
        }
    }
    for (int i = 0; i < 2; i++)
    {
        if (bufs[i].state == INIT_META_READ_DONE)
        {
            // Handle result
            uint64_t loaded = 0;
            int r = bs->heap->load_blocks(bufs[i].offset-bs->dsk.meta_block_size, bufs[i].size, bufs[i].buf, bs->skip_corrupted_meta_entries, loaded);
            if (r != 0)
                exit(1);
            entries_loaded += loaded;
            bufs[i].state = 0;
            bs->ringloop->wakeup();
        }
    }
    if (submitted > 0)
    {
        wait_state = 4;
        return 1;
    }
    // metadata read finished
    bs->heap->finish_load();
    printf("Metadata entries loaded: %ju, rechecking unfinished writes and garbage entries\n", entries_loaded);
    if (zero_on_init && !bs->dsk.disable_meta_fsync)
    {
        GET_SQE();
        io_uring_prep_fsync(sqe, bs->dsk.meta_fd, IORING_FSYNC_DATASYNC);
        last_read_offset = 0;
        data->iov = { 0 };
        data->callback = [this](ring_data_t *data) { handle_event(data, -1); };
        submitted++;
        bs->ringloop->submit();
    resume_5:
        if (submitted > 0)
        {
            wait_state = 5;
            return 1;
        }
    }
    // asynchronous recheck
resume_6:
    wait_state = 6;
    bs->heap->recheck_small_writes([this](bool is_data, uint64_t offset, uint64_t len, uint8_t *buf, std::function<void()> cb)
    {
        if (!buf)
        {
            wait_state = 7;
            bs->ringloop->wakeup();
            return;
        }
        GET_SQE();
        data->iov = (iovec){ buf, len };
        data->callback = [offset, cb](ring_data_t *data)
        {
            if (data->res < 0)
            {
                fprintf(stderr, "Buffer area read failed at offset %ju: %d\n", offset, data->res);
                exit(1);
            }
            cb();
        };
        io_uring_prep_readv(sqe, (is_data ? bs->dsk.data_fd : bs->dsk.journal_fd), &data->iov, 1,
            (is_data ? bs->dsk.data_offset : bs->dsk.journal_offset) + offset);
        bs->ringloop->submit();
    }, bs->meta_write_recheck_parallelism);
    return 1;
resume_7:
    if (bs->heap->finish_recheck() != 0)
    {
        exit(1);
    }
    recheck_mod = bs->heap->get_recheck_modified_blocks();
    if (bs->readonly)
    {
        recheck_mod.clear();
        printf("Actual metadata entries: %ju\n", bs->heap->get_live_entries());
    }
    else
    {
        printf("Actual metadata entries: %ju, clearing garbage in %zu metadata blocks\n", bs->heap->get_live_entries(), recheck_mod.size());
    }
    for (i = 0; i < recheck_mod.size(); i++)
    {
resume_8:
        if (wait_count >= bs->meta_write_recheck_parallelism || !(sqe = bs->get_sqe()))
        {
            bs->ringloop->submit();
            wait_state = 8;
            return 1;
        }
        uint32_t block_num = recheck_mod[i];
        uint64_t block_offset = bs->dsk.meta_offset + (uint64_t)(block_num+1) * bs->dsk.meta_block_size;
        data = ((ring_data_t*)sqe->user_data);
        uint8_t *buf = (uint8_t*)memalign_or_die(MEM_ALIGNMENT, bs->dsk.meta_block_size);
        bs->heap->get_meta_block(block_num, buf);
        data->iov = { buf, bs->dsk.meta_block_size };
        data->callback = [this, buf, block_offset](ring_data_t *data)
        {
            wait_count--;
            free(buf);
            if (data->res != bs->dsk.meta_block_size)
            {
                throw std::runtime_error(
                    "write metadata failed at offset " + std::to_string(block_offset) + ": " + strerror(-data->res)
                );
            }
        };
        io_uring_prep_writev(sqe, bs->dsk.meta_fd, &data->iov, 1, block_offset);
        wait_count++;
    }
resume_9:
    if (wait_count > 0)
    {
        bs->ringloop->submit();
        wait_state = 9;
        return 1;
    }
    free(metadata_buffer);
    metadata_buffer = NULL;
    printf("Loading finished. Data used: %ju / %ju bytes (%s / %s)\n",
        bs->heap->get_data_used_space(), bs->dsk.block_count * bs->dsk.data_block_size,
        format_size(bs->heap->get_data_used_space()).c_str(),
        format_size(bs->dsk.block_count * bs->dsk.data_block_size).c_str());
    return 0;
}
