// 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/analysis/features.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/scope.dart';
import 'package:analyzer/dart/element/type.dart' show DartType;
import 'package:collection/collection.dart';
import 'package:dartdoc/src/element_type.dart';
import 'package:dartdoc/src/model/accessor.dart';
import 'package:dartdoc/src/model/constructor.dart';
import 'package:dartdoc/src/model/container.dart';
import 'package:dartdoc/src/model/library.dart';
import 'package:dartdoc/src/model/model_element.dart';
import 'package:dartdoc/src/model/package_graph.dart';
import 'package:dartdoc/src/model/prefix.dart';
import 'package:dartdoc/src/warnings.dart';
import 'package:meta/meta.dart';

/// Something that has a name, and can be referenced in a doc comment.
mixin Referable {
  String get name;

  /// A "fully" qualified name, used for things like for warnings printed in the
  /// terminal; not for display use in rendered HTML.
  ///
  /// "Fully" means the name is qualified through the library. For example, a
  /// method named 'baz' in a class named 'Bar' in a library named 'foo' has a
  /// fully qualified name of 'foo.Bar.baz'.
  ///
  /// As dartdoc can document multiple packages at once, note that such
  /// qualifying names may not be unique across all documented packages.
  String get fullyQualifiedName => name;

  /// The name to use as text in the rendered documentation.
  String get displayName => name;

  /// The name to use in breadcrumbs in the rendered documentation.
  String get breadcrumbName => name;

  /// The name that should be used in documentation to refer to this object if
  /// not the name that was actually used in the reference.
  ///
  /// This is `null` for most objects in which case the reference's original
  /// text is used, but returns the public name of a private named parameter:
  ///
  ///     class C {
  ///       /// Make with [_x].
  ///       C({this._x});
  ///     }
  ///
  /// Here, the generated doc comment will use "x" as the name for the parameter
  /// reference, not "_x".
  String? get documentedName => null;

  /// Whether this is "package-public."
  ///
  /// A "package-public" element satisfies the following requirements:
  /// * is not documented with the `@nodoc` directive,
  /// * for a library, adheres to the documentation for [Library.isPublic],
  /// * for a library member, is in a _public_ library's exported namespace, and
  ///   is not privately named, nor an unnamed extension,
  /// * for a container (class, enum, extension, extension type, mixin) member,
  ///   is in a _public_ container, and is not privately named.
  bool get isPublic => name.isNotEmpty && !name.startsWith('_');

  @override
  String toString() => name;

  PackageGraph get packageGraph;

  /// Returns the [ModelElement] for [element], instantiating it if needed.
  ///
  /// A convenience method for [ModelElement.for_], see its documentation.
  ModelElement getModelFor(
    Element element,
    Library? library, {
    Container? enclosingContainer,
  }) =>
      ModelElement.for_(
        element,
        library,
        packageGraph,
        enclosingContainer: enclosingContainer,
      );

  /// Returns the [ModelElement] for [element], instantiating it if needed.
  ///
  /// A convenience method for [ModelElement.forElement], see its
  /// documentation.
  ModelElement getModelForElement(Element element) =>
      ModelElement.forElement(element, packageGraph);

  /// Returns the [ModelElement] for [element], instantiating it if needed.
  ///
  /// A convenience method for [ModelElement.forPropertyInducingElement], see
  /// its documentation.
  // TODO(srawlins): Most callers seem to determine `getter` and `setter`
  // immediately before calling this method, and I imagine could instead just
  // call `getModelFor`.
  ModelElement getModelForPropertyInducingElement(
    PropertyInducingElement element,
    Library library, {
    required Accessor? getter,
    required Accessor? setter,
    Container? enclosingContainer,
  }) =>
      ModelElement.forPropertyInducingElement(
        element,
        library,
        packageGraph,
        getter: getter,
        setter: setter,
        enclosingContainer: enclosingContainer,
      );

  /// Returns the [ElementType] for [type], instantiating it if needed.
  ElementType getTypeFor(DartType type, Library? library) =>
      ElementType.for_(type, library, packageGraph);

  /// For any [Referable] where an analyzer [Scope] exists (or can be
  /// constructed), implement this.  This will take priority over lookups via
  /// [referenceChildren].  Can be cached.
  @visibleForOverriding
  Scope? get scope => null;

  String? get href => null;

  /// Looks up a comment reference by its component parts.
  ///
  /// If [tryParents] is true, try looking up the same reference in any parents
  /// of `this`. Will skip over results that do not pass a given [filter] and
  /// keep searching.
  @nonVirtual
  Referable? referenceBy(
    List<String> reference, {
    required bool Function(Referable?) filter,
    bool tryParents = true,
    Iterable<Referable>? parentOverrides,
  }) {
    parentOverrides ??= referenceParents;
    if (reference.isEmpty) {
      return tryParents ? null : this;
    }
    for (var referenceLookup in _childLookups(reference)) {
      // First attempt: Ask analyzer's `Scope.lookup` API.
      var result = _lookupViaScope(referenceLookup, filter: filter);
      if (result != null) {
        if (result is Prefix &&
            result.name == '_' &&
            library!.element.featureSet.isEnabled(Feature.wildcard_variables)) {
          // A wildcard import prefix is non-binding.
          continue;
        }
        return result;
      }

      // Second attempt: Look through `referenceChildren`.
      final referenceChildren = this.referenceChildren;
      final childrenResult = referenceChildren[referenceLookup.lookup];
      if (childrenResult != null) {
        var result = _recurseChildrenAndFilter(
          referenceLookup,
          childrenResult,
          filter: filter,
        );
        if (result != null) {
          return result;
        }
      }
    }
    // If we can't find it in children, try searching parents if allowed.
    if (tryParents) {
      for (var parent in parentOverrides) {
        var result = parent.referenceBy(
          reference,
          tryParents: true,
          parentOverrides: referenceGrandparentOverrides,
          filter: filter,
        );
        if (result != null) return result;
      }
    }
    return null;
  }

  /// Looks up references by [scope], skipping over results that do not match
  /// [filter].
  ///
  /// Override if [Scope.lookup] may return elements not corresponding to a
  /// [Referable], but you still want to have an implementation of
  /// [scope].
  Referable? _lookupViaScope(
    _ReferenceChildrenLookup referenceLookup, {
    required bool Function(Referable?) filter,
  }) {
    Element? resultElement;
    final scope = this.scope;
    if (scope != null) {
      resultElement = scope.lookupPreferGetter(referenceLookup.lookup);
      if (resultElement == null) return null;
    }

    if (resultElement == null) {
      if (this case ModelElement(:var modelNode?)) {
        var references = modelNode.commentData?.references;
        if (references != null) {
          resultElement = references[referenceLookup.lookup]?.element;
        }
      }
    }

    if (resultElement == null) {
      return null;
    }

    // If the element was brought in by a @docImport or has an unlinked library
    // fragment, we must drop it gracefully instead of crashing on `library!`.
    // See: https://github.com/dart-lang/sdk/issues/62812
    // TODO(zarah): Investigate if this bailout is still necessary after the
    // issue in the analyzer is resolved. We need to determine whether there are
    // other edge cases that could lead to an element's library not being found.
    if (packageGraph.findButDoNotCreateLibraryFor(resultElement) == null &&
        resultElement.kind != ElementKind.DYNAMIC &&
        resultElement.kind != ElementKind.NEVER) {
      packageGraph.warnOnElement(
        switch (this) { Warnable w => w, _ => null },
        PackageWarning.internalError,
        message: 'Unable to find library for element $resultElement',
      );
      return null;
    }

    ModelElement result;
    if (resultElement is PropertyAccessorElement) {
      final variable = resultElement.variable;
      if (!variable.isOriginDeclaration) {
        // First, cache the synthetic variable, so that the
        // PropertyAccessorElement getter and/or setter are set (see
        // `Field.new` regarding `enclosingCombo`).
        packageGraph.getModelForElement(variable);
        // Then, use the result for the PropertyAccessorElement.
        result = packageGraph.getModelForElement(resultElement);
      } else {
        result = packageGraph.getModelForElement(variable);
      }
    } else {
      result = packageGraph.getModelForElement(resultElement);
    }
    return _recurseChildrenAndFilter(referenceLookup, result, filter: filter);
  }

  /// Given a [result] found in an implementation of [_lookupViaScope] or
  /// [_ReferenceChildrenLookup], recurse through children, skipping over
  /// results that do not match the filter.
  Referable? _recurseChildrenAndFilter(
    _ReferenceChildrenLookup referenceLookup,
    Referable result, {
    required bool Function(Referable?) filter,
  }) {
    Referable? returnValue = result;
    if (referenceLookup.remaining.isNotEmpty) {
      returnValue = result.referenceBy(referenceLookup.remaining,
          tryParents: false, filter: filter);
    } else if (!filter(result)) {
      returnValue = result.referenceBy([referenceLookup.lookup],
          tryParents: false, filter: filter);
    }
    if (!filter(returnValue)) {
      returnValue = null;
    }
    return returnValue;
  }

  /// A list of lookups that should be attempted on children based on
  /// [reference].
  ///
  /// This allows us to deal with libraries that may have separators in them.
  /// [referenceBy] stops at the first one found.
  Iterable<_ReferenceChildrenLookup> _childLookups(List<String> reference) =>
      reference
          .mapIndexed((index, _) => _ReferenceChildrenLookup(
              reference.sublist(0, index + 1).join('.'),
              reference.sublist(index + 1)))
          .toList(growable: false);

  /// Map of [referenceName] to the elements that are a member of `this`, but
  /// not this model element itself.  Can be cached.
  ///
  /// There is no need to duplicate references here that can be found via
  /// [scope].
  Map<String, Referable> get referenceChildren;

  /// Iterable of immediate "parents" to try resolving component parts.
  /// [referenceBy] stops at the first parent where a part is found.
  /// Can be cached.
  // TODO(jcollins-g): Rationalize the different "enclosing" types so that
  // this doesn't duplicate `[enclosingElement]` in many cases.
  // TODO(jcollins-g): Implement comment reference resolution via categories,
  // making the iterable make sense here.
  Iterable<Referable> get referenceParents;

  /// Replace the parents of parents.  [referenceBy] ignores whatever might
  /// otherwise be implied by the [referenceParents] of [referenceParents],
  /// replacing them with this.
  Iterable<Referable>? get referenceGrandparentOverrides => null;

  // TODO(jcollins-g): Enforce that reference name is always the same
  // as [ModelElement.name].  Easier/possible after old lookup code is gone.
  String get referenceName => name;

  // TODO(jcollins-g): Eliminate need for this in markdown_processor.
  Library? get library => null;

  /// For testing / comparison only, get the [Referable] from where this
  /// `ElementType` was defined.  Override where an [Element] is available.
  @internal
  Referable get definingReferable => this;
}

/// Compares [a] with [b] by name.
int byName(Referable a, Referable b) {
  if (a is Library && b is Library) {
    return compareAsciiLowerCaseNatural(a.displayName, b.displayName);
  }

  if (a is Constructor && b is Constructor) {
    var aName = a.name.replaceFirst('.new', '');
    var bName = b.name.replaceFirst('.new', '');
    return compareAsciiLowerCaseNatural(aName, bName);
  }

  var stringCompare = compareAsciiLowerCaseNatural(a.name, b.name);
  if (stringCompare != 0) {
    return stringCompare;
  }

  return a.hashCode.compareTo(b.hashCode);
}

/// A set of utility methods for helping build [Referable.referenceChildren] out
/// of collections of other [Referable]s.
extension ReferableEntryGenerators<T extends Referable> on Iterable<T> {
  /// Creates reference entries for this Iterable.
  ///
  /// If there is a conflict with [referable], the included [MapEntry] uses
  /// [referable]'s [Referable.referenceName] as a prefix.
  Map<String, T> explicitOnCollisionWith(Referable referable) => {
        for (var r in this)
          if (r.referenceName == referable.referenceName)
            '${referable.referenceName}.${r.referenceName}': r
          else
            r.referenceName: r,
      };

  /// A mapping from each [Referable]'s name to itself.
  Map<String, T> get asMapByName => {
        for (var r in this) r.referenceName: r,
      };

  /// Returns all values not of this type.
  List<T> whereNotType<U>() => [
        for (var referable in this)
          if (referable is! U) referable,
      ];
}

class _ReferenceChildrenLookup {
  final String lookup;
  final List<String> remaining;

  _ReferenceChildrenLookup(this.lookup, this.remaining);

  @override
  String toString() =>
      '$lookup ($lookup${remaining.isNotEmpty ? ".${remaining.join(".")}" : ''})';
}

extension on Scope {
  /// Prefer the getter for a bundled lookup if both exist.
  Element? lookupPreferGetter(String id) {
    var result = lookup(id);
    return result.getter ?? result.setter;
  }
}
