// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

#pragma once
#include <string_view>
#include <memory>

namespace slint::cbindgen_private {
struct PropertyAnimation;
struct ChangeTracker
{
    void *inner;
};
}

#include "private/slint_properties_internal.h"
#include "private/slint_builtin_structs_internal.h"

namespace slint::private_api {

using cbindgen_private::StateInfo;

inline void slint_property_set_animated_binding_helper(
        const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, int *),
        void *user_data, void (*drop_user_data)(void *),
        cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t **))
{
    cbindgen_private::slint_property_set_animated_binding_int(handle, binding, user_data,
                                                              drop_user_data, transition_data);
}

inline void slint_property_set_animated_binding_helper(
        const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, float *),
        void *user_data, void (*drop_user_data)(void *),
        cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t **))
{
    cbindgen_private::slint_property_set_animated_binding_float(handle, binding, user_data,
                                                                drop_user_data, transition_data);
}

inline void slint_property_set_animated_binding_helper(
        const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, Color *),
        void *user_data, void (*drop_user_data)(void *),
        cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t **))
{
    cbindgen_private::slint_property_set_animated_binding_color(handle, binding, user_data,
                                                                drop_user_data, transition_data);
}

inline void slint_property_set_animated_binding_helper(
        const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, Brush *),
        void *user_data, void (*drop_user_data)(void *),
        cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t **))
{
    cbindgen_private::slint_property_set_animated_binding_brush(handle, binding, user_data,
                                                                drop_user_data, transition_data);
}

template<typename T>
struct Property
{
    Property() { cbindgen_private::slint_property_init(&inner); }
    ~Property() { cbindgen_private::slint_property_drop(&inner); }
    Property(const Property &) = delete;
    Property(Property &&) = delete;
    Property &operator=(const Property &) = delete;
    explicit Property(const T &value) : value(value)
    {
        cbindgen_private::slint_property_init(&inner);
    }

    /* Should it be implicit?
    void operator=(const T &value) {
        set(value);
    }*/

    void set(const T &value) const
    {
        if ((inner._0 & 0b10) == 0b10 || this->value != value) {
            this->value = value;
            cbindgen_private::slint_property_set_changed(&inner, &this->value);
        }
    }

    const T &get() const
    {
        cbindgen_private::slint_property_update(&inner, &value);
        return value;
    }

    template<typename F>
    void set_binding(F binding) const
    {
        cbindgen_private::slint_property_set_binding(
                &inner,
                [](void *user_data, void *value) {
                    *reinterpret_cast<T *>(value) = (*reinterpret_cast<F *>(user_data))();
                },
                new F(binding), [](void *user_data) { delete reinterpret_cast<F *>(user_data); },
                nullptr, nullptr);
    }

    inline void set_animated_value(const T &value,
                                   const cbindgen_private::PropertyAnimation &animation_data) const;

    template<typename F, typename Trans>
    inline void set_animated_binding(F binding, Trans animation) const
    {
        struct UserData
        {
            F binding;
            Trans animation;
        };
        private_api::slint_property_set_animated_binding_helper(
                &inner,
                [](void *user_data, T *value) {
                    *reinterpret_cast<T *>(value) =
                            reinterpret_cast<UserData *>(user_data)->binding();
                },
                new UserData { binding, animation },
                [](void *user_data) { delete reinterpret_cast<UserData *>(user_data); },
                [](void *user_data, uint64_t **instant) {
                    return reinterpret_cast<UserData *>(user_data)->animation(instant);
                });
    }

    bool is_dirty() const { return cbindgen_private::slint_property_is_dirty(&inner); }
    void mark_dirty() const { cbindgen_private::slint_property_mark_dirty(&inner); }

    static void link_two_way(const Property<T> *p1, const Property<T> *p2)
    {
        auto value = p2->get();
        cbindgen_private::PropertyHandleOpaque handle {};
        if ((p2->inner._0 & 0b10) == 0b10) {
            std::swap(handle, const_cast<Property<T> *>(p2)->inner);
        }
        auto common_property = std::make_shared<Property<T>>(handle, std::move(value));
        cbindgen_private::slint_property_set_binding(
                &p1->inner, TwoWayBinding::call_fn, new TwoWayBinding { common_property },
                TwoWayBinding::del_fn, TwoWayBinding::intercept_fn,
                TwoWayBinding::intercept_binding_fn);
        cbindgen_private::slint_property_set_binding(
                &p2->inner, TwoWayBinding::call_fn, new TwoWayBinding { common_property },
                TwoWayBinding::del_fn, TwoWayBinding::intercept_fn,
                TwoWayBinding::intercept_binding_fn);
    }

    template<typename T2, typename M1, typename M2>
    static void link_two_way_with_map(const Property<T> *prop1, const Property<T2> *prop2, M1 map1,
                                      M2 map2)
    {
        // TODO: neither this nor link_two_way manages to re-use a common_property like the Rust
        // equivalent does.

        auto value = prop1->get();
        cbindgen_private::PropertyHandleOpaque handle {};
        if ((prop1->inner._0 & 0b10) == 0b10) {
            std::swap(handle, const_cast<Property<T> *>(prop1)->inner);
        }
        auto common_property = std::make_shared<Property<T>>(handle, std::move(value));

        struct TwoWayBindingWithMap
        {
            std::shared_ptr<Property<T>> common_property;
            M1 map_to;
            M2 map_from;
        };
        auto del_fn = [](void *user_data) {
            delete reinterpret_cast<TwoWayBindingWithMap *>(user_data);
        };
        auto call_fn = [](void *user_data, void *value) {
            auto self = reinterpret_cast<TwoWayBindingWithMap *>(user_data);
            *reinterpret_cast<T2 *>(value) = self->map_to(self->common_property->get());
        };
        auto intercept_fn = [](void *user_data, const void *value) {
            auto self = reinterpret_cast<TwoWayBindingWithMap *>(user_data);
            T old = self->common_property->get();
            self->map_from(old, *reinterpret_cast<const T2 *>(value));
            self->common_property->set(old);
            return true;
        };
        auto intercept_binding_fn = [](void *user_data, void *t2_binding) {
            struct BindingMapper
            {
                void *t2_binding;
                M1 map_to;
                M2 map_from;
                ~BindingMapper() { cbindgen_private::slint_property_delete_binding(t2_binding); }
                const BindingMapper &operator=(const BindingMapper &) = delete;
            };
            auto self = reinterpret_cast<TwoWayBindingWithMap *>(user_data);
            cbindgen_private::slint_property_set_binding(
                    &self->common_property->inner,
                    [](void *user_data, void *value) {
                        auto self = reinterpret_cast<BindingMapper *>(user_data);
                        T &v = *reinterpret_cast<T *>(value);
                        T2 sub_value = self->map_to(v);
                        cbindgen_private::slint_property_evaluate_binding(self->t2_binding,
                                                                          &sub_value);
                        self->map_from(v, sub_value);
                    },
                    new BindingMapper { t2_binding, self->map_to, self->map_from },
                    [](void *user_data) { delete reinterpret_cast<BindingMapper *>(user_data); },
                    nullptr, nullptr);
            return true;
        };

        cbindgen_private::slint_property_set_binding(
                &prop1->inner, TwoWayBinding::call_fn, new TwoWayBinding { common_property },
                TwoWayBinding::del_fn, TwoWayBinding::intercept_fn,
                TwoWayBinding::intercept_binding_fn);

        cbindgen_private::slint_property_set_binding(
                &prop2->inner, call_fn, new TwoWayBindingWithMap { common_property, map1, map2 },
                del_fn, intercept_fn, intercept_binding_fn);
    }

    /// Internal (private) constructor used by link_two_way
    explicit Property(cbindgen_private::PropertyHandleOpaque inner, T value)
        : inner(inner), value(std::move(value))
    {
    }

    const T &get_internal() const { return value; }

    void set_constant() const { cbindgen_private::slint_property_set_constant(&inner); }

private:
    cbindgen_private::PropertyHandleOpaque inner;
    mutable T value {};
    template<typename F>
    friend void set_state_binding(const Property<StateInfo> &property, F binding);

    template<typename T2>
    friend struct Property;

    struct TwoWayBinding
    {
        std::shared_ptr<Property<T>> common_property;

        static void del_fn(void *user_data)
        {
            delete reinterpret_cast<TwoWayBinding *>(user_data);
        };
        static void call_fn(void *user_data, void *value)
        {
            *reinterpret_cast<T *>(value) =
                    reinterpret_cast<TwoWayBinding *>(user_data)->common_property->get();
        };
        static bool intercept_fn(void *user_data, const void *value)
        {
            reinterpret_cast<TwoWayBinding *>(user_data)->common_property->set(
                    *reinterpret_cast<const T *>(value));
            return true;
        };
        static bool intercept_binding_fn(void *user_data, void *value)
        {
            cbindgen_private::slint_property_set_binding_internal(
                    &reinterpret_cast<TwoWayBinding *>(user_data)->common_property->inner, value);
            return true;
        };
    };
};

template<>
inline void Property<int32_t>::set_animated_value(
        const int32_t &new_value, const cbindgen_private::PropertyAnimation &animation_data) const
{
    cbindgen_private::slint_property_set_animated_value_int(&inner, value, new_value,
                                                            &animation_data);
}

template<>
inline void
Property<float>::set_animated_value(const float &new_value,
                                    const cbindgen_private::PropertyAnimation &animation_data) const
{
    cbindgen_private::slint_property_set_animated_value_float(&inner, value, new_value,
                                                              &animation_data);
}

template<>
inline void
Property<Color>::set_animated_value(const Color &new_value,
                                    const cbindgen_private::PropertyAnimation &animation_data) const
{
    cbindgen_private::slint_property_set_animated_value_color(&inner, value, new_value,
                                                              &animation_data);
}

template<typename F>
void set_state_binding(const Property<StateInfo> &property, F binding)
{
    cbindgen_private::slint_property_set_state_binding(
            &property.inner,
            [](void *user_data) -> int32_t { return (*reinterpret_cast<F *>(user_data))(); },
            new F(binding), [](void *user_data) { delete reinterpret_cast<F *>(user_data); });
}

/// PropertyTracker allows keeping track of when properties change and lazily evaluate code
/// if necessary.
/// Once constructed, you can call evaluate() with a functor that will be invoked. Any
/// Property<T> types that have their value read from within the invoked functor or any code that's
/// reached from there are added to internal book-keeping. When after returning from evaluate(),
/// any of these accessed properties change their value, the property tracker's is_dirt() function
/// will return true.
///
/// PropertyTracker instances nest, so if during the evaluation of one tracker, another tracker's
/// evaluate() function gets called and properties from within that evaluation change their value
/// later, both tracker instances will report true for is_dirty(). If you would like to disable the
/// nesting, use the evaluate_as_dependency_root() function instead.
struct PropertyTracker
{
    /// Constructs a new property tracker instance.
    PropertyTracker() { cbindgen_private::slint_property_tracker_init(&inner); }
    /// Destroys the property tracker.
    ~PropertyTracker() { cbindgen_private::slint_property_tracker_drop(&inner); }
    /// The copy constructor is intentionally deleted, property trackers cannot be copied.
    PropertyTracker(const PropertyTracker &) = delete;
    /// The assignment operator is intentionally deleted, property trackers cannot be copied.
    PropertyTracker &operator=(const PropertyTracker &) = delete;

    /// Returns true if any properties accessed during the last evaluate() call have changed their
    /// value since then.
    bool is_dirty() const { return cbindgen_private::slint_property_tracker_is_dirty(&inner); }

    /// Invokes the provided functor \a f and tracks accessed to any properties during that
    /// invocation.
    template<typename F>
    auto evaluate(const F &f) const -> std::enable_if_t<std::is_same_v<decltype(f()), void>>
    {
        cbindgen_private::slint_property_tracker_evaluate(
                &inner, [](void *f) { (*reinterpret_cast<const F *>(f))(); }, const_cast<F *>(&f));
    }

    /// Invokes the provided functor \a f and tracks accessed to any properties during that
    /// invocation. Use this overload if your functor returns a value, as evaluate() will pass it on
    /// and return it.
    template<typename F>
    auto evaluate(const F &f) const
            -> std::enable_if_t<!std::is_same_v<decltype(f()), void>, decltype(f())>
    {
        decltype(f()) result;
        this->evaluate([&] { result = f(); });
        return result;
    }

    /// Invokes the provided functor \a f and tracks accessed to any properties during that
    /// invocation.
    ///
    /// This starts a new dependency chain and if called during the evaluation of another
    /// property tracker, the outer tracker will not be notified if any accessed properties change.
    template<typename F>
    auto evaluate_as_dependency_root(const F &f) const
            -> std::enable_if_t<std::is_same_v<decltype(f()), void>>
    {
        cbindgen_private::slint_property_tracker_evaluate_as_dependency_root(
                &inner, [](void *f) { (*reinterpret_cast<const F *>(f))(); }, const_cast<F *>(&f));
    }

    /// Invokes the provided functor \a f and tracks accessed to any properties during that
    /// invocation. Use this overload if your functor returns a value, as evaluate() will pass it on
    /// and return it.
    ///
    /// This starts a new dependency chain and if called during the evaluation of another
    /// property tracker, the outer tracker will not be notified if any accessed properties change.
    template<typename F>
    auto evaluate_as_dependency_root(const F &f) const
            -> std::enable_if_t<!std::is_same_v<decltype(f()), void>, decltype(f())>
    {
        decltype(f()) result;
        this->evaluate_as_dependency_root([&] { result = f(); });
        return result;
    }

private:
    cbindgen_private::PropertyTrackerOpaque inner;
};

struct ChangeTracker
{
    ChangeTracker() { cbindgen_private::slint_change_tracker_construct(&inner); }
    ~ChangeTracker() { cbindgen_private::slint_change_tracker_drop(&inner); }
    ChangeTracker(const ChangeTracker &) = delete;
    ChangeTracker &operator=(const ChangeTracker &) = delete;

    template<typename Data, typename FnEval, typename FnNotify>
    void init(Data data, FnEval fn_eval, FnNotify fn_notify)
    {
        using Value = std::invoke_result_t<FnEval, Data>;
        struct Inner
        {
            Data data;
            FnEval fn_eval;
            FnNotify fn_notify;
            Value value;
        };
        auto data_ptr =
                new Inner { std::move(data), std::move(fn_eval), std::move(fn_notify), Value() };
        cbindgen_private::slint_change_tracker_init(
                &inner, data_ptr, [](void *d) { delete reinterpret_cast<Inner *>(d); },
                [](void *d) {
                    auto inner = reinterpret_cast<Inner *>(d);
                    auto v = inner->fn_eval(inner->data);
                    bool r = v != inner->value;
                    inner->value = v;
                    return r;
                },
                [](void *d) {
                    auto inner = reinterpret_cast<Inner *>(d);
                    inner->fn_notify(inner->data, inner->value);
                });
    }

private:
    cbindgen_private::ChangeTracker inner;
};

} // namespace slint::private_api
