// Copyright 2016 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Provides utilities around component type-checking.
library over_react.component_declaration.component_type_checking;

import 'package:over_react/src/component_declaration/component_base.dart' show UiFactory;
import 'package:over_react/src/util/react_wrappers.dart';
import 'package:react/react_client.dart';
import 'package:react/react_client/js_interop_helpers.dart';

// ----------------------------------------------------------------------
//   Component type registration and internal type metadata management
// ----------------------------------------------------------------------


Expando<ReactDartComponentFactoryProxy> _typeAliasToFactory = new Expando<ReactDartComponentFactoryProxy>();

/// Registers a type alias for the specified factory, so that [getComponentTypeFromAlias] can be
/// called with [typeAlias] to retrieve [factory]'s [ReactClass] type.
void registerComponentTypeAlias(ReactDartComponentFactoryProxy factory, dynamic typeAlias) {
  if (typeAlias != null) {
    _typeAliasToFactory[typeAlias] = factory;
  }
}

/// Key used to store/retrieve [ComponentTypeMeta] on a [ReactClass] component type by
/// [setComponentTypeMeta] and [getComponentTypeMeta].
const String _componentTypeMetaKey = '_componentTypeMeta';

/// Associates a new [ComponentTypeMeta] instance instantiated from [isWrapper]/[parentType] with
/// the component type of the specified [factory].
///
/// This meta is retrievable via [getComponentTypeMeta].
void setComponentTypeMeta(ReactDartComponentFactoryProxy factory, {
    bool isWrapper,
    ReactDartComponentFactoryProxy parentType
}) {
  setProperty(factory.type, _componentTypeMetaKey, new ComponentTypeMeta(isWrapper, parentType));
}

/// Returns the [ComponentTypeMeta] associated with the component type [type] in [setComponentTypeMeta],
/// or `const ComponentTypeMeta.none()` if there is no associated meta.
ComponentTypeMeta getComponentTypeMeta(dynamic type) {
  assert(isPotentiallyValidComponentType(type) &&
      '`type` should be a valid component type (and not null or a type alias).' is String);

  if (type is! String) {
    return getProperty(type, _componentTypeMetaKey) ?? const ComponentTypeMeta.none();
  }

  return const ComponentTypeMeta.none();
}


class ComponentTypeMeta {
  /// Whether the component clones or passes through its children and needs to be
  /// treated as if it were the wrapped component when passed into [isComponentOfType].
  final bool isWrapper;

  /// The factory of this component's "parent type".
  ///
  /// Used to enable inheritance in component type-checking in [isComponentOfType].
  ///
  /// E.g., if component `Bar` is a subtype of component `Foo`, then:
  ///
  ///     isComponentOfType(Bar()(), Bar); // true (due to normal type-checking)
  ///     isComponentOfType(Bar()(), Foo); // true (due to parent type-checking)
  final ReactDartComponentFactoryProxy parentType;

  ComponentTypeMeta(this.isWrapper, this.parentType);

  const ComponentTypeMeta.none() :
      this.isWrapper = false,
      this.parentType = null;
}


// ----------------------------------------------------------------------
//   Internal component type utilities
// ----------------------------------------------------------------------


/// Returns the canonical "type" for a component ([ReactClass] or [String] `tagName`)
/// associated with [typeAlias], which can be a component's:
///
/// * [UiFactory] (Dart components only)
/// * [UiComponent] [Type] (Dart components only)
/// * [ReactComponentFactoryProxy]
/// * [ReactClass] component factory
/// * [String] tag name (DOM components only)
///
/// If there is no type associated with [typeAlias], then `null` is returned.
///
/// This may be the case if [typeAlias] is invalid, or if the associated component hasn't been
/// registered yet due to lazy-instantiation of the [ReactComponentFactoryProxy] variables.
///
/// Consumers of this function should be sure to take the latter case into consideration.
///
/// __CAVEAT:__ Due to type-checking limitations on JS-interop types, when [typeAlias] is a [Function],
/// and it is not found to be an alias for another type, it will be returned as if it were a valid type.
dynamic getComponentTypeFromAlias(dynamic typeAlias) {
  /// If `typeAlias` is a factory, return its type.
  if (typeAlias is ReactComponentFactoryProxy) {
    return typeAlias.type;
  }

  /// Check the to see if any factories are associated with `typeAlias`.
  /// Type-check it so we don't pass an illegal type to Expando's `operator[]` method.
  if (typeAlias != null &&
      typeAlias is! num &&
      typeAlias is! String &&
      typeAlias is! bool) {
    var aliasedType = _typeAliasToFactory[typeAlias]?.type;
    if (aliasedType != null) {
      return aliasedType;
    }
  }

  /// If `typeAlias` is an actual type, return it.
  if (isPotentiallyValidComponentType(typeAlias)) {
    return typeAlias;
  }

  return null;
}

/// Returns whether [type] potentially represents a valid component type.
///
/// Valid types:
///
/// * [String] tag name (DOM components)
/// * [Function] ([ReactClass]) factory (Dart/JS composite components)
///
///     Note: It's impossible to determine know whether something is a ReactClass due to type-checking restrictions
///         for JS-interop classes, so a Function type-check is the best we can do.
bool isPotentiallyValidComponentType(dynamic type) {
  return type is Function || type is String;
}

/// Returns an [Iterable] of all component types that are ancestors of [typeAlias].
///
/// For example, given components A, B, and C, where B subtypes A and C subtypes B:
///
///     getParentTypes(getTypeFromAlias(A)); // []
///     getParentTypes(getTypeFromAlias(B)); // [A].map(getTypeFromAlias)
///     getParentTypes(getTypeFromAlias(C)); // [B, A].map(getTypeFromAlias)
Iterable<dynamic> getParentTypes(dynamic type) sync* {
  assert(isPotentiallyValidComponentType(type) &&
      '`type` should be a valid component type (and not null or a type alias).' is String);

  var currentType = type;
  var parentType;

  while ((parentType = getComponentTypeMeta(currentType).parentType) != null) {
    currentType = getComponentTypeFromAlias(parentType);
    yield currentType ?? parentType;
  }
}


// ----------------------------------------------------------------------
//   Consumer-facing component type-checking methods
// ----------------------------------------------------------------------


/// Returns whether [instance] is of the type associated with [typeAlias], which can be a component's:
///
/// * [UiFactory] (Dart components only)
/// * [UiComponent] [Type] (Dart components only)
/// * [ReactComponentFactoryProxy]
/// * [ReactClass] component factory
/// * [String] tag name (DOM components only)
bool isComponentOfType(ReactElement instance, dynamic typeAlias, {
    bool traverseWrappers: true,
    bool matchParentTypes: true
}) {
  if (instance == null) {
    return false;
  }

  var type = getComponentTypeFromAlias(typeAlias);
  if (type == null) {
    return false;
  }

  var instanceType = instance.type;
  var instanceTypeMeta = getComponentTypeMeta(instanceType);

  // Type-check instance wrappers.
  if (traverseWrappers && instanceTypeMeta.isWrapper) {
    assert(isDartComponent(instance) &&
       'Non-Dart components should not be wrappers' is String);

    List children = getProps(instance)['children'];
    if (children == null || children.isEmpty) {
      return false;
    }

    return isComponentOfType(children.first, type, traverseWrappers: true,
        matchParentTypes: matchParentTypes);
  }

  // Check parent types.
  if (matchParentTypes && instanceTypeMeta.parentType != null) {
    return instanceType == type || getParentTypes(instanceType).contains(type);
  }

  return instanceType == type;
}

/// Returns whether [instance] is a valid ReactElement of the type associated with
/// [typeAlias], which can be a component's:
///
/// * [UiFactory] (Dart components only)
/// * [UiComponent] [Type] (Dart components only)
/// * [ReactComponentFactoryProxy]
/// * [ReactClass] component factory
/// * [String] tag name (DOM components only)
bool isValidElementOfType(dynamic instance, dynamic typeAlias) {
  return isValidElement(instance) && isComponentOfType(instance, typeAlias);
}