#include "move.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);
}

void transformDestinationAgainstSource(Move& insertion, Move& extraction)
{
    if (insertion.db != extraction.sb)
        return;
    if (insertion.dq <= extraction.sq)
    {
        if (insertion.enabled())
            ++extraction.sp;
        ++extraction.sq;
    }
    else if (extraction.enabled())
    {
        --insertion.dp;
    }
}
}

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

const Slot& Buffer::slot(Index q) const
{
    require(0 <= q && q < effectsSize(), "q-position outside buffer");
    return slots_[asSize(q)];
}

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(qFromP(p) == q, "source p/q positions disagree");
    source.visible = false;
}

void Buffer::insert(Index p, Index q, Slot inserted)
{
    require(inserted.visible, "visible insertion required");
    require(0 <= q && q <= effectsSize(), "destination q-position outside buffer");
    const auto visibleBefore = static_cast<Index>(std::count_if(
        slots_.begin(), slots_.begin() + q, [](const Slot& slot) { return slot.visible; }));
    require(visibleBefore == p, "destination p/q positions disagree");
    slots_.insert(slots_.begin() + q, inserted);
}

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

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

void Move::apply(Document& document) const
{
    require(0 <= sb && asSize(sb) < document.size(), "source buffer outside document");
    require(0 <= db && asSize(db) < document.size(), "destination buffer outside document");
    if (enabled())
    {
        document[asSize(sb)].remove(sp, sq, character);
        document[asSize(db)].insert(dp, dq, Slot{character, value, true});
    }
    else
    {
        document[asSize(db)].insertPlaceholder(dp, dq, Slot{character, value, false});
    }
}

void dualInclusionTransform(Move& a, Move& b)
{
    require(a.id != b.id, "cannot transform an operation against itself");

    if (a.sb == b.sb)
    {
        if (a.sq < b.sq)
        {
            if (a.enabled())
                --b.sp;
        }
        else if (b.sq < a.sq)
        {
            if (b.enabled())
                --a.sp;
        }
        else
        {
            if (a.enabled())
            {
                b.sb = a.db;
                b.sq = a.dq;
                b.sp = a.dp;
            }
            else if (a.db == b.sb && a.dq <= b.sq)
            {
                ++b.sq;
            }

            if (b.enabled())
            {
                a.sb = b.db;
                a.sq = b.dq;
                a.sp = b.dp;
            }
            else if (b.db == a.sb && b.dq <= a.sq)
            {
                ++a.sq;
            }

            if (a.db == b.db)
            {
                if (a.dq < b.dq || (a.dq == b.dq && a.id.site < b.id.site))
                    ++b.dq;
                else
                    ++a.dq;
            }

            if (b.id.site < a.id.site)
                ++b.dc;
            else
                ++a.dc;
            return;
        }
    }

    transformDestinationAgainstSource(a, b);
    transformDestinationAgainstSource(b, a);

    if (a.db == b.db)
    {
        if (a.dq < b.dq || (a.dq == b.dq && a.id.site < b.id.site))
        {
            if (a.enabled())
                ++b.dp;
            ++b.dq;
        }
        else
        {
            if (b.enabled())
                ++a.dp;
            ++a.dq;
        }
    }
}

Move inclusionTransform(Move first, const Move& second)
{
    Move copy = second;
    dualInclusionTransform(first, copy);
    return first;
}

std::string describe(const Move& move)
{
    std::ostringstream out;
    out << (move.enabled() ? "" : "*") << move.value << '#' << move.character.origin << ' '
        << move.sb << ':' << move.sp << '<' << move.sq << "> -> "
        << move.db << ':' << move.dp << '<' << move.dq << "> @(" << move.id.site << ','
        << move.id.time << ") dc=" << move.dc;
    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();
}
