#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 document coordinate");
    return static_cast<std::size_t>(value);
}
}

Document::Document(Index initialSize)
{
    require(initialSize >= 0, "negative initial document size");
    for (Index i = 0; i < initialSize; ++i)
        elements_.push_back(Element{OpId{-1, i}, static_cast<char>('a' + i % 26), true});
}

Index Document::visibleSize() const
{
    Index result = 0;
    for (const auto& element : elements_)
        result += element.visible ? 1 : 0;
    return result;
}

Index Document::effectsSize() const
{
    return static_cast<Index>(elements_.size());
}

Index Document::qFromP(Index p) const
{
    require(0 <= p && p <= visibleSize(), "visible position is outside the document");
    Index visiblePosition = 0;
    for (Index q = 0; q < effectsSize(); ++q)
    {
        if (elements_[asSize(q)].visible)
        {
            if (visiblePosition == p)
                return q;
            ++visiblePosition;
        }
    }
    return effectsSize();
}

char Document::visibleCharacter(Index p) const
{
    require(0 <= p && p < visibleSize(), "visible character is outside the document");
    Index visiblePosition = 0;
    for (const auto& element : elements_)
        if (element.visible && visiblePosition++ == p)
            return element.value;
    throw std::runtime_error("visible character was not found");
}

std::string Document::visibleText() const
{
    std::string result;
    for (const auto& element : elements_)
        if (element.visible)
            result += element.value;
    return result;
}

std::string Document::effectsText() const
{
    std::string result;
    for (const auto& element : elements_)
        result += element.visible ? element.value : '*';
    return result;
}

void Document::insert(Index p, Index q, char value, OpId identity)
{
    require(0 <= q && q <= effectsSize(), "insertion q-coordinate is outside the effects document");
    const auto visibleBeforeQ = static_cast<Index>(std::count_if(
        elements_.begin(), elements_.begin() + q, [](const Element& element) { return element.visible; }));
    require(visibleBeforeQ == p, "insertion p/q coordinates disagree");
    elements_.insert(elements_.begin() + q, Element{identity, value, true});
}

char Document::erase(Index p, Index q)
{
    require(0 <= q && q < effectsSize(), "deletion q-coordinate is outside the effects document");
    require(elements_[asSize(q)].visible, "deletion targets an absent character");
    const auto visibleBeforeQ = static_cast<Index>(std::count_if(
        elements_.begin(), elements_.begin() + q, [](const Element& element) { return element.visible; }));
    require(visibleBeforeQ == p, "deletion p/q coordinates disagree");
    elements_[asSize(q)].visible = false;
    return elements_[asSize(q)].value;
}

void Operation::apply(Document& document) const
{
    if (!enabled)
        return;
    if (insertion)
        document.insert(p, q, value, id);
    else
        require(document.erase(p, q) == value, "deletion character does not match");
}

void inclusionTransform(Operation& first, const Operation& second)
{
    if (second.insertion)
    {
        if (first.insertion)
        {
            if (second.q < first.q || (second.q == first.q && second.id.site < first.id.site))
            {
                ++first.q;
                ++first.p;
            }
        }
        else if (second.q <= first.q)
        {
            ++first.q;
            ++first.p;
        }
    }
    else if (first.insertion)
    {
        if (second.enabled && second.q < first.q)
            --first.p;
    }
    else
    {
        if (second.enabled && second.q < first.q)
            --first.p;
        if (second.enabled && second.q == first.q)
            first.enabled = false;
    }
}

void exclusionTransform(Operation& first, const Operation& second)
{
    if (second.insertion)
    {
        if (second.q < first.q)
        {
            --first.q;
            --first.p;
        }
    }
    else if (first.insertion)
    {
        if (second.enabled && second.q < first.q)
            ++first.p;
    }
    else
    {
        if (second.enabled && second.q < first.q)
            ++first.p;
        if (second.enabled && second.q == first.q)
            first.enabled = true;
    }
}

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

std::string describe(const Operation& operation)
{
    std::ostringstream out;
    out << (operation.enabled ? "" : "*") << (operation.insertion ? 'I' : 'D')
        << operation.p << '<' << operation.q << '>' << operation.value
        << "@(" << operation.id.site << ',' << operation.id.time << ')';
    return out.str();
}
