#include "commit_relations.h"
#include "commit_revert.h"
#include "helpers.h"
#include "query_generator.h"
#include "sql_strings.h"

#include <boost/format.hpp>

namespace maps::wiki::revision {

namespace {

const ContributingCommitsOptions ALL_FIND_DRAFT_COMMITS_OPTIONS {
    ContributingCommitsOption::Reference,
    ContributingCommitsOption::Revert
};

const boost::format WRITE_DEPENDENT_COMMIT_IDS (
// %1% is commit id filter.
    "SELECT DISTINCT next_commit_id AS commit_id"
    "    FROM revision.object_revision"
    "    WHERE next_commit_id > 0 AND %1%"
);

const boost::format WRITE_CONTRIBUTING_COMMIT_IDS (
// %1% is commit id filter.
    "SELECT DISTINCT prev_commit_id AS commit_id"
    "    FROM revision.object_revision"
    "    WHERE prev_commit_id > 0 AND %1%"
);

const boost::format REFERENCE_DEPENDENT_COMMIT_IDS(
// %1% is commit id filter.
    "WITH current AS ("
    "   SELECT commit_id, object_id, master_object_id, slave_object_id"
    "       FROM revision.object_revision"
    "       WHERE %1%"
    ") SELECT DISTINCT revisions.commit_id"
    "   FROM revision.object_revision_relation AS revisions JOIN current"
    "       ON revisions.commit_id > current.commit_id AND"
// This condition allows Postgres to use constraint and search in necessary tables only:
// See performance remarks.
    "           current.slave_object_id = 0 AND"
    "           (revisions.slave_object_id = current.object_id OR"
    "               revisions.master_object_id = current.object_id)"
    " UNION DISTINCT " // union of selects allows to build better execution plan of query.
    "SELECT DISTINCT revisions.commit_id"
    "   FROM revision.object_revision AS revisions JOIN current"
    "       ON revisions.commit_id > current.commit_id AND"
// Optimization constraint:
    "           current.master_object_id > 0 AND"
    "           revisions.slave_object_id = 0 AND"
    "           (current.slave_object_id = revisions.object_id OR"
    "               current.master_object_id = revisions.object_id)"
);

// Performance remarks:
// It may seem that the following condition:
//              ((revisions.slave_object_id > 0 AND revisions.slave_object_id = current.object_id) OR
//                  (revisions.master_object_id > 0 revisions.master_object_id = current.object_id))
// should be better performed by the planner for previous query, namely for sprav base
// where relations and children are physically stored in different tables,
// but in practice it only degrades performance.

const boost::format REFERENCE_CONTRIBUTING_COMMIT_IDS (
// %1% is commit id filter.
    "WITH current AS ("
    "   SELECT DISTINCT commit_id, object_id, master_object_id, slave_object_id"
    "   FROM revision.object_revision"
    "   WHERE %1%"
    ") SELECT DISTINCT revisions.commit_id"
    "   FROM revision.object_revision AS revisions JOIN current"
    "       ON current.commit_id > revisions.commit_id AND "
// Optimization constraint:
    "           current.master_object_id > 0 AND"
    "           revisions.slave_object_id = 0 AND"
    "           (revisions.object_id = current.master_object_id OR "
    "               revisions.object_id = current.slave_object_id)"
    " UNION DISTINCT " // union of selects allows to build better execution plan of query.
    "SELECT DISTINCT revisions.commit_id"
    "   FROM revision.object_revision_relation AS revisions JOIN current"
    "       ON current.commit_id > revisions.commit_id AND"
// This condition allows Postgres to use constraint and search in necessary tables only:
    "           current.slave_object_id = 0 AND"
    "           (revisions.master_object_id = current.object_id OR"
    "               revisions.slave_object_id = current.object_id)"
);

const boost::format FILTER_TRUNK_COMMITS_IDS (
// %1% is a subquery.
    "SELECT ids.*"
    "   FROM (%1%) ids JOIN revision.commit"
    "       ON ids.commit_id = id"
    "   WHERE trunk"
);

const boost::format FILTER_DRAFT_COMMITS_IDS (
// %1% is a subquery.
    "SELECT ids.*"
    "   FROM (%1%) ids JOIN revision.commit"
    "       ON ids.commit_id = id"
    "   WHERE state = 'draft'"
);

const boost::format COMMITS_TRUNK_FLAG (
 // %1% is commit id filter.
    "SELECT " + sql::col::ID + ", " + sql::col::IS_TRUNK +
    "    FROM revision.commit"
    "    WHERE %1%"
);

const boost::format COMMITS_STATE (
 // %1% is commit id filter.
    "SELECT " + sql::col::ID + ", " + sql::col::STATE +
    "    FROM revision.commit"
    "    WHERE %1%"
);

void checkTrunkFlag(Transaction& work, const DBIDSet& commitIds)
{
    boost::format query = COMMITS_TRUNK_FLAG;
    query % QueryGenerator::buildFilterByAttrValues(sql::col::ID, commitIds);
    auto rows = work.exec(query.str());
    REQUIRE(rows.size() == commitIds.size(), "some commit ids do not exist");

    DBIDSet notTrunkCommitIds;
    for (const auto& row : rows) {
        if (!row[sql::col::IS_TRUNK].as<bool>()) {
            notTrunkCommitIds.insert(row[sql::col::ID].as<DBID>());
        }
    }
    REQUIRE(
        notTrunkCommitIds.empty(),
        "Not trunk commits: " << helpers::valuesToString(notTrunkCommitIds)
    );
}

DBIDSet eraseApproved(Transaction& work, const DBIDSet& commitIds)
{
    DBIDSet draftCommitIds;
    if (!commitIds.empty()) {
        boost::format query = COMMITS_STATE;
        query % QueryGenerator::buildFilterByAttrValues(sql::col::ID, commitIds);
        auto rows = work.exec(query.str());
        REQUIRE(rows.size() == commitIds.size(), "some commit ids do not exist");

        for (const auto& row : rows) {
            if (helpers::stringToCommitState(row[sql::col::STATE].as<std::string>()) == CommitState::Draft) {
                draftCommitIds.insert(row[sql::col::ID].as<DBID>());
            }
        }
    }
    return draftCommitIds;
}

DBIDSet findCommits(Transaction& work, boost::format query, boost::format subquery, const DBIDSet& commitIds)
{
    subquery % QueryGenerator::buildFilterByAttrValues(sql::col::COMMIT_ID, commitIds);
    query % subquery.str();
    DBIDSet foundCommitIds;
    for (const auto& row : work.exec(query.str())) {
        foundCommitIds.insert(row[sql::col::COMMIT_ID].as<DBID>());
    }
    return foundCommitIds;
}

DBIDSet findWriteDependentCommitsInTrunk(Transaction& work, const DBIDSet& commitIds)
{
    return findCommits(work, FILTER_TRUNK_COMMITS_IDS, WRITE_DEPENDENT_COMMIT_IDS, commitIds);
}

DBIDSet findReferenceDependentCommitsInTrunk(Transaction& work, const DBIDSet& commitIds)
{
    return findCommits(work, FILTER_TRUNK_COMMITS_IDS, REFERENCE_DEPENDENT_COMMIT_IDS, commitIds);
}

DBIDSet findDraftWriteContributingCommits(Transaction& work, const DBIDSet& commitIds)
{
    return findCommits(work, FILTER_DRAFT_COMMITS_IDS, WRITE_CONTRIBUTING_COMMIT_IDS, commitIds);
}

DBIDSet findDraftReferenceContributingCommits(Transaction& work, const DBIDSet& commitIds)
{
    return findCommits(work, FILTER_DRAFT_COMMITS_IDS, REFERENCE_CONTRIBUTING_COMMIT_IDS, commitIds);
}

class ResultAccumulator
{
public:
    ResultAccumulator(DBIDSet initCommitIds, ResultChecker resultChecker)
        : result_(std::move(initCommitIds))
        , resultChecker_(std::move(resultChecker))
    {}

    template<typename Functor>
    DBIDSet accumulateIteratively(const Functor& functor)
    {
        if (checkResult() == SearchPredicateResult::Stop) {
            return std::move(result_);
        }

        auto current = result_;
        while (!current.empty()) {
            current = accumulateWave(std::move(current), functor);
            if (checkResult() == SearchPredicateResult::Stop) {
                break;
            }
        }
        return std::move(result_);
    }

    SearchPredicateResult checkResult() const
    {
        return resultChecker_(result_);
    }

private:
    template<typename Functor>
    DBIDSet accumulateWave(DBIDSet current, const Functor& functor)
    {
        auto next = functor(std::move(current));

        for (auto it = next.begin(); it != next.end();) {
            if (!result_.insert(*it).second) {
                it = next.erase(it);
            } else {
                ++it;
            }
        }
        return next;
    }

    DBIDSet result_;
    ResultChecker resultChecker_;
};

} // end of anonymous namespace

bool hasDependentCommitsInTrunk(Transaction& work, const DBIDSet& commitIds)
{
    checkTrunkFlag(work, commitIds);
    if (!findWriteDependentCommitsInTrunk(work, commitIds).empty()) {
        return true;
    }
    return !findReferenceDependentCommitsInTrunk(work, commitIds).empty();
}

DBIDSet findDependentCommitsInTrunk(
    Transaction& work,
    const DBIDSet& commitIds,
    const ResultChecker& resultChecker)
{
    checkTrunkFlag(work, commitIds);

    ResultAccumulator accumulator(commitIds, resultChecker);
    return accumulator.accumulateIteratively(
        [&](const DBIDSet& commitIds) {
            auto writeCommitIds =
                findWriteDependentCommitsInTrunk(work, commitIds);
            accumulator.checkResult();
            return helpers::join(
                std::move(writeCommitIds),
                findReferenceDependentCommitsInTrunk(work, commitIds));
        }
    );
}

bool hasDraftContributingCommitsWithoutOriginal(
    Transaction& work,
    const DBIDSet& commitIds,
    const DBIDSet& excludeCommitIds)
{
    const auto draftCommitIds = eraseApproved(work, commitIds);
    if (draftCommitIds.empty()) {
        return false;
    }

    auto resultCommitIds = findDraftWriteContributingCommits(work, draftCommitIds);
    helpers::erase(resultCommitIds, commitIds);
    helpers::erase(resultCommitIds, excludeCommitIds);
    if (!resultCommitIds.empty()) {
        return true;
    }

    resultCommitIds = findDraftReferenceContributingCommits(work, draftCommitIds);
    helpers::erase(resultCommitIds, commitIds);
    helpers::erase(resultCommitIds, excludeCommitIds);
    if (!resultCommitIds.empty()) {
        return true;
    }

    resultCommitIds = eraseApproved(work, getRevertedCommitIds(work, draftCommitIds));
    helpers::erase(resultCommitIds, commitIds);
    helpers::erase(resultCommitIds, excludeCommitIds);
    if (!resultCommitIds.empty()) {
        return true;
    }

    return false;
}

DBIDSet findDraftContributingCommits(
    Transaction& work,
    const DBIDSet& commitIds,
    const DBIDSet& excludeCommitIds,
    const ContributingCommitsOptions& options)
{
    ResultAccumulator accumulator(
        eraseApproved(work, commitIds),
        RESULT_CHECKER_DUMMY);
    return accumulator.accumulateIteratively(
        [&](DBIDSet commitIds) {
            helpers::append(
                commitIds,
                helpers::erase(
                    findDraftWriteContributingCommits(work, commitIds),
                    excludeCommitIds
                )
            );

            if (options.count(ContributingCommitsOption::Reference)) {
                helpers::append(
                    commitIds,
                    helpers::erase(
                        findDraftReferenceContributingCommits(work, commitIds),
                        excludeCommitIds
                    )
                );
            }

            if (options.count(ContributingCommitsOption::Revert)) {
                helpers::append(
                    commitIds,
                    helpers::erase(
                        eraseApproved(work, getRevertedCommitIds(work, commitIds)),
                        excludeCommitIds
                    )
                );
            }

            return commitIds;
        }
    );
}

DBIDSet findAllDraftContributingCommits(
    Transaction& work,
    const DBIDSet& commitIds,
    const DBIDSet& excludeCommitIds)
{
    return findDraftContributingCommits(
        work, commitIds, excludeCommitIds, ALL_FIND_DRAFT_COMMITS_OPTIONS);
}

} // namespace maps::wiki::revision
