/*
  This file is part of KDBindings.

  SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
  Author: Sean Harmer <sean.harmer@kdab.com>

  SPDX-License-Identifier: MIT

  Contact KDAB at <info@kdab.com> for commercial licensing options.
*/

#pragma once

#include <kdbindings/node.h>
#include <kdbindings/node_operators.h>
#include <kdbindings/node_functions.h>
#include <kdbindings/make_node.h>
#include <kdbindings/binding_evaluator.h>
#include <kdbindings/property_updater.h>

namespace KDBindings {

/**
 * @brief A combination of a root Node with an evaluator.
 *
 * A root Node is formed whenever multiple properties are combined inside
 * a expression and an evaluator is responsible for re-evaluating such
 * an expression whenever any of the constituent properties change.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 * @tparam EvaluatorT The type of the evaluator that is used to evaluate the Binding.
 */
template<typename T, typename EvaluatorT = BindingEvaluator>
class Binding : public PropertyUpdater<T>, public Private::Dirtyable
{
    static_assert(
            std::is_base_of<BindingEvaluator, EvaluatorT>::value,
            "The EvaluatorT type must inherit from BindingEvaluator.");

public:
    /**
     * @brief Construct a new Binding with a specific evaluator.
     *
     * @param rootNode Represents that expression contained in the Binding.
     * @param evaluator Used to evaluate the expression contained in the Binding.
     */
    explicit Binding(Private::Node<T> &&rootNode, EvaluatorT const &evaluator)
        : m_rootNode{ std::move(rootNode) }
        , m_evaluator{ evaluator }
    {
        m_bindingId = m_evaluator.insert(this);
        m_rootNode.setParent(this);
    }

    /** Destructs the Binding by deregistering it from its evaluator. */
    ~Binding() override
    {
        m_evaluator.remove(m_bindingId);
    }

    /** A Binding is not default constructible. */
    Binding() = delete;

    /** A Binding cannot be copy constructed. */
    Binding(Binding const &other) = delete;
    /** A Binding cannot be copy assigned. */
    Binding &operator=(Binding const &other) = delete;

    // Move construction would invalidate the this pointer passed to the evaluator
    // in the constructor
    /** A Binding can not be move constructed. */
    Binding(Binding &&other) = delete;
    /** A Binding can not be move assigned. */
    Binding &operator=(Binding &&other) = delete;

    /** Set the function that should be used to notify
     * associated properties when the Binding re-evaluates.
     */
    void setUpdateFunction(std::function<void(T &&)> const &updateFunction) override
    {
        m_propertyUpdateFunction = updateFunction;
    }

    /** Returns the current value of the Binding. */
    T get() const override { return m_rootNode.evaluate(); }

    /** Re-evaluates the value of the Binding and notifies all dependants of the change. */
    void evaluate()
    {
        T value = m_rootNode.evaluate();

        // Use this to update any associated property via the PropertyUpdater's update function
        m_propertyUpdateFunction(std::move(value));
    }

protected:
    Private::Dirtyable **parentVariable() override { return nullptr; }
    const bool *dirtyVariable() const override { return nullptr; }

    /** The root Node of the Binding represents the expression contained by the Binding. */
    Private::Node<T> m_rootNode;
    /** The evaluator responsible for evaluating this Binding. */
    EvaluatorT m_evaluator;
    /** The function used to notify associated properties when the Binding re-evaluates */
    std::function<void(T &&)> m_propertyUpdateFunction = [](T &&) {};
    /** The id of the Binding, used for keeping track of the Binding in its evaluator. */
    int m_bindingId = -1;
};

/**
 * @brief Helper function to create a Binding from a Property.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 * @tparam EvaluatorT The type of the evaluator that is used to evaluate the Binding.
 * @param evaluator The evaluator that is used to evaluate the Binding.
 * @param property The Property to create a Binding from.
 * @return std::unique_ptr<Binding<T, EvaluatorT>> A new Binding that is powered by the evaluator.
 *
 * *Note: For the difference between makeBinding and makeBoundProperty, see the
 * ["Reassigning a Binding"](../getting-started/data-binding/#reassigning-a-binding) section in the Getting Started guide.*
 */
template<typename T, typename EvaluatorT>
inline std::unique_ptr<Binding<T, EvaluatorT>> makeBinding(EvaluatorT &evaluator, Property<T> &property)
{
    return std::make_unique<Binding<T, EvaluatorT>>(Private::makeNode(property), evaluator);
}

/**
 * @brief Creates a binding from a const property using a specified evaluator.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 * @tparam EvaluatorT The type of the evaluator that is used to evaluate the Binding.
 * @param evaluator The evaluator that is used to evaluate the Binding.
 * @param property The const Property to create a Binding from.
 * @return std::unique_ptr<Binding<T, EvaluatorT>> A new Binding that is powered by the evaluator.
 *
 * *Note: Using a const Property ensures that the source cannot be modified through this binding,
 * maintaining data integrity and supporting scenarios where data should only be observed, not altered.*
 */
template<typename T, typename EvaluatorT>
inline std::unique_ptr<Binding<T, EvaluatorT>> makeBinding(EvaluatorT &evaluator, const Property<T> &property)
{
    return std::make_unique<Binding<T, EvaluatorT>>(Private::makeNode(property), evaluator);
}

/**
 * @brief Helper function to create a Binding from a root Node.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 * @tparam EvaluatorT The type of the evaluator that is used to evaluate the Binding.
 * @param evaluator The evaluator that is used to evaluate the Binding.
 * @param rootNode Represents the expression that will be evaluated by the Binding.
 * @return std::unique_ptr<Binding<T, EvaluatorT>> A new Binding that combines the rootNode with the evaluator.
 *
 * *Note: For the difference between makeBinding and makeBoundProperty, see the
 * ["Reassigning a Binding"](../getting-started/data-binding/#reassigning-a-binding) section in the Getting Started guide.*
 */
template<typename T, typename EvaluatorT>
inline std::unique_ptr<Binding<T, EvaluatorT>> makeBinding(EvaluatorT &evaluator, Private::Node<T> &&rootNode)
{
    return std::make_unique<Binding<T, EvaluatorT>>(std::move(rootNode), evaluator);
}

/**
 * @brief Helper function to create a Binding from a function and its arguments.
 *
 * @tparam EvaluatorT The type of the evaluator that is used to evaluate the Binding.
 * @param evaluator The evaluator that is used to evaluate the Binding.
 * @tparam Func The type of the function - may be any type that implements operator().
 * @param func The function object.
 * @tparam Args The function argument types
 * @param args The function arguments - Possible values include: Properties, Constants and Nodes
 *              They will be automatically unwrapped, i.e. a Property<T> will pass a value of type T to func.
 * @return std::unique_ptr<Binding<ReturnType, EvaluatorT>> where ReturnType is the type that results from evaluationg func with the given arguments.
 *          The Binding will be powered by the new evaluator.
 *
 * *Note: For the difference between makeBinding and makeBoundProperty, see the
 * ["Reassigning a Binding"](../getting-started/data-binding/#reassigning-a-binding) section in the Getting Started guide.*
 */
template<typename EvaluatorT, typename Func, typename... Args, typename = std::enable_if_t<sizeof...(Args) != 0>, typename ResultType = Private::operator_node_result_t<Func, Args...>>
inline std::unique_ptr<Binding<ResultType, EvaluatorT>> makeBinding(EvaluatorT &evaluator, Func &&func, Args &&...args)
{
    return std::make_unique<Binding<ResultType, EvaluatorT>>(Private::makeNode(std::forward<Func>(func), std::forward<Args>(args)...), evaluator);
}

/**
 * @brief Provides a convenience for old-school, immediate mode Bindings.
 *
 * This works in conjunction with a do-nothing ImmediateBindingEvaluator class to update the
 * result of the Binding immediately upon any of the dependent bindables (i.e. Property instances)
 * notifying that they have changed. This can lead to a Property Binding being evaluated many
 * times before the result is ever used in a typical GUI application.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 */
template<typename T>
class Binding<T, ImmediateBindingEvaluator> : public Binding<T, BindingEvaluator>
{
public:
    /**
     * @brief Construct a new Binding with an immediate mode evaluator.
     *
     * @param rootNode Represents that expression contained in the Binding.
     */
    explicit Binding(Private::Node<T> &&rootNode)
        : Binding<T, BindingEvaluator>(std::move(rootNode), ImmediateBindingEvaluator::instance())
    {
    }

    /** A Binding is not default constructible. */
    Binding() = delete;

    virtual ~Binding() = default;

    /** A Binding cannot be copy constructed. */
    Binding(Binding const &other) = delete;
    /** A Binding cannot be copy assigned. */
    Binding &operator=(Binding const &other) = delete;

    /** A Binding can not be move constructed. */
    Binding(Binding &&other) = delete;
    /** A Binding can not be move assigned. */
    Binding &operator=(Binding &&other) = delete;

    void markDirty() override
    {
        Binding::evaluate();
    }
};

/**
 * @brief Helper function to create an immediate mode Binding from a Property.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 * @param property The Property to create a Binding from.
 * @return std::unique_ptr<Binding<T, ImmediateBindingEvaluator>>
 *        An new Binding bound to an existing Property with immediate evaluation.
 *
 * *Note: For the difference between makeBinding and makeBoundProperty, see the
 * ["Reassigning a Binding"](../getting-started/data-binding/#reassigning-a-binding) section in the Getting Started guide.*
 */
template<typename T>
inline std::unique_ptr<Binding<T, ImmediateBindingEvaluator>> makeBinding(Property<T> &property)
{
    return std::make_unique<Binding<T, ImmediateBindingEvaluator>>(Private::makeNode(property));
}

/**
 * @brief Creates an immediate mode binding from a const property.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 * @param property The const Property to create a Binding from.
 * @return std::unique_ptr<Binding<T, ImmediateBindingEvaluator>> A new Binding that is powered by an
 *         immediate mode evaluator, ensuring quick updates to changes.
 *
 * *Note: This binding type is especially suited for scenarios where you need to reflect
 * changes in a property to some dependent components without the need to alter the
 * source property itself.*
 */
template<typename T>
inline std::unique_ptr<Binding<T, ImmediateBindingEvaluator>> makeBinding(const Property<T> &property)
{
    return std::make_unique<Binding<T, ImmediateBindingEvaluator>>(Private::makeNode(property));
}

/**
 * @brief Helper function to create an immediate mode Binding from a root Node.
 *
 * @tparam T The type of the value that the Binding expression evaluates to.
 * @param rootNode Represents the expression that will be evaluated by the Binding.
 *                  Typically constructed from a unary/binary operator on a Property.
 * @return std::unique_ptr<Binding<<T, ImmediateBindingEvaluator>> An new Binding bound to a root Node with immediate evaluation.
 *
 * *Note: For the difference between makeBinding and makeBoundProperty, see the
 * ["Reassigning a Binding"](../getting-started/data-binding/#reassigning-a-binding) section in the Getting Started guide.*
 */
template<typename T>
inline std::unique_ptr<Binding<T, ImmediateBindingEvaluator>> makeBinding(Private::Node<T> &&rootNode)
{
    return std::make_unique<Binding<T, ImmediateBindingEvaluator>>(std::move(rootNode));
}

/**
 * @brief Helper function to create an immediate mode Binding from a function and its arguments.
 *
 * @tparam Func The type of the function - may be any type that implements operator().
 * @param func The function object.
 * @tparam Args The function argument types
 * @param args The function arguments - Possible values include: Properties, Constants and Nodes
 *              They will be automatically unwrapped, i.e. a Property<T> will pass a value of type T to func.
 * @return std::unique_ptr<Binding<ReturnType, ImmediateBindingEvaluator>> where ReturnType is the type that results from evaluationg func with the given arguments.
 *          The Binding will feature immediate evaluation.
 *
 * *Note: For the difference between makeBinding and makeBoundProperty, see the
 * ["Reassigning a Binding"](../getting-started/data-binding/#reassigning-a-binding) section in the Getting Started guide.*
 */
template<typename Func, typename... Args, typename = std::enable_if_t<sizeof...(Args) != 0>, typename ResultType = Private::operator_node_result_t<Func, Args...>>
inline std::unique_ptr<Binding<ResultType, ImmediateBindingEvaluator>> makeBinding(Func &&func, Args &&...args)
{
    return std::make_unique<Binding<ResultType, ImmediateBindingEvaluator>>(Private::makeNode(std::forward<Func>(func), std::forward<Args>(args)...));
}

/**
 * @brief Helper function to create a Property with a Binding.
 *
 * This function can take:
 * - Another Property.
 * - A Node, typically created by combining Property instances using operators.
 * - A function with arguments (Nodes, Constants or Properties)
 * By default this will construct a Property with an immediate binding evaluation.
 *
 * Alternatively a BindingEvaluator can be passed as the first argument to this function to control
 * when evaluation takes place.
 *
 * See the documentation for the various overloads of the free @ref makeBinding function for a
 * detailed description of which arguments can be used in which order.
 *
 * Examples:
 * - @ref 05-property-bindings/main.cpp
 * - @ref 06-lazy-property-bindings/main.cpp
 *
 * @return Property A new Property that is bound to the inputs
 *
 * *Note: For the difference between makeBinding and makeBoundProperty, see the
 * ["Reassigning a Binding"](../getting-started/data-binding/#reassigning-a-binding) section in the Getting Started guide.*
 */
template<typename... T>
inline auto makeBoundProperty(T &&...args)
{
    auto binding = makeBinding(std::forward<T>(args)...);
    return Property<decltype(binding->get())>(std::move(binding));
}

} // namespace KDBindings
