
#pragma once

#include "ClipProp.h"
#include "DrawingContext.h"
#include "JsiDomDeclarationNode.h"
#include "JsiDomNode.h"
#include "LayerProp.h"
#include "MatrixProp.h"
#include "PaintProps.h"
#include "PointProp.h"
#include "RRectProp.h"
#include "RectProp.h"
#include "TransformProp.h"

#include <memory>
#include <string>
#include <vector>

namespace RNSkia {

class JsiDomRenderNode : public JsiDomNode {
public:
  JsiDomRenderNode(std::shared_ptr<RNSkPlatformContext> context,
                   const char *type)
      : JsiDomNode(context, type, NodeClass::RenderNode) {}

  void render(DrawingContext *context) {
#if SKIA_DOM_DEBUG
    printDebugInfo("Begin Render");
#endif

    auto parentPaint = context->getPaint();
    auto cache =
        _paintCache.parent == parentPaint ? _paintCache.child : nullptr;

    auto shouldRestore =
        context->saveAndConcat(_paintProps, getChildren(), cache);

    auto shouldTransform = _matrixProp->isSet() || _transformProp->isSet();
    auto shouldSave =
        shouldTransform || _clipProp->isSet() || _layerProp->isSet();

    // Handle matrix/transforms
    if (shouldSave) {
      // Save canvas state
      if (_layerProp->isSet()) {
        if (_layerProp->isBool()) {
#if SKIA_DOM_DEBUG_VERBOSE
          printDebugInfo("canvas->saveLayer()");
#endif
          context->getCanvas()->saveLayer(
              SkCanvas::SaveLayerRec(nullptr, nullptr, nullptr, 0));
        } else {
#if SKIA_DOM_DEBUG_VERBOSE
          printDebugInfo("canvas->saveLayer(paint)");
#endif
          context->getCanvas()->saveLayer(SkCanvas::SaveLayerRec(
              nullptr, _layerProp->getDerivedValue().get(), nullptr, 0));
        }
      } else {
#if SKIA_DOM_DEBUG_VERBOSE
        printDebugInfo("canvas->save()");
#endif
        context->getCanvas()->save();
      }

      if (_originProp->isSet()) {
#if SKIA_DOM_DEBUG_VERBOSE
        printDebugInfo("canvas->translate(origin)");
#endif
        // Handle origin
        context->getCanvas()->translate(_originProp->getDerivedValue()->x(),
                                        _originProp->getDerivedValue()->y());
      }

      if (shouldTransform) {
#if SKIA_DOM_DEBUG_VERBOSE
        printDebugInfo(
            "canvas->concat(" +
            std::string(_matrixProp->isSet() ? "matrix" : "transform") +
            std::string(")"));
#endif
        auto matrix = _matrixProp->isSet() ? _matrixProp->getDerivedValue()
                                           : _transformProp->getDerivedValue();

        // Concat canvas' matrix with our matrix
        context->getCanvas()->concat(*matrix);
      }

      // Clipping
      if (_clipProp->isSet()) {
        auto invert = _invertClip->isSet() && _invertClip->value().getAsBool();
        clip(context, context->getCanvas(), invert);
      }

      if (_originProp->isSet()) {
#if SKIA_DOM_DEBUG_VERBOSE
        printDebugInfo("canvas->translate(-origin)");
#endif
        // Handle origin
        context->getCanvas()->translate(-_originProp->getDerivedValue()->x(),
                                        -_originProp->getDerivedValue()->y());
      }
    }

    // Render the node
    renderNode(context);

    // Restore if needed
    if (shouldSave) {
#if SKIA_DOM_DEBUG_VERBOSE
      printDebugInfo("canvas->restore()");
#endif
      context->getCanvas()->restore();
    }

    if (shouldRestore) {
      _paintCache.parent = parentPaint;
      _paintCache.child = context->getPaint();
      context->restore();
    }

#if SKIA_DOM_DEBUG
    printDebugInfo("End Render");
#endif
  }

  /**
   Override reset (last thing that happens in the render cycle) to also reset
   the changed flag on the local drawing context if necessary.
   */
  void resetPendingChanges() override { JsiDomNode::resetPendingChanges(); }

  /**
   Overridden dispose to release resources
   */
  void dispose(bool immediate) override {
    JsiDomNode::dispose(immediate);
    _paintCache.clear();
  }

protected:
  /**
   Invalidates and marks then context as changed.
   */
  void invalidateContext() override {
    enqueAsynOperation([weakSelf = weak_from_this()]() {
      auto self = weakSelf.lock();
      if (self) {
        std::static_pointer_cast<JsiDomRenderNode>(self)->_paintCache.parent =
            nullptr;
        std::static_pointer_cast<JsiDomRenderNode>(self)->_paintCache.child =
            nullptr;
      }
    });
  }

  /**
   Override to implement rendering where the current state of the drawing
   context is correctly set.
   */
  virtual void renderNode(DrawingContext *context) = 0;

  /**
   Define common properties for all render nodes
   */
  void defineProperties(NodePropsContainer *container) override {
    JsiDomNode::defineProperties(container);

    _paintProps = container->defineProperty<PaintProps>();

    _matrixProp = container->defineProperty<MatrixProp>("matrix");
    _transformProp = container->defineProperty<TransformProp>("transform");
    _originProp = container->defineProperty<PointProp>("origin");
    _clipProp = container->defineProperty<ClipProp>("clip");
    _invertClip = container->defineProperty<NodeProp>("invertClip");
    _layerProp = container->defineProperty<LayerProp>("layer");
  }

  /**
   Validates that only declaration nodes can be children
   */
  void addChild(std::shared_ptr<JsiDomNode> child) override {
    JsiDomNode::addChild(child);
    _paintCache.parent = nullptr;
    _paintCache.child = nullptr;
  }

  /**
   Validates that only declaration nodes can be children
   */
  void insertChildBefore(std::shared_ptr<JsiDomNode> child,
                         std::shared_ptr<JsiDomNode> before) override {
    JsiDomNode::insertChildBefore(child, before);
    _paintCache.parent = nullptr;
    _paintCache.child = nullptr;
  }

  /**
   A property changed
   */
  void onPropertyChanged(BaseNodeProp *prop) override {
    static std::vector<const char *> paintProps = {
        JsiPropId::get("color"),      JsiPropId::get("strokeWidth"),
        JsiPropId::get("blendMode"),  JsiPropId::get("strokeCap"),
        JsiPropId::get("strokeJoin"), JsiPropId::get("strokeMiter"),
        JsiPropId::get("style"),      JsiPropId::get("antiAlias"),
        JsiPropId::get("opacity"),    JsiPropId::get("dither")};

    // We'll invalidate paint if a prop change happened in a paint property
    if (std::find(paintProps.begin(), paintProps.end(), prop->getName()) !=
        paintProps.end()) {
      invalidateContext();
    }
  }

private:
  /**
   Clips the canvas depending on the clip property
   */
  void clip(DrawingContext *context, SkCanvas *canvas, bool invert) {
    auto op = invert ? SkClipOp::kDifference : SkClipOp::kIntersect;
    if (_clipProp->getRect() != nullptr) {
#if SKIA_DOM_DEBUG
      printDebugInfo("canvas->clipRect()");
#endif
      canvas->clipRect(*_clipProp->getRect(), op, true);
    } else if (_clipProp->getRRect() != nullptr) {
#if SKIA_DOM_DEBUG
      printDebugInfo("canvas->clipRRect()");
#endif
      canvas->clipRRect(*_clipProp->getRRect(), op, true);
    } else if (_clipProp->getPath() != nullptr) {
#if SKIA_DOM_DEBUG
      printDebugInfo("canvas->clipPath()");
#endif
      canvas->clipPath(*_clipProp->getPath(), op, true);
    }
  }

  struct PaintCache {
    void clear() {
      parent = nullptr;
      child = nullptr;
    }
    std::shared_ptr<SkPaint> parent;
    std::shared_ptr<SkPaint> child;
  };

  PaintCache _paintCache;

  PointProp *_originProp;
  MatrixProp *_matrixProp;
  TransformProp *_transformProp;
  NodeProp *_invertClip;
  ClipProp *_clipProp;
  LayerProp *_layerProp;
  PaintProps *_paintProps;
};

} // namespace RNSkia
