#include "operation.h"

#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);
}
}

void Operation::apply(Document& document) const
{
    Attribute& target = document.at(asSize(attribute));
    require(target.value == oldValue, "assignment old value does not match current value");
    require(0 <= q && q <= static_cast<Index>(target.effects.size()),
            "assignment q-position outside effects document");
    target.effects.insert(target.effects.begin() + q, Effect{id, newValue});
    if (q == 0) target.value = newValue;
}

void inclusionTransform(Operation& first, const Operation& second)
{
    require(first.id.site != second.id.site, "IT requires operations from different sites");
    if (first.attribute != second.attribute) return;
    if (second.q < first.q || (second.q == first.q && second.id.site < first.id.site)) ++first.q;
    if (second.q == 0) first.oldValue = second.newValue;
}

void exclusionTransform(Operation& first, const Operation& second)
{
    require(first.id.site != second.id.site, "ET requires operations from different sites");
    if (first.attribute != second.attribute) return;
    if (second.q < first.q)
    {
        require(first.q > 0, "negative assignment q-position");
        --first.q;
    }
    if (second.q == 0) first.oldValue = second.oldValue;
}

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

void transpose(Operation& first, Operation& second)
{
    exclusionTransform(second, first);
    inclusionTransform(first, second);
}

void validate(const Document& document)
{
    for (const auto& attribute : document)
    {
        const Index expected = attribute.effects.empty() ? attribute.initialValue
                                                         : attribute.effects.front().value;
        require(attribute.value == expected, "attribute value disagrees with effects winner");
    }
}

std::string describe(const Operation& operation)
{
    std::ostringstream out;
    out << "A" << operation.attribute << ' ' << operation.oldValue << "->" << operation.newValue
        << " q=" << operation.q << " @(" << operation.id.site << ',' << operation.id.time << ')';
    return out.str();
}

std::string describe(const Document& document)
{
    std::ostringstream out;
    for (std::size_t i = 0; i < document.size(); ++i)
    {
        if (i) out << ' ';
        out << 'A' << i << '=' << document[i].value << " [";
        for (const auto& effect : document[i].effects)
            out << effect.value << "@" << effect.id.site << ':' << effect.id.time << ' ';
        out << ']';
    }
    return out.str();
}
