/*
 *  Copyright (c) 2014-present, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

#import "CKTreeVerificationHelpers.h"

#import <queue>

#import <RenderCore/RCAssert.h>
#import <ComponentKit/CKMountable.h>
#import <ComponentKit/RCComponentDescriptionHelper.h>
#import <ComponentKit/CKComponentInternal.h>
#import <ComponentKit/CKComponentScopeRoot.h>
#import <ComponentKit/CKRootTreeNode.h>
#import <ComponentKit/CKEmptyComponent.h>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
static NSArray<id<CKMountable>> *generateComponentBacktrace(id<CKMountable> component,
                                                          NSMapTable<id<CKMountable>, id<CKMountable>> *componentsToParentComponents)
{
  NSMutableArray<id<CKMountable>> *componentBacktrace = [NSMutableArray arrayWithObject:component];
  auto parentComponent = [componentsToParentComponents objectForKey:component];
  while (parentComponent) {
    [componentBacktrace addObject:parentComponent];
    parentComponent = [componentsToParentComponents objectForKey:parentComponent];
  }
  return componentBacktrace;
}

CKDuplicateComponentInfo CKFindDuplicateComponent(const RCLayout &layout)
{
  std::queue<const RCLayout> queue;
  NSMutableSet<id<NSObject>> *const previouslySeenComponent = [NSMutableSet new];
  NSMapTable<id<CKMountable>, id<CKMountable>> *const componentsToParentComponents = [NSMapTable strongToStrongObjectsMapTable];
  queue.push(layout);
  while (!queue.empty()) {
    const auto componentLayout = queue.front();
    queue.pop();
    auto const component = componentLayout.component;

    if (component.class == CKEmptyComponent.class) {
      continue;
    }

    if (component && [previouslySeenComponent containsObject:component]) {
      return {
        .component = component,
        .backtraceDescription = RCComponentBacktraceDescription(generateComponentBacktrace(component, componentsToParentComponents)),
      };
    }
    if (component) {
      [previouslySeenComponent addObject:component];
    }
    if (componentLayout.children) {
      for (const auto& childComponentLayout : *componentLayout.children) {
        queue.push(childComponentLayout.layout);
        [componentsToParentComponents setObject:componentLayout.component forKey:childComponentLayout.layout.component];
      }
    }
  }
  return {};
}

void CKDetectDuplicateComponent(const RCLayout &layout) {
#if CK_ASSERTIONS_ENABLED
  auto const info = CKFindDuplicateComponent(layout);
  if (info.component) {
    RCCFailAssertWithCategory(RCComponentCompactDescription(info.component),
                              @"Duplicate component in the tree. Attempting to use %@ more than once in the component tree can lead to an incorrect and unexpected behavior\n"
                              @"Please make sure to create another instance of %@ if needed. \nComponent backtrace:\n%@",
                              info.component.className,
                              info.component.className,
                              info.backtraceDescription);
  }
#endif
}

#if CK_ASSERTIONS_ENABLED
static void CKVerifyTreeNodeWithParent(const CKRootTreeNode &rootNode, const RCLayout &layout, CKTreeNode *parentNode)
{
  if (layout.component == nil) {
    return;
  }

  CKTreeNode *treeNode = nil;
  if ([layout.component isKindOfClass:[CKComponent class]]) {
    auto const c = (CKComponent *)layout.component;
    if (c.treeNode) {
      treeNode = c.treeNode;
      auto const registeredParentNode = rootNode.parentForNodeIdentifier(treeNode.nodeIdentifier);
      if (registeredParentNode == nil) {
        RCCFailAssertWithCategory(RCComponentCompactDescription(c),
                                  @"Missing link from node to its parent on the CKRootTreeNode; \n"
                                  @"make sure your component returns all its children on the RCIterable methods.\n"
                                  @"Component:%@\n"
                                  @"Parent component:%@",
                                  c,
                                  parentNode.component);
      } else if (registeredParentNode != parentNode) {
        RCCFailAssertWithCategory(RCComponentCompactDescription(c),
                                  @"Incorrect link from node to its parent on the CKRootTreeNode; \n"
                                  @"make sure your component returns all its children on the RCIterable methods.\n"
                                  @"Component:%@\n"
                                  @"Parent component:%@\n"
                                  @"Registered parent component:%@",
                                  c,
                                  parentNode.component,
                                  registeredParentNode.component);
      }
    }
  }

  // Continue the check on the children; if the component has no tree node, pass the previous one.
  if (layout.children) {
    for (const auto &childLayout : *layout.children) {
      CKVerifyTreeNodeWithParent(rootNode, childLayout.layout, treeNode ?: parentNode);
    }
  }
}

#endif

void CKVerifyTreeNodesToParentLinks(CKComponentScopeRoot *scopeRoot, const RCLayout &layout)
{
  #if CK_ASSERTIONS_ENABLED
  if (scopeRoot.hasRenderComponentInTree) {
    CKVerifyTreeNodeWithParent(scopeRoot.rootNode, layout, scopeRoot.rootNode.node());
  }
  #endif
}

#pragma clang diagnostic pop
