AttributedString BaseTextShadowNode::getAttributedString(
    const TextAttributes &textAttributes,
    const SharedShadowNode &parentNode) const {
  auto attributedString = AttributedString{};

  for (const auto &childNode : parentNode->getChildren()) {
    // RawShadowNode
    auto rawTextShadowNode =
        std::dynamic_pointer_cast<const RawTextShadowNode>(childNode);
    if (rawTextShadowNode) {
      auto fragment = AttributedString::Fragment{};
      fragment.string = rawTextShadowNode->getProps()->text;
      fragment.textAttributes = textAttributes;
      fragment.parentShadowNode = parentNode;
      attributedString.appendFragment(fragment);
      continue;
    }

    // TextShadowNode
    auto textShadowNode =
        std::dynamic_pointer_cast<const TextShadowNode>(childNode);
    if (textShadowNode) {
      auto localTextAttributes = textAttributes;
      localTextAttributes.apply(textShadowNode->getProps()->textAttributes);
      attributedString.appendAttributedString(
          textShadowNode->getAttributedString(
              localTextAttributes, textShadowNode));
      continue;
    }

    // Any other kind of ShadowNode
    auto fragment = AttributedString::Fragment{};
    fragment.shadowNode = childNode;
    fragment.textAttributes = textAttributes;
    attributedString.appendFragment(fragment);
  }

  return attributedString;
}
void calculateMutationInstructions(
  TreeMutationInstructionList &instructions,
  SharedShadowNode parentNode,
  SharedShadowNodeSharedList oldChildNodes,
  SharedShadowNodeSharedList newChildNodes
) {
  // The current version of the algorithm is otimized for simplicity,
  // not for performance of optimal result.

  // TODO(shergin): Consider to use Minimal Edit Distance algorithm to produce
  // optimal set of instructions and improve mounting performance.
  // https://en.wikipedia.org/wiki/Edit_distance
  // https://www.geeksforgeeks.org/dynamic-programming-set-5-edit-distance/

  if (oldChildNodes == newChildNodes) {
    return;
  }

  if (oldChildNodes->size() == 0 && newChildNodes->size() == 0) {
    return;
  }

  std::unordered_set<Tag> insertedTags = {};
  int index = 0;

  TreeMutationInstructionList createInstructions = {};
  TreeMutationInstructionList deleteInstructions = {};
  TreeMutationInstructionList insertInstructions = {};
  TreeMutationInstructionList removeInstructions = {};
  TreeMutationInstructionList replaceInstructions = {};
  TreeMutationInstructionList downwardInstructions = {};

  // Stage 1: Collectings Updates
  for (index = 0; index < oldChildNodes->size() && index < newChildNodes->size(); index++) {
    SharedShadowNode oldChildNode = oldChildNodes->at(index);
    SharedShadowNode newChildNode = newChildNodes->at(index);

    if (oldChildNode->getTag() != newChildNode->getTag()) {
      // Totally different nodes, updating is impossible.
      break;
    }

    if (*oldChildNode != *newChildNode) {
      replaceInstructions.push_back(
        TreeMutationInstruction::Replace(
          parentNode,
          oldChildNode,
          newChildNode,
          index
        )
      );
    }

    calculateMutationInstructions(
      downwardInstructions,
      oldChildNode,
      oldChildNode->getChildren(),
      newChildNode->getChildren()
    );
  }

  int lastIndexAfterFirstStage = index;

  // Stage 2: Collectings Insertions
  for (; index < newChildNodes->size(); index++) {
    SharedShadowNode newChildNode = newChildNodes->at(index);

    insertInstructions.push_back(
      TreeMutationInstruction::Insert(
        parentNode,
        newChildNode,
        index
      )
    );

    insertedTags.insert(newChildNode->getTag());

    SharedShadowNode newChildSourceNode = newChildNode->getSourceNode();
    SharedShadowNodeSharedList newChildSourceChildNodes =
      newChildSourceNode ? newChildSourceNode->getChildren() : ShadowNode::emptySharedShadowNodeSharedList();

    calculateMutationInstructions(
      downwardInstructions,
      newChildNode,
      newChildSourceChildNodes,
      newChildNode->getChildren()
    );
  }

  // Stage 3: Collectings Deletions and Removals
  for (index = lastIndexAfterFirstStage; index < oldChildNodes->size(); index++) {
    SharedShadowNode oldChildNode = oldChildNodes->at(index);

    // Even if the old node was (re)inserted, we have to generate `remove`
    // instruction.
    removeInstructions.push_back(
      TreeMutationInstruction::Remove(
        parentNode,
        oldChildNode,
        index
      )
    );

    auto numberOfRemovedTags = insertedTags.erase(oldChildNode->getTag());
    assert(numberOfRemovedTags == 0 || numberOfRemovedTags == 1);

    if (numberOfRemovedTags == 0) {
      // The old node was *not* (re)inserted,
      // so we have to generate `delete` instruction and apply the algorithm
      // recursively.
      deleteInstructions.push_back(
        TreeMutationInstruction::Delete(
          oldChildNode
        )
      );

      calculateMutationInstructions(
        downwardInstructions,
        oldChildNode,
        oldChildNode->getChildren(),
        ShadowNode::emptySharedShadowNodeSharedList()
      );
    }
  }

  // Stage 4: Collectings Creations
  for (index = lastIndexAfterFirstStage; index < newChildNodes->size(); index++) {
    SharedShadowNode newChildNode = newChildNodes->at(index);
    if (insertedTags.find(newChildNode->getTag()) == insertedTags.end()) {
      // The new node was (re)inserted, so there is no need to create it.
      continue;
    }

    createInstructions.push_back(
      TreeMutationInstruction::Create(
        newChildNode
      )
    );
  }

  // All instructions in an optimal order:
  instructions.insert(instructions.end(), replaceInstructions.begin(), replaceInstructions.end());
  instructions.insert(instructions.end(), removeInstructions.begin(), removeInstructions.end());
  instructions.insert(instructions.end(), deleteInstructions.begin(), deleteInstructions.end());
  instructions.insert(instructions.end(), createInstructions.begin(), createInstructions.end());
  instructions.insert(instructions.end(), insertInstructions.begin(), insertInstructions.end());
  instructions.insert(instructions.end(), downwardInstructions.begin(), downwardInstructions.end());
}