#include "operation.h"

#include <algorithm>
#include <sstream>
#include <stdexcept>

namespace
{
void require(bool condition, const std::string& message)
{
    if (!condition)
        throw std::runtime_error(message);
}

std::size_t asSize(Index value)
{
    require(value >= 0, "negative index");
    return static_cast<std::size_t>(value);
}

Index visibleBefore(const std::vector<Slot>& slots, Index q)
{
    return static_cast<Index>(std::count_if(slots.begin(), slots.begin() + q,
        [](const Slot& slot) { return slot.visible; }));
}
}

void Buffer::append(CharacterId character, char value)
{
    slots_.push_back(Slot{character, value, true});
}

Index Buffer::visibleSize() const
{
    return static_cast<Index>(std::count_if(slots_.begin(), slots_.end(),
        [](const Slot& slot) { return slot.visible; }));
}

Index Buffer::effectsSize() const { return static_cast<Index>(slots_.size()); }

Index Buffer::qFromP(Index p) const
{
    require(0 <= p && p <= visibleSize(), "p-position outside buffer");
    Index visible = 0;
    for (Index q = 0; q < effectsSize(); ++q)
    {
        if (!slots_[asSize(q)].visible)
            continue;
        if (visible == p)
            return q;
        ++visible;
    }
    return effectsSize();
}

const Slot& Buffer::visibleSlot(Index p) const
{
    require(0 <= p && p < visibleSize(), "visible character outside buffer");
    Index visible = 0;
    for (const auto& slot : slots_)
        if (slot.visible && visible++ == p)
            return slot;
    throw std::runtime_error("visible character not found");
}

void Buffer::remove(Index p, Index q, CharacterId expected)
{
    require(0 <= q && q < effectsSize(), "source q-position outside buffer");
    Slot& source = slots_[asSize(q)];
    require(source.visible, "source character is not visible");
    require(source.character == expected, "source character identity mismatch");
    require(visibleBefore(slots_, q) == p, "source p/q positions disagree");
    source.visible = false;
}

void Buffer::insert(Index p, Index q, Slot inserted)
{
    require(0 <= q && q <= effectsSize(), "destination q-position outside buffer");
    require(visibleBefore(slots_, q) == p, "destination p/q positions disagree");
    slots_.insert(slots_.begin() + q, inserted);
}

void Buffer::insertPlaceholder(Index p, Index q, Slot inserted)
{
    require(0 <= q && q <= effectsSize(), "placeholder q-position outside buffer");
    require(0 <= p && p <= visibleSize(), "placeholder p-position outside buffer");
    inserted.visible = false;
    slots_.insert(slots_.begin() + q, inserted);
}

std::string Buffer::describe() const
{
    std::ostringstream out;
    out << '[';
    for (const auto& slot : slots_)
        out << (slot.visible ? slot.value : '*') << slot.character.origin << ' ';
    return out.str() + ']';
}

void Operation::apply(Document& document) const
{
    if (enabled())
    {
        document.at(asSize(sb)).remove(sp, sq, character);
        document.at(asSize(db)).insert(dp, dq, Slot{character, value, true});
    }
    else
    {
        document.at(asSize(db)).insertPlaceholder(dp, dq, Slot{character, value, false});
    }
}

void inclusionTransform(Operation& first, const Operation& second)
{
    require(first.id.site != second.id.site, "IT requires operations from different sites");

    if (first.sb == second.sb && second.sq < first.sq && second.enabled())
        --first.sp;

    const bool sameCharacter = first.sb == second.sb && first.sq == second.sq;
    if (sameCharacter &&
        (second.enableq < first.enableq ||
         (second.enableq == first.enableq && second.id.site < first.id.site)))
        ++first.enableq;

    if (sameCharacter && second.enabled())
    {
        first.sb = second.db;
        first.sq = second.dq;
        first.sp = second.dp;
    }
    else if (second.db == first.sb && second.dq <= first.sq)
    {
        ++first.sq;
        if (second.enabled())
            ++first.sp;
    }

    if (!sameCharacter && second.enabled() && first.db == second.sb && second.sq < first.dq)
        --first.dp;

    if (first.db == second.db &&
        (second.dq < first.dq ||
         (second.dq == first.dq && second.id.site < first.id.site)))
    {
        ++first.dq;
        if (!sameCharacter && second.enabled())
            ++first.dp;
    }
}

void exclusionTransform(Operation& first, const Operation& second)
{
    require(first.id.site != second.id.site, "ET requires operations from different sites");

    bool destinationDecremented = false;
    if (first.db == second.db && second.dq < first.dq)
    {
        destinationDecremented = true;
        --first.dq;
    }

    bool sameCharacter = second.db == first.sb && second.dq == first.sq;
    if (sameCharacter)
    {
        require(second.enabled(), "tracking an insertion from a disabled move");
        first.sb = second.sb;
        first.sq = second.sq;
        first.sp = second.sp;
    }
    else
    {
        if (second.enabled() && first.db == second.sb && second.sq < first.dq)
            ++first.dp;
        if (second.db == first.sb && second.dq < first.sq)
        {
            if (second.enabled())
                --first.sp;
            --first.sq;
        }
        if (first.sb == second.sb && first.sq == second.sq)
            sameCharacter = true;
    }

    if (sameCharacter)
    {
        if (second.enableq < first.enableq)
        {
            require(first.enableq > 0, "negative enable coordinate");
            --first.enableq;
        }
    }
    else if (second.enabled())
    {
        if (destinationDecremented)
            --first.dp;
        if (first.sb == second.sb && second.sq < first.sq)
            ++first.sp;
    }
}

void dualInclusionTransform(Operation& first, Operation& second)
{
    const Operation originalFirst = first;
    inclusionTransform(first, second);
    inclusionTransform(second, originalFirst);
}

void transpose(Operation& first, Operation& second)
{
    const Operation originalFirst = first;
    const Operation originalSecond = second;
    exclusionTransform(second, first);
    inclusionTransform(first, second);
    Operation recoveredSecond = second;
    inclusionTransform(recoveredSecond, originalFirst);
    require(recoveredSecond == originalSecond, "ET/IT inverse check failed during transpose");
}

std::string describe(const Operation& operation)
{
    std::ostringstream out;
    out << (operation.enabled() ? "" : "*") << operation.value << '#'
        << operation.character.origin << ' ' << operation.sb << ':' << operation.sp << '<'
        << operation.sq << "> -> " << operation.db << ':' << operation.dp << '<'
        << operation.dq << "> @(" << operation.id.site << ',' << operation.id.time
        << ") e=" << operation.enableq;
    return out.str();
}

std::string describe(const Document& document)
{
    std::ostringstream out;
    for (std::size_t i = 0; i < document.size(); ++i)
        out << (i == 0 ? "" : " ") << 'B' << i << '=' << document[i].describe();
    return out.str();
}
