// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/scope.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/model_utils.dart' as model_utils;
import 'package:meta/meta.dart';

/// A [Container] represents a Dart construct that can contain methods,
/// operators, and fields, such as [Class], [Enum], or [Extension].
///
/// Member naming in [Container] follows these general rules:
///
/// * **instance** : children of this container that can be referenced from
///   within the container without a prefix. These are usually overridden in
///   subclasses with calls to 'super'.
/// * **constant** : children declared constant.
/// * **variable** : The opposite of constant. These are available for the
///   templating system.
/// * **static** : static children of this container.
/// * **public** : Filtered versions of the above members, containing only
///   public items. These are available mostly for the templating system.
/// * **sorted** : Filtered versions of the above members as a list sorted by
///   name.  These are available for the templating system.
/// * **has** : boolean getters indicating whether the underlying collections
///   are empty.  These are available mostly for the templating system.
abstract class Container extends ModelElement with HasLibrary, TypeParameters {
  Container(Library super.library, super.packageGraph);

  @override
  Element get element;

  // TODO(jcollins-g): Implement a ContainerScope that flattens supertypes?
  @override
  Scope? get scope => null;

  @override
  bool get hasParameters => false;

  bool get isExtension => element is ExtensionElement;

  /// Whether this is an enum.
  bool get isEnum => element is EnumElement;

  /// Whether this is an interface (e.g. class, enum, mixin, or extension type).
  bool get isInterface => element is InterfaceElement;

  /// Whether this is a mixin.
  bool get isMixin => element is MixinElement;

  /// Whether this container represents the Object class from 'dart:core'.
  bool get isDartCoreObject =>
      element.name == 'Object' && element.library?.name == 'dart.core';

  /// The model elements of all of the members of this container, including
  /// declared and inherited ones.
  Iterable<ModelElement> get allModelElements => [
        ...instanceMethods,
        ...instanceFields,
        ...instanceOperators,
        ...instanceAccessors,
        ...staticFields,
        ...staticAccessors,
        ...staticMethods,
      ];

  late final List<ModelElement> allCanonicalModelElements =
      allModelElements.where((e) => e.isCanonical).toList(growable: false);

  /// All methods, including operators and statics, declared on [element].
  ///
  /// [declaredMethods] must be the union of [instanceMethods],
  /// [staticMethods], and [instanceOperators].
  Iterable<Method> get declaredMethods;

  /// The instance methods available on [element], including declared and
  /// inherited ones.
  Iterable<Method> get instanceMethods;

  /// Whether all instance fields are inherited.
  bool get publicInheritedInstanceFields => false;

  /// Whether all instance methods are inherited.
  bool get publicInheritedInstanceMethods => false;

  /// Whether all instance operators are inherited.
  bool get publicInheritedInstanceOperators => false;

  /// Override if this is [Constructable].
  bool get hasPublicConstructors => false;

  List<Constructor> get publicConstructorsSorted => const [];

  bool get hasAvailableInstanceMethods =>
      availableInstanceMethodsSorted.isNotEmpty;

  List<Method> get availableInstanceMethodsSorted;

  @nonVirtual
  late final List<Operator> declaredOperators =
      declaredMethods.whereType<Operator>().toList(growable: false);

  @override
  ModelElement get enclosingElement;

  /// The instance operators declared on [element].
  Iterable<Operator> get instanceOperators => declaredOperators;

  bool get hasAvailableInstanceOperators =>
      availableInstanceOperatorsSorted.isNotEmpty;

  List<Operator> get availableInstanceOperatorsSorted;

  /// Fields fully declared in this [Container].
  Iterable<Field> get declaredFields;

  /// All instance fields declared in this [Container].
  Iterable<Field> get instanceFields =>
      declaredFields.where((f) => !f.isStatic);

  bool get hasInstanceFields => instanceFields.isNotEmpty;

  @nonVirtual
  bool get hasAvailableInstanceFields => instanceFields.any((e) => e.isPublic);

  List<Field> get availableInstanceFieldsSorted;

  Iterable<Field> get constantFields => declaredFields.where((f) => f.isConst);

  bool get hasPublicConstantFields => constantFields.any((e) => e.isPublic);

  List<Field> get publicConstantFieldsSorted =>
      constantFields.wherePublic.toList(growable: false)..sort(byName);

  /// The total list of public enum values.
  ///
  /// This is defined on [Container] instead of just [Enum], because the
  /// `ContainerSidebar` Mustache template needs to refer to this field.
  Iterable<Field> get publicEnumValues => const [];

  /// Whether this container has any public enum values.
  ///
  /// This is defined on [Container] instead of just [Enum], because the
  /// `ContainerSidebar` Mustache template needs to refer to this field.
  bool get hasPublicEnumValues => publicEnumValues.isNotEmpty;

  /// The instance accessors declared on [element].
  Iterable<Accessor> get instanceAccessors =>
      instanceFields.expand((f) => f.allAccessors);

  /// The static accessors declared on [element].
  Iterable<Accessor> get staticAccessors =>
      staticFields.expand((f) => f.allAccessors);

  /// This container might be canonical for elements it does not contain.
  /// See [Inheritable.canonicalEnclosingContainer].
  bool containsElement(Element? element) => _allElements.contains(element);

  late final Set<Element> _allElements =
      allModelElements.map((e) => e.element).toSet();

  bool get hasPublicStaticFields => staticFields.any((e) => e.isPublic);

  List<Field> get publicStaticFieldsSorted =>
      staticFields.wherePublic.toList(growable: false)..sort();

  Iterable<Field> get staticFields => declaredFields.where((f) => f.isStatic);

  Iterable<Field> get variableStaticFields =>
      staticFields.where((f) => !f.isConst);

  bool get hasPublicVariableStaticFields =>
      variableStaticFields.any((e) => e.isPublic);

  List<Field> get publicVariableStaticFieldsSorted =>
      variableStaticFields.wherePublic.toList(growable: false)..sort();

  /// The static methods declared on [element].
  Iterable<Method> get staticMethods =>
      declaredMethods.where((m) => m.isStatic);

  bool get hasPublicStaticMethods => staticMethods.any((e) => e.isPublic);

  List<Method> get publicStaticMethodsSorted =>
      staticMethods.wherePublic.toList(growable: false)..sort();

  /// For subclasses to add items after the main pass but before the
  /// parameter-global.
  @visibleForOverriding
  Map<String, Referable> get extraReferenceChildren;

  @override
  @mustCallSuper
  late final Map<String, Referable> referenceChildren = {
    for (var modelElement in allModelElements)
      // Don't complain about references to parameter names, but prefer
      // referring to anything else.
      // TODO(jcollins-g): Figure out something good to do in the ecosystem
      // here to wean people off the habit of unscoped parameter references.
      if (modelElement.hasParameters) ...modelElement.parameters.asMapByName,
    ...extraReferenceChildren,
    for (var element in allModelElements
        .whereNotType<Accessor>()
        .whereNotType<Constructor>())
      element.referenceName: element,
  };

  @override
  Iterable<Referable> get referenceParents => [library];

  /// The full path of this element's sidebar file.
  String get sidebarPath;

  @override
  String get aboveSidebarPath => canonicalLibraryOrThrow.sidebarPath;

  @override
  String get belowSidebarPath => sidebarPath;

  /// The CSS class to use in an inheritance list.
  String get relationshipsClass;
}
