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

#include "blockstore_impl.h"
#include "blockstore_internal.h"
#include "allocator.h"

#define _REDIRECT_INTENT 0x101

bool blockstore_impl_t::enqueue_write(blockstore_op_t *op)
{
    clock_gettime(CLOCK_REALTIME, &PRIV(op)->tv_begin);
    return true;
}

void blockstore_impl_t::prepare_meta_block_write(uint32_t modified_block)
{
    if (modified_blocks.find(modified_block) != modified_blocks.end())
        return;
    io_uring_sqe *sqe = get_sqe();
    assert(sqe != NULL);
    ring_data_t *data = ((ring_data_t*)sqe->user_data);
    uint8_t *buf = (uint8_t*)memalign_or_die(MEM_ALIGNMENT, dsk.meta_block_size);
    data->iov = (struct iovec){ buf, (size_t)dsk.meta_block_size };
    data->callback = [this, modified_block](ring_data_t *data)
    {
        live = true;
        if (data->res != data->iov.iov_len)
        {
            // FIXME: our state becomes corrupted after a write error. maybe do something better than just die
            disk_error_abort("data write", data->res, data->iov.iov_len);
        }
        auto it = modified_blocks.find(modified_block);
        assert(it != modified_blocks.end());
        free(it->second.buf);
        modified_blocks.erase(it);
        heap->complete_block_write(modified_block);
        ringloop->wakeup();
    };
    io_uring_prep_writev(
        sqe, dsk.meta_fd, &data->iov, 1, dsk.meta_offset + ((uint64_t)modified_block+1)*dsk.meta_block_size
    );
    unsynced_meta_write_count++;
    pending_modified_blocks.push_back(modified_block);
    modified_blocks[modified_block] = { .sent = false, .buf = buf };
}

bool blockstore_impl_t::meta_block_is_pending(uint32_t modified_block)
{
    auto mb_it = modified_blocks.find(modified_block);
    return mb_it != modified_blocks.end();
}

bool blockstore_impl_t::intent_write_allowed(blockstore_op_t *op, heap_entry_t *obj)
{
    // Parallel writes to the same object are forbidden so "one intent at a time" is fulfilled automatically
    // Intent writes are disabled when metadata fsync is enabled
    if (!dsk.disable_meta_fsync)
    {
        return false;
    }
    // Intent writes are only for replication
    if (op->opcode != BS_OP_WRITE_STABLE)
    {
        return false;
    }
    // Operation size should be less than or equal to atomic write size
    if (!op->len || op->len > dsk.atomic_write_size)
    {
        return false;
    }
    // Intent-writes are disabled if "absolutely correct during compaction" checksum validation algorithm is enabled
    // We could also do RMW here when perfect_csum_update is enabled, but it's unclear if we need it
    if (perfect_csum_update && dsk.csum_block_size > dsk.bitmap_granularity &&
        ((op->offset % dsk.csum_block_size) || (op->len % dsk.csum_block_size)))
    {
        return false;
    }
    bool ok = true;
    heap->iterate_with_stable(obj, obj->lsn, [&](heap_entry_t *wr, bool stable)
    {
        // Intent writes are not allowed over buffered writes
        auto t = wr->type();
        if (t == BS_HEAP_SMALL_WRITE)
        {
            ok = false;
            return false;
        }
        // Intent writes are not allowed over unstable writes
        if (!stable)
        {
            ok = false;
            return false;
        }
        // Intent writes are not allowed over unfinished intent writes
        if ((t == BS_HEAP_INTENT_WRITE || t == BS_HEAP_BIG_INTENT) && wr->lsn > heap->get_fsynced_lsn())
        {
            ok = false;
            return false;
        }
        // Intent writes are allowed over BIG_WRITEs even with fsyncs because BIG_WRITE is always counted as fsynced
        if (t == BS_HEAP_BIG_WRITE || t == BS_HEAP_BIG_INTENT)
        {
            return false;
        }
        return true;
    });
    return ok;
}

int blockstore_impl_t::dequeue_write(blockstore_op_t *op)
{
    if (PRIV(op)->op_state)
    {
        return continue_write(op);
    }
    if (write_iodepth >= max_write_iodepth)
    {
        return 0;
    }
    PRIV(op)->modified_block = UINT32_MAX;
    PRIV(op)->write_type = 0;
    heap_entry_t *obj = heap->read_entry(op->oid);
    if (op->opcode == BS_OP_DELETE)
    {
        // Delete
        if (!obj || obj->type() == BS_HEAP_DELETE)
        {
            // Already deleted
            op->retval = 0;
            FINISH_OP(op);
            return 2;
        }
        PRIV(op)->write_type = BS_HEAP_DELETE;
        BS_SUBMIT_CHECK_SQES(1);
        int res = heap->add_delete(obj, &PRIV(op)->modified_block);
        if (res == ENOSPC)
            goto enospc;
        assert(res == 0);
        prepare_meta_block_write(PRIV(op)->modified_block);
        PRIV(op)->pending_ops++;
        PRIV(op)->op_state = 5;
        write_iodepth++;
    }
    // FIXME: Allow to do initial writes as buffered, not redirected
    // FIXME: Allow to do direct writes over holes
    else if (!obj || obj->type() == BS_HEAP_DELETE || op->offset == 0 && op->len == dsk.data_block_size)
    {
        // Big (redirect) write
        PRIV(op)->write_type = dsk.disable_data_fsync || op->opcode != BS_OP_WRITE_STABLE ? BS_HEAP_BIG_WRITE : _REDIRECT_INTENT;
        BS_SUBMIT_CHECK_SQES(1);
        PRIV(op)->location = heap->find_free_data();
        if (PRIV(op)->location == UINT64_MAX)
        {
enospc:
            if (!heap->get_to_compact_count())
            {
                // no space
                op->retval = -ENOSPC;
                FINISH_OP(op);
                return 2;
            }
            PRIV(op)->wait_for = WAIT_COMPACTION;
            PRIV(op)->wait_detail = heap->get_compacted_count();
            flusher->request_trim();
            return 0;
        }
        uint64_t loc = PRIV(op)->location;
#ifdef BLOCKSTORE_DEBUG
        printf(
            "Allocate offset %ju for %jx:%jx v%ju\n",
            loc, op->oid.inode, op->oid.stripe, op->version
        );
#endif
        heap->use_data(op->oid.inode, PRIV(op)->location);
        io_uring_sqe *sqe = get_sqe();
        ring_data_t *data = ((ring_data_t*)sqe->user_data);
        data->iov = (struct iovec){ op->buf, op->len };
        data->callback = [this, op](ring_data_t *data) { handle_write_event(data, op); };
        io_uring_prep_writev(sqe, dsk.data_fd, &data->iov, 1, dsk.data_offset + loc + op->offset);
        PRIV(op)->pending_ops++;
        write_iodepth++;
        if (PRIV(op)->write_type == BS_HEAP_BIG_WRITE)
        {
            PRIV(op)->op_state = 1;
            inflight_big++;
        }
        else
            PRIV(op)->op_state = 3;
    }
    else if (intent_write_allowed(op, obj))
    {
        // Direct intent-write
        BS_SUBMIT_CHECK_SQES(1);
        int res = 0;
        if (dsk.csum_block_size <= dsk.bitmap_granularity &&
            (obj->entry_type == (BS_HEAP_BIG_INTENT|BS_HEAP_STABLE) ||
            obj->entry_type == (BS_HEAP_BIG_WRITE|BS_HEAP_STABLE)))
        {
            // Even more simplified BIG_INTENT writes
            // FIXME: Support RMW mode for csum_block_size > bitmap_granularity
            PRIV(op)->write_type = BS_HEAP_BIG_INTENT;
            PRIV(op)->location = obj->big_location(heap);
            res = heap->add_big_intent(op->oid, &obj, op->version, op->offset, op->len, op->bitmap,
                (uint8_t*)op->buf, NULL, &PRIV(op)->modified_block);
            if (res == ENOSPC)
                goto enospc;
            assert(res == 0);
            PRIV(op)->lsn = obj->lsn;
        }
        else
        {
            PRIV(op)->write_type = BS_HEAP_INTENT_WRITE;
            auto wr = obj;
            while (wr && (wr->type() == BS_HEAP_INTENT_WRITE || wr->type() == BS_HEAP_COMMIT || wr->type() == BS_HEAP_ROLLBACK))
            {
                wr = heap->prev(wr);
            }
            assert(wr && (wr->type() == BS_HEAP_BIG_WRITE || wr->type() == BS_HEAP_BIG_INTENT));
            PRIV(op)->location = wr->big_location(heap);
            res = heap->add_small_write(op->oid, &obj, (BS_HEAP_INTENT_WRITE | (op->opcode == BS_OP_WRITE_STABLE ? BS_HEAP_STABLE : 0)),
                op->version, op->offset, op->len, 0, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
            if (res == ENOSPC)
                goto enospc;
            assert(res == 0);
            PRIV(op)->lsn = obj->lsn;
        }
        prepare_meta_block_write(PRIV(op)->modified_block);
        PRIV(op)->pending_ops++;
        PRIV(op)->op_state = 9;
        write_iodepth++;
    }
    else
    {
        // Small (buffered) overwrite
        // First check if there is free buffer space
        PRIV(op)->write_type = BS_HEAP_SMALL_WRITE;
        uint64_t loc = !op->len ? 0 : heap->find_free_buffer_area(op->len);
        if (loc == UINT64_MAX)
        {
            PRIV(op)->wait_for = WAIT_COMPACTION;
            PRIV(op)->wait_detail = heap->get_compacted_count();
            flusher->request_trim();
            return 0;
        }
        // There is sufficient space. Check SQE(s)
        BS_SUBMIT_CHECK_SQES(1 + (op->len > 0 ? 1 : 0));
        int res = heap->add_small_write(op->oid, &obj, (BS_HEAP_SMALL_WRITE | (op->opcode == BS_OP_WRITE_STABLE ? BS_HEAP_STABLE : 0)),
            op->version, op->offset, op->len, loc, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
        if (res == ENOSPC)
            goto enospc;
        assert(res == 0);
        PRIV(op)->lsn = obj->lsn;
        prepare_meta_block_write(PRIV(op)->modified_block);
        PRIV(op)->pending_ops++;
        if (op->len > 0)
        {
            // Prepare buffered data write
            heap->use_buffer_area(op->oid.inode, loc, op->len);
            if (dsk.inmemory_journal)
            {
                memcpy((uint8_t*)buffer_area + loc, op->buf, op->len);
            }
            BS_SUBMIT_GET_SQE(sqe2, data2);
            data2->iov = (struct iovec){ op->buf, op->len };
            data2->callback = [this, op](ring_data_t *data) { handle_write_event(data, op); };
            io_uring_prep_writev(sqe2, dsk.journal_fd, &data2->iov, 1, dsk.journal_offset + loc);
            PRIV(op)->pending_ops++;
        }
        else
        {
            // Zero-length overwrite. Allowed to bump object version in EC placement groups without actually writing data
        }
        assert(PRIV(op)->pending_ops);
        PRIV(op)->op_state = 5;
        write_iodepth++;
    }
    return 1;
}

int blockstore_impl_t::continue_write(blockstore_op_t *op)
{
    int op_state = PRIV(op)->op_state;
again:
    if (op_state == 2)
        goto resume_2;
    else if (op_state == 4)
        goto resume_4;
    else if (op_state == 6)
        goto resume_6;
    else if (op_state == 8)
        goto resume_8;
    else if (op_state == 10)
        goto resume_10;
    else if (op_state == 11)
        goto resume_11;
    else if (op_state == 12)
        goto resume_12;
    else
    {
        // In progress
        assert(op_state < 10);
        if (PRIV(op)->modified_block != UINT32_MAX &&
            !meta_block_is_pending(PRIV(op)->modified_block))
        {
            PRIV(op)->pending_ops--;
            PRIV(op)->modified_block = UINT32_MAX;
        }
        if (PRIV(op)->pending_ops > 0)
            return 1;
        op_state++;
        goto again;
    }
resume_2:
    // We must fsync all big writes to avoid complex write workflows
    // It's OK for all HDDs and for server SSDs, but slightly worse for desktop SSDs
    inflight_big--;
    if (!dsk.disable_data_fsync)
    {
        // fsync data in a batch
resume_11:
        if (inflight_big > 0)
        {
            PRIV(op)->op_state = 11;
            return 1;
        }
        if (fsyncing_data)
        {
resume_12:
            if (fsyncing_data)
            {
                PRIV(op)->op_state = 12;
                return 1;
            }
            goto resume_4;
        }
        fsyncing_data = true;
        BS_SUBMIT_GET_SQE(sqe, data);
        io_uring_prep_fsync(sqe, dsk.data_fd, IORING_FSYNC_DATASYNC);
        data->iov = { 0 };
        data->callback = [this, op](ring_data_t *data)
        {
            fsyncing_data = false;
            handle_write_event(data, op);
        };
        PRIV(op)->pending_ops++;
        PRIV(op)->op_state = 3;
        return 1;
    }
resume_4:
    {
        BS_SUBMIT_CHECK_SQES(1);
        auto obj = heap->read_entry(op->oid);
        int res = 0;
        if (PRIV(op)->write_type == _REDIRECT_INTENT)
        {
            res = heap->add_redirect_intent(op->oid, &obj, op->version, op->offset, op->len,
                PRIV(op)->location, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
        }
        else
        {
            res = heap->add_big_write(op->oid, obj, op->opcode == BS_OP_WRITE_STABLE,
                op->version, op->offset, op->len, PRIV(op)->location, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
        }
        if (res == ENOSPC)
        {
            if (!heap->get_to_compact_count())
            {
                // no space
                heap->free_data(op->oid.inode, PRIV(op)->location);
                write_iodepth--;
                op->retval = -ENOSPC;
                FINISH_OP(op);
                return 2;
            }
            PRIV(op)->wait_for = WAIT_COMPACTION;
            PRIV(op)->wait_detail = heap->get_compacted_count();
            flusher->request_trim();
            return 0;
        }
        assert(res == 0);
        prepare_meta_block_write(PRIV(op)->modified_block);
        PRIV(op)->pending_ops++;
        PRIV(op)->op_state = 5;
        return 1;
    }
resume_6:
    // Apply throttling to not fill the journal too quickly for the SSD+HDD case
    if (PRIV(op)->write_type == BS_HEAP_SMALL_WRITE && throttle_small_writes)
    {
        // Apply throttling
        timespec tv_end;
        clock_gettime(CLOCK_REALTIME, &tv_end);
        uint64_t exec_us =
            (tv_end.tv_sec - PRIV(op)->tv_begin.tv_sec)*1000000 +
            (tv_end.tv_nsec - PRIV(op)->tv_begin.tv_nsec)/1000;
        // Compare with target execution time
        // 100% free -> target time = 0
        // 0% free -> target time = iodepth/parallelism * (iops + size/bw) / write per second
        uint64_t buffer_free_space = dsk.journal_len - heap->get_buffer_area_used_space();
        uint64_t ref_us =
            (write_iodepth <= throttle_target_parallelism ? 100 : 100*write_iodepth/throttle_target_parallelism)
            * (1000000/throttle_target_iops + op->len*1000000/throttle_target_mbs/1024/1024)
            / 100;
        ref_us -= ref_us * buffer_free_space / dsk.journal_len;
        if (ref_us > exec_us + throttle_threshold_us)
        {
            // Pause reply
            PRIV(op)->pending_ops++;
            PRIV(op)->op_state = 7;
            // Remember that the timer can in theory be called right here
            tfd->set_timer_us(ref_us-exec_us, false, [this, op](int timer_id)
            {
                PRIV(op)->pending_ops--;
                ringloop->wakeup();
            });
            return 1;
        }
    }
resume_8:
    // Acknowledge write
#ifdef BLOCKSTORE_DEBUG
    printf("Ack write %jx:%jx v%ju\n", op->oid.inode, op->oid.stripe, op->version);
#endif
    op->retval = op->len;
    if (PRIV(op)->write_type == BS_HEAP_BIG_WRITE)
    {
        unsynced_data_write_count++;
    }
    else if (PRIV(op)->write_type == BS_HEAP_SMALL_WRITE)
    {
        unsynced_buffer_write_count++;
        heap->complete_lsn_write(PRIV(op)->lsn);
    }
    else if (PRIV(op)->write_type == BS_HEAP_BIG_INTENT ||
        PRIV(op)->write_type == BS_HEAP_INTENT_WRITE)
    {
        unsynced_data_write_count++;
        intent_write_counter++;
        heap->complete_lsn_write(PRIV(op)->lsn);
    }
    else if (PRIV(op)->write_type == _REDIRECT_INTENT)
    {
        unsynced_data_write_count++;
        intent_write_counter++;
    }
    write_iodepth--;
    FINISH_OP(op);
    return 2;
resume_10:
    // Direct intent-write
    // LSN is not marked as completed so big_write won't be freed
    BS_SUBMIT_GET_SQE(sqe, data);
    data->iov = (struct iovec){ op->buf, op->len };
    data->callback = [this, op](ring_data_t *data) { handle_write_event(data, op); };
    io_uring_prep_writev(sqe, dsk.data_fd, &data->iov, 1, dsk.data_offset + PRIV(op)->location + op->offset);
    if (dsk.use_atomic_flag)
        sqe->rw_flags = RWF_ATOMIC;
    PRIV(op)->pending_ops++;
    PRIV(op)->op_state = 7;
    return 1;
}

void blockstore_impl_t::handle_write_event(ring_data_t *data, blockstore_op_t *op)
{
    live = true;
    if (data->res != data->iov.iov_len)
    {
        // FIXME: our state becomes corrupted after a write error. maybe do something better than just die
        disk_error_abort("data write", data->res, data->iov.iov_len);
    }
    PRIV(op)->pending_ops--;
    assert(PRIV(op)->pending_ops >= 0);
    if (PRIV(op)->pending_ops == 0)
    {
        ringloop->wakeup();
    }
}
