/*************************************************************************
 *
 * Copyright 2017 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_SYNC_IMPL_INSTRUCTION_APPLIER_HPP
#define REALM_SYNC_IMPL_INSTRUCTION_APPLIER_HPP

#include <realm/sync/instructions.hpp>
#include <realm/sync/changeset.hpp>
#include <realm/util/logger.hpp>
#include <realm/list.hpp>
#include <realm/dictionary.hpp>

#include <tuple>

namespace realm {
namespace sync {

struct Changeset;

struct InstructionApplier {
    explicit InstructionApplier(Transaction&) noexcept;

    /// Throws BadChangesetError if application fails due to a problem with the
    /// changeset.
    ///
    /// FIXME: Consider using std::error_code instead of throwing
    /// BadChangesetError.
    void apply(const Changeset&);

    void begin_apply(const Changeset&) noexcept;
    void end_apply() noexcept;

protected:
    util::Optional<Obj> get_top_object(const Instruction::ObjectInstruction&,
                                       const std::string_view& instr = "(unspecified)");
    static LstBasePtr get_list_from_path(Obj& obj, ColKey col);
    StringData get_string(InternString) const;
    StringData get_string(StringBufferRange) const;
    BinaryData get_binary(StringBufferRange) const;
    TableRef get_table(const Instruction::TableInstruction&, const std::string_view& instr = "(unspecified)");
#define REALM_DECLARE_INSTRUCTION_HANDLER(X) virtual void operator()(const Instruction::X&);
    REALM_FOR_EACH_INSTRUCTION_TYPE(REALM_DECLARE_INSTRUCTION_HANDLER)
#undef REALM_DECLARE_INSTRUCTION_HANDLER
    friend struct Instruction; // to allow visitor

    template <class A>
    static void apply(A& applier, const Changeset&);

    // Allows for in-place modification of changeset while applying it
    template <class A>
    static void apply(A& applier, Changeset&);

    TableRef table_for_class_name(StringData) const; // Throws

    Transaction& m_transaction;

    bool check_links_exist(const Instruction::Payload& payload);
    bool allows_null_links(const Instruction::PathInstruction& instr, const std::string_view& instr_name);
    std::string to_string(const Instruction::PathInstruction& instr) const;

    struct PathResolver {
        enum class Status { Pending, Success, DidNotResolve };
        PathResolver(InstructionApplier* applier, const Instruction::PathInstruction& instr,
                     const std::string_view& instr_name);
        virtual ~PathResolver();
        virtual Status resolve();

        [[nodiscard]] virtual Status on_property(Obj&, ColKey);
        virtual void on_list(LstBase&);
        [[nodiscard]] virtual Status on_list_index(LstBase&, uint32_t);
        virtual void on_dictionary(Dictionary&);
        [[nodiscard]] virtual Status on_dictionary_key(Dictionary&, Mixed);
        virtual void on_set(SetBase&);
        virtual void on_error(const std::string&);
        [[nodiscard]] virtual Status on_mixed_type_changed(const std::string&);
        virtual void on_column_advance(ColKey);
        virtual void on_dict_key_advance(StringData);
        [[nodiscard]] virtual Status on_list_index_advance(uint32_t);
        [[nodiscard]] virtual Status on_null_link_advance(StringData, StringData);
        [[nodiscard]] virtual Status on_dict_key_not_found(StringData, StringData, StringData);
        [[nodiscard]] virtual Status on_begin(const util::Optional<Obj>& obj);
        virtual void on_finish();
        virtual StringData get_string(InternString);

    protected:
        [[nodiscard]] Status resolve_field(Obj& obj, InternString field);
        [[nodiscard]] Status resolve_list_element(LstBase& list, uint32_t index);
        [[nodiscard]] Status resolve_dictionary_element(Dictionary& dict, InternString key);

        InstructionApplier* m_applier;
        const Instruction::PathInstruction& m_path_instr;
        std::string_view m_instr_name;
        Instruction::Path::const_iterator m_it_begin;
        Instruction::Path::const_iterator m_it_end;
    };
    friend struct PathResolver;

private:
    const Changeset* m_log = nullptr;

    Group::TableNameBuffer m_table_name_buffer;
    InternString m_last_table_name;
    InternString m_last_field_name;
    TableRef m_last_table;
    ColKey m_last_field;
    util::Optional<Instruction::PrimaryKey> m_last_object_key;
    util::Optional<Instruction::Path> m_current_path;
    util::Optional<Obj> m_last_object;
    std::unique_ptr<LstBase> m_last_list;

    StringData get_table_name(const Instruction::TableInstruction&, const std::string_view& instr = "(unspecified)");

    // Note: This may return a non-invalid ObjKey if the key is dangling.
    ObjKey get_object_key(Table& table, const Instruction::PrimaryKey&,
                          const std::string_view& instr = "(unspecified)") const;

    template <class F>
    void visit_payload(const Instruction::Payload&, F&& visitor);

    REALM_NORETURN void bad_transaction_log(const std::string& msg) const;
    template <class... Params>
    REALM_NORETURN void bad_transaction_log(const char* msg, Params&&... params) const;
};


// Implementation

inline InstructionApplier::InstructionApplier(Transaction& group) noexcept
    : m_transaction(group)
{
}

inline void InstructionApplier::begin_apply(const Changeset& log) noexcept
{
    m_log = &log;
}

inline void InstructionApplier::end_apply() noexcept
{
    m_log = nullptr;
    m_last_table_name = InternString{};
    m_last_field_name = InternString{};
    m_last_table = TableRef{};
    m_last_field = ColKey{};
    m_last_object.reset();
    m_last_object_key.reset();
    m_last_list.reset();
}

template <class A>
inline void InstructionApplier::apply(A& applier, const Changeset& changeset)
{
    applier.begin_apply(changeset);
    for (auto instr : changeset) {
        if (!instr)
            continue;
        instr->visit(applier); // Throws
    }
    applier.end_apply();
}

template <class A>
inline void InstructionApplier::apply(A& applier, Changeset& changeset)
{
    applier.begin_apply(changeset);
    for (auto instr : changeset) {
        if (!instr)
            continue;
        instr->visit(applier); // Throws
#if REALM_DEBUG
        applier.m_table_info_cache.verify();
#endif
    }
    applier.end_apply();
}

inline void InstructionApplier::apply(const Changeset& log)
{
    apply(*this, log); // Throws
}

} // namespace sync
} // namespace realm

#endif // REALM_SYNC_IMPL_INSTRUCTION_APPLIER_HPP
