/*
 * Copyright (c) 2022, Frhun <serenitystuff@frhun.de>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#pragma once

#include <AK/JsonValue.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibGfx/Rect.h>
#include <LibGfx/Size.h>
#include <initializer_list>

namespace GUI {

// The constants used for special values
// Their order here, also defines their order among each other for min, max; operations, excluding Regular
enum class SpecialDimension : int {
    Regular = 0, // only really useful for is_one_of
    Grow = -1,
    OpportunisticGrow = -2,
    Fit = -3,
    Shrink = -4,
};

class UIDimension {
    friend constexpr auto AK::max<GUI::UIDimension>(GUI::UIDimension const&, GUI::UIDimension const&) -> GUI::UIDimension;
    friend constexpr auto AK::min<GUI::UIDimension>(GUI::UIDimension const&, GUI::UIDimension const&) -> GUI::UIDimension;

public:
    UIDimension() = delete;

    UIDimension(int value)
        : m_value(value)
    {
        VERIFY(value >= 0);
    }

    UIDimension(SpecialDimension special)
        : m_value(to_underlying(special))
    {
    }

    [[nodiscard]] inline bool is_special_value() const
    {
        return m_value < 0;
    }

    [[nodiscard]] inline bool is_int() const
    {
        return m_value >= 0;
    }

    [[nodiscard]] inline bool is_shrink() const
    {
        return m_value == to_underlying(SpecialDimension::Shrink);
    }

    [[nodiscard]] inline bool is_grow() const
    {
        return m_value == to_underlying(SpecialDimension::Grow);
    }

    [[nodiscard]] inline bool is_opportunistic_grow() const
    {
        return m_value == to_underlying(SpecialDimension::OpportunisticGrow);
    }

    [[nodiscard]] inline bool is_fit() const
    {
        return m_value == to_underlying(SpecialDimension::Fit);
    }

    [[nodiscard]] inline bool is_one_of(std::initializer_list<SpecialDimension> valid_values) const
    {
        for (SpecialDimension v : valid_values) {
            if (m_value == to_underlying(v) || (v == SpecialDimension::Regular && is_int()))
                return true;
        }
        return false;
    }

    [[nodiscard]] ALWAYS_INLINE constexpr bool is(SpecialDimension special_value) const
    {
        return m_value == to_underlying(special_value) || (special_value == SpecialDimension::Regular && is_int());
    }

    template<typename... Ts>
    [[nodiscard]] bool is_one_of(Ts... valid_values) const
    {
        return (... || (is(forward<Ts>(valid_values))));
    }

    [[nodiscard]] inline bool operator==(UIDimension other) const
    {
        return m_value == other.m_value;
    }

    [[nodiscard]] inline UIDimension must_sum_with(UIDimension other) const
    {
        VERIFY(is_int() && other.is_int());
        return UIDimension { m_value + other.m_value };
    }

    inline void must_add(int to_add)
    {
        VERIFY(is_int());
        VERIFY(m_value >= -to_add);
        m_value += to_add;
    }

    inline void add_if_int(int to_add)
    {
        if (is_int()) {
            m_value += to_add;
        }
    }

    [[nodiscard]] inline ErrorOr<int> shrink_value() const
    {
        if (m_value >= 0)
            return m_value;
        if (m_value == to_underlying(SpecialDimension::Shrink))
            return 0;
        return Error::from_string_literal("value is neither shrink nor an integer ≥0");
    }

    [[nodiscard]] inline int as_int() const
    {
        VERIFY(is_int());
        return m_value;
    }

    [[nodiscard]] AK::JsonValue as_json_value() const
    {
        if (is_int())
            return m_value;
        if (is_shrink())
            return "shrink";
        if (is_grow())
            return "grow";
        if (is_opportunistic_grow())
            return "opportunistic_grow";
        if (is_fit())
            return "fit";

        VERIFY_NOT_REACHED();
    }

    /// The returned source code, if any, can be used to construct this UIDimension in C++.
    ErrorOr<String> as_cpp_source() const
    {
        String value_source = {};
        if (is_int())
            value_source = TRY(String::number(m_value));
        else if (is_shrink())
            value_source = "GUI::SpecialDimension::Shrink"_string;
        else if (is_grow())
            value_source = "GUI::SpecialDimension::Grow"_string;
        else if (is_opportunistic_grow())
            value_source = "GUI::SpecialDimension::OpportunisticGrow"_string;
        else if (is_fit())
            value_source = "GUI::SpecialDimension::Fit"_string;
        return String::formatted("GUI::UIDimension {{ {} }}", value_source);
    }

    [[nodiscard]] static Optional<UIDimension> construct_from_json_value(AK::JsonValue const value)
    {
        if (value.is_string()) {
            DeprecatedString value_literal = value.as_string();
            if (value_literal == "shrink")
                return UIDimension { SpecialDimension::Shrink };
            else if (value_literal == "grow")
                return UIDimension { SpecialDimension::Grow };
            else if (value_literal == "opportunistic_grow")
                return UIDimension { SpecialDimension::OpportunisticGrow };
            else if (value_literal == "fit")
                return UIDimension { SpecialDimension::Fit };
            else
                return {};
        } else if (value.is_integer<i32>()) {
            auto value_int = value.as_integer<i32>();
            if (value_int < 0)
                return {};
            return UIDimension(value_int);
        }
        return {};
    }

private:
    int m_value;
};

class UISize : public Gfx::Size<UIDimension> {

public:
    UISize() = delete;

    UISize(int in_width, int in_height)
        : Gfx::Size<UIDimension>(in_width, in_height)
    {
    }

    UISize(Gfx::IntSize size)
        : UISize(size.width(), size.height())
    {
    }

    UISize(Array<i64, 2> size)
        : UISize(size[0], size[1])
    {
    }

    UISize(SpecialDimension special)
        : Gfx::Size<UIDimension>(UIDimension { special }, UIDimension { special })
    {
    }

    UISize(UIDimension width, UIDimension height)
        : Gfx::Size<UIDimension>(width, height)
    {
    }

    inline UISize replace_component_if_matching_with(UIDimension to_match, UISize replacement)
    {
        if (width() == to_match)
            set_width(replacement.width());
        if (height() == to_match)
            set_height(replacement.height());
        return *this;
    }

    [[nodiscard]] inline bool has_only_int_values() const
    {
        return width().is_int() && height().is_int();
    }

    [[nodiscard]] inline bool either_is(UIDimension to_match) const
    {
        return (width() == to_match || height() == to_match);
    }

    explicit operator Gfx::IntSize() const
    {
        return Gfx::IntSize(width().as_int(), height().as_int());
    }
};

}

namespace AK {

template<>
inline auto max<GUI::UIDimension>(GUI::UIDimension const& a, GUI::UIDimension const& b) -> GUI::UIDimension
{
    if ((a.is_int() && b.is_int()) || (a.is_special_value() && b.is_special_value()))
        return a.m_value > b.m_value ? a : b;
    if (a.is_grow() || b.is_grow())
        return GUI::SpecialDimension::Grow;
    if (a.is_opportunistic_grow() || b.is_opportunistic_grow())
        return GUI::SpecialDimension::OpportunisticGrow;
    if (a.is_fit() || b.is_fit())
        return GUI::SpecialDimension::Fit;
    if (a.is_shrink())
        return b;
    if (b.is_shrink())
        return a;
    VERIFY_NOT_REACHED();
}

template<>
inline auto min<GUI::UIDimension>(GUI::UIDimension const& a, GUI::UIDimension const& b) -> GUI::UIDimension
{
    if ((a.is_int() && b.is_int()) || (a.is_special_value() && b.is_special_value()))
        return a.m_value < b.m_value ? a : b;
    if (a.is_shrink() || b.is_shrink())
        return GUI::SpecialDimension::Shrink;
    if (a.is_int())
        return a;
    if (b.is_int())
        return b;
    if (a.is_fit() || b.is_fit())
        return GUI::SpecialDimension::Fit;
    if (a.is_opportunistic_grow() || b.is_opportunistic_grow())
        return GUI::SpecialDimension::OpportunisticGrow;
    VERIFY_NOT_REACHED();
}

template<>
inline auto clamp<GUI::UIDimension>(GUI::UIDimension const& input, GUI::UIDimension const& lower_bound, GUI::UIDimension const& upper_bound) -> GUI::UIDimension
{
    return min(max(input, lower_bound), upper_bound);
}

}

#define REGISTER_UI_DIMENSION_PROPERTY(property_name, getter, setter)         \
    register_property(                                                        \
        property_name,                                                        \
        [this] {                                                              \
            return this->getter().as_json_value();                            \
        },                                                                    \
        [this](auto& value) {                                                 \
            auto result = GUI::UIDimension::construct_from_json_value(value); \
            if (result.has_value())                                           \
                this->setter(result.value());                                 \
            return result.has_value();                                        \
        });

#define REGISTER_READONLY_UI_DIMENSION_PROPERTY(property_name, getter) \
    register_property(                                                 \
        property_name,                                                 \
        [this] {                                                       \
            return this->getter().as_json_value();                     \
        });

#define REGISTER_UI_SIZE_PROPERTY(property_name, getter, setter)               \
    register_property(                                                         \
        property_name,                                                         \
        [this] {                                                               \
            auto size = this->getter();                                        \
            JsonObject size_object;                                            \
            size_object.set("width"sv, size.width().as_json_value());          \
            size_object.set("height"sv, size.height().as_json_value());        \
            return size_object;                                                \
        },                                                                     \
        [this](auto& value) {                                                  \
            if (!value.is_object())                                            \
                return false;                                                  \
            auto result_width = GUI::UIDimension::construct_from_json_value(   \
                value.as_object().get("width"sv).value_or({}));                \
            auto result_height = GUI::UIDimension::construct_from_json_value(  \
                value.as_object().get("height"sv).value_or({}));               \
            if (result_width.has_value() && result_height.has_value()) {       \
                GUI::UISize size(result_width.value(), result_height.value()); \
                setter(size);                                                  \
                return true;                                                   \
            }                                                                  \
            return false;                                                      \
        });

#define REGISTER_READONLY_UI_SIZE_PROPERTY(property_name, getter)     \
    register_property(                                                \
        property_name,                                                \
        [this] {                                                      \
            auto size = this->getter();                               \
            JsonObject size_object;                                   \
            size_object.set("width", size.width().as_json_value());   \
            size_object.set("height", size.height().as_json_value()); \
            return size_object;                                       \
        });
