////////////////////////////////////////////////////////////////////////////
//
// Copyright 2015 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

#ifndef REALM_PROPERTY_HPP
#define REALM_PROPERTY_HPP

#include <realm/util/features.h>
#include <realm/util/assert.hpp>
#include <realm/keys.hpp>
#include <realm/util/optional.hpp>
#include <realm/util/tagged_bool.hpp>

#include <string>
namespace realm {
class BinaryData;
class Decimal128;
class Obj;
class ObjectId;
class StringData;
class Table;
class Timestamp;
class UUID;
class Mixed;

enum class PropertyType : unsigned short {
    Int = 0,
    Bool = 1,
    String = 2,
    Data = 3,
    Date = 4,
    Float = 5,
    Double = 6,
    Object = 7,         // currently must be either Array xor Nullable
    LinkingObjects = 8, // currently must be Array and not Nullable

    Mixed = 9,
    ObjectId = 10,
    Decimal = 11,
    UUID = 12,

    // Flags which can be combined with any of the above types except as noted
    Required = 0,
    Nullable = 64,
    Array = 128,

    Set = 256,
    Dictionary = 512,

    Collection = Array | Set | Dictionary,
    Flags = Nullable | Collection
};

struct Property {
    using IsPrimary = util::TaggedBool<class IsPrimaryTag>;
    using IsIndexed = util::TaggedBool<class IsIndexedTag>;
    using IsFulltextIndexed = util::TaggedBool<class IsFulltextIndexedTag>;

    // The internal column name used in the Realm file.
    std::string name;

    // The public name used by the binding to represent the internal column name in the Realm file. Bindings can use
    // this to expose a different name in the binding API, e.g. to map between different naming conventions.
    //
    // Public names are only ever user defined, they are not persisted on disk, so reading the schema from the file
    // will leave this field empty. If `public_name` is empty, the internal and public name are considered to be the
    // same.
    //
    // ObjectStore will ensure that no conflicts occur between persisted properties and the public name, so
    // the public name is just as unique an identifier as the internal name in the file.
    //
    // In order to respect public names bindings should use `ObjectSchema::property_for_public_name()` in the schema
    // and `Object::value_for_property()` in the Object accessor for reading fields defined by the public name.
    //
    // For queries, bindings should provide an appropriate `KeyPathMapping` definition. Bindings are responsible
    // for creating this.
    std::string public_name;
    PropertyType type = PropertyType::Int;
    std::string object_type;
    std::string link_origin_property_name;
    IsPrimary is_primary = false;
    IsIndexed is_indexed = false;
    IsFulltextIndexed is_fulltext_indexed = false;

    ColKey column_key;

    Property() = default;

    Property(std::string name, PropertyType type, IsPrimary primary = false, IsIndexed indexed = false,
             std::string public_name = "");

    // Text property with fulltext index
    Property(std::string name, IsFulltextIndexed indexed, std::string public_name = "");

    // Link to another object
    Property(std::string name, PropertyType type, std::string object_type, std::string link_origin_property_name = "",
             std::string public_name = "");

    Property(Property const&) = default;
    Property(Property&&) noexcept = default;
    Property& operator=(Property const&) = default;
    Property& operator=(Property&&) noexcept = default;

    bool requires_index() const
    {
        return is_indexed || is_primary;
    }

    bool requires_fulltext_index() const
    {
        return is_fulltext_indexed;
    }

    bool type_is_indexable() const noexcept;
    bool type_is_nullable() const noexcept;
    size_t type_nesting_levels() const noexcept;

    std::string type_string() const;
};

template <typename E>
constexpr auto to_underlying(E e)
{
    return static_cast<typename std::underlying_type<E>::type>(e);
}

inline constexpr PropertyType operator&(PropertyType a, PropertyType b)
{
    return static_cast<PropertyType>(to_underlying(a) & to_underlying(b));
}

inline constexpr PropertyType operator|(PropertyType a, PropertyType b)
{
    return static_cast<PropertyType>(to_underlying(a) | to_underlying(b));
}

inline constexpr PropertyType operator^(PropertyType a, PropertyType b)
{
    return static_cast<PropertyType>(to_underlying(a) ^ to_underlying(b));
}

inline constexpr PropertyType operator~(PropertyType a)
{
    return static_cast<PropertyType>(~to_underlying(a));
}

inline constexpr bool operator==(PropertyType a, PropertyType b)
{
    return to_underlying(a & ~PropertyType::Flags) == to_underlying(b & ~PropertyType::Flags);
}

inline constexpr bool operator!=(PropertyType a, PropertyType b)
{
    return !(a == b);
}

inline PropertyType& operator&=(PropertyType& a, PropertyType b)
{
    a = a & b;
    return a;
}

inline PropertyType& operator|=(PropertyType& a, PropertyType b)
{
    a = a | b;
    return a;
}

inline PropertyType& operator^=(PropertyType& a, PropertyType b)
{
    a = a ^ b;
    return a;
}

inline constexpr bool is_array(PropertyType a)
{
    return to_underlying(a & PropertyType::Array) == to_underlying(PropertyType::Array);
}

inline constexpr bool is_set(PropertyType a)
{
    return to_underlying(a & PropertyType::Set) == to_underlying(PropertyType::Set);
}

inline constexpr bool is_dictionary(PropertyType a)
{
    return to_underlying(a & PropertyType::Dictionary) == to_underlying(PropertyType::Dictionary);
}

inline constexpr bool is_collection(PropertyType a)
{
    return to_underlying(a & PropertyType::Collection) != 0;
}

inline constexpr bool is_nullable(PropertyType a)
{
    return to_underlying(a & PropertyType::Nullable) == to_underlying(PropertyType::Nullable);
}

inline constexpr bool is_mixed(PropertyType a)
{
    return to_underlying(a & PropertyType::Mixed) == to_underlying(PropertyType::Mixed);
}

// Some of the places we use switch_on_type() the Obj version isn't instantiatable
// or reachable, so we want to map it to a valid type to let the unreachable code compile
template <typename T>
struct NonObjType {
    using type = std::remove_reference_t<T>;
};
template <>
struct NonObjType<Obj&> {
    using type = int64_t;
};
template <typename T>
using NonObjTypeT = typename NonObjType<T>::type;

template <typename ObjType = Obj, typename Fn>
static auto switch_on_type(PropertyType type, Fn&& fn)
{
    using PT = PropertyType;
    bool is_optional = is_nullable(type);
    switch (type & ~PropertyType::Flags) {
        case PT::Int:
            return is_optional ? fn((util::Optional<int64_t>*)0) : fn((int64_t*)0);
        case PT::Bool:
            return is_optional ? fn((util::Optional<bool>*)0) : fn((bool*)0);
        case PT::Float:
            return is_optional ? fn((util::Optional<float>*)0) : fn((float*)0);
        case PT::Double:
            return is_optional ? fn((util::Optional<double>*)0) : fn((double*)0);
        case PT::String:
            return fn((StringData*)0);
        case PT::Data:
            return fn((BinaryData*)0);
        case PT::Date:
            return fn((Timestamp*)0);
        case PT::Object:
            return fn((ObjType*)0);
        case PT::ObjectId:
            return is_optional ? fn((util::Optional<ObjectId>*)0) : fn((ObjectId*)0);
        case PT::Decimal:
            return fn((Decimal128*)0);
        case PT::UUID:
            return is_optional ? fn((util::Optional<UUID>*)0) : fn((UUID*)0);
        case PT::Mixed:
            return fn((Mixed*)0);
        default:
            REALM_COMPILER_HINT_UNREACHABLE();
    }
}

static const char* string_for_property_type(PropertyType type)
{
    if (is_array(type)) {
        if (type == PropertyType::LinkingObjects)
            return "linking objects";
        return "array";
    }
    if (is_set(type)) {
        return "set";
    }
    if (is_dictionary(type)) {
        return "dictionary";
    }
    switch (type & ~PropertyType::Flags) {
        case PropertyType::String:
            return "string";
        case PropertyType::Int:
            return "int";
        case PropertyType::Bool:
            return "bool";
        case PropertyType::Date:
            return "date";
        case PropertyType::Data:
            return "data";
        case PropertyType::Double:
            return "double";
        case PropertyType::Float:
            return "float";
        case PropertyType::Object:
            return "object";
        case PropertyType::Mixed:
            return "mixed";
        case PropertyType::UUID:
            return "uuid";
        case PropertyType::LinkingObjects:
            return "linking objects";
        case PropertyType::ObjectId:
            return "object id";
        case PropertyType::Decimal:
            return "decimal";
        default:
            REALM_COMPILER_HINT_UNREACHABLE();
    }
}

inline Property::Property(std::string name, PropertyType type, IsPrimary primary, IsIndexed indexed,
                          std::string public_name)
    : name(std::move(name))
    , public_name(std::move(public_name))
    , type(type)
    , is_primary(primary)
    , is_indexed(indexed)
{
}

inline Property::Property(std::string name, IsFulltextIndexed indexed, std::string public_name)
    : name(std::move(name))
    , public_name(std::move(public_name))
    , type(PropertyType::String)
    , is_fulltext_indexed(indexed)
{
}

inline Property::Property(std::string name, PropertyType type, std::string object_type,
                          std::string link_origin_property_name, std::string public_name)
    : name(std::move(name))
    , public_name(std::move(public_name))
    , type(type)
    , object_type(std::move(object_type))
    , link_origin_property_name(std::move(link_origin_property_name))
{
}

inline bool Property::type_is_indexable() const noexcept
{
    return (!is_collection(type) || (is_array(type) && type == PropertyType::String)) &&
           (type == PropertyType::Int || type == PropertyType::Bool || type == PropertyType::Date ||
            type == PropertyType::String || type == PropertyType::ObjectId || type == PropertyType::UUID ||
            type == PropertyType::Mixed);
}

inline bool Property::type_is_nullable() const noexcept
{
    return !((is_array(type) || is_set(type)) && type == PropertyType::Object) &&
           type != PropertyType::LinkingObjects;
}

inline std::string Property::type_string() const
{
    if (is_array(type)) {
        if (type == PropertyType::Object)
            return "array<" + object_type + ">";
        if (type == PropertyType::LinkingObjects)
            return "linking objects<" + object_type + ">";
        return std::string("array<") + string_for_property_type(type & ~PropertyType::Flags) + ">";
    }
    if (is_set(type)) {
        REALM_ASSERT(type != PropertyType::LinkingObjects);
        if (type == PropertyType::Object)
            return "set<" + object_type + ">";
        return std::string("set<") + string_for_property_type(type & ~PropertyType::Flags) + ">";
    }
    if (is_dictionary(type)) {
        REALM_ASSERT(type != PropertyType::LinkingObjects);
        if (type == PropertyType::Object)
            return "dictionary<string, " + object_type + ">";
        return std::string("dictionary<string, ") + string_for_property_type(type & ~PropertyType::Flags) + ">";
    }
    switch (auto base_type = (type & ~PropertyType::Flags)) {
        case PropertyType::Object:
            return "<" + object_type + ">";
        case PropertyType::LinkingObjects:
            return "linking objects<" + object_type + ">";
        default:
            return string_for_property_type(base_type);
    }
}

inline bool operator==(Property const& lft, Property const& rgt)
{
    // note: not checking column_key
    // ordered roughly by the cost of the check
    return to_underlying(lft.type) == to_underlying(rgt.type) && lft.is_primary == rgt.is_primary &&
           lft.requires_index() == rgt.requires_index() &&
           lft.requires_fulltext_index() == rgt.requires_fulltext_index() && lft.name == rgt.name &&
           lft.object_type == rgt.object_type && lft.link_origin_property_name == rgt.link_origin_property_name;
}
} // namespace realm

#endif // REALM_PROPERTY_HPP
