#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
{
    if (isInsertion())
    {
        auto& destination = document.at(asSize(destinationDocument));
        require(0 <= destinationPosition && destinationPosition <= static_cast<Index>(destination.size()),
                "insertion position outside effects document");
        destination.insert(destination.begin() + destinationPosition, Entry{value, true, false});
    }
    else if (isDeletion())
    {
        Entry& source = document.at(asSize(sourceDocument)).at(asSize(sourcePosition));
        require(source.value == value && source.present, "deletion source mismatch");
        source.deleted = true;
    }
    else
    {
        auto& destination = document.at(asSize(destinationDocument));
        require(0 <= destinationPosition && destinationPosition <= static_cast<Index>(destination.size()),
                "move destination outside effects document");
        destination.insert(destination.begin() + destinationPosition,
                           Entry{value, enable == 0, false});
        Entry& source = document.at(asSize(sourceDocument)).at(asSize(sourcePosition));
        require(source.value == value && source.present, "move source mismatch");
        if (enable == 0)
        {
            source.present = false;
            if (source.deleted)
            {
                source.deleted = false;
                document.at(asSize(destinationDocument)).at(asSize(destinationPosition)).deleted = true;
            }
        }
    }
}

void dualInclusionTransform(Operation& first, Operation& second)
{
    require(first.id.site != second.id.site, "DualIT requires different sites");
    if (!first.isDeletion() && !second.isDeletion() &&
        first.destinationDocument == second.destinationDocument)
    {
        if (second.destinationPosition < first.destinationPosition ||
            (second.destinationPosition == first.destinationPosition && second.id.site < first.id.site))
            ++first.destinationPosition;
        else
            ++second.destinationPosition;
    }
    if (!first.isDeletion() && !second.isInsertion() &&
        first.destinationDocument == second.sourceDocument &&
        first.destinationPosition <= second.sourcePosition)
        ++second.sourcePosition;
    if (!second.isDeletion() && !first.isInsertion() &&
        second.destinationDocument == first.sourceDocument &&
        second.destinationPosition <= first.sourcePosition)
        ++first.sourcePosition;
    if (!first.isInsertion() && !second.isInsertion() &&
        first.sourceDocument == second.sourceDocument && first.sourcePosition == second.sourcePosition)
    {
        if (first.isMove() && first.enable == 0)
        {
            second.sourceDocument = first.destinationDocument;
            second.sourcePosition = first.destinationPosition;
        }
        if (second.isMove() && second.enable == 0)
        {
            first.sourceDocument = second.destinationDocument;
            first.sourcePosition = second.destinationPosition;
        }
        if (first.isMove() && second.isMove())
        {
            if (second.enable < first.enable ||
                (second.enable == first.enable && second.id.site < first.id.site))
                ++first.enable;
            else
                ++second.enable;
        }
    }
}

void transpose(Operation& first, Operation& second)
{
    require(first.id.site != second.id.site, "transpose requires different sites");
    const Index previousSecondDestination = second.destinationPosition;
    if (!second.isDeletion() && !first.isInsertion() &&
        second.destinationDocument == first.sourceDocument &&
        second.destinationPosition <= first.sourcePosition)
        ++first.sourcePosition;
    if (!first.isDeletion() && !second.isDeletion() &&
        first.destinationDocument == second.destinationDocument)
    {
        if (first.destinationPosition < second.destinationPosition) --second.destinationPosition;
        else ++first.destinationPosition;
    }
    if ((!first.isInsertion() && !second.isInsertion() &&
         first.sourceDocument == second.sourceDocument && first.sourcePosition == second.sourcePosition) ||
        (!first.isDeletion() && !second.isInsertion() &&
         first.destinationDocument == second.sourceDocument &&
         first.destinationPosition == second.sourcePosition))
    {
        if (first.isMove() && first.enable == 0)
        {
            second.sourceDocument = first.sourceDocument;
            second.sourcePosition = first.sourcePosition;
        }
        if (first.isMove() && second.isMove())
        {
            if (first.enable < second.enable) --second.enable;
            else ++first.enable;
            require(second.enable >= 0, "negative enable position");
        }
        if (second.isMove() && second.enable == 0)
        {
            first.sourceDocument = second.destinationDocument;
            first.sourcePosition = previousSecondDestination;
        }
    }
    if (!first.isDeletion() && !second.isInsertion() &&
        first.destinationDocument == second.sourceDocument &&
        first.destinationPosition < second.sourcePosition)
        --second.sourcePosition;
}

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

void exclusionTransform(Operation& first, const Operation& second)
{
    Operation copy = second;
    transpose(copy, first);
}

std::string describe(const Operation& operation)
{
    std::ostringstream out;
    const char type = operation.isMove() ? 'M' : operation.isInsertion() ? 'I' : 'D';
    out << type << '#' << operation.value;
    if (!operation.isInsertion()) out << ' ' << operation.sourceDocument << ':' << operation.sourcePosition;
    if (operation.isMove()) out << " ->";
    if (!operation.isDeletion()) out << ' ' << operation.destinationDocument << ':' << operation.destinationPosition;
    if (operation.isMove()) out << " e=" << operation.enable;
    out << " @(" << operation.id.site << ',' << operation.id.time << ')';
    return out.str();
}

std::string describe(const Document& document)
{
    std::ostringstream out;
    for (std::size_t d = 0; d < document.size(); ++d)
    {
        if (d) out << ' ';
        out << 'B' << d << "=[";
        for (const auto& entry : document[d])
            out << entry.value << (entry.present ? (entry.deleted ? 'x' : '+') : '-') << ' ';
        out << ']';
    }
    return out.str();
}
