/*
 * SPDX-FileCopyrightText: 2019-2026 Mattia Basaglia <dev@dragon.best>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#pragma once

#include <set>
#include <array>

#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>

#include "glaxnimate/io/lottie/lottie_private_common.hpp"
#include "glaxnimate/io/svg/svg_parser.hpp"
#include "glaxnimate/model/animation/join_animatables.hpp"

namespace glaxnimate::io::lottie::detail {

struct FontInfo
{
    QString name;
    QString family;
    QString style;
};

class LottieImporterState
{
public:
    LottieImporterState(
        model::Document* document,
        io::lottie::LottieFormat* format
    ) : document(document), format(format)
    {}

    void load(const QJsonObject& json)
    {
        load_version(json);
        load_meta(json["meta"]);
        main = document->assets()->compositions->values.insert(std::make_unique<model::Composition>(document));
        auto comps = load_assets(json["assets"].toArray());
        load_fonts(json["fonts"]["list"].toArray());
        load_composition(json, main);
        load_comps(comps);
    }

private:
    void load_version(const QJsonObject& json)
    {
        if ( json.contains("v") )
        {
            auto parts = json["v"].toString().split(".");
            if ( parts.size() == 3 )
            {
                for ( int i = 0; i < 3; i++ )
                    version[i] = parts[i].toInt();
            }
        }
    }

    // https://github.com/airbnb/lottie-web/blob/0d658b34c40d4e81eafdccbf698815346454a899/player/js/utils/DataManager.js#L170
    bool is_version_older_than(int major, int minor, int patch) {
        if (major > version[0]) {
            return true;
        } if (version[0] > major) {
            return false;
        }

        if (minor > version[1]) {
            return true;
        } if (version[1] > minor) {
            return false;
        }

        if (patch > version[2]) {
            return true;
        } if (version[2] > patch) {
            return false;
        }

        return false;
    }

    bool animated(const QJsonObject& obj)
    {
        if ( obj.contains("a") )
            return obj["a"].toInt();

        if ( !obj["k"].isArray() )
            return 0;

        auto karr = obj["k"].toArray();
        return karr.size() > 0 && karr[0].isObject() && karr[0].toObject().contains("s");
    }

    template<class T>
    auto make_node(model::Document* document)
    {
        auto ptr = std::make_unique<T>(document);
        current_node = ptr.get();
        return ptr;
    }

    void warning(QString str, const QJsonObject& json)
    {
        if ( json.contains("nm") )
            str = json["nm"].toString() + ": " + str;
        Q_EMIT format->warning(str);
    }

    void load_stretchable_animation_container(const QJsonObject& json, model::StretchableTime* animation)
    {
        animation->start_time.set(json["st"].toDouble());
        animation->stretch.set(json["sr"].toDouble(1));
    }

    void load_animation_container(const QJsonObject& json, model::AnimationContainer* animation)
    {
        animation->first_frame.set(json["ip"].toDouble());
        animation->last_frame.set(json["op"].toDouble());
    }

    void load_composition(const QJsonObject& json, model::Composition* composition)
    {
        this->composition = composition;
        layer_indices.clear();
        deferred.clear();

        if ( composition != main )
        {
            composition->width.set(main->width.get());
            composition->height.set(main->height.get());
            composition->fps.set(main->fps.get());
            composition->animation->first_frame.set(main->animation->first_frame.get());
            composition->animation->last_frame.set(main->animation->last_frame.get());
        }

        if ( json.contains("fr") )
            composition->fps.set(json["fr"].toDouble());
        if ( json.contains("w") )
            composition->width.set(json["w"].toInt());
        if ( json.contains("h") )
            composition->height.set(json["h"].toInt());

        load_animation_container(json, composition->animation.get());
        load_basic(json, composition);


        std::vector<LayerData> layers;
        std::map<int, int> lottie_to_array_index;
        auto raw_layers = json["layers"].toArray();
        layers.reserve(layers.size());

        // First pass, get index info
        for ( auto val : raw_layers )
        {
            LayerData data;
            data.json = val.toObject();

            if ( data.json.contains("parent") )
                data.has_parent = true;

            if ( data.json.contains("ind") )
            {
                data.lottie_index = data.json["ind"].toInt();
                data.has_index = true;
                lottie_to_array_index[data.lottie_index] = layers.size();
            }

            data.name = data.json["nm"].toString();

            layers.emplace_back(std::move(data));
        }

        // Second pass, figure out matte + parent links
        for ( std::size_t i = 0; i < layers.size(); i++ )
        {
            auto& layer = layers[i];
            if ( layer.json["tt"].toInt() )
            {
                layer.matte_type = layer.json["tt"].toInt();
                if ( layer.json.contains("tp") )
                {
                    int lottie_index = layer.json["tp"].toInt();
                    auto it = lottie_to_array_index.find(lottie_index);
                    if ( it == lottie_to_array_index.end() || it->second == int(i) )
                    {
                        warning(i18n("Mask parent %1 of layer %2 could not be found", lottie_index, layer.identifier()), layer.json);
                        layer.matte_type = -1;
                    }
                    else
                    {
                        layer.matte = &layers[it->second];
                    }
                }
                else if ( i == 0 )
                {
                    warning(i18n("Mask parent of layer %1 could not be found", layer.identifier()), layer.json);
                }
                else
                {
                    layer.matte = &layers[i - 1];
                }

                if ( layer.matte )
                    layer.matte->matte_count += 1;
            }

            if ( layer.has_parent )
            {
                int parent_index = layer.json["parent"].toInt();
                auto it = lottie_to_array_index.find(parent_index);
                if ( it == lottie_to_array_index.end() )
                {
                    layer.has_parent = false;
                    warning(
                        i18n("Invalid parent layer %1", parent_index),
                        json
                    );
                }
                else
                {
                    layer.parent_array_index = it->second;
                    layers[it->second].is_parent = true;
                }
            }
        }

        // Third pass precompose layers that act as mattes for multiple other layers
        for ( auto& layer : layers )
        {
            if ( layer.matte_count )
            {
                layer.skip = true;
                if ( layer.matte_count > 1 )
                {
                    auto inner_comp = document->assets()->compositions->values.insert(std::make_unique<model::Composition>(document));
                    inner_comp->width.set(main->width.get());
                    inner_comp->height.set(main->height.get());
                    inner_comp->fps.set(main->fps.get());
                    inner_comp->animation->first_frame.set(main->animation->first_frame.get());
                    inner_comp->animation->last_frame.set(main->animation->last_frame.get());
                    auto props = load_basic_setup(layer.json);
                    load_properties(inner_comp, fields["DocumentNode"], layer.json, props);
                    if ( inner_comp->name.get().isEmpty() )
                        document->set_best_name(inner_comp);
                    inner_comp->shapes.insert(create_layer(layer, inner_comp));
                    // Gotta set this after we create the layer for the inner comp!
                    layer.matte_comp = inner_comp;
                    // Transform already applied within the comp
                    layer.json.remove("ks");
                }
            }
        }

        // Fourth pass, create layers
        for ( auto& layer : layers )
        {
            if ( !layer.skip )
            {
                composition->shapes.insert(create_layer(layer, composition), 0);
            }
        }

        // Fifth pass, assign parents
        for ( const auto& layer : layers )
        {
            if ( layer.has_parent )
            {
                const auto& parent = layers[layer.parent_array_index];
                if ( parent.layer )
                    layer.layer->parent.set(parent.layer);
            }
        }
    }

    struct LayerData
    {
        QJsonObject json = {};
        QString name = {};
        bool is_parent = false;
        bool has_parent = false;
        int parent_array_index = -1;
        bool skip = false;
        int matte_count = 0;
        int lottie_index = -1;
        bool has_index = false;
        int matte_type = -1;
        LayerData* matte = nullptr;
        model::Composition* matte_comp = nullptr;
        model::Layer* layer = nullptr;

        QString identifier() const
        {
            if ( has_index )
                return QString::number(lottie_index);
            return {};
        }

    };

    void load_visibility(model::VisualNode* node, const QJsonObject& json)
    {
        if ( json.contains("hd") && json["hd"].toBool() )
            node->visible.set(false);
    }


    std::unique_ptr<model::ShapeElement> layer_object_from_ty(LayerData& data, std::set<QString>& props)
    {
        int ty = 3; // default to null layer
        if ( !data.json.contains("ty") || !data.json["ty"].isDouble() )
        {
            warning(i18n("Missing type for layer %1", data.identifier()), data.json);
        }
        else
        {
            ty = data.json["ty"].toInt();
        }

        switch ( ty )
        {
            case 0: // precomp
                return load_precomp_layer(data.json, props);
            case 1: // solid color
            {
                props.erase("sw");
                props.erase("sh");
                props.erase("sc");

                auto g = make_node<model::Group>(document);

                auto color_name = data.json["sc"].toString();
                auto color = svg::parse_color(color_name);

                auto stroke = std::make_unique<model::Fill>(document);
                stroke->color.set(color);
                stroke->visible.set(false);
                g->shapes.insert(std::move(stroke));

                auto fill = std::make_unique<model::Fill>(document);
                fill->color.set(color);
                g->shapes.insert(std::move(fill));

                auto rect = std::make_unique<model::Rect>(document);
                auto w = data.json["sw"].toDouble();
                auto h = data.json["sh"].toDouble();
                rect->size.set(QSizeF(w, h));
                rect->position.set(QPointF(w/2, h/2));
                g->shapes.insert(std::move(rect));

                return g;
            }
            case 2: // image layer
            {
                return create_image_layer(data.json, props);
            }
            case 3: // empty
            {
                auto layer = std::make_unique<model::Layer>(document);
                data.layer = layer.get();
                return layer;
            }
            case 4: // shape
            {
                props.erase("shapes");
                auto layer = make_node<model::Layer>(document);
                current_layer = layer.get();
                data.layer = layer.get();
                load_shapes(layer->shapes, data.json["shapes"].toArray());
                return layer;
            }
            case 5: // text
            {
                props.erase("t");
                auto layer = make_node<model::Layer>(document);
                current_layer = layer.get();
                data.layer = layer.get();
                load_text_layer(layer->shapes, data.json["t"].toObject());
                return layer;
            }
            default:
            {
                QString type = data.json["ty"].toVariant().toString();
                auto it = unsupported_layers.find(ty);
                if ( it != unsupported_layers.end() )
                    type = *it;
                warning(i18n("Unsupported layer of type %1", type), data.json);
                auto layer = std::make_unique<model::Layer>(document);
                data.layer = layer.get();
                return layer;
            }
        }
    }

    std::unique_ptr<model::ShapeElement> create_layer(LayerData& data, model::Composition* comp)
    {
        std::unique_ptr<model::ShapeElement> inner_shape;
        std::set<QString> props = load_basic_setup(data.json);

        if ( data.matte_comp )
        {
            // rendered as a precomp because multiple layers use it as matte
            auto precomp = make_node<model::PreCompLayer>(document);
            props.erase("td");
            load_properties(precomp.get(), fields["DocumentNode"], data.json, props);
            load_properties(precomp.get(), fields["__Layer__"], data.json, props);
            load_visibility(precomp.get(), data.json);
            props.erase("ks");
            load_transform(data.json["ks"].toObject(), precomp->transform.get(), &precomp->opacity);
            precomp->composition.set(data.matte_comp);
            precomp->unbounded.set(true);
            inner_shape = std::move(precomp);
        }
        else
        {
            inner_shape = layer_object_from_ty(data, props);

            qreal ip = comp->animation->first_frame.get();
            qreal op = comp->animation->last_frame.get();
            if ( data.layer )
            {
                load_layer(data, props);
            }
            else
            {
                // Some non-layer composables need to be wrapped within an actual layer
                if (
                    data.json.contains("parent") ||
                    data.is_parent ||
                    !qFuzzyCompare(data.json["ip"].toDouble(ip), ip) ||
                    !qFuzzyCompare(data.json["op"].toDouble(op), op) ||
                    data.matte ||
                    data.json.contains("masksProperties")
                )
                {

                    auto layer = make_node<model::Layer>(document);
                    layer->name.set(inner_shape->name.get());
                    if ( auto composable = inner_shape->cast<model::Composable>() )
                    {
                        layer->blend_mode.set(composable->blend_mode.get());
                        composable->blend_mode.set(renderer::BlendMode::Normal);
                    }
                    layer->shapes.insert(std::move(inner_shape), 0);
                    data.layer = layer.get();
                    load_layer(data, props);
                    inner_shape = std::move(layer);
                }
                else
                {
                    ignore_properties(fields["__Layer__"], props);
                }
            }
        }

        // handle mattes
        if ( data.matte && data.matte_type > 0 )
        {
            auto mask_layer = std::make_unique<model::Layer>(document);
            load_layer_ip_op(data.json, mask_layer.get());
            mask_layer->name.set(data.name);

            mask_layer->shapes.insert(create_layer(*data.matte, comp));
            mask_layer->shapes.insert(std::move(inner_shape));

            switch ( data.matte_type )
            {
                case 1:
                    mask_layer->mask->mask.set(model::MaskSettings::Alpha);
                    break;
                case 2:
                    mask_layer->mask->mask.set(model::MaskSettings::Alpha);
                    mask_layer->mask->inverted.set(true);
                    break;
                case 3:
                    mask_layer->mask->mask.set(model::MaskSettings::Luma);
                    break;
                case 4:
                    mask_layer->mask->mask.set(model::MaskSettings::Luma);
                    mask_layer->mask->inverted.set(true);
                    break;
            }

            inner_shape = std::move(mask_layer);
        }


        load_basic_check(props);
        return inner_shape;
    }

    std::unique_ptr<model::PreCompLayer> load_precomp_layer(const QJsonObject& json, std::set<QString>& props)
    {
        auto precomp = make_node<model::PreCompLayer>(document);
        load_visibility(precomp.get(), json);

        load_stretchable_animation_container(json, precomp->timing.get());

        for ( const FieldInfo& field : fields["__Layer__"] )
            props.erase(field.lottie);

        load_basic_properties(json, precomp.get(), props);

        auto comp = precomp_ids[json["refId"].toString()];
        if ( comp )
        {
            precomp->composition.set(comp);
            if ( !json.contains("nm") )
                precomp->name.set(comp->name.get());
        }
        props.erase("w");
        props.erase("h");
        precomp->size.set(QSize(
            json["w"].toInt(),
            json["h"].toInt()
        ));

        load_composable(json, precomp.get(), props);

        return precomp;
    }

    void load_mask(const QJsonObject& json, model::Group* parent, bool subgroup, model::MaskSettings* mask)
    {
        model::Group* group = parent;
        if ( subgroup )
        {
            auto clip_group_p = make_node<model::Group>(document);
            group = clip_group_p.get();
            parent->shapes.insert(std::move(clip_group_p));
            document->set_best_name(group, i18n("Clip"));
        }
        char mode = json["mode"].toString("a")[0].toLatin1();
        switch ( mode )
        {
            // 'l', 'a' don't need anything
            // TODO 'i' intersect
            case 'n':
                group->visible.set(false);
                break;
            case 's':
            case 'f':
            case 'd':
                if ( subgroup )
                    group->blend_mode.set(renderer::BlendMode::Difference);
                else
                    mask->inverted.set(!json["inv"].toBool(false));
                break;
        }

        // TODO this doesn't work with multiple mask properties with different values of "inv"
        if ( subgroup && json["inv"].toBool(false) )
        {
            mask->inverted.set(true);
        }

        auto fill = make_node<model::Fill>(document);
        fill->color.set(QColor(255, 255, 255));
        document->set_best_name(fill.get());
        load_animated(&fill->opacity, json["o"], {});
        group->shapes.insert(std::move(fill));

        auto j_stroke = json["x"].toObject();
        if ( animated(j_stroke) || j_stroke["k"].toDouble() != 0 )
        {
            auto stroke = make_node<model::Stroke>(document);
            stroke->color.set(QColor(255, 255, 255));
            load_animated(&stroke->opacity, json["o"], {});
            document->set_best_name(stroke.get());
            load_animated(&stroke->width, json["x"], {});
            group->shapes.insert(std::move(stroke));
        }

        auto path = make_node<model::Path>(document);
        document->set_best_name(path.get());
        load_animated(&path->shape, json["pt"], {});
        group->shapes.insert(std::move(path));
    }

    void load_layer_ip_op(const QJsonObject& json, model::Layer* layer)
    {
        if ( !json.contains("ip") && !json.contains("op") )
        {
            auto comp = layer->owner_composition();
            layer->animation->first_frame.set(comp->animation->first_frame.get());
            layer->animation->last_frame.set(comp->animation->last_frame.get());
        }
        else
        {
            load_animation_container(json, layer->animation.get());
        }
    }

    void load_composable(const QJsonObject& json, model::Composable* layer, std::set<QString>& props)
    {
        props.erase("ks");
        props.erase("bm");
        load_transform(json["ks"].toObject(), layer->transform.get(), &layer->opacity);
        layer->blend_mode.set(renderer::BlendMode(json["bm"].toInt(0)));
    }

    void load_layer(const LayerData& data, std::set<QString>& props)
    {
        current_node = current_layer = data.layer;

        load_layer_ip_op(data.json, data.layer);

        props.erase("ind");

        load_properties(data.layer, fields["DocumentNode"], data.json, props);
        load_properties(data.layer, fields["Composable"], data.json, props);
        load_properties(data.layer, fields["__Layer__"], data.json, props);

        load_composable(data.json, data.layer, props);
        load_visibility(data.layer, data.json);

        props.erase("hasMask");
        props.erase("masksProperties");
        if ( data.json.contains("masksProperties") )
        {
            auto masks = data.json["masksProperties"].toArray();
            if ( !masks.empty() )
            {
                data.layer->mask->mask.set(model::MaskSettings::Alpha);

                // Group clip shapes go into
                auto clip_p = make_node<model::Group>(document);
                auto clip = clip_p.get();
                data.layer->shapes.insert(std::move(clip_p));

                document->set_best_name(clip, i18n("Clip"));
                if ( masks.size() == 1 )
                {
                    load_mask(masks[0].toObject(), clip, false, data.layer->mask.get());
                }
                else
                {
                    for ( const auto& mask : masks )
                    {
                        load_mask(mask.toObject(), clip, true, data.layer->mask.get());
                    }
                }
            }
        }
    }

    std::unique_ptr<model::Image> create_image_layer(const QJsonObject& json, std::set<QString>& props)
    {
        auto image = make_node<model::Image>(document);
        props.erase("refId");
        image->image.set(bitmap_ids[json["refId"].toString()]);
        load_basic_properties(json, image.get(), props);
        load_composable(json, image.get(), props);
        return image;
    }

    void load_shapes(model::ShapeListProperty& shapes, const QJsonArray& jshapes)
    {
        deferred.clear();

        for ( int i = jshapes.size() - 1; i >= 0; i-- )
            create_shape(jshapes[i].toObject(), shapes);

        auto deferred_shapes = std::move(deferred);
        deferred.clear();

        for ( const auto& pair: deferred_shapes )
            load_shape(pair.second, static_cast<model::ShapeElement*>(pair.first));
    }

    void create_shape(const QJsonObject& json, model::ShapeListProperty& shapes)
    {
        if ( !json.contains("ty") || !json["ty"].isString() )
        {
            warning(i18n("Missing shape type"), json);
            return;
        }

        QString base_type = json["ty"].toString();
        QString type = shape_types.key(base_type);
        if ( type.isEmpty() )
        {
            type = shape_types_repeat[base_type];
            if ( type.isEmpty() )
            {
                // "mm" is marked as unsupported by lottie and it appears in several animations so we ignore the warning
                if ( base_type != "mm" )
                    warning(i18n("Unsupported shape type %1", json["ty"].toString()), json);
                return;
            }
        }

        model::ShapeElement* shape = static_cast<model::ShapeElement*>(
            model::Factory::instance().build(type, document)
        );
        if ( !shape )
        {
            warning(i18n("Unsupported shape type %1", json["ty"].toString()), json);
            return;
        }

        deferred.emplace_back(shape, json);
        shapes.insert(std::unique_ptr<model::ShapeElement>(shape), shapes.size());
    }

    std::set<QString> load_basic_setup(const QJsonObject& json_obj)
    {
        std::set<QString> props;

        for ( auto it = json_obj.begin(); it != json_obj.end(); ++it )
            props.insert(it.key());

        return props;
    }

    void load_basic_check(const std::set<QString>& props)
    {
        for ( const auto& not_found : props )
        {
            Q_EMIT format->information(
                i18n("Unknown field %2%1", not_found, object_error_string(nullptr))
            );
        }
    }

    void load_basic(const QJsonObject& json_obj, model::Object* obj)
    {
        std::set<QString> props = load_basic_setup(json_obj);
        load_basic_properties(json_obj, obj, props);
        load_basic_check(props);
    }

    void load_basic_properties(const QJsonObject& json_obj, model::Object* obj, std::set<QString>& props)
    {

        for ( const QMetaObject* mo = obj->metaObject(); mo; mo = mo->superClass() )
            load_properties(
                obj,
                fields[model::detail::naked_type_name(mo)],
                json_obj,
                props
            );

    }

    void load_basic(const QJsonObject& json_obj, model::DocumentNode* obj)
    {
        load_basic(json_obj, static_cast<model::Object*>(obj));
        if ( obj->name.get().isEmpty() )
            document->set_best_name(obj);
    }

    void load_transform(const QJsonObject& transform, model::Transform* tf, model::AnimatedPropertyBase* opacity)
    {
        load_basic(transform, tf);
        if ( transform.contains("o") && opacity )
            load_animated(opacity, transform["o"], FloatMult(100));

        if ( transform.contains("p") )
        {
            auto pos = transform["p"].toObject();
            if ( pos.contains("x") && pos.contains("y") )
            {
                model::Document dummydoc("");
                model::Object dummy(&dummydoc);
                model::AnimatedProperty<float> px(&dummy, {}, 0);
                model::AnimatedProperty<float> py(&dummy, {}, 0);
                load_animated(&px, pos["x"], {});
                load_animated(&py, pos["y"], {});

                model::JoinAnimatables join({&px, &py});
                join.apply_to(&tf->position, [](float x, float y) -> QPointF {
                    return QPointF(x, y);
                }, &px, &py);
            }
            else
            {
                load_animated(&tf->position, transform["p"], {});
            }
        }
    }

    void load_styler(model::Styler* styler, const QJsonObject& json_obj)
    {
        load_visibility(styler, json_obj);

        std::set<QString> props = load_basic_setup(json_obj);
        load_basic_properties(json_obj, styler, props);

        if ( json_obj.contains("fillEnabled") )
            styler->visible.set(json_obj["fillEnabled"].toBool());

        if ( json_obj["ty"].toString().startsWith('g') )
        {
            auto gradient = document->assets()->gradients->values.insert(std::make_unique<model::Gradient>(document));
            styler->use.set(gradient);
            auto colors = document->assets()->gradient_colors->values.insert(std::make_unique<model::GradientColors>(document));
            gradient->colors.set(colors);
            load_properties(gradient, fields["Gradient"], json_obj, props);

            if ( json_obj.contains("h") )
            {
                model::Document dummydoc("");
                model::Object dummy(&dummydoc);
                model::AnimatedProperty<float> length(&dummy, {}, 0);
                if ( json_obj.contains("h") )
                    load_animated(&length, json_obj["h"], {});

                glaxnimate::model::JoinAnimatables join({&gradient->start_point, &gradient->end_point, &length, &gradient->angle});
                join.apply_to(&gradient->highlight, [](const QPointF& p, const QPointF& e, float length, float angle) -> QPointF {
                    angle = math::deg2rad(angle);
                    length = math::length(e - p) * length / 100;
                    return p + math::from_polar<QPointF>(length, angle);
                }, &gradient->start_point, &gradient->end_point, &length, &gradient->angle);
            }
            else
            {
                gradient->highlight.set(gradient->start_point.get());
            }

            auto jcolors = json_obj["g"].toObject();
            load_animated(&colors->colors, jcolors["k"], GradientLoad{jcolors["p"].toInt()});
        }
        else
        {
            load_animated(&styler->color, json_obj["c"], {});
        }

        if ( styler->name.get().isEmpty() )
            document->set_best_name(styler);

        load_basic_check(props);
    }

    void load_shape(const QJsonObject& json, model::ShapeElement* shape)
    {
        current_node = shape;

        if ( auto styler = shape->cast<model::Styler>() )
            return load_styler(styler, json);

        load_basic(json, shape);
        load_visibility(shape, json);

        QString type_name = shape->type_name();
        if ( type_name == "Group" )
        {
            auto gr = static_cast<model::Group*>(shape);
            QJsonArray shapes = json["it"].toArray();
            QJsonObject transform;

            for ( int i = shapes.size() - 1; i >= 0; i-- )
            {
                QJsonObject shi = shapes[i].toObject();
                if ( shi["ty"] == "tr" )
                {
                    transform = shi;
                    transform.remove("ty");
                    shapes.erase(shapes.begin() + i);
                    break;
                }
            }
            if ( !transform.empty() )
                load_transform(transform, gr->transform.get(), &gr->opacity);

            load_shapes(gr->shapes, shapes);
        }
        else if ( type_name == "Repeater" )
        {
            auto repeater = static_cast<model::Repeater*>(shape);
            QJsonObject transform = json["tr"].toObject();
            load_animated(&repeater->start_opacity, transform["so"], FloatMult(100));
            load_animated(&repeater->end_opacity, transform["eo"], FloatMult(100));
            transform.remove("so");
            transform.remove("eo");
            transform.remove("ty");
            load_transform(transform, repeater->transform.get(), nullptr);
        }
        else if ( type_name == "Path" )
        {
            auto path = static_cast<model::Path*>(shape);
            if ( version[0] < 5 && json.contains("closed") )
                path->shape.set_closed(json["closed"].toBool());

            path->closed.set_value(path->shape.closed());
        }
    }

    void ignore_properties(
        const QVector<FieldInfo>& fields,
        std::set<QString>& avail_obj_keys
        )
    {
        for ( const FieldInfo& field : fields )
            avail_obj_keys.erase(field.lottie);
    }

    void load_properties(
        model::Object* obj,
        const QVector<FieldInfo>& fields,
        const QJsonObject& json_obj,
        std::set<QString>& avail_obj_keys
    )
    {
        for ( const FieldInfo& field : fields )
        {
            avail_obj_keys.erase(field.lottie);
            if ( field.mode >= Ignored || !json_obj.contains(field.lottie) )
                continue;

            model::BaseProperty * prop = obj->get_property(field.name);
            if ( !prop )
            {
                logger.stream() << field.name << "is not a property";
                continue;
            }

            if ( prop->traits().flags & model::PropertyTraits::Animated )
            {
                load_animated(static_cast<model::AnimatedPropertyBase*>(prop), json_obj[field.lottie], field.transform);
            }
            else if ( field.mode == AnimatedToStatic )
            {
                load_static(prop, json_obj[field.lottie], field.transform);
            }
            else
            {
                load_value(prop, json_obj[field.lottie], field.transform);
            }
        }
    }

    template<class T>
    bool compound_value_2d_raw(const QJsonValue& val, T& out, double mul = 1)
    {
        QJsonArray arr = val.toArray();
        if ( arr.size() < 2 || !arr[0].isDouble() || !arr[1].isDouble() )
            return false;

        out = T(arr[0].toDouble() * mul, arr[1].toDouble() * mul);
        return true;
    }

    template<class T>
    std::optional<QVariant> compound_value_2d(const QJsonValue& val, double mul = 1)
    {
        T v;
        if ( !compound_value_2d_raw(val, v, mul) )
            return {};
        return QVariant::fromValue(v);
    }

    bool is_scalar(model::BaseProperty * prop)
    {
        switch ( prop->traits().type )
        {
            case model::PropertyTraits::Bool:
            case model::PropertyTraits::Int:
            case model::PropertyTraits::Float:
            case model::PropertyTraits::String:
            case model::PropertyTraits::Uuid:
            case model::PropertyTraits::Enum:
            case model::PropertyTraits::Bezier:
                return true;
            default:
                return false;
        }
    }

    bool compound_value_color(const QJsonValue& val, QColor& out)
    {
        QJsonArray arr = val.toArray();

        // https://github.com/airbnb/lottie-web/blob/0d658b34c40d4e81eafdccbf698815346454a899/player/js/utils/DataManager.js#L329
        if ( is_version_older_than(4, 1, 9) )
        {
            auto iter = std::find_if(arr.begin(), arr.end(), [](const QJsonValue& v){ return v.toDouble() > 1; });
            if ( iter != arr.end() )
            {
                if ( arr.size() == 3 )
                    out = QColor::fromRgb(
                        arr[0].toInt(), arr[1].toInt(), arr[2].toInt()
                    );
                else if ( arr.size() == 4 )
                    out = QColor::fromRgb(
                        arr[0].toInt(), arr[1].toInt(), arr[2].toInt(), qMin(255, arr[3].toInt())
                    );
                else
                    return false;

                return true;
            }
        }

        if ( arr.size() == 3 )
            out = QColor::fromRgbF(
                arr[0].toDouble(), arr[1].toDouble(), arr[2].toDouble()
            );
        else if ( arr.size() == 4 )
            out = QColor::fromRgbF(
                arr[0].toDouble(), arr[1].toDouble(), arr[2].toDouble(), qMin(1., arr[3].toDouble())
            );
        else
            return false;

        return true;
    }

    std::optional<QVariant> value_to_variant(model::BaseProperty * prop, const QJsonValue& val)
    {
        switch ( prop->traits().type )
        {
            case model::PropertyTraits::Bool:
            case model::PropertyTraits::Int:
            case model::PropertyTraits::Float:
            case model::PropertyTraits::String:
                return val.toVariant();
            case model::PropertyTraits::Uuid:
            {
                QUuid uuid = val.toVariant().toUuid();
                if ( uuid.isNull() )
                    uuid = QUuid::createUuid();
                return QVariant::fromValue(uuid);
            }
            case model::PropertyTraits::Point:
                return compound_value_2d<QPointF>(val);
            case model::PropertyTraits::Size:
                return compound_value_2d<QSizeF>(val);
            case model::PropertyTraits::Scale:
                return compound_value_2d<QVector2D>(val, 0.01);
            case model::PropertyTraits::Color:
            {
                QColor col;
                if ( compound_value_color(val, col) )
                    return QVariant::fromValue(col);
                return {};
            }
            case model::PropertyTraits::Bezier:
            {
                QJsonObject jsbez = val.toObject();
                math::bezier::Bezier bezier;
                bezier.set_closed(jsbez["c"].toBool());
                QJsonArray pos = jsbez["v"].toArray();
                QJsonArray tan_in = jsbez["i"].toArray();
                QJsonArray tan_out = jsbez["o"].toArray();
                int sz = std::min(pos.size(), std::min(tan_in.size(), tan_out.size()));
                for ( int i = 0; i < sz; i++ )
                {
                    QPointF p, ti, to;
                    if ( !compound_value_2d_raw(pos[i], p) )
                    {
                        Q_EMIT format->warning(
                            i18n("Invalid bezier point %1 in %2", i, property_error_string(prop))
                        );
                        continue;
                    }
                    compound_value_2d_raw(tan_in[i], ti);
                    compound_value_2d_raw(tan_out[i], to);
                    bezier.push_back(math::bezier::Point::from_relative(p, ti, to));
                }
                return QVariant::fromValue(bezier);
            }
            case model::PropertyTraits::Enum:
                return val.toInt();
            case model::PropertyTraits::Gradient:
                return val.toArray().toVariantList();
            default:
                logger.stream(log::Error) << "Unsupported type" << prop->traits().type << "for" << property_error_string(prop);
                return {};
        }
    }

    QString object_error_string(model::Object* ignore)
    {
        QString str;
        if ( current_layer && current_node != current_layer )
            str = "(" + current_layer->object_name() + ") ";

        if ( current_node && current_node != ignore )
            str += current_node->object_name() + ".";

        return str;

    }

    QString property_error_string(model::BaseProperty * prop)
    {
        QString str = object_error_string(prop->object());
        str += prop->object()->object_name() + "." + prop->name();

        return str;
    }

    void load_value(model::BaseProperty * prop, const QJsonValue& val, const TransformFunc& trans)
    {
        auto v = value_to_variant(prop, val);
        if ( !v || !prop->set_value(trans.from_lottie(*v, 0)) )
            Q_EMIT format->warning(i18n("Invalid value for %1", prop->name()));
    }

    void load_static(model::BaseProperty * prop, const QJsonValue& val, const TransformFunc& trans)
    {
        if ( val.isObject() )
        {
            QJsonObject obj = val.toObject();
            if ( obj.contains("k") )
            {
                load_value(prop, obj["k"], trans);
                return;
            }
        }

        load_value(prop, val, trans);
    }

    void load_animated(model::AnimatedPropertyBase* prop, const QJsonValue& val, const TransformFunc& trans)
    {
        if ( !val.isObject() )
        {
            Q_EMIT format->warning(i18n("Invalid value for %1", property_error_string(prop)));
            return;
        }

        QJsonObject obj = val.toObject();
        if ( !obj.contains("k") )
        {
            Q_EMIT format->warning(i18n("Invalid value for %1", property_error_string(prop)));
            return;
        }

        if ( animated(obj) )
        {
            if ( !obj["k"].isArray() )
            {
                Q_EMIT format->warning(i18n("Invalid keyframes for %1", property_error_string(prop)));
                return;
            }

            bool position = prop->traits().type == model::PropertyTraits::Point;

            auto karr = obj["k"].toArray();
            for ( int i = 0; i < karr.size(); i++ )
            {
                QJsonValue jkf = karr[i];
                model::FrameTime time = jkf["t"].toDouble();
                QJsonValue s = jkf["s"];
                if ( s.isUndefined() && i == karr.size() - 1 && i > 0 )
                    s = karr[i-1].toObject()["e"];
                if ( s.isArray() && is_scalar(prop) )
                    s = s.toArray()[0];

                auto v = value_to_variant(prop, s);
                model::KeyframeBase* kf = nullptr;
                if ( v )
                    kf = prop->set_keyframe(time, trans.from_lottie(*v, time));

                if ( kf )
                {
                    kf->set_transition({
                        keyframe_bezier_handle(jkf["o"]),
                        keyframe_bezier_handle(jkf["i"]),
                        jkf["h"].toInt() ? model::KeyframeTransition::Special::Hold : model::KeyframeTransition::Special::Normal
                    });

                    if ( position )
                    {
                        auto pkf = static_cast<model::Keyframe<QPointF>*>(kf);
                        QPointF tan_out;
                        compound_value_2d_raw(jkf["to"], tan_out);
                        tan_out += pkf->get();

                        QPointF tan_in;
                        if ( i > 0 )
                            compound_value_2d_raw(karr[i-1].toObject()["ti"], tan_in);
                        tan_in += pkf->get();

                        pkf->set_point({pkf->get(), tan_in, tan_out});
                    }
                }
                else
                {
                    QString value;
                    if ( !v )
                    {
                        value = i18n("(null)");
                    }
                    else
                    {
                        value = v->toString();
                        if ( value == "" )
                            value = i18n("(empty)");
                        value += " ";
                        value += QMetaType(v->userType()).name();
                    }
                    Q_EMIT format->warning(i18n(
                        "Cannot load keyframe at %1 for %2 with value %3",
                        time,
                        property_error_string(prop),
                        value
                    ));
                }
            }
        }
        else
        {
            load_value(prop, obj["k"], trans);
        }
    }

    qreal keyframe_bezier_handle_comp(const QJsonValue& comp)
    {
        if ( comp.isArray() )
            return comp[0].toDouble();
        return comp.toDouble();
    }

    QPointF keyframe_bezier_handle(const QJsonValue& val)
    {
        return {keyframe_bezier_handle_comp(val["x"]), keyframe_bezier_handle_comp(val["y"])};
    }

    std::vector<std::pair<QJsonObject, model::Composition*>> load_assets(const QJsonArray& assets)
    {
        std::vector<std::pair<QJsonObject, model::Composition*>> comps;

        for ( const auto& assetv : assets )
        {
            QJsonObject asset = assetv.toObject();
            if ( asset.contains("e") && asset.contains("p") && asset.contains("w") )
                load_asset_bitmap(asset);
            else if ( asset.contains("layers") )
                comps.emplace_back(asset, load_asset_precomp(asset));
        }

        return comps;
    }

    void load_comps(const std::vector<std::pair<QJsonObject, model::Composition*>>& comps)
    {
        for ( const auto& p : comps )
            load_composition(p.first, p.second);
    }

    void load_asset_bitmap(const QJsonObject& asset)
    {
        auto bmp = document->assets()->images->values.insert(std::make_unique<model::Bitmap>(document));

        QString id = asset["id"].toString();
        if ( bitmap_ids.count(id) )
            format->warning(i18n("Duplicate Bitmap ID: %1", id));
        bitmap_ids[id] = bmp;

        if ( asset.contains("nm") )
            bmp->name.set(asset["nm"].toString());

        if ( asset["e"].toInt() )
        {
            bmp->from_url(QUrl(asset["p"].toString()));
        }
        else
        {
            QString path = asset["u"].toString();
            if ( path.contains("://") )
            {
                path += asset["p"].toString();
                bmp->from_url(QUrl(path));
            }
            else
            {
                QDir dir(path);
                bmp->from_file(dir.filePath(asset["p"].toString()));
            }
        }
    }

    model::Composition* load_asset_precomp(QJsonObject asset)
    {
        auto comp = document->assets()->compositions->values.insert(std::make_unique<model::Composition>(document));

        QString id = asset["id"].toString();
        if ( precomp_ids.count(id) )
            format->warning(i18n("Duplicate Composition ID: %1", id));
        precomp_ids[id] = comp;

        comp->name.set(id);
        return comp;
    }

    enum class FontOrigin
    {
        System = 0,
        CssUrl = 1,
        ScriptUrl = 2,
        FontUrl = 3,
    };

    void load_fonts(const QJsonArray& fonts_arr)
    {
        for ( const auto& fontv : fonts_arr )
        {
            QJsonObject font = fontv.toObject();
            FontInfo info;
            info.family = font["fFamily"].toString();
            info.name = font["fName"].toString();
            info.style = font["fStyle"].toString();
            fonts[info.name] = info;

            FontOrigin font_origin = FontOrigin::System;
            if ( font.contains("origin") )
            {
                font_origin = FontOrigin(font["origin"].toInt());
            }
            else if ( font.contains("fOrigin") )
            {
                QString fOrigin = font["fOrigin"].toString();
                fOrigin.append(" ");
                switch ( fOrigin[0].toLatin1() )
                {
                    case 'n': font_origin = FontOrigin::System; break;
                    case 'g': font_origin = FontOrigin::CssUrl; break;
                    case 't': font_origin = FontOrigin::ScriptUrl; break;
                    case 'p': font_origin = FontOrigin::FontUrl; break;
                }
            }

            switch ( font_origin )
            {
                case FontOrigin::System:
                    // nothing to do
                    break;
                case FontOrigin::CssUrl:
                case FontOrigin::FontUrl:
                    // Queue dynamic font loading
                    document->add_pending_asset(info.family, QUrl(font["fPath"].toString()));
                    break;
                case FontOrigin::ScriptUrl:
                    // idk how these work
                    break;
            }
        }
    }

    FontInfo get_font(const QString& name)
    {
        auto it = fonts.find(name);
        if ( it != fonts.end() )
            return *it;
        return {"", name, "Regular"};
    }

    void load_text_layer(model::ShapeListProperty& shapes, const QJsonObject& text)
    {
        // TODO "a" "m" "p"

        model::Group* prev = nullptr;
        model::KeyframeTransition jump({}, {}, model::KeyframeTransition::Special::Hold);

        for ( const auto& v : text["d"].toObject()["k"].toArray() )
        {
            auto keyframe = v.toObject();
            qreal time = keyframe["t"].toDouble();
            auto text_document = keyframe["s"].toObject();

            auto group = std::make_unique<model::Group>(document);
            if ( time > 0 )
                group->opacity.set_keyframe(0, 0)->set_transition(jump);
            group->opacity.set_keyframe(time, 1)->set_transition(jump);
            if ( prev )
                prev->opacity.set_keyframe(time, 0)->set_transition(jump);
            prev = group.get();

            auto fill = std::make_unique<model::Fill>(document);
            QColor color;
            compound_value_color(text_document["fc"], color);
            fill->color.set(color);
            group->shapes.insert(std::move(fill));

            auto shape = make_node<model::TextShape>(document);
            auto font = get_font(text_document["f"].toString());
            shape->font->family.set(font.family);
            shape->font->style.set(font.style);
            shape->font->size.set(text_document["s"].toDouble());
            shape->text.set(text_document["t"].toString().replace('\r', '\n'));
            group->shapes.insert(std::move(shape));

            shapes.insert(std::move(group), shapes.size());
        }
    }

    void load_meta(const QJsonValue& meta)
    {
        if ( !meta.isObject() )
            return;

        document->info().author = meta["a"].toString();
        document->info().description = meta["d"].toString();
        for ( const auto& kw : meta["k"].toArray() )
            document->info().keywords.push_back(kw.toString());
    }

    model::Document* document;
    io::lottie::LottieFormat* format;
    QMap<int, model::Layer*> layer_indices;
    std::set<int> invalid_indices;
    std::vector<std::pair<model::Object*, QJsonObject>> deferred;
    model::Composition* composition = nullptr;
    log::Log logger{"Lottie Import"};
    QMap<QString, model::Bitmap*> bitmap_ids;
    QMap<QString, model::Composition*> precomp_ids;
    QMap<QString, FontInfo> fonts;
    model::DocumentNode* current_node = nullptr;
    model::Layer* current_layer = nullptr;
    std::array<int, 3> version = {5,5,1};
    model::Composition* main = nullptr;
};


} // namespace glaxnimate::io::lottie::detail
