////////////////////////////////////////////////////////////////////////////
//
// Copyright 2021 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 DEEP_CHANGE_CHECKER_HPP
#define DEEP_CHANGE_CHECKER_HPP

#include <realm/object-store/object_changeset.hpp>
#include <realm/object-store/impl/collection_change_builder.hpp>
#include <realm/collection_parent.hpp>

#include <array>

namespace realm {
class CollectionBase;
class Group;
class Mixed;
class Realm;
class Table;
class TableRef;
class Transaction;

using ref_type = size_t;

namespace _impl {
class RealmCoordinator;

struct CollectionChangeInfo {
    TableKey table_key;
    ObjKey obj_key;
    StablePath path;
    CollectionChangeBuilder* changes;
};

struct TransactionChangeInfo {
    std::vector<CollectionChangeInfo> collections;
    std::unordered_map<TableKey, ObjectChangeSet> tables;
    bool schema_changed = false;
};

/**
 * The `DeepChangeChecker` serves two purposes:
 * - Given an initial `Table` and an optional `KeyPathArray` it find all tables related to that initial table.
 *   A `RelatedTable` is a `Table` that can be reached via a link from another `Table`.
 * - The `DeepChangeChecker` also offers a way to check if a specific `ObjKey` was changed.
 */
class DeepChangeChecker {
public:
    /**
     * `RelatedTable` is used to describe the connections of a `Table` to other tables.
     * Tables count as related if they can be reached via a forward link.
     * A table counts as being related to itself.
     */
    struct RelatedTable {
        // The key of the table for which this struct holds all outgoing links.
        TableKey table_key;
        // All outgoing links to the table specified by `table_key`.
        std::vector<ColKey> links;
    };

    typedef std::vector<RelatedTable> RelatedTables;
    DeepChangeChecker(TransactionChangeInfo const& info, Table const& root_table, RelatedTables const& related_tables,
                      const KeyPathArray& key_path_array, bool all_callbacks_filtered);

    /**
     * Check if the object identified by `object_key` was changed.
     *
     * @param object_key The `ObjKey::value` for the object that is supposed to be checked.
     *
     * @return True if the object was changed, false otherwise.
     */
    bool operator()(ObjKey object_key);
    bool operator()(int64_t i)
    {
        return operator()(ObjKey(i));
    }

    /**
     * Search for related tables within the specified `table`.
     * Related tables are all tables that can be reached via links from the `table`.
     * A table is always related to itself.
     *
     * Example schema:
     * {
     *   {"root_table",
     *       {
     *           {"link", PropertyType::Object | PropertyType::Nullable, "linked_table"},
     *       }
     *   },
     *   {"linked_table",
     *       {
     *           {"value", PropertyType::Int}
     *       }
     *   },
     * }
     *
     * Asking for related tables for `root_table` based on this schema will result in a `std::vector<RelatedTable>`
     * with two entries, one for `root_table` and one for `linked_table`. The function would be called once for
     * each table involved until there are no further links.
     *
     * Likewise a search for related tables starting with `linked_table` would only return this table.
     *
     * Filter:
     * Using a `key_path_array` that only consists of the table key for `root_table` would result
     * in `out` just having this one entry.
     *
     * @param out Return value containing all tables that can be reached from the given `table` including
     *            some additional information about those tables    .
     * @param table The table that the related tables will be searched for.
     * @param key_path_array A collection of all `KeyPath`s passed to the `NotificationCallback`s for this
     *                        `CollectionNotifier`.
     */
    static void find_related_tables(std::vector<RelatedTable>& out, Table const& table,
                                    const KeyPathArray& key_path_array);

protected:
    friend class ObjectKeyPathChangeChecker;

    TransactionChangeInfo const& m_info;

    // The `Table` this `DeepChangeChecker` is based on.
    Table const& m_root_table;

    // The `m_key_path_array` contains all columns filtered for. We need this when checking for
    // changes in `operator()` to make sure only columns actually filtered for send notifications.
    const KeyPathArray& m_key_path_array;

    // The `ObjectChangeSet` for `root_table` if it is contained in `m_info`.
    ObjectChangeSet const* const m_root_object_changes;

    // Contains all `ColKey`s that we filter for in the root table.
    std::vector<ColKey> m_filtered_columns_in_root_table;
    std::vector<ColKey> m_filtered_columns;

private:
    RelatedTables const& m_related_tables;

    std::unordered_map<TableKey, std::unordered_set<ObjKey>> m_not_modified;

    struct Path {
        ObjKey obj_key;
        ColKey col_key;
        bool depth_exceeded;
    };
    std::array<Path, 4> m_current_path;

    /**
     * Checks if a specific object, identified by it's `ObjKey` in a given `Table` was changed.
     *
     * @param table The `Table` that contains the `ObjKey` that will be checked.
     * @param object_key The `ObjKey` identifying the object to be checked for changes.
     * @param filtered_columns A `std::vector` of all `ColKey`s filtered in any of the `NotificationCallbacks`.
     * @param depth Determines how deep the search will be continued if the change could not be found
     *              on the first level.
     *
     * @return True if the object was changed, false otherwise.
     */
    bool check_row(Table const& table, ObjKey object_key, const std::vector<ColKey>& filtered_columns,
                   size_t depth = 0);

    /**
     * Check the `table` within `m_related_tables` for changes in it's outgoing links.
     *
     * @param table The table to check for changed links.
     * @param object_key The key for the object to look for.
     * @param depth The maximum depth that should be considered for this search.
     *
     * @return True if the specified `table` does have linked objects that have been changed.
     *         False if the `table` is not contained in `m_related_tables` or the `table` does not have any
     *         outgoing links at all or the `table` does not have linked objects with changes.
     */
    bool check_outgoing_links(Table const& table, ObjKey object_key, const std::vector<ColKey>& filtered_columns,
                              size_t depth = 0);

    bool do_check_for_collection_modifications(const Obj& obj, ColKey col,
                                               const std::vector<ColKey>& filtered_columns, size_t depth);
    template <typename T>
    bool check_collection(ref_type ref, const Obj& obj, ColKey col, const std::vector<ColKey>& filtered_columns,
                          size_t depth);
    bool do_check_mixed_for_link(Group&, TableRef& cached_linked_table, Mixed value,
                                 const std::vector<ColKey>& filtered_columns, size_t depth);
};

/**
 * The `CollectionKeyPathChangeChecker` is a specialised version of `DeepChangeChecker` that offers a check by
 * traversing and only traversing the given `KeyPathArray`. With this it supports any depth (as opposed to the maxium
 * depth of 4 on the `DeepChangeChecker`) and backlinks.
 */
class CollectionKeyPathChangeChecker : DeepChangeChecker {
public:
    CollectionKeyPathChangeChecker(TransactionChangeInfo const& info, Table const& root_table,
                                   std::vector<RelatedTable> const& related_tables,
                                   const KeyPathArray& key_path_array, bool all_callbacks_filtered);

    /**
     * Check if the `Object` identified by `object_key` was changed and it is included in the `KeyPathArray` provided
     * when construction this `CollectionKeyPathChangeChecker`.
     *
     * @param object_key The `ObjKey::value` for the `Object` that is supposed to be checked.
     *
     * @return True if the `Object` was changed, false otherwise.
     */
    bool operator()(ObjKey object_key);

private:
    friend class ObjectKeyPathChangeChecker;

    /**
     * Traverses down a given `KeyPath` and checks the objects along the way for changes.
     *
     * @param changed_columns The list of `ColKeyType`s that was changed in the root object.
     *                        A key will be added to this list if it turns out to be changed.
     * @param key_path The `KeyPath` used to traverse the given object with.
     * @param depth The current depth in the key_path.
     * @param table The `TableKey` for the current depth.
     * @param object_key_value The `ObjKeyType` that is to be checked for changes.
     */
    void find_changed_columns(std::vector<ColKey>& changed_columns, const KeyPath& key_path, size_t depth,
                              const Table& table, const ObjKey& object_key_value);
};

/**
 * The `ObjectKeyPathChangeChecker` is a specialised version of `CollectionKeyPathChangeChecker` that offers a deep
 * change check for `Object` which is different from the checks done for `Collection`. Like
 * `CollectionKeyPathChangeChecker` it is only traversing the given KeyPathArray and has no depth limit.
 *
 * This difference is mainly seen in the fact that for `Object` we notify about the specific columns that have been
 * changed which we do not for `Collection`.
 */
class ObjectKeyPathChangeChecker : CollectionKeyPathChangeChecker {
public:
    ObjectKeyPathChangeChecker(TransactionChangeInfo const& info, Table const& root_table,
                               std::vector<DeepChangeChecker::RelatedTable> const& related_tables,
                               const KeyPathArray& key_path_array, bool all_callbacks_filtered);

    /**
     * Check if the `Object` identified by `object_key` was changed and it is included in the `KeyPathArray` provided
     * when construction this `ObjectKeyPathChangeChecker`.
     *
     * @param object_key The `ObjKey::value` for the `Object` that is supposed to be checked.
     *
     * @return A list of columns changed in the root `Object`.
     */
    std::vector<ColKey> operator()(ObjKey object_key);
};


} // namespace _impl
} // namespace realm

#endif /* DEEP_CHANGE_CHECKER_HPP */
