#pragma once

#include <type_traits>

#include <Core/Types.h>
#include <Common/UInt128.h>


namespace DB
{

/** Allows get the result type of the functions +, -, *, /, %, intDiv (integer division).
  * The rules are different from those used in C++.
  */

namespace NumberTraits
{

struct Error {};

constexpr size_t max(size_t x, size_t y)
{
    return x > y ? x : y;
}

constexpr size_t min(size_t x, size_t y)
{
    return x < y ? x : y;
}

constexpr size_t nextSize(size_t size)
{
    return min(size * 2, 8);
}

template <bool is_signed, bool is_floating, size_t size>
struct Construct
{
    using Type = Error;
};

template <> struct Construct<false, false, 1> { using Type = UInt8; };
template <> struct Construct<false, false, 2> { using Type = UInt16; };
template <> struct Construct<false, false, 4> { using Type = UInt32; };
template <> struct Construct<false, false, 8> { using Type = UInt64; };
template <> struct Construct<false, true, 1> { using Type = Float32; };
template <> struct Construct<false, true, 2> { using Type = Float32; };
template <> struct Construct<false, true, 4> { using Type = Float32; };
template <> struct Construct<false, true, 8> { using Type = Float64; };
template <> struct Construct<true, false, 1> { using Type = Int8; };
template <> struct Construct<true, false, 2> { using Type = Int16; };
template <> struct Construct<true, false, 4> { using Type = Int32; };
template <> struct Construct<true, false, 8> { using Type = Int64; };
template <> struct Construct<true, true, 1> { using Type = Float32; };
template <> struct Construct<true, true, 2> { using Type = Float32; };
template <> struct Construct<true, true, 4> { using Type = Float32; };
template <> struct Construct<true, true, 8> { using Type = Float64; };


/** The result of addition or multiplication is calculated according to the following rules:
    * - if one of the arguments is floating-point, the result is a floating point, otherwise - the whole;
    * - if one of the arguments is signed, the result is signed, otherwise it is unsigned;
    * - the result contains more bits (not only meaningful) than the maximum in the arguments
    *   (for example, UInt8 + Int32 = Int64).
    */
template <typename A, typename B> struct ResultOfAdditionMultiplication
{
    using Type = typename Construct<
        std::is_signed_v<A> || std::is_signed_v<B>,
        std::is_floating_point_v<A> || std::is_floating_point_v<B>,
        nextSize(max(sizeof(A), sizeof(B)))>::Type;
};

template <typename A, typename B> struct ResultOfSubtraction
{
    using Type = typename Construct<
        true,
        std::is_floating_point_v<A> || std::is_floating_point_v<B>,
        nextSize(max(sizeof(A), sizeof(B)))>::Type;
};

/** When dividing, you always get a floating-point number.
    */
template <typename A, typename B> struct ResultOfFloatingPointDivision
{
    using Type = Float64;
};

/** For integer division, we get a number with the same number of bits as in divisible.
    */
template <typename A, typename B> struct ResultOfIntegerDivision
{
    using Type = typename Construct<
        std::is_signed_v<A> || std::is_signed_v<B>,
        false,
        sizeof(A)>::Type;
};

/** Division with remainder you get a number with the same number of bits as in divisor.
    */
template <typename A, typename B> struct ResultOfModulo
{
    using Type = typename Construct<
        std::is_signed_v<A> || std::is_signed_v<B>,
        false,
        sizeof(B)>::Type;
};

template <typename A> struct ResultOfNegate
{
    using Type = typename Construct<
        true,
        std::is_floating_point_v<A>,
        std::is_signed_v<A> ? sizeof(A) : nextSize(sizeof(A))>::Type;
};

template <typename A> struct ResultOfAbs
{
    using Type = typename Construct<
        false,
        std::is_floating_point_v<A>,
        sizeof(A)>::Type;
};

/** For bitwise operations, an integer is obtained with number of bits is equal to the maximum of the arguments.
    */
template <typename A, typename B> struct ResultOfBit
{
    using Type = typename Construct<
        std::is_signed_v<A> || std::is_signed_v<B>,
        false,
        std::is_floating_point_v<A> || std::is_floating_point_v<B> ? 8 : max(sizeof(A), sizeof(B))>::Type;
};

template <typename A> struct ResultOfBitNot
{
    using Type = typename Construct<
        std::is_signed_v<A>,
        false,
        sizeof(A)>::Type;
};


/** Type casting for `if` function:
  * UInt<x>,  UInt<y>   ->  UInt<max(x,y)>
  * Int<x>,   Int<y>    ->   Int<max(x,y)>
  * Float<x>, Float<y>  -> Float<max(x, y)>
  * UInt<x>,  Int<y>    ->   Int<max(x*2, y)>
  * Float<x>, [U]Int<y> -> Float<max(x, y*2)>
  * Decimal<x>, Decimal<y> -> Decimal<max(x,y)>
  * UUID, UUID          -> UUID
  * UInt64 ,  Int<x>    -> Error
  * Float<x>, [U]Int64  -> Error
  */
template <typename A, typename B>
struct ResultOfIf
{
    static constexpr bool has_float = std::is_floating_point_v<A> || std::is_floating_point_v<B>;
    static constexpr bool has_integer = std::is_integral_v<A> || std::is_integral_v<B>;
    static constexpr bool has_signed = std::is_signed_v<A> || std::is_signed_v<B>;
    static constexpr bool has_unsigned = !std::is_signed_v<A> || !std::is_signed_v<B>;

    static constexpr size_t max_size_of_unsigned_integer = max(std::is_signed_v<A> ? 0 : sizeof(A), std::is_signed_v<B> ? 0 : sizeof(B));
    static constexpr size_t max_size_of_signed_integer = max(std::is_signed_v<A> ? sizeof(A) : 0, std::is_signed_v<B> ? sizeof(B) : 0);
    static constexpr size_t max_size_of_integer = max(std::is_integral_v<A> ? sizeof(A) : 0, std::is_integral_v<B> ? sizeof(B) : 0);
    static constexpr size_t max_size_of_float = max(std::is_floating_point_v<A> ? sizeof(A) : 0, std::is_floating_point_v<B> ? sizeof(B) : 0);

    using ConstructedType = typename Construct<has_signed, has_float,
        ((has_float && has_integer && max_size_of_integer >= max_size_of_float)
            || (has_signed && has_unsigned && max_size_of_unsigned_integer >= max_size_of_signed_integer))
                ? max(sizeof(A), sizeof(B)) * 2
                : max(sizeof(A), sizeof(B))>::Type;

    using ConstructedWithUUID = std::conditional_t<std::is_same_v<A, UInt128> && std::is_same_v<B, UInt128>, A, ConstructedType>;

    using Type = std::conditional_t<!IsDecimalNumber<A> && !IsDecimalNumber<B>, ConstructedWithUUID,
        std::conditional_t<IsDecimalNumber<A> && IsDecimalNumber<B>, std::conditional_t<(sizeof(A) > sizeof(B)), A, B>, Error>>;
};

/** Before applying operator `%` and bitwise operations, operands are casted to whole numbers. */
template <typename A> struct ToInteger
{
    using Type = typename Construct<
        std::is_signed_v<A>,
        false,
        std::is_floating_point_v<A> ? 8 : sizeof(A)>::Type;
};


// CLICKHOUSE-29. The same depth, different signs
// NOTE: This case is applied for 64-bit integers only (for backward compability), but could be used for any-bit integers
template <typename A, typename B>
constexpr bool LeastGreatestSpecialCase =
    std::is_integral_v<A> && std::is_integral_v<B>
    && (8 == sizeof(A) && sizeof(A) == sizeof(B))
    && (std::is_signed_v<A> ^ std::is_signed_v<B>);

template <typename A, typename B>
using ResultOfLeast = std::conditional_t<LeastGreatestSpecialCase<A, B>,
    typename Construct<true, false, sizeof(A)>::Type,
    typename ResultOfIf<A, B>::Type>;

template <typename A, typename B>
using ResultOfGreatest = std::conditional_t<LeastGreatestSpecialCase<A, B>,
    typename Construct<false, false, sizeof(A)>::Type,
    typename ResultOfIf<A, B>::Type>;

}

}
