////////////////////////////////////////////////////////////////////////////
//
// 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_SCHEMA_HPP
#define REALM_SCHEMA_HPP

#include <ostream>
#include <string>
#include <vector>

#include <realm/object-store/object_schema.hpp>
#include <realm/util/features.h>

namespace realm {
class SchemaChange;
class StringData;
struct TableKey;
struct Property;

// How to handle update_schema() being called on a file which has
// already been initialized with a different schema
enum class SchemaMode : uint8_t {
    // If the schema version has increased, automatically apply all
    // changes, then call the migration function.
    //
    // If the schema version has not changed, verify that the only
    // changes are to add new tables and add or remove indexes, and then
    // apply them if so. Does not call the migration function.
    //
    // This mode does not automatically remove tables which are not
    // present in the schema that must be manually done in the migration
    // function, to support sharing a Realm file between processes using
    // different class subsets.
    //
    // This mode allows using schemata with different subsets of tables
    // on different threads, but the tables which are shared must be
    // identical.
    Automatic,

    // Open the file in immutable mode. Schema version must match the
    // version in the file, and all tables present in the file must
    // exactly match the specified schema, except for indexes. Tables
    // are allowed to be missing from the file.
    Immutable,

    // Open the Realm in read-only mode, transactions are not allowed to
    // be performed on the Realm instance. The schema of the existing Realm
    // file won't be changed through this Realm instance. Extra tables and
    // extra properties are allowed in the existing Realm schema. The
    // difference of indexes is allowed as well. Other schema differences
    // than those will cause an exception. This is different from Immutable
    // mode, sync Realm can be opened with ReadOnly mode. Changes
    // can be made to the Realm file through another writable Realm instance.
    // Thus, notifications are also allowed in this mode.
    ReadOnly,

    // If the schema version matches and the only schema changes are new
    // tables and indexes being added or removed, apply the changes to
    // the existing file.
    // Otherwise delete the file and recreate it from scratch.
    // The migration function is not used.
    //
    // This mode allows using schemata with different subsets of tables
    // on different threads, but the tables which are shared must be
    // identical.
    SoftResetFile,

    // Delete the file and recreate it from scratch.
    // The migration function is not used.
    HardResetFile,

    // The only changes allowed are to add new tables, add columns to
    // existing tables, and to add or remove indexes from existing
    // columns. Extra tables not present in the schema are ignored.
    // The migration function is not used.
    // This should be used when including discovered user classes.
    // Previously called Additive.
    //
    // This mode allows updating the schema with additive changes even
    // if the Realm is already open on another thread.
    AdditiveDiscovered,

    // The same additive properties as AdditiveDiscovered, except
    // in this mode, all classes in the schema have been explicitly
    // included by the user. This means that stricter schema checks are
    // run such as throwing an error when an embedded object type which
    // is not linked from any top level object types is included.
    AdditiveExplicit,

    // Verify that the schema version has increased, call the migration
    // function, and then verify that the schema now matches.
    // The migration function is mandatory for this mode.
    //
    // This mode requires that all threads and processes which open a
    // file use identical schemata.
    Manual
};

// Options for how to handle the schema when the file has classes and/or
// properties not in the schema.
//
// Most schema modes allow the requested schema to be a subset of the actual
// schema of the Realm file. By default, any properties or object types not in
// the requested schema are simply ignored entirely and the Realm's in-memory
// schema will always exactly match the requested one.
struct SchemaSubsetMode {
    // Add additional tables present in the Realm file to the schema. This is
    // applicable to all schema modes except for Manual and ResetFile.
    bool include_types : 1;

    // Add additional columns in the tables present in the Realm file to the
    // object schema for those types. The additional properties are always
    // added to the end of persisted_properties. This is only applicable to
    // Additive and ReadOnly schema modes.
    bool include_properties : 1;

    // The reported schema will always exactly match the requested one.
    static const SchemaSubsetMode Strict;
    // Additional object classes present in the Realm file are added to the
    // requested schema, but all object types present in the requested schema
    // will always exactly match even if there are additional columns in the
    // tables.
    static const SchemaSubsetMode AllClasses;
    // Additional properties present in the Realm file are added to the
    // requested schema, but tables not present in the schema are ignored.
    static const SchemaSubsetMode AllProperties;
    // Always report the complete schema.
    static const SchemaSubsetMode Complete;

    friend bool operator==(const SchemaSubsetMode& x, const SchemaSubsetMode& y)
    {
        return x.include_types == y.include_types && x.include_properties == y.include_properties;
    }
};

inline constexpr SchemaSubsetMode SchemaSubsetMode::Strict = {false, false};
inline constexpr SchemaSubsetMode SchemaSubsetMode::AllClasses = {true, false};
inline constexpr SchemaSubsetMode SchemaSubsetMode::AllProperties = {false, true};
inline constexpr SchemaSubsetMode SchemaSubsetMode::Complete = {true, true};


class Schema : private std::vector<ObjectSchema> {
private:
    using base = std::vector<ObjectSchema>;

public:
    Schema() noexcept;
    ~Schema();
    // Create a schema from a vector of ObjectSchema
    Schema(base types) noexcept;
    Schema(std::initializer_list<ObjectSchema> types);

    Schema(Schema const&);
    Schema(Schema&&) noexcept;
    Schema& operator=(Schema const&);
    Schema& operator=(Schema&&) noexcept;

    // find an ObjectSchema by name
    iterator find(StringData name) noexcept;
    const_iterator find(StringData name) const noexcept;

    // find an ObjectSchema with the same name as the passed in one
    iterator find(ObjectSchema const& object) noexcept;
    const_iterator find(ObjectSchema const& object) const noexcept;

    // find an ObjectSchema by table key
    iterator find(TableKey table_key) noexcept;
    const_iterator find(TableKey table_key) const noexcept;

    // Verify that this schema is internally consistent (i.e. all properties are
    // valid, links link to types that actually exist, etc.)
    void validate(SchemaValidationMode validation_mode = SchemaValidationMode::Basic) const;

    // Get the changes which must be applied to this schema to produce the passed-in schema
    std::vector<SchemaChange> compare(Schema const&, SchemaMode = SchemaMode::Automatic,
                                      bool include_removals = false) const;

    void copy_keys_from(Schema const&, SchemaSubsetMode subset_mode);

    friend bool operator==(Schema const&, Schema const&) noexcept;
    friend bool operator!=(Schema const& a, Schema const& b) noexcept
    {
        return !(a == b);
    }
    friend std::ostream& operator<<(std::ostream&, const Schema&);

    using base::begin;
    using base::const_iterator;
    using base::empty;
    using base::end;
    using base::iterator;
    using base::size;

private:
    template <typename T, typename U, typename Func>
    static void zip_matching(T&& a, U&& b, Func&& func);
    // sort all the classes by name in order to speed up find(StringData name)
    void sort_schema();
};

namespace schema_change {
struct AddTable {
    const ObjectSchema* object;
};

struct RemoveTable {
    const ObjectSchema* object;
};

struct ChangeTableType {
    const ObjectSchema* object;
    const ObjectSchema::ObjectType* old_table_type;
    const ObjectSchema::ObjectType* new_table_type;
};

struct AddInitialProperties {
    const ObjectSchema* object;
};

struct AddProperty {
    const ObjectSchema* object;
    const Property* property;
};

struct RemoveProperty {
    const ObjectSchema* object;
    const Property* property;
};

struct ChangePropertyType {
    const ObjectSchema* object;
    const Property* old_property;
    const Property* new_property;
};

struct MakePropertyNullable {
    const ObjectSchema* object;
    const Property* property;
};

struct MakePropertyRequired {
    const ObjectSchema* object;
    const Property* property;
};

struct AddIndex {
    const ObjectSchema* object;
    const Property* property;
    IndexType type;
};

struct RemoveIndex {
    const ObjectSchema* object;
    const Property* property;
};

struct ChangePrimaryKey {
    const ObjectSchema* object;
    const Property* property;
};
} // namespace schema_change

#define REALM_FOR_EACH_SCHEMA_CHANGE_TYPE(macro)                                                                     \
    macro(AddTable) macro(RemoveTable) macro(ChangeTableType) macro(AddInitialProperties) macro(AddProperty)         \
        macro(RemoveProperty) macro(ChangePropertyType) macro(MakePropertyNullable) macro(MakePropertyRequired)      \
            macro(AddIndex) macro(RemoveIndex) macro(ChangePrimaryKey)

class SchemaChange {
public:
#define REALM_SCHEMA_CHANGE_CONSTRUCTOR(name)                                                                        \
    SchemaChange(schema_change::name value)                                                                          \
        : m_kind(Kind::name)                                                                                         \
    {                                                                                                                \
        name = value;                                                                                                \
    }
    REALM_FOR_EACH_SCHEMA_CHANGE_TYPE(REALM_SCHEMA_CHANGE_CONSTRUCTOR)
#undef REALM_SCHEMA_CHANGE_CONSTRUCTOR

    template <typename Visitor>
    auto visit(Visitor&& visitor) const
    {
        switch (m_kind) {
#define REALM_SWITCH_CASE(name)                                                                                      \
    case Kind::name:                                                                                                 \
        return visitor(name);
            REALM_FOR_EACH_SCHEMA_CHANGE_TYPE(REALM_SWITCH_CASE)
#undef REALM_SWITCH_CASE
        }
        REALM_COMPILER_HINT_UNREACHABLE();
    }

    friend bool operator==(SchemaChange const& lft, SchemaChange const& rgt) noexcept;

private:
    enum class Kind {
#define REALM_SCHEMA_CHANGE_TYPE(name) name,
        REALM_FOR_EACH_SCHEMA_CHANGE_TYPE(REALM_SCHEMA_CHANGE_TYPE)
#undef REALM_SCHEMA_CHANGE_TYPE

    } m_kind;
    union {
#define REALM_DEFINE_FIELD(name) schema_change::name name;
        REALM_FOR_EACH_SCHEMA_CHANGE_TYPE(REALM_DEFINE_FIELD)
#undef REALM_DEFINE_FIELD
    };
};

#undef REALM_FOR_EACH_SCHEMA_CHANGE_TYPE
} // namespace realm

#endif /* defined(REALM_SCHEMA_HPP) */
