/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#pragma once

#include <functional>
#include <memory>

#include <react/renderer/core/ShadowNode.h>

#include <react/renderer/element/ElementFragment.h>

namespace facebook::react {

/*
 * `Element<>` is an abstraction layer that allows describing component
 * hierarchy in a declarative way. Creating `Element`s themself does not create
 * a component tree (aka `ShadowNode` tree) but describes some hierarchical
 * structure that might be used to build an actual component tree (similar to
 * XML Elements).
 * `Element` provides some basic type-safety guarantees: all modifications
 * of element objects require using objects (such as Props or State) of
 * compatible type.
 */
template <typename ShadowNodeT>
class Element final {
 public:
  using ConcreteProps = typename ShadowNodeT::ConcreteProps;
  using SharedConcreteProps = std::shared_ptr<const ConcreteProps>;
  using ConcreteState = typename ShadowNodeT::ConcreteState;
  using ConcreteStateData = typename ShadowNodeT::ConcreteStateData;
  using SharedConcreteState = std::shared_ptr<const ConcreteState>;
  using ConcreteShadowNode = ShadowNodeT;
  using ConcreteUnsharedShadowNode = std::shared_ptr<ConcreteShadowNode>;

  using ConcreteReferenceCallback =
      std::function<void(const std::shared_ptr<const ShadowNodeT>& shadowNode)>;

  /*
   * Constructs an `Element`.
   */
  Element() {
    fragment_.componentHandle = ShadowNodeT::Handle();
    fragment_.componentName = ShadowNodeT::Name();
    fragment_.props = ShadowNodeT::defaultSharedProps();
  }

  /*
   * Converts to `ElementFragment` object.
   */
  operator ElementFragment() {
    return fragment_;
  }

  /*
   * Sets `tag`.
   */
  Element& tag(Tag tag) {
    fragment_.tag = tag;
    return *this;
  }

  /*
   * Sets `surfaceId`.
   */
  Element& surfaceId(SurfaceId surfaceId) {
    fragment_.surfaceId = surfaceId;
    return *this;
  }

  /*
   * Sets `props`.
   */
  Element& props(SharedConcreteProps props) {
    fragment_.props = props;
    return *this;
  }

  /*
   * Sets `props` using callback.
   */
  Element& props(std::function<SharedConcreteProps()> callback) {
    fragment_.props = callback();
    return *this;
  }

  /*
   * Sets `state` using callback.
   */
  Element& stateData(std::function<void(ConcreteStateData&)> callback) {
    fragment_.stateCallback =
        [callback = std::move(callback)](
            const State::Shared& state) -> StateData::Shared {
      auto stateData =
          static_cast<const ConcreteState*>(state.get())->getData();
      callback(stateData);
      return std::make_shared<ConcreteStateData>(stateData);
    };
    return *this;
  }

  /*
   * Sets children.
   */
  Element& children(std::vector<ElementFragment> children) {
    auto fragments = ElementFragment::List{};
    fragments.reserve(children.size());
    for (const auto& child : children) {
      fragments.push_back(child);
    }
    fragment_.children = fragments;
    return *this;
  }

  /*
   * Calls the callback during component construction with a pointer to the
   * component which is being constructed.
   */
  Element& reference(
      std::function<void(const ConcreteUnsharedShadowNode& shadowNode)>
          callback) {
    fragment_.referenceCallback =
        [callback = std::move(callback)](const ShadowNode::Shared& shadowNode) {
          callback(std::const_pointer_cast<ConcreteShadowNode>(
              std::static_pointer_cast<const ConcreteShadowNode>(shadowNode)));
        };
    return *this;
  }

  /*
   * During component construction, assigns a given pointer to a component
   * that is being constructed.
   */
  Element& reference(ConcreteUnsharedShadowNode& outShadowNode) {
    fragment_.referenceCallback = [&](const ShadowNode::Shared& shadowNode) {
      outShadowNode = std::const_pointer_cast<ConcreteShadowNode>(
          std::static_pointer_cast<const ConcreteShadowNode>(shadowNode));
    };
    return *this;
  }

  /*
   * Calls the callback with a reference to a just constructed component.
   */
  Element& finalize(
      std::function<void(ConcreteShadowNode& shadowNode)> finalizeCallback) {
    fragment_.finalizeCallback = [=](ShadowNode& shadowNode) {
      return finalizeCallback(static_cast<ConcreteShadowNode&>(shadowNode));
    };
    return *this;
  }

 private:
  friend class ComponentBuilder;
  ElementFragment fragment_;
};

} // namespace facebook::react
