#include "query-builder.h"

#include "magic_strings.h"
#include "helpers.h"

#include <yandex/maps/stringutils/join.h>

#include <boost/optional.hpp>

#include <set>

namespace maps {
namespace wiki {
namespace ymapsdf_splitter {
namespace sql {
namespace {

using Columns = std::set<std::string>;


Columns getColumns(const std::string& connStr, const std::string& schema, const std::string& table)
{
    Columns result;
    std::string query =
        "SELECT column_name\n"
        "FROM information_schema.columns\n"
        "WHERE\n"
        "  table_schema = '" + schema + "' AND\n"
        "  table_name = '" + table + "';";

    for (const auto& row: exec(connStr, query)) {
        result.insert(row["column_name"].as<std::string>());
    }
    return result;
}



// The 'shape' is the column that slowdowns queries with DISTINCT clause
// heavily, therefore this column is removed from the clause if this column
// exists.
boost::optional<Columns> getColumnsForDistinct(
    const std::string& connStr, const std::string& schema, const std::string& table)
{
    auto columns = getColumns(connStr, schema, table);

    if (columns.count(column::SHAPE))
    {
        columns.erase(column::SHAPE);
        return columns;
    }

    return boost::none;
}


std::string distinctClause(
    const std::string& connStr, const std::string& fromSchema, const std::string& fromTable)
{
    std::string result = "DISTINCT";

    auto columns = getColumnsForDistinct(connStr, fromSchema, fromTable);

    if (!columns) {
        return result;
    }

    result += " ON(" + stringutils::join(*columns, ",") + ")";

    return result;
}

} // namespace


Join::Join(
    const std::string& joinTable,
    const std::string& fromTableColumn,
    const std::string& joinTableColumn)
    : joinTable_{joinTable}
    , fromTableColumn_{fromTableColumn}
    , joinTableColumn_{joinTableColumn}
{}


Join& Join::left()
{
    type_ = Type::Left;
    return *this;
}


Join& Join::as(const std::string& as)
{
    as_ = as;
    return *this;
}


const std::string& Join::as() const
{
    return as_.empty() ? joinTable_ : as_;
}


Join& Join::notUseInWhere()
{
    useInWhere_ = false;
    return *this;
}


bool Join::useInWhere() const
{
    return useInWhere_;
}


std::string Join::operator () (
    const std::string& fromSchema,
    const std::string& fromTable) const
{
    std::string result;

    switch (type_) {
        case Type::Inner: break;
        case Type::Left: result += "LEFT "; break;
    }

    result += "JOIN " + fromSchema + "." + joinTable_ + (as_.empty() ? "" : " " + as_) + " ";

    if (joinTableColumn_.empty()) {
        result += usingClause();
    } else {
        result += onClause(fromTable);
    }

    return result;
}


std::string Join::onClause(const std::string& fromTable) const
{
    return
        "ON " + fromTable + "." + fromTableColumn_ + " = " +
        as() + "." + joinTableColumn_;
}


std::string Join::usingClause() const
{
    return "USING(" + fromTableColumn_ + ")";
}


std::string query(
    const std::string& connStr,
    const std::string& fromSchema,
    const std::string& toSchema,
    const std::string& table,
    const Joins& joins,
    const std::string& isocodes)
{
    std::string result;

    result =
        "CREATE TABLE " + toSchema + "." + table + " AS\n"
        "SELECT " + distinctClause(connStr, fromSchema, table) + " " + fromSchema + "." + table + ".*\n"
        "FROM " + fromSchema + "." + table + "\n";

    const auto& joinsRange = joins.equal_range(table);
    for (auto joinIt = joinsRange.first; joinIt != joinsRange.second; ++joinIt) {
        result += joinIt->second(fromSchema, table) + "\n";
    }

    result += "WHERE\n";
    const auto& wheresRange = joins.equal_range(table);
    for (auto whereIt = wheresRange.first; whereIt != wheresRange.second; ++whereIt) {
        if (whereIt->second.useInWhere()) {
            result += "  " + whereIt->second.as() + "." + column::ISOCODE + " IN (" + isocodes + ") OR\n";
        }
    }
    result.resize(result.size() - 4); // " OR\n".size() == 4

    return result;
}


Tables getTables(const Joins& joins)
{
    std::set<std::string> result;
    for (const auto& join: joins) {
        result.insert(join.first);
    };
    return result;
}


} // namespace sql
} // namespace ymapsdf_splitter
} // namespace wiki
} // namespace maps
