#pragma once
#include <drogon/exports.h>
#include <drogon/orm/SqlBinder.h>
#include <assert.h>
#include <memory>
#include <string>
#include <tuple>
#include <type_traits>
namespace Json
{
class Value;
}
namespace drogon
{
namespace orm
{
enum class CompareOperator
{
    EQ,
    NE,
    GT,
    GE,
    LT,
    LE,
    Like,
    NotLike,
    In,
    NotIn,
    IsNull,
    IsNotNull
};
struct CustomSql
{
    explicit CustomSql(std::string content) : content_(std::move(content))
    {
    }
    std::string content_;
};
inline CustomSql operator""_sql(const char *str, size_t)
{
    return CustomSql(str);
}
class DROGON_EXPORT Criteria
{
  public:
    explicit operator bool() const
    {
        return !conditionString_.empty();
    }
    std::string criteriaString() const
    {
        return conditionString_;
    }
    template <typename... Arguments>
    explicit Criteria(const CustomSql &sql, Arguments &&...args)
    {
        conditionString_ = sql.content_;
        outputArgumentsFunc_ =
            [args = std::make_tuple(std::forward<Arguments>(args)...)](
                internal::SqlBinder &binder) mutable {
                return std::apply(
                    [&binder](auto &&...args) {
                        (void)std::initializer_list<int>{
                            (binder << std::forward<Arguments>(args), 0)...};
                    },
                    std::move(args));
            };
    }
    template <typename... Arguments>
    explicit Criteria(CustomSql &&sql, Arguments &&...args)
    {
        conditionString_ = std::move(sql.content_);
        outputArgumentsFunc_ =
            [args = std::make_tuple(std::forward<Arguments>(args)...)](
                internal::SqlBinder &binder) mutable {
                return std::apply(
                    [&binder](auto &&...args) {
                        (void)std::initializer_list<int>{
                            (binder << std::forward<Arguments>(args), 0)...};
                    },
                    std::move(args));
            };
    }
    template <typename T>
    Criteria(const std::string &colName, const CompareOperator &opera, T &&arg)
    {
        assert(opera != CompareOperator::IsNotNull &&
               opera != CompareOperator::IsNull);
        conditionString_ = colName;
        switch (opera)
        {
            case CompareOperator::EQ:
                conditionString_ += " = $?";
                break;
            case CompareOperator::NE:
                conditionString_ += " != $?";
                break;
            case CompareOperator::GT:
                conditionString_ += " > $?";
                break;
            case CompareOperator::GE:
                conditionString_ += " >= $?";
                break;
            case CompareOperator::LT:
                conditionString_ += " < $?";
                break;
            case CompareOperator::LE:
                conditionString_ += " <= $?";
                break;
            case CompareOperator::Like:
                conditionString_ += " like $?";
                break;
            case CompareOperator::NotLike:
                conditionString_ += " not like $?";
                break;
            default:
                break;
        }
        outputArgumentsFunc_ =
            [arg = std::forward<T>(arg)](internal::SqlBinder &binder) {
                binder << arg;
            };
    }
    template <typename T>
    Criteria(const std::string &colName,
             const CompareOperator &opera,
             const std::vector<T> &args)
    {
        const auto argsSize = args.size();
        assert(((opera == CompareOperator::In) ||
                (opera == CompareOperator::NotIn)) &&
               (argsSize > 0));
        if (opera == CompareOperator::In)
        {
            conditionString_ = colName + " in (";
        }
        else if (opera == CompareOperator::NotIn)
        {
            conditionString_ = colName + " not in (";
        }
        for (size_t i = 0; i < argsSize; ++i)
        {
            if (i < (argsSize - 1))
                conditionString_.append("$?,");
            else
                conditionString_.append("$?");
        }
        conditionString_.append(")");
        outputArgumentsFunc_ = [args](internal::SqlBinder &binder) {
            for (auto &arg : args)
            {
                binder << arg;
            }
        };
    }
    template <typename T>
    Criteria(const std::string &colName,
             const CompareOperator &opera,
             std::vector<T> &&args)
    {
        const auto argsSize = args.size();
        assert(((opera == CompareOperator::In) ||
                (opera == CompareOperator::NotIn)) &&
               (argsSize > 0));
        if (opera == CompareOperator::In)
        {
            conditionString_ = colName + " in (";
        }
        else if (opera == CompareOperator::NotIn)
        {
            conditionString_ = colName + " not in (";
        }
        for (size_t i = 0; i < argsSize; ++i)
        {
            if (i < (argsSize - 1))
                conditionString_.append("$?,");
            else
                conditionString_.append("$?");
        }
        conditionString_.append(")");
        outputArgumentsFunc_ =
            [args = std::move(args)](internal::SqlBinder &binder) {
                for (auto &arg : args)
                {
                    binder << arg;
                }
            };
    }
    template <typename T>
    Criteria(const std::string &colName,
             const CompareOperator &opera,
             std::vector<T> &args)
        : Criteria(colName, opera, (const std::vector<T> &)args)
    {
    }
    template <typename T>
    Criteria(const std::string &colName, T &&arg)
        : Criteria(colName, CompareOperator::EQ, std::forward<T>(arg))
    {
    }
    Criteria(const std::string &colName, const CompareOperator &opera)
    {
        assert(opera == CompareOperator::IsNotNull ||
               opera == CompareOperator::IsNull);
        conditionString_ = colName;
        switch (opera)
        {
            case CompareOperator::IsNull:
                conditionString_ += " is null";
                break;
            case CompareOperator::IsNotNull:
                conditionString_ += " is not null";
                break;
            default:
                break;
        }
    }
    Criteria(const std::string &colName, CompareOperator &opera)
        : Criteria(colName, (const CompareOperator &)opera)
    {
    }
    Criteria(const std::string &colName, CompareOperator &&opera)
        : Criteria(colName, (const CompareOperator &)opera)
    {
    }
    explicit Criteria(const Json::Value &json) noexcept(false);
    Criteria() = default;
    void outputArgs(internal::SqlBinder &binder) const
    {
        if (outputArgumentsFunc_)
            outputArgumentsFunc_(binder);
    }
  private:
    friend DROGON_EXPORT const Criteria operator&&(Criteria cond1,
                                                   Criteria cond2);
    friend DROGON_EXPORT const Criteria operator||(Criteria cond1,
                                                   Criteria cond2);
    std::string conditionString_;
    std::function<void(internal::SqlBinder &)> outputArgumentsFunc_;
};  
DROGON_EXPORT const Criteria operator&&(Criteria cond1, Criteria cond2);
DROGON_EXPORT const Criteria operator||(Criteria cond1, Criteria cond2);
}  
}  