/*
 * Copyright (c) 2020 Samsung Electronics Co., Ltd. All rights reserved.

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

//#define DEBUG_PARSER

// This parser implements JSON token-by-token parsing with an API that is
// more direct; we don't have to create  handler object and
// callbacks. Instead, we retrieve values from the JSON stream by calling
// GetInt(), GetDouble(), GetString() and GetBool(), traverse into structures
// by calling EnterObject() and EnterArray(), and skip over unwanted data by
// calling SkipValue(). As we know the lottie file structure this way will be
// the efficient way of parsing the file.
//
// If you aren't sure of what's next in the JSON data, you can use PeekType()
// and PeekValue() to look ahead to the next object before reading it.
//
// If you call the wrong retrieval method--e.g. GetInt when the next JSON token
// is not an int, EnterObject or EnterArray when there isn't actually an object
// or array to read--the stream parsing will end immediately and no more data
// will be delivered.
//
// After calling EnterObject, you retrieve keys via NextObjectKey() and values
// via the normal getters. When NextObjectKey() returns null, you have exited
// the object, or you can call SkipObject() to skip to the end of the object
// immediately. If you fetch the entire object (i.e. NextObjectKey() returned
// null), you should not call SkipObject().
//
// After calling EnterArray(), you must alternate between calling
// NextArrayValue() to see if the array has more data, and then retrieving
// values via the normal getters. You can call SkipArray() to skip to the end of
// the array immediately. If you fetch the entire array (i.e. NextArrayValue()
// returned null), you should not call SkipArray().
//
// This parser uses in-situ strings, so the JSON buffer will be altered during
// the parse.

#include <array>

#include "lottie_lottiemodel.h"
#include "lottie_rapidjson_document.h"

RAPIDJSON_DIAG_PUSH
#ifdef __GNUC__
RAPIDJSON_DIAG_OFF(effc++)
#endif

using namespace rapidjson;

using namespace rlottie::internal;

class LookaheadParserHandler {
public:
    bool Null()
    {
        st_ = kHasNull;
        v_.SetNull();
        return true;
    }
    bool Bool(bool b)
    {
        st_ = kHasBool;
        v_.SetBool(b);
        return true;
    }
    bool Int(int i)
    {
        st_ = kHasNumber;
        v_.SetInt(i);
        return true;
    }
    bool Uint(unsigned u)
    {
        st_ = kHasNumber;
        v_.SetUint(u);
        return true;
    }
    bool Int64(int64_t i)
    {
        st_ = kHasNumber;
        v_.SetInt64(i);
        return true;
    }
    bool Uint64(int64_t u)
    {
        st_ = kHasNumber;
        v_.SetUint64(u);
        return true;
    }
    bool Double(double d)
    {
        st_ = kHasNumber;
        v_.SetDouble(d);
        return true;
    }
    bool RawNumber(const char *, SizeType, bool) { return false; }
    bool String(const char *str, SizeType length, bool)
    {
        st_ = kHasString;
        v_.SetString(str, length);
        return true;
    }
    bool StartObject()
    {
        st_ = kEnteringObject;
        return true;
    }
    bool Key(const char *str, SizeType length, bool)
    {
        st_ = kHasKey;
        v_.SetString(str, length);
        return true;
    }
    bool EndObject(SizeType)
    {
        st_ = kExitingObject;
        return true;
    }
    bool StartArray()
    {
        st_ = kEnteringArray;
        return true;
    }
    bool EndArray(SizeType)
    {
        st_ = kExitingArray;
        return true;
    }

    void Error()
    {
        st_ = kError;
    }
protected:
    explicit LookaheadParserHandler(char *str);

protected:
    enum LookaheadParsingState {
        kInit,
        kError,
        kHasNull,
        kHasBool,
        kHasNumber,
        kHasString,
        kHasKey,
        kEnteringObject,
        kExitingObject,
        kEnteringArray,
        kExitingArray
    };

    Value                 v_;
    LookaheadParsingState st_;
    Reader                r_;
    InsituStringStream    ss_;

    static const int parseFlags = kParseDefaultFlags | kParseInsituFlag;
};

class LottieParserImpl : public LookaheadParserHandler {
public:
    LottieParserImpl(char *str, std::string dir_path, model::ColorFilter filter)
        : LookaheadParserHandler(str),
          mColorFilter(std::move(filter)),
          mDirPath(std::move(dir_path))
    {
    }
    bool VerifyType();
    bool ParseNext();

public:
    VArenaAlloc &allocator() { return compRef->mArenaAlloc; }
    bool         EnterObject();
    bool         EnterArray();
    const char * NextObjectKey();
    bool         NextArrayValue();
    int          GetInt();
    double       GetDouble();
    const char * GetString();
    std::string  GetStringObject();
    bool         GetBool();
    void         GetNull();

    void   SkipObject();
    void   SkipArray();
    void   SkipValue();
    Value *PeekValue();
    int    PeekType() const;
    bool   IsValid() { return st_ != kError; }

    void                  Skip(const char *key);
    model::BlendMode      getBlendMode();
    CapStyle              getLineCap();
    JoinStyle             getLineJoin();
    FillRule              getFillRule();
    model::Trim::TrimType getTrimType();
    model::MatteType      getMatteType();
    model::Layer::Type    getLayerType();

    std::shared_ptr<model::Composition> composition() const
    {
        return mComposition;
    }
    void             parseComposition();
    void             parseMarkers();
    void             parseMarker();
    void             parseAssets(model::Composition *comp);
    model::Asset *   parseAsset();
    void             parseLayers(model::Composition *comp);
    model::Layer *   parseLayer();
    void             parseMaskProperty(model::Layer *layer);
    void             parseShapesAttr(model::Layer *layer);
    void             parseObject(model::Group *parent);
    model::Mask *    parseMaskObject();
    model::Object *  parseObjectTypeAttr();
    model::Object *  parseGroupObject();
    model::Rect *    parseRectObject();
    model::RoundedCorner *    parseRoundedCorner();
    void updateRoundedCorner(model::Group *parent, model::RoundedCorner *rc);

    model::Ellipse * parseEllipseObject();
    model::Path *    parseShapeObject();
    model::Polystar *parsePolystarObject();

    model::Transform *     parseTransformObject(bool ddd = false);
    model::Fill *          parseFillObject();
    model::GradientFill *  parseGFillObject();
    model::Stroke *        parseStrokeObject();
    model::GradientStroke *parseGStrokeObject();
    model::Trim *          parseTrimObject();
    model::Repeater *      parseReapeaterObject();

    void parseGradientProperty(model::Gradient *gradient, const char *key);

    VPointF parseInperpolatorPoint();

    void getValue(VPointF &pt);
    void getValue(float &fval);
    void getValue(model::Color &color);
    void getValue(int &ival);
    void getValue(model::PathData &shape);
    void getValue(model::Gradient::Data &gradient);
    void getValue(std::vector<VPointF> &v);
    void getValue(model::Repeater::Transform &);

    template <typename T, typename Tag>
    bool parseKeyFrameValue(const char *, model::Value<T, Tag> &)
    {
        return false;
    }

    template <typename T>
    bool parseKeyFrameValue(const char *                      key,
                            model::Value<T, model::Position> &value);
    template <typename T, typename Tag>
    void parseKeyFrame(model::KeyFrames<T, Tag> &obj);
    template <typename T>
    void parseProperty(model::Property<T> &obj);
    template <typename T, typename Tag>
    void parsePropertyHelper(model::Property<T, Tag> &obj);

    void parseShapeProperty(model::Property<model::PathData> &obj);
    void parseDashProperty(model::Dash &dash);

    VInterpolator *interpolator(VPointF, VPointF, std::string);

    model::Color toColor(const char *str);

    void resolveLayerRefs();
    void parsePathInfo();

private:
    model::ColorFilter mColorFilter;
    struct {
        std::vector<VPointF> mInPoint;  /* "i" */
        std::vector<VPointF> mOutPoint; /* "o" */
        std::vector<VPointF> mVertices; /* "v" */
        std::vector<VPointF> mResult;
        bool                 mClosed{false};

        void convert()
        {
            // shape data could be empty.
            if (mInPoint.empty() || mOutPoint.empty() || mVertices.empty()) {
                mResult.clear();
                return;
            }

            /*
             * Convert the AE shape format to
             * list of bazier curves
             * The final structure will be Move +size*Cubic + Cubic (if the path
             * is closed one)
             */
            if (mInPoint.size() != mOutPoint.size() ||
                mInPoint.size() != mVertices.size()) {
                mResult.clear();
            } else {
                auto size = mVertices.size();
                mResult.push_back(mVertices[0]);
                for (size_t i = 1; i < size; i++) {
                    mResult.push_back(
                        mVertices[i - 1] +
                        mOutPoint[i - 1]);  // CP1 = start + outTangent
                    mResult.push_back(mVertices[i] +
                                      mInPoint[i]);   // CP2 = end + inTangent
                    mResult.push_back(mVertices[i]);  // end point
                }

                if (mClosed) {
                    mResult.push_back(
                        mVertices[size - 1] +
                        mOutPoint[size - 1]);  // CP1 = start + outTangent
                    mResult.push_back(mVertices[0] +
                                      mInPoint[0]);   // CP2 = end + inTangent
                    mResult.push_back(mVertices[0]);  // end point
                }
            }
        }
        void reset()
        {
            mInPoint.clear();
            mOutPoint.clear();
            mVertices.clear();
            mResult.clear();
            mClosed = false;
        }
        void updatePath(VPath &out)
        {
            if (mResult.empty()) return;

            auto size = mResult.size();
            auto points = mResult.data();
            /* reserve exact memory requirement at once
             * ptSize = size + 1(size + close)
             * elmSize = size/3 cubic + 1 move + 1 close
             */
            out.reserve(size + 1, size / 3 + 2);
            out.moveTo(points[0]);
            for (size_t i = 1; i < size; i += 3) {
                out.cubicTo(points[i], points[i + 1], points[i + 2]);
            }
            if (mClosed) out.close();
        }
    } mPathInfo;

protected:
    std::unordered_map<std::string, VInterpolator *> mInterpolatorCache;
    std::shared_ptr<model::Composition>              mComposition;
    model::Composition *                             compRef{nullptr};
    model::Layer *                                   curLayerRef{nullptr};
    std::vector<model::Layer *>                      mLayersToUpdate;
    std::string                                      mDirPath;
    void                                             SkipOut(int depth);
};

LookaheadParserHandler::LookaheadParserHandler(char *str)
    : v_(), st_(kInit), ss_(str)
{
    r_.IterativeParseInit();
}

bool LottieParserImpl::VerifyType()
{
    /* Verify the media type is lottie json.
       Could add more strict check. */
    return ParseNext();
}

bool LottieParserImpl::ParseNext()
{
    if (r_.HasParseError()) {
        st_ = kError;
        return false;
    }

    if (!r_.IterativeParseNext<parseFlags>(ss_, *this)) {
        vCritical << "Lottie file parsing error";
        st_ = kError;
        return false;
    }
    return true;
}

bool LottieParserImpl::EnterObject()
{
    if (st_ != kEnteringObject) {
        st_ = kError;
        return false;
    }

    ParseNext();
    return true;
}

bool LottieParserImpl::EnterArray()
{
    if (st_ != kEnteringArray) {
        st_ = kError;
        return false;
    }

    ParseNext();
    return true;
}

const char *LottieParserImpl::NextObjectKey()
{
    if (st_ == kHasKey) {
        const char *result = v_.GetString();
        ParseNext();
        return result;
    }

    /* SPECIAL CASE
     * The parser works with a prdefined rule that it will be only
     * while (NextObjectKey()) for each object but in case of our nested group
     * object we can call multiple time NextObjectKey() while exiting the object
     * so ignore those and don't put parser in the error state.
     * */
    if (st_ == kExitingArray || st_ == kEnteringObject) {
        // #ifdef DEBUG_PARSER
        //         vDebug<<"Object: Exiting nested loop";
        // #endif
        return nullptr;
    }

    if (st_ != kExitingObject) {
        st_ = kError;
        return nullptr;
    }

    ParseNext();
    return nullptr;
}

bool LottieParserImpl::NextArrayValue()
{
    if (st_ == kExitingArray) {
        ParseNext();
        return false;
    }

    /* SPECIAL CASE
     * same as  NextObjectKey()
     */
    if (st_ == kExitingObject) {
        return false;
    }

    if (st_ == kError || st_ == kHasKey) {
        st_ = kError;
        return false;
    }

    return true;
}

int LottieParserImpl::GetInt()
{
    if (st_ != kHasNumber || !v_.IsInt()) {
        st_ = kError;
        return 0;
    }

    int result = v_.GetInt();
    ParseNext();
    return result;
}

double LottieParserImpl::GetDouble()
{
    if (st_ != kHasNumber) {
        st_ = kError;
        return 0.;
    }

    double result = v_.GetDouble();
    ParseNext();
    return result;
}

bool LottieParserImpl::GetBool()
{
    if (st_ != kHasBool) {
        st_ = kError;
        return false;
    }

    bool result = v_.GetBool();
    ParseNext();
    return result;
}

void LottieParserImpl::GetNull()
{
    if (st_ != kHasNull) {
        st_ = kError;
        return;
    }

    ParseNext();
}

const char *LottieParserImpl::GetString()
{
    if (st_ != kHasString) {
        st_ = kError;
        return nullptr;
    }

    const char *result = v_.GetString();
    ParseNext();
    return result;
}

std::string LottieParserImpl::GetStringObject()
{
    auto str = GetString();

    if (str) {
        return std::string(str);
    }

    return {};
}

void LottieParserImpl::SkipOut(int depth)
{
    do {
        if (st_ == kEnteringArray || st_ == kEnteringObject) {
            ++depth;
        } else if (st_ == kExitingArray || st_ == kExitingObject) {
            --depth;
        } else if (st_ == kError) {
            return;
        }

        ParseNext();
    } while (depth > 0);
}

void LottieParserImpl::SkipValue()
{
    SkipOut(0);
}

void LottieParserImpl::SkipArray()
{
    SkipOut(1);
}

void LottieParserImpl::SkipObject()
{
    SkipOut(1);
}

Value *LottieParserImpl::PeekValue()
{
    if (st_ >= kHasNull && st_ <= kHasKey) {
        return &v_;
    }

    return nullptr;
}

// returns a rapidjson::Type, or -1 for no value (at end of
// object/array)
int LottieParserImpl::PeekType() const
{
    if (st_ >= kHasNull && st_ <= kHasKey) {
        return v_.GetType();
    }

    if (st_ == kEnteringArray) {
        return kArrayType;
    }

    if (st_ == kEnteringObject) {
        return kObjectType;
    }

    return -1;
}

void LottieParserImpl::Skip(const char * /*key*/)
{
    if (PeekType() == kArrayType) {
        EnterArray();
        SkipArray();
    } else if (PeekType() == kObjectType) {
        EnterObject();
        SkipObject();
    } else {
        SkipValue();
    }
}

model::BlendMode LottieParserImpl::getBlendMode()
{
    auto mode = model::BlendMode::Normal;

    switch (GetInt()) {
    case 1:
        mode = model::BlendMode::Multiply;
        break;
    case 2:
        mode = model::BlendMode::Screen;
        break;
    case 3:
        mode = model::BlendMode::OverLay;
        break;
    default:
        break;
    }
    return mode;
}

void LottieParserImpl::resolveLayerRefs()
{
    for (const auto &layer : mLayersToUpdate) {
        auto search = compRef->mAssets.find(layer->extra()->mPreCompRefId);
        if (search != compRef->mAssets.end()) {
            if (layer->mLayerType == model::Layer::Type::Image) {
                layer->extra()->mAsset = search->second;
            } else if (layer->mLayerType == model::Layer::Type::Precomp) {
                layer->mChildren = search->second->mLayers;
                layer->setStatic(layer->isStatic() &&
                                 search->second->isStatic());
            }
        }
    }
}

void LottieParserImpl::parseComposition()
{
    EnterObject();
    std::shared_ptr<model::Composition> sharedComposition =
        std::make_shared<model::Composition>();
    model::Composition *comp = sharedComposition.get();
    compRef = comp;
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "v")) {
            comp->mVersion = GetStringObject();
        } else if (0 == strcmp(key, "w")) {
            comp->mSize.setWidth(GetInt());
        } else if (0 == strcmp(key, "h")) {
            comp->mSize.setHeight(GetInt());
        } else if (0 == strcmp(key, "ip")) {
            comp->mStartFrame = GetDouble();
        } else if (0 == strcmp(key, "op")) {
            comp->mEndFrame = GetDouble();
        } else if (0 == strcmp(key, "fr")) {
            comp->mFrameRate = GetDouble();
        } else if (0 == strcmp(key, "assets")) {
            parseAssets(comp);
        } else if (0 == strcmp(key, "layers")) {
            parseLayers(comp);
        } else if (0 == strcmp(key, "markers")) {
            parseMarkers();
        } else {
#ifdef DEBUG_PARSER
            vWarning << "Composition Attribute Skipped : " << key;
#endif
            Skip(key);
        }
    }

    if (comp->mVersion.empty() || !comp->mRootLayer) {
        // don't have a valid bodymovin header
        return;
    }
    if (comp->mStartFrame > comp->mEndFrame) {
        // reveresed animation? missing data?
        return;
    }
    if (!IsValid()) {
        return;
    }

    resolveLayerRefs();
    comp->setStatic(comp->mRootLayer->isStatic());
    comp->mRootLayer->mInFrame = comp->mStartFrame;
    comp->mRootLayer->mOutFrame = comp->mEndFrame;

    mComposition = sharedComposition;
}

void LottieParserImpl::parseMarker()
{
    EnterObject();
    std::string comment;
    int         timeframe{0};
    int         duration{0};
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "cm")) {
            comment = GetStringObject();
        } else if (0 == strcmp(key, "tm")) {
            timeframe = GetDouble();
        } else if (0 == strcmp(key, "dr")) {
            duration = GetDouble();

        } else {
#ifdef DEBUG_PARSER
            vWarning << "Marker Attribute Skipped : " << key;
#endif
            Skip(key);
        }
    }
    compRef->mMarkers.emplace_back(std::move(comment), timeframe,
                                   timeframe + duration);
}

void LottieParserImpl::parseMarkers()
{
    EnterArray();
    while (NextArrayValue()) {
        parseMarker();
    }
    // update the precomp layers with the actual layer object
}

void LottieParserImpl::parseAssets(model::Composition *composition)
{
    EnterArray();
    while (NextArrayValue()) {
        auto asset = parseAsset();
        composition->mAssets[asset->mRefId] = asset;
    }
    // update the precomp layers with the actual layer object
}

static constexpr const unsigned char B64index[256] = {
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  0,  62, 63, 62, 62, 63, 52, 53, 54, 55, 56, 57,
    58, 59, 60, 61, 0,  0,  0,  0,  0,  0,  0,  0,  1,  2,  3,  4,  5,  6,
    7,  8,  9,  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
    25, 0,  0,  0,  0,  63, 0,  26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
    37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51};

std::string b64decode(const char *data, const size_t len)
{
    auto         p = reinterpret_cast<const unsigned char *>(data);
    int          pad = len > 0 && (len % 4 || p[len - 1] == '=');
    const size_t L = ((len + 3) / 4 - pad) * 4;
    std::string  str(L / 4 * 3 + pad, '\0');

    for (size_t i = 0, j = 0; i < L; i += 4) {
        int n = B64index[p[i]] << 18 | B64index[p[i + 1]] << 12 |
                B64index[p[i + 2]] << 6 | B64index[p[i + 3]];
        str[j++] = n >> 16;
        str[j++] = n >> 8 & 0xFF;
        str[j++] = n & 0xFF;
    }
    if (pad) {
        int n = B64index[p[L]] << 18 | B64index[p[L + 1]] << 12;
        str[str.size() - 1] = n >> 16;

        if (len > L + 2 && p[L + 2] != '=') {
            n |= B64index[p[L + 2]] << 6;
            str.push_back(n >> 8 & 0xFF);
        }
    }
    return str;
}

static std::string convertFromBase64(const std::string &str)
{
    // usual header look like "data:image/png;base64,"
    // so need to skip till ','.
    size_t startIndex = str.find(",", 0);
    startIndex += 1;  // skip ","
    size_t length = str.length() - startIndex;

    const char *b64Data = str.c_str() + startIndex;

    return b64decode(b64Data, length);
}

/*
 *  std::to_string() function is missing in VS2017
 *  so this is workaround for windows build
 */
#include <sstream>
template <class T>
static std::string toString(const T &value)
{
    std::ostringstream os;
    os << value;
    return os.str();
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/layers/shape.json
 *
 */
model::Asset *LottieParserImpl::parseAsset()
{
    auto        asset = allocator().make<model::Asset>();
    std::string filename;
    std::string relativePath;
    bool        embededResource = false;
    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "w")) {
            asset->mWidth = GetInt();
        } else if (0 == strcmp(key, "h")) {
            asset->mHeight = GetInt();
        } else if (0 == strcmp(key, "p")) { /* image name */
            asset->mAssetType = model::Asset::Type::Image;
            filename = GetStringObject();
        } else if (0 == strcmp(key, "u")) { /* relative image path */
            relativePath = GetStringObject();
        } else if (0 == strcmp(key, "e")) { /* relative image path */
            embededResource = GetInt();
        } else if (0 == strcmp(key, "id")) { /* reference id*/
            if (PeekType() == kStringType) {
                asset->mRefId = GetStringObject();
            } else {
                asset->mRefId = toString(GetInt());
            }
        } else if (0 == strcmp(key, "layers")) {
            asset->mAssetType = model::Asset::Type::Precomp;
            EnterArray();
            bool staticFlag = true;
            while (NextArrayValue()) {
                auto layer = parseLayer();
                if (layer) {
                    staticFlag = staticFlag && layer->isStatic();
                    asset->mLayers.push_back(layer);
                }
            }
            asset->setStatic(staticFlag);
        } else {
#ifdef DEBUG_PARSER
            vWarning << "Asset Attribute Skipped : " << key;
#endif
            Skip(key);
        }
    }

    if (asset->mAssetType == model::Asset::Type::Image) {
        if (embededResource) {
            // embeder resource should start with "data:"
            if (filename.compare(0, 5, "data:") == 0) {
                asset->loadImageData(convertFromBase64(filename));
            }
        } else {
            asset->loadImagePath(mDirPath + relativePath + filename);
        }
    }

    return asset;
}

void LottieParserImpl::parseLayers(model::Composition *comp)
{
    comp->mRootLayer = allocator().make<model::Layer>();
    comp->mRootLayer->mLayerType = model::Layer::Type::Precomp;
    comp->mRootLayer->setName("__");
    bool staticFlag = true;
    EnterArray();
    while (NextArrayValue()) {
        auto layer = parseLayer();
        if (layer) {
            staticFlag = staticFlag && layer->isStatic();
            comp->mRootLayer->mChildren.push_back(layer);
        }
    }
    comp->mRootLayer->setStatic(staticFlag);
}

model::Color LottieParserImpl::toColor(const char *str)
{
    if (!str) return {};

    model::Color color;
    auto         len = strlen(str);

    // some resource has empty color string
    // return a default color for those cases.
    if (len != 7 || str[0] != '#') return color;

    char tmp[3] = {'\0', '\0', '\0'};
    tmp[0] = str[1];
    tmp[1] = str[2];
    color.r = std::strtol(tmp, nullptr, 16) / 255.0f;

    tmp[0] = str[3];
    tmp[1] = str[4];
    color.g = std::strtol(tmp, nullptr, 16) / 255.0f;

    tmp[0] = str[5];
    tmp[1] = str[6];
    color.b = std::strtol(tmp, nullptr, 16) / 255.0f;

    return color;
}

model::MatteType LottieParserImpl::getMatteType()
{
    switch (GetInt()) {
    case 1:
        return model::MatteType::Alpha;
        break;
    case 2:
        return model::MatteType::AlphaInv;
        break;
    case 3:
        return model::MatteType::Luma;
        break;
    case 4:
        return model::MatteType::LumaInv;
        break;
    default:
        return model::MatteType::None;
        break;
    }
}

model::Layer::Type LottieParserImpl::getLayerType()
{
    switch (GetInt()) {
    case 0:
        return model::Layer::Type::Precomp;
        break;
    case 1:
        return model::Layer::Type::Solid;
        break;
    case 2:
        return model::Layer::Type::Image;
        break;
    case 3:
        return model::Layer::Type::Null;
        break;
    case 4:
        return model::Layer::Type::Shape;
        break;
    case 5:
        return model::Layer::Type::Text;
        break;
    default:
        return model::Layer::Type::Null;
        break;
    }
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/layers/shape.json
 *
 */
model::Layer *LottieParserImpl::parseLayer()
{
    model::Layer *layer = allocator().make<model::Layer>();
    curLayerRef = layer;
    bool ddd = true;
    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "ty")) { /* Type of layer*/
            layer->mLayerType = getLayerType();
        } else if (0 == strcmp(key, "nm")) { /*Layer name*/
            layer->setName(GetString());
        } else if (0 == strcmp(key, "ind")) { /*Layer index in AE. Used for
                                                 parenting and expressions.*/
            layer->mId = GetInt();
        } else if (0 == strcmp(key, "ddd")) { /*3d layer */
            ddd = GetInt();
        } else if (0 ==
                   strcmp(key,
                          "parent")) { /*Layer Parent. Uses "ind" of parent.*/
            layer->mParentId = GetInt();
        } else if (0 == strcmp(key, "refId")) { /*preComp Layer reference id*/
            layer->extra()->mPreCompRefId = GetStringObject();
            layer->mHasGradient = true;
            mLayersToUpdate.push_back(layer);
        } else if (0 == strcmp(key, "sr")) {  // "Layer Time Stretching"
            layer->mTimeStreatch = GetDouble();
        } else if (0 == strcmp(key, "tm")) {  // time remapping
            parseProperty(layer->extra()->mTimeRemap);
        } else if (0 == strcmp(key, "ip")) {
            layer->mInFrame = std::lround(GetDouble());
        } else if (0 == strcmp(key, "op")) {
            layer->mOutFrame = std::lround(GetDouble());
        } else if (0 == strcmp(key, "st")) {
            layer->mStartFrame = GetDouble();
        } else if (0 == strcmp(key, "bm")) {
            layer->mBlendMode = getBlendMode();
        } else if (0 == strcmp(key, "ks")) {
            EnterObject();
            layer->mTransform = parseTransformObject(ddd);
        } else if (0 == strcmp(key, "shapes")) {
            parseShapesAttr(layer);
        } else if (0 == strcmp(key, "w")) {
            layer->mLayerSize.setWidth(GetInt());
        } else if (0 == strcmp(key, "h")) {
            layer->mLayerSize.setHeight(GetInt());
        } else if (0 == strcmp(key, "sw")) {
            layer->mLayerSize.setWidth(GetInt());
        } else if (0 == strcmp(key, "sh")) {
            layer->mLayerSize.setHeight(GetInt());
        } else if (0 == strcmp(key, "sc")) {
            layer->extra()->mSolidColor = toColor(GetString());
        } else if (0 == strcmp(key, "tt")) {
            layer->mMatteType = getMatteType();
        } else if (0 == strcmp(key, "hasMask")) {
            layer->mHasMask = GetBool();
        } else if (0 == strcmp(key, "masksProperties")) {
            parseMaskProperty(layer);
        } else if (0 == strcmp(key, "ao")) {
            layer->mAutoOrient = GetInt();
        } else if (0 == strcmp(key, "hd")) {
            layer->setHidden(GetBool());
        } else {
#ifdef DEBUG_PARSER
            vWarning << "Layer Attribute Skipped : " << key;
#endif
            Skip(key);
        }
    }

    if (!layer->mTransform) {
        // not a valid layer
        return nullptr;
    }

    // make sure layer data is not corrupted.
    if (layer->hasParent() && (layer->id() == layer->parentId()))
        return nullptr;

    if (layer->mExtra) layer->mExtra->mCompRef = compRef;

    if (layer->hidden()) {
        // if layer is hidden, only data that is usefull is its
        // transform matrix(when it is a parent of some other layer)
        // so force it to be a Null Layer and release all resource.
        layer->setStatic(layer->mTransform->isStatic());
        layer->mLayerType = model::Layer::Type::Null;
        layer->mChildren = {};
        return layer;
    }

    // update the static property of layer
    bool staticFlag = true;
    for (const auto &child : layer->mChildren) {
        staticFlag &= child->isStatic();
    }

    if (layer->hasMask() && layer->mExtra) {
        for (const auto &mask : layer->mExtra->mMasks) {
            staticFlag &= mask->isStatic();
        }
    }

    layer->setStatic(staticFlag && layer->mTransform->isStatic());

    return layer;
}

void LottieParserImpl::parseMaskProperty(model::Layer *layer)
{
    EnterArray();
    while (NextArrayValue()) {
        layer->extra()->mMasks.push_back(parseMaskObject());
    }
}

model::Mask *LottieParserImpl::parseMaskObject()
{
    auto obj = allocator().make<model::Mask>();

    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "inv")) {
            obj->mInv = GetBool();
        } else if (0 == strcmp(key, "mode")) {
            const char *str = GetString();
            if (!str) {
                obj->mMode = model::Mask::Mode::None;
                continue;
            }
            switch (str[0]) {
            case 'n':
                obj->mMode = model::Mask::Mode::None;
                break;
            case 'a':
                obj->mMode = model::Mask::Mode::Add;
                break;
            case 's':
                obj->mMode = model::Mask::Mode::Substarct;
                break;
            case 'i':
                obj->mMode = model::Mask::Mode::Intersect;
                break;
            case 'f':
                obj->mMode = model::Mask::Mode::Difference;
                break;
            default:
                obj->mMode = model::Mask::Mode::None;
                break;
            }
        } else if (0 == strcmp(key, "pt")) {
            parseShapeProperty(obj->mShape);
        } else if (0 == strcmp(key, "o")) {
            parseProperty(obj->mOpacity);
        } else {
            Skip(key);
        }
    }
    obj->mIsStatic = obj->mShape.isStatic() && obj->mOpacity.isStatic();
    return obj;
}

void LottieParserImpl::parseShapesAttr(model::Layer *layer)
{
    EnterArray();
    while (NextArrayValue()) {
        parseObject(layer);
    }
}

model::Object *LottieParserImpl::parseObjectTypeAttr()
{
    const char *type = GetString();
    if (0 == strcmp(type, "gr")) {
        return parseGroupObject();
    } else if (0 == strcmp(type, "rc")) {
        return parseRectObject();
    } else if (0 == strcmp(type, "rd")) {
        curLayerRef->mHasRoundedCorner = true;
        return parseRoundedCorner();
    }  else if (0 == strcmp(type, "el")) {
        return parseEllipseObject();
    } else if (0 == strcmp(type, "tr")) {
        return parseTransformObject();
    } else if (0 == strcmp(type, "fl")) {
        return parseFillObject();
    } else if (0 == strcmp(type, "st")) {
        return parseStrokeObject();
    } else if (0 == strcmp(type, "gf")) {
        curLayerRef->mHasGradient = true;
        return parseGFillObject();
    } else if (0 == strcmp(type, "gs")) {
        curLayerRef->mHasGradient = true;
        return parseGStrokeObject();
    } else if (0 == strcmp(type, "sh")) {
        return parseShapeObject();
    } else if (0 == strcmp(type, "sr")) {
        return parsePolystarObject();
    } else if (0 == strcmp(type, "tm")) {
        curLayerRef->mHasPathOperator = true;
        return parseTrimObject();
    } else if (0 == strcmp(type, "rp")) {
        curLayerRef->mHasRepeater = true;
        return parseReapeaterObject();
    } else if (0 == strcmp(type, "mm")) {
        vWarning << "Merge Path is not supported yet";
        return nullptr;
    } else {
#ifdef DEBUG_PARSER
        vDebug << "The Object Type not yet handled = " << type;
#endif
        return nullptr;
    }
}

void LottieParserImpl::parseObject(model::Group *parent)
{
    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "ty")) {
            auto child = parseObjectTypeAttr();
            if (child && !child->hidden()) {
                if (child->type() == model::Object::Type::RoundedCorner) {
                    updateRoundedCorner(parent, static_cast<model::RoundedCorner *>(child));
                }
                parent->mChildren.push_back(child);
            }
        } else {
            Skip(key);
        }
    }
}

void LottieParserImpl::updateRoundedCorner(model::Group *group, model::RoundedCorner *rc)
{
    for(auto &e : group->mChildren)
    {
        if (e->type() == model::Object::Type::Rect) {
            static_cast<model::Rect *>(e)->mRoundedCorner = rc;
            if (!rc->isStatic()) {
                e->setStatic(false);
                group->setStatic(false);
                //@TODO need to propagate.
            }
        } else if ( e->type() == model::Object::Type::Group) {
            updateRoundedCorner(static_cast<model::Group *>(e), rc);
        }
    }
}

model::Object *LottieParserImpl::parseGroupObject()
{
    auto group = allocator().make<model::Group>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            group->setName(GetString());
        } else if (0 == strcmp(key, "it")) {
            EnterArray();
            while (NextArrayValue()) {
                parseObject(group);
            }
            if (!group->mChildren.empty()
                    && group->mChildren.back()->type()
                            == model::Object::Type::Transform) {
                group->mTransform =
                    static_cast<model::Transform *>(group->mChildren.back());
                group->mChildren.pop_back();
            }
        } else {
            Skip(key);
        }
    }
    bool staticFlag = true;
    for (const auto &child : group->mChildren) {
        staticFlag &= child->isStatic();
    }

    if (group->mTransform) {
        group->setStatic(staticFlag && group->mTransform->isStatic());
    }

    return group;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/rect.json
 */
model::Rect *LottieParserImpl::parseRectObject()
{
    auto obj = allocator().make<model::Rect>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "p")) {
            parseProperty(obj->mPos);
        } else if (0 == strcmp(key, "s")) {
            parseProperty(obj->mSize);
        } else if (0 == strcmp(key, "r")) {
            parseProperty(obj->mRound);
        } else if (0 == strcmp(key, "d")) {
            obj->mDirection = GetInt();
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
            Skip(key);
        }
    }
    obj->setStatic(obj->mPos.isStatic() && obj->mSize.isStatic() &&
                   obj->mRound.isStatic());
    return obj;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/rect.json
 */
model::RoundedCorner *LottieParserImpl::parseRoundedCorner()
{
    auto obj = allocator().make<model::RoundedCorner>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "r")) {
            parseProperty(obj->mRadius);
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
            Skip(key);
        }
    }
    obj->setStatic(obj->mRadius.isStatic());
    return obj;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/ellipse.json
 */
model::Ellipse *LottieParserImpl::parseEllipseObject()
{
    auto obj = allocator().make<model::Ellipse>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "p")) {
            parseProperty(obj->mPos);
        } else if (0 == strcmp(key, "s")) {
            parseProperty(obj->mSize);
        } else if (0 == strcmp(key, "d")) {
            obj->mDirection = GetInt();
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
            Skip(key);
        }
    }
    obj->setStatic(obj->mPos.isStatic() && obj->mSize.isStatic());
    return obj;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/shape.json
 */
model::Path *LottieParserImpl::parseShapeObject()
{
    auto obj = allocator().make<model::Path>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "ks")) {
            parseShapeProperty(obj->mShape);
        } else if (0 == strcmp(key, "d")) {
            obj->mDirection = GetInt();
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
#ifdef DEBUG_PARSER
            vDebug << "Shape property ignored :" << key;
#endif
            Skip(key);
        }
    }
    obj->setStatic(obj->mShape.isStatic());

    return obj;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/star.json
 */
model::Polystar *LottieParserImpl::parsePolystarObject()
{
    auto obj = allocator().make<model::Polystar>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "p")) {
            parseProperty(obj->mPos);
        } else if (0 == strcmp(key, "pt")) {
            parseProperty(obj->mPointCount);
        } else if (0 == strcmp(key, "ir")) {
            parseProperty(obj->mInnerRadius);
        } else if (0 == strcmp(key, "is")) {
            parseProperty(obj->mInnerRoundness);
        } else if (0 == strcmp(key, "or")) {
            parseProperty(obj->mOuterRadius);
        } else if (0 == strcmp(key, "os")) {
            parseProperty(obj->mOuterRoundness);
        } else if (0 == strcmp(key, "r")) {
            parseProperty(obj->mRotation);
        } else if (0 == strcmp(key, "sy")) {
            int starType = GetInt();
            if (starType == 1) obj->mPolyType = model::Polystar::PolyType::Star;
            if (starType == 2)
                obj->mPolyType = model::Polystar::PolyType::Polygon;
        } else if (0 == strcmp(key, "d")) {
            obj->mDirection = GetInt();
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
#ifdef DEBUG_PARSER
            vDebug << "Polystar property ignored :" << key;
#endif
            Skip(key);
        }
    }
    obj->setStatic(
        obj->mPos.isStatic() && obj->mPointCount.isStatic() &&
        obj->mInnerRadius.isStatic() && obj->mInnerRoundness.isStatic() &&
        obj->mOuterRadius.isStatic() && obj->mOuterRoundness.isStatic() &&
        obj->mRotation.isStatic());

    return obj;
}

model::Trim::TrimType LottieParserImpl::getTrimType()
{
    switch (GetInt()) {
    case 1:
        return model::Trim::TrimType::Simultaneously;
        break;
    case 2:
        return model::Trim::TrimType::Individually;
        break;
    default:
        Error();
        return model::Trim::TrimType::Simultaneously;
        break;
    }
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/trim.json
 */
model::Trim *LottieParserImpl::parseTrimObject()
{
    auto obj = allocator().make<model::Trim>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "s")) {
            parseProperty(obj->mStart);
        } else if (0 == strcmp(key, "e")) {
            parseProperty(obj->mEnd);
        } else if (0 == strcmp(key, "o")) {
            parseProperty(obj->mOffset);
        } else if (0 == strcmp(key, "m")) {
            obj->mTrimType = getTrimType();
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
#ifdef DEBUG_PARSER
            vDebug << "Trim property ignored :" << key;
#endif
            Skip(key);
        }
    }
    obj->setStatic(obj->mStart.isStatic() && obj->mEnd.isStatic() &&
                   obj->mOffset.isStatic());
    return obj;
}

void LottieParserImpl::getValue(model::Repeater::Transform &obj)
{
    EnterObject();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "a")) {
            parseProperty(obj.mAnchor);
        } else if (0 == strcmp(key, "p")) {
            parseProperty(obj.mPosition);
        } else if (0 == strcmp(key, "r")) {
            parseProperty(obj.mRotation);
        } else if (0 == strcmp(key, "s")) {
            parseProperty(obj.mScale);
        } else if (0 == strcmp(key, "so")) {
            parseProperty(obj.mStartOpacity);
        } else if (0 == strcmp(key, "eo")) {
            parseProperty(obj.mEndOpacity);
        } else {
            Skip(key);
        }
    }
}

model::Repeater *LottieParserImpl::parseReapeaterObject()
{
    auto obj = allocator().make<model::Repeater>();

    obj->setContent(allocator().make<model::Group>());

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "c")) {
            parseProperty(obj->mCopies);
            float maxCopy = 0.0;
            if (!obj->mCopies.isStatic()) {
                for (auto &keyFrame : obj->mCopies.animation().frames_) {
                    if (maxCopy < keyFrame.value_.start_)
                        maxCopy = keyFrame.value_.start_;
                    if (maxCopy < keyFrame.value_.end_)
                        maxCopy = keyFrame.value_.end_;
                }
            } else {
                maxCopy = obj->mCopies.value();
            }
            obj->mMaxCopies = maxCopy;
        } else if (0 == strcmp(key, "o")) {
            parseProperty(obj->mOffset);
        } else if (0 == strcmp(key, "tr")) {
            getValue(obj->mTransform);
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
#ifdef DEBUG_PARSER
            vDebug << "Repeater property ignored :" << key;
#endif
            Skip(key);
        }
    }
    obj->setStatic(obj->mCopies.isStatic() && obj->mOffset.isStatic() &&
                   obj->mTransform.isStatic());

    return obj;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/transform.json
 */
model::Transform *LottieParserImpl::parseTransformObject(bool ddd)
{
    auto objT = allocator().make<model::Transform>();

    auto obj = allocator().make<model::Transform::Data>();
    if (ddd) {
        obj->createExtraData();
        obj->mExtra->m3DData = true;
    }

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            objT->setName(GetString());
        } else if (0 == strcmp(key, "a")) {
            parseProperty(obj->mAnchor);
        } else if (0 == strcmp(key, "p")) {
            EnterObject();
            bool separate = false;
            while (const char *key = NextObjectKey()) {
                if (0 == strcmp(key, "k")) {
                    parsePropertyHelper(obj->mPosition);
                } else if (0 == strcmp(key, "s")) {
                    obj->createExtraData();
                    obj->mExtra->mSeparate = GetBool();
                    separate = true;
                } else if (separate && (0 == strcmp(key, "x"))) {
                    parseProperty(obj->mExtra->mSeparateX);
                } else if (separate && (0 == strcmp(key, "y"))) {
                    parseProperty(obj->mExtra->mSeparateY);
                } else {
                    Skip(key);
                }
            }
        } else if (0 == strcmp(key, "r")) {
            parseProperty(obj->mRotation);
        } else if (0 == strcmp(key, "s")) {
            parseProperty(obj->mScale);
        } else if (0 == strcmp(key, "o")) {
            parseProperty(obj->mOpacity);
        } else if (0 == strcmp(key, "hd")) {
            objT->setHidden(GetBool());
        } else if (0 == strcmp(key, "rx")) {
            if (!obj->mExtra) return nullptr;
            parseProperty(obj->mExtra->m3DRx);
        } else if (0 == strcmp(key, "ry")) {
            if (!obj->mExtra) return nullptr;
            parseProperty(obj->mExtra->m3DRy);
        } else if (0 == strcmp(key, "rz")) {
            if (!obj->mExtra) return nullptr;
            parseProperty(obj->mExtra->m3DRz);
        } else {
            Skip(key);
        }
    }
    bool isStatic = obj->mAnchor.isStatic() && obj->mPosition.isStatic() &&
                    obj->mRotation.isStatic() && obj->mScale.isStatic() &&
                    obj->mOpacity.isStatic();
    if (obj->mExtra) {
        isStatic = isStatic && obj->mExtra->m3DRx.isStatic() &&
                   obj->mExtra->m3DRy.isStatic() &&
                   obj->mExtra->m3DRz.isStatic() &&
                   obj->mExtra->mSeparateX.isStatic() &&
                   obj->mExtra->mSeparateY.isStatic();
    }

    objT->set(obj, isStatic);

    return objT;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/fill.json
 */
model::Fill *LottieParserImpl::parseFillObject()
{
    auto obj = allocator().make<model::Fill>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "c")) {
            parseProperty(obj->mColor);
        } else if (0 == strcmp(key, "o")) {
            parseProperty(obj->mOpacity);
        } else if (0 == strcmp(key, "fillEnabled")) {
            obj->mEnabled = GetBool();
        } else if (0 == strcmp(key, "r")) {
            obj->mFillRule = getFillRule();
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
#ifdef DEBUG_PARSER
            vWarning << "Fill property skipped = " << key;
#endif
            Skip(key);
        }
    }
    obj->setStatic(obj->mColor.isStatic() && obj->mOpacity.isStatic());

    return obj;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/helpers/lineCap.json
 */
CapStyle LottieParserImpl::getLineCap()
{
    switch (GetInt()) {
    case 1:
        return CapStyle::Flat;
        break;
    case 2:
        return CapStyle::Round;
        break;
    default:
        return CapStyle::Square;
        break;
    }
}

FillRule LottieParserImpl::getFillRule()
{
    switch (GetInt()) {
    case 1:
        return FillRule::Winding;
        break;
    case 2:
        return FillRule::EvenOdd;
        break;
    default:
        return FillRule::Winding;
        break;
    }
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/helpers/lineJoin.json
 */
JoinStyle LottieParserImpl::getLineJoin()
{
    switch (GetInt()) {
    case 1:
        return JoinStyle::Miter;
        break;
    case 2:
        return JoinStyle::Round;
        break;
    default:
        return JoinStyle::Bevel;
        break;
    }
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/stroke.json
 */
model::Stroke *LottieParserImpl::parseStrokeObject()
{
    auto obj = allocator().make<model::Stroke>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "c")) {
            parseProperty(obj->mColor);
        } else if (0 == strcmp(key, "o")) {
            parseProperty(obj->mOpacity);
        } else if (0 == strcmp(key, "w")) {
            parseProperty(obj->mWidth);
        } else if (0 == strcmp(key, "fillEnabled")) {
            obj->mEnabled = GetBool();
        } else if (0 == strcmp(key, "lc")) {
            obj->mCapStyle = getLineCap();
        } else if (0 == strcmp(key, "lj")) {
            obj->mJoinStyle = getLineJoin();
        } else if (0 == strcmp(key, "ml")) {
            obj->mMiterLimit = GetDouble();
        } else if (0 == strcmp(key, "d")) {
            parseDashProperty(obj->mDash);
        } else if (0 == strcmp(key, "hd")) {
            obj->setHidden(GetBool());
        } else {
#ifdef DEBUG_PARSER
            vWarning << "Stroke property skipped = " << key;
#endif
            Skip(key);
        }
    }
    obj->setStatic(obj->mColor.isStatic() && obj->mOpacity.isStatic() &&
                   obj->mWidth.isStatic() && obj->mDash.isStatic());
    return obj;
}

void LottieParserImpl::parseGradientProperty(model::Gradient *obj,
                                             const char *     key)
{
    if (0 == strcmp(key, "t")) {
        obj->mGradientType = GetInt();
    } else if (0 == strcmp(key, "o")) {
        parseProperty(obj->mOpacity);
    } else if (0 == strcmp(key, "s")) {
        parseProperty(obj->mStartPoint);
    } else if (0 == strcmp(key, "e")) {
        parseProperty(obj->mEndPoint);
    } else if (0 == strcmp(key, "h")) {
        parseProperty(obj->mHighlightLength);
    } else if (0 == strcmp(key, "a")) {
        parseProperty(obj->mHighlightAngle);
    } else if (0 == strcmp(key, "g")) {
        EnterObject();
        while (const char *key = NextObjectKey()) {
            if (0 == strcmp(key, "k")) {
                parseProperty(obj->mGradient);
            } else if (0 == strcmp(key, "p")) {
                obj->mColorPoints = GetInt();
            } else {
                Skip(nullptr);
            }
        }
    } else if (0 == strcmp(key, "hd")) {
        obj->setHidden(GetBool());
    } else {
#ifdef DEBUG_PARSER
        vWarning << "Gradient property skipped = " << key;
#endif
        Skip(key);
    }
    obj->setStatic(
        obj->mOpacity.isStatic() && obj->mStartPoint.isStatic() &&
        obj->mEndPoint.isStatic() && obj->mHighlightAngle.isStatic() &&
        obj->mHighlightLength.isStatic() && obj->mGradient.isStatic());
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/gfill.json
 */
model::GradientFill *LottieParserImpl::parseGFillObject()
{
    auto obj = allocator().make<model::GradientFill>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "r")) {
            obj->mFillRule = getFillRule();
        } else {
            parseGradientProperty(obj, key);
        }
    }
    return obj;
}

void LottieParserImpl::parseDashProperty(model::Dash &dash)
{
    EnterArray();
    while (NextArrayValue()) {
        EnterObject();
        while (const char *key = NextObjectKey()) {
            if (0 == strcmp(key, "v")) {
                dash.mData.emplace_back();
                parseProperty(dash.mData.back());
            } else {
                Skip(key);
            }
        }
    }
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/gstroke.json
 */
model::GradientStroke *LottieParserImpl::parseGStrokeObject()
{
    auto obj = allocator().make<model::GradientStroke>();

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "nm")) {
            obj->setName(GetString());
        } else if (0 == strcmp(key, "w")) {
            parseProperty(obj->mWidth);
        } else if (0 == strcmp(key, "lc")) {
            obj->mCapStyle = getLineCap();
        } else if (0 == strcmp(key, "lj")) {
            obj->mJoinStyle = getLineJoin();
        } else if (0 == strcmp(key, "ml")) {
            obj->mMiterLimit = GetDouble();
        } else if (0 == strcmp(key, "d")) {
            parseDashProperty(obj->mDash);
        } else {
            parseGradientProperty(obj, key);
        }
    }

    obj->setStatic(obj->isStatic() && obj->mWidth.isStatic() &&
                   obj->mDash.isStatic());
    return obj;
}

void LottieParserImpl::getValue(std::vector<VPointF> &v)
{
    EnterArray();
    while (NextArrayValue()) {
        EnterArray();
        VPointF pt;
        getValue(pt);
        v.push_back(pt);
    }
}

void LottieParserImpl::getValue(VPointF &pt)
{
    float val[4] = {0.f};
    int   i = 0;

    if (PeekType() == kArrayType) EnterArray();

    while (NextArrayValue()) {
        const auto value = GetDouble();
        if (i < 4) {
            val[i++] = value;
        }
    }
    pt.setX(val[0]);
    pt.setY(val[1]);
}

void LottieParserImpl::getValue(float &val)
{
    if (PeekType() == kArrayType) {
        EnterArray();
        if (NextArrayValue()) val = GetDouble();
        // discard rest
        while (NextArrayValue()) {
            GetDouble();
        }
    } else if (PeekType() == kNumberType) {
        val = GetDouble();
    } else {
        Error();
    }
}

void LottieParserImpl::getValue(model::Color &color)
{
    float val[4] = {0.f};
    int   i = 0;
    if (PeekType() == kArrayType) EnterArray();

    while (NextArrayValue()) {
        const auto value = GetDouble();
        if (i < 4) {
            val[i++] = value;
        }
    }

    if (mColorFilter) mColorFilter(val[0], val[1], val[2]);

    color.r = val[0];
    color.g = val[1];
    color.b = val[2];
}

void LottieParserImpl::getValue(model::Gradient::Data &grad)
{
    if (PeekType() == kArrayType) EnterArray();

    while (NextArrayValue()) {
        grad.mGradient.push_back(GetDouble());
    }
}

void LottieParserImpl::getValue(int &val)
{
    if (PeekType() == kArrayType) {
        EnterArray();
        while (NextArrayValue()) {
            val = GetInt();
        }
    } else if (PeekType() == kNumberType) {
        val = GetInt();
    } else {
        Error();
    }
}

void LottieParserImpl::parsePathInfo()
{
    mPathInfo.reset();

    /*
     * The shape object could be wrapped by a array
     * if its part of the keyframe object
     */
    bool arrayWrapper = (PeekType() == kArrayType);
    if (arrayWrapper) EnterArray();

    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "i")) {
            getValue(mPathInfo.mInPoint);
        } else if (0 == strcmp(key, "o")) {
            getValue(mPathInfo.mOutPoint);
        } else if (0 == strcmp(key, "v")) {
            getValue(mPathInfo.mVertices);
        } else if (0 == strcmp(key, "c")) {
            mPathInfo.mClosed = GetBool();
        } else {
            Error();
            Skip(nullptr);
        }
    }
    // exit properly from the array
    if (arrayWrapper) NextArrayValue();

    mPathInfo.convert();
}

void LottieParserImpl::getValue(model::PathData &obj)
{
    parsePathInfo();
    obj.mPoints = mPathInfo.mResult;
    obj.mClosed = mPathInfo.mClosed;
}

VPointF LottieParserImpl::parseInperpolatorPoint()
{
    VPointF cp;
    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "x")) {
            getValue(cp.rx());
        }
        if (0 == strcmp(key, "y")) {
            getValue(cp.ry());
        }
    }
    return cp;
}

template <typename T>
bool LottieParserImpl::parseKeyFrameValue(
    const char *key, model::Value<T, model::Position> &value)
{
    if (0 == strcmp(key, "ti")) {
        value.hasTangent_ = true;
        getValue(value.inTangent_);
    } else if (0 == strcmp(key, "to")) {
        value.hasTangent_ = true;
        getValue(value.outTangent_);
    } else {
        return false;
    }
    return true;
}

VInterpolator *LottieParserImpl::interpolator(VPointF     inTangent,
                                              VPointF     outTangent,
                                              std::string key)
{
    if (key.empty()) {
        std::array<char, 20> temp;
        snprintf(temp.data(), temp.size(), "%.2f_%.2f_%.2f_%.2f", inTangent.x(),
                 inTangent.y(), outTangent.x(), outTangent.y());
        key = temp.data();
    }

    auto search = mInterpolatorCache.find(key);

    if (search != mInterpolatorCache.end()) {
        return search->second;
    }

    auto obj = allocator().make<VInterpolator>(outTangent, inTangent);
    mInterpolatorCache[std::move(key)] = obj;
    return obj;
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/properties/multiDimensionalKeyframed.json
 */
template <typename T, typename Tag>
void LottieParserImpl::parseKeyFrame(model::KeyFrames<T, Tag> &obj)
{
    struct ParsedField {
        std::string interpolatorKey;
        bool        interpolator{false};
        bool        value{false};
        bool        hold{false};
        bool        noEndValue{true};
    };

    EnterObject();
    ParsedField                              parsed;
    typename model::KeyFrames<T, Tag>::Frame keyframe;
    VPointF                                  inTangent;
    VPointF                                  outTangent;

    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "i")) {
            parsed.interpolator = true;
            inTangent = parseInperpolatorPoint();
        } else if (0 == strcmp(key, "o")) {
            outTangent = parseInperpolatorPoint();
        } else if (0 == strcmp(key, "t")) {
            keyframe.start_ = GetDouble();
        } else if (0 == strcmp(key, "s")) {
            parsed.value = true;
            getValue(keyframe.value_.start_);
            continue;
        } else if (0 == strcmp(key, "e")) {
            parsed.noEndValue = false;
            getValue(keyframe.value_.end_);
            continue;
        } else if (0 == strcmp(key, "n")) {
            if (PeekType() == kStringType) {
                parsed.interpolatorKey = GetStringObject();
            } else {
                EnterArray();
                while (NextArrayValue()) {
                    if (parsed.interpolatorKey.empty()) {
                        parsed.interpolatorKey = GetStringObject();
                    } else {
                        // skip rest of the string
                        Skip(nullptr);
                    }
                }
            }
            continue;
        } else if (parseKeyFrameValue(key, keyframe.value_)) {
            continue;
        } else if (0 == strcmp(key, "h")) {
            parsed.hold = GetInt();
            continue;
        } else {
#ifdef DEBUG_PARSER
            vDebug << "key frame property skipped = " << key;
#endif
            Skip(key);
        }
    }

    auto &list = obj.frames_;
    if (!list.empty()) {
        // update the endFrame value of current keyframe
        list.back().end_ = keyframe.start_;
        // if no end value provided, copy start value to previous frame
        if (parsed.value && parsed.noEndValue) {
            list.back().value_.end_ = keyframe.value_.start_;
        }
    }

    if (parsed.hold) {
        keyframe.value_.end_ = keyframe.value_.start_;
        keyframe.end_ = keyframe.start_;
        list.push_back(std::move(keyframe));
    } else if (parsed.interpolator) {
        keyframe.interpolator_ = interpolator(
            inTangent, outTangent, std::move(parsed.interpolatorKey));
        list.push_back(std::move(keyframe));
    } else {
        // its the last frame discard.
    }
}

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/properties/shapeKeyframed.json
 */

/*
 * https://github.com/airbnb/lottie-web/blob/master/docs/json/properties/shape.json
 */
void LottieParserImpl::parseShapeProperty(model::Property<model::PathData> &obj)
{
    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "k")) {
            if (PeekType() == kArrayType) {
                EnterArray();
                while (NextArrayValue()) {
                    parseKeyFrame(obj.animation());
                }
            } else {
                if (!obj.isStatic()) {
                    st_ = kError;
                    return;
                }
                getValue(obj.value());
            }
        } else {
#ifdef DEBUG_PARSER
            vDebug << "shape property ignored = " << key;
#endif
            Skip(nullptr);
        }
    }
    obj.cache();
}

template <typename T, typename Tag>
void LottieParserImpl::parsePropertyHelper(model::Property<T, Tag> &obj)
{
    if (PeekType() == kNumberType) {
        if (!obj.isStatic()) {
            st_ = kError;
            return;
        }
        /*single value property with no animation*/
        getValue(obj.value());
    } else {
        EnterArray();
        while (NextArrayValue()) {
            /* property with keyframe info*/
            if (PeekType() == kObjectType) {
                parseKeyFrame(obj.animation());
            } else {
                /* Read before modifying.
                 * as there is no way of knowing if the
                 * value of the array is either array of numbers
                 * or array of object without entering the array
                 * thats why this hack is there
                 */
                if (!obj.isStatic()) {
                    st_ = kError;
                    return;
                }
                /*multi value property with no animation*/
                getValue(obj.value());
                /*break here as we already reached end of array*/
                break;
            }
        }
        obj.cache();
    }
}

/*
 * https://github.com/airbnb/lottie-web/tree/master/docs/json/properties
 */
template <typename T>
void LottieParserImpl::parseProperty(model::Property<T> &obj)
{
    EnterObject();
    while (const char *key = NextObjectKey()) {
        if (0 == strcmp(key, "k")) {
            parsePropertyHelper(obj);
        } else {
            Skip(key);
        }
    }
}

#ifdef LOTTIE_DUMP_TREE_SUPPORT

class ObjectInspector {
public:
    void visit(model::Composition *obj, std::string level)
    {
        vDebug << " { " << level << "Composition:: a: " << !obj->isStatic()
               << ", v: " << obj->mVersion << ", stFm: " << obj->startFrame()
               << ", endFm: " << obj->endFrame()
               << ", W: " << obj->size().width()
               << ", H: " << obj->size().height() << "\n";
        level.append("\t");
        visit(obj->mRootLayer, level);
        level.erase(level.end() - 1, level.end());
        vDebug << " } " << level << "Composition End\n";
    }
    void visit(model::Layer *obj, std::string level)
    {
        vDebug << level << "{ " << layerType(obj->mLayerType)
               << ", name: " << obj->name() << ", id:" << obj->mId
               << " Pid:" << obj->mParentId << ", a:" << !obj->isStatic()
               << ", " << matteType(obj->mMatteType)
               << ", mask:" << obj->hasMask() << ", inFm:" << obj->mInFrame
               << ", outFm:" << obj->mOutFrame << ", stFm:" << obj->mStartFrame
               << ", ts:" << obj->mTimeStreatch << ", ao:" << obj->autoOrient()
               << ", W:" << obj->layerSize().width()
               << ", H:" << obj->layerSize().height();

        if (obj->mLayerType == model::Layer::Type::Image)
            vDebug << level << "\t{ "
                   << "ImageInfo:"
                   << " W :" << obj->extra()->mAsset->mWidth
                   << ", H :" << obj->extra()->mAsset->mHeight << " }"
                   << "\n";
        else {
            vDebug << level;
        }
        visitChildren(static_cast<model::Group *>(obj), level);
        vDebug << level << "} " << layerType(obj->mLayerType).c_str()
               << ", id: " << obj->mId << "\n";
    }
    void visitChildren(model::Group *obj, std::string level)
    {
        level.append("\t");
        for (const auto &child : obj->mChildren) visit(child, level);
        if (obj->mTransform) visit(obj->mTransform, level);
    }

    void visit(model::Object *obj, std::string level)
    {
        switch (obj->type()) {
        case model::Object::Type::Repeater: {
            auto r = static_cast<model::Repeater *>(obj);
            vDebug << level << "{ Repeater: name: " << obj->name()
                   << " , a:" << !obj->isStatic()
                   << ", copies:" << r->maxCopies()
                   << ", offset:" << r->offset(0);
            visitChildren(r->mContent, level);
            vDebug << level << "} Repeater";
            break;
        }
        case model::Object::Type::Group: {
            vDebug << level << "{ Group: name: " << obj->name()
                   << " , a:" << !obj->isStatic();
            visitChildren(static_cast<model::Group *>(obj), level);
            vDebug << level << "} Group";
            break;
        }
        case model::Object::Type::Layer: {
            visit(static_cast<model::Layer *>(obj), level);
            break;
        }
        case model::Object::Type::Trim: {
            vDebug << level << "{ Trim: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::Rect: {
            vDebug << level << "{ Rect: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::RoundedCorner: {
            vDebug << level << "{ RoundedCorner: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::Ellipse: {
            vDebug << level << "{ Ellipse: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::Path: {
            vDebug << level << "{ Shape: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::Polystar: {
            vDebug << level << "{ Polystar: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::Transform: {
            vDebug << level << "{ Transform: name: " << obj->name()
                   << " , a: " << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::Stroke: {
            vDebug << level << "{ Stroke: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::GStroke: {
            vDebug << level << "{ GStroke: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::Fill: {
            vDebug << level << "{ Fill: name: " << obj->name()
                   << " , a:" << !obj->isStatic() << " }";
            break;
        }
        case model::Object::Type::GFill: {
            auto f = static_cast<model::GradientFill *>(obj);
            vDebug << level << "{ GFill: name: " << obj->name()
                   << " , a:" << !f->isStatic() << ", ty:" << f->mGradientType
                   << ", s:" << f->mStartPoint.value(0)
                   << ", e:" << f->mEndPoint.value(0) << " }";
            break;
        }
        default:
            break;
        }
    }

    std::string matteType(model::MatteType type)
    {
        switch (type) {
        case model::MatteType::None:
            return "Matte::None";
            break;
        case model::MatteType::Alpha:
            return "Matte::Alpha";
            break;
        case model::MatteType::AlphaInv:
            return "Matte::AlphaInv";
            break;
        case model::MatteType::Luma:
            return "Matte::Luma";
            break;
        case model::MatteType::LumaInv:
            return "Matte::LumaInv";
            break;
        default:
            return "Matte::Unknown";
            break;
        }
    }
    std::string layerType(model::Layer::Type type)
    {
        switch (type) {
        case model::Layer::Type::Precomp:
            return "Layer::Precomp";
            break;
        case model::Layer::Type::Null:
            return "Layer::Null";
            break;
        case model::Layer::Type::Shape:
            return "Layer::Shape";
            break;
        case model::Layer::Type::Solid:
            return "Layer::Solid";
            break;
        case model::Layer::Type::Image:
            return "Layer::Image";
            break;
        case model::Layer::Type::Text:
            return "Layer::Text";
            break;
        default:
            return "Layer::Unknown";
            break;
        }
    }
};

#endif

std::shared_ptr<model::Composition> model::parse(char *             str,
                                                 std::string        dir_path,
                                                 model::ColorFilter filter)
{
    LottieParserImpl obj(str, std::move(dir_path), std::move(filter));

    if (obj.VerifyType()) {
        obj.parseComposition();
        auto composition = obj.composition();
        if (composition) {
            composition->processRepeaterObjects();
            composition->updateStats();

#ifdef LOTTIE_DUMP_TREE_SUPPORT
            ObjectInspector inspector;
            inspector.visit(composition.get(), "");
#endif

            return composition;
        }
    }

    vWarning << "Input data is not Lottie format!";
    return {};
}

RAPIDJSON_DIAG_POP
