////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 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_COLLECTION_NOTIFICATIONS_HPP
#define REALM_COLLECTION_NOTIFICATIONS_HPP

#include <realm/path.hpp>
#include <realm/object-store/index_set.hpp>
#include <realm/object-store/util/atomic_shared_ptr.hpp>

#include <exception>
#include <memory>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include <set>

namespace realm {
namespace _impl {
class CollectionNotifier;
}

// A token which keeps an asynchronous query alive
struct NotificationToken {
    NotificationToken() = default;
    NotificationToken(std::shared_ptr<_impl::CollectionNotifier> notifier, uint64_t token);
    ~NotificationToken();

    NotificationToken(NotificationToken&&);
    NotificationToken& operator=(NotificationToken&&);

    NotificationToken(NotificationToken const&) = delete;
    NotificationToken& operator=(NotificationToken const&) = delete;

    // Stop sending notifications for the callback associated with this token.
    // This is equivalent to (*this) = {};
    void unregister();

    void suppress_next();

private:
    util::AtomicSharedPtr<_impl::CollectionNotifier> m_notifier;
    uint64_t m_token;
};

struct CollectionChangeSet {
    struct Move {
        size_t from;
        size_t to;

        bool operator==(Move m) const noexcept
        {
            return from == m.from && to == m.to;
        }
    };

    // Indices which were removed from the _old_ collection
    IndexSet deletions;

    // Indices in the _new_ collection which are new insertions
    IndexSet insertions;

    // Indices of objects in the _old_ collection which were modified
    IndexSet modifications;

    // Indices in the _new_ collection which were modified. This will always
    // have the same number of indices as `modifications` and conceptually
    // represents the same entries, just in different versions of the collection.
    // It exists for the sake of code which finds it easier to process
    // modifications after processing deletions and insertions rather than before.
    IndexSet modifications_new;

    // Rows in the collection which moved.
    //
    // Every `from` index will also be present in `deletions` and every `to`
    // index will be present in `insertions`.
    //
    // This is currently not reliably calculated for all types of collections. A
    // reported move will always actually be a move, but there may also be
    // unreported moves which show up only as a delete/insert pair.
    std::vector<Move> moves;

    // This flag indicates whether the underlying object which is the source of this
    // collection was deleted. This applies to lists, dictionaries and sets.
    // This enables notifiers to report a change on empty collections that have been deleted.
    bool collection_root_was_deleted = false;

    // This flag indicates if the collection was cleared.
    bool collection_was_cleared = false;

    // Per-column version of `modifications`
    std::unordered_map<int64_t, IndexSet> columns;

    std::set<StableIndex> paths;

    bool empty() const noexcept
    {
        return deletions.empty() && insertions.empty() && modifications.empty() && modifications_new.empty() &&
               moves.empty() && !collection_root_was_deleted && !collection_was_cleared;
    }
};

// A type-erasing wrapper for the callback for collection notifications. Can be
// constructed with either any callable compatible with the signature
// `void (CollectionChangeSet)`, an object with member
// functions `void before(CollectionChangeSet)`, `void after(CollectionChangeSet)`,
// or a pointer to such an object. If a pointer
// is given, the caller is responsible for ensuring that the pointed-to object
// outlives the collection.
class CollectionChangeCallback {
public:
    CollectionChangeCallback(std::nullptr_t = {}) {}

    template <typename Callback>
    CollectionChangeCallback(Callback cb)
        : m_impl(make_impl(std::move(cb)))
    {
    }
    template <typename Callback>
    CollectionChangeCallback& operator=(Callback cb)
    {
        m_impl = make_impl(std::move(cb));
        return *this;
    }

    // Explicitly default the copy/move constructors as otherwise they'll use
    // the above ones and add an extra layer of wrapping
    CollectionChangeCallback(CollectionChangeCallback&&) = default;
    CollectionChangeCallback(CollectionChangeCallback const&) = default;
    CollectionChangeCallback& operator=(CollectionChangeCallback&&) = default;
    CollectionChangeCallback& operator=(CollectionChangeCallback const&) = default;

    void before(CollectionChangeSet const& c)
    {
        m_impl->before(c);
    }
    void after(CollectionChangeSet const& c)
    {
        m_impl->after(c);
    }

    explicit operator bool() const
    {
        return !!m_impl;
    }

private:
    struct Base {
        virtual ~Base() {}
        virtual void before(CollectionChangeSet const&) = 0;
        virtual void after(CollectionChangeSet const&) = 0;
    };

    template <typename Callback, typename = decltype(std::declval<Callback>()(CollectionChangeSet()))>
    std::shared_ptr<Base> make_impl(Callback cb)
    {
        return std::make_shared<Impl<Callback>>(std::move(cb));
    }

    template <typename Callback, typename = decltype(std::declval<Callback>().after(CollectionChangeSet())),
              typename = void>
    std::shared_ptr<Base> make_impl(Callback cb)
    {
        return std::make_shared<Impl2<Callback>>(std::move(cb));
    }

    template <typename Callback, typename = decltype(std::declval<Callback>().after(CollectionChangeSet())),
              typename = void>
    std::shared_ptr<Base> make_impl(Callback* cb)
    {
        return std::make_shared<Impl3<Callback>>(cb);
    }

    template <typename T>
    struct Impl : public Base {
        T impl;
        Impl(T impl)
            : impl(std::move(impl))
        {
        }
        void before(CollectionChangeSet const&) override {}
        void after(CollectionChangeSet const& change) override
        {
            impl(change);
        }
    };
    template <typename T>
    struct Impl2 : public Base {
        T impl;
        Impl2(T impl)
            : impl(std::move(impl))
        {
        }
        void before(CollectionChangeSet const& c) override
        {
            impl.before(c);
        }
        void after(CollectionChangeSet const& c) override
        {
            impl.after(c);
        }
    };
    template <typename T>
    struct Impl3 : public Base {
        T* impl;
        Impl3(T* impl)
            : impl(impl)
        {
        }
        void before(CollectionChangeSet const& c) override
        {
            impl->before(c);
        }
        void after(CollectionChangeSet const& c) override
        {
            impl->after(c);
        }
    };

    std::shared_ptr<Base> m_impl;
};
} // namespace realm

#endif // REALM_COLLECTION_NOTIFICATIONS_HPP
