// Copyright (c) 2014, 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:analysis_server/protocol/protocol_generated.dart'
    show HoverInformation;
import 'package:analysis_server/src/computer/computer_documentation.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/element_locator.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:path/path.dart' as path;

/// Information about a library to display in a hover.
typedef _LibraryInfo = ({String? libraryName, String? libraryPath})?;
typedef _OffsetLength = ({int offset, int length});

/// A computer for the hover at the specified offset of a Dart
/// [CompilationUnit].
class DartUnitHoverComputer {
  final CompilationUnit _unit;
  final int _offset;
  final DocumentationPreference documentationPreference;
  final DartDocumentationComputer _documentationComputer;

  new(
    DartdocDirectiveInfo dartdocInfo,
    this._unit,
    this._offset, {
    this.documentationPreference = DocumentationPreference.full,
  }) : _documentationComputer = DartDocumentationComputer(dartdocInfo);

  /// Returns the computed hover, maybe `null`.
  HoverInformation? compute() {
    var node = _unit.nodeCovering(offset: _offset);
    if (node == null) {
      return null;
    }

    var locationEntity = _locationEntity(node, _offset);
    node = _targetNode(node);
    if (node == null || locationEntity == null) {
      return null;
    }

    if (node is CompilationUnitMember ||
        node is CatchClauseParameter ||
        node is EnumConstantDeclaration ||
        node is Expression ||
        node is FormalParameter ||
        node is MethodDeclaration ||
        node is NamedArgument ||
        node is NamedType ||
        node is ConstructorDeclaration ||
        node is DeclaredIdentifier ||
        node is RecordLiteralNamedField ||
        node is VariableDeclaration ||
        node is VariablePattern ||
        node is PatternFieldName ||
        node is PrimaryConstructorBody ||
        node is PrimaryConstructorDeclaration ||
        node is PrimaryConstructorName ||
        node is DartPattern ||
        (node is LibraryDirective && node.name == null) ||
        (node is SimpleIdentifier && node.parent is ImportDirective) ||
        node is ImportPrefixReference) {
      var range = _hoverRange(node, locationEntity);
      var hover = HoverInformation(range.offset, range.length);
      // element
      var element = ElementLocator.locate(node);
      if (element != null) {
        // short code that illustrates the element meaning.
        hover.elementDescription = _elementDisplayString(node, element);
        hover.elementKind = element.kind.displayName;
        hover.isDeprecated = element.isDeprecatedWithKind('use');
        // not local element
        if (element.enclosingElement is! ExecutableElement) {
          // containing class
          hover.containingClassDescription = _containingClass(element);
          // containing library
          var libraryInfo = _libraryInfo(element);
          hover.containingLibraryName = libraryInfo?.libraryName;
          hover.containingLibraryPath = libraryInfo?.libraryPath;
        }
        // documentation
        hover.dartdoc = _documentationComputer.computePreferred(
          element,
          documentationPreference,
        );
      }
      // parameter
      hover.parameter = _parameterDisplayString(node);
      // types
      hover.staticType = _typeDisplayString(node, element);
      // done
      return hover;
    }
    // not an expression
    return null;
  }

  /// Gets the name of the containing class of [element].
  String? _containingClass(Element element) {
    var containingClass = element.thisOrAncestorOfType<InterfaceElement>();
    return containingClass != null && containingClass != element
        ? containingClass.displayName
        : null;
  }

  /// Gets the display string for [element].
  ///
  /// This is usually `element.getDisplayString()` but may contain additional
  /// information to disambiguate things like constructors from types (and
  /// whether they are const).
  String? _elementDisplayString(AstNode node, Element? element) {
    var displayString = element?.displayString(multiline: true);

    if (displayString != null) {
      if (node is InstanceCreationExpression && node.keyword == null) {
        var prefix = node.isConst ? '(const) ' : '(new) ';
        displayString = prefix + displayString;
      } else if (node is DotShorthandConstructorInvocation) {
        var prefix = node.isConst ? '(const) ' : '(new) ';
        displayString = prefix + displayString;
      }
    }

    return displayString;
  }

  /// Computes the range this hover applies to.
  ///
  /// This is usually the range of [entity] but may be adjusted for entities
  /// like constructor names.
  _OffsetLength _hoverRange(AstNode node, SyntacticEntity entity) {
    // For constructors, the location should cover the type name and
    // constructor name (for both calls and declarations).
    if (node is InstanceCreationExpression) {
      return (
        offset: node.constructorName.offset,
        length: node.constructorName.length,
      );
    } else if (node is DotShorthandConstructorInvocation) {
      return (offset: node.offset, length: node.length);
    } else if (node is ConstructorDeclaration) {
      var offset =
          node.typeName?.offset ??
          node.newKeyword?.offset ??
          node.factoryKeyword!.offset;
      var end =
          node.name?.end ??
          node.typeName?.end ??
          node.newKeyword?.end ??
          node.factoryKeyword!.end;
      var length = end - offset;
      return (offset: offset, length: length);
    } else if (node is PrimaryConstructorDeclaration) {
      if (node.constructorName != null) {
        return (offset: entity.offset, length: entity.length);
      }
      var offset = node.typeName.offset;
      var end = node.typeName.end;
      var length = end - offset;
      return (offset: offset, length: length);
    } else {
      return (offset: entity.offset, length: entity.length);
    }
  }

  /// Returns information about the library that contains [element].
  _LibraryInfo _libraryInfo(Element element) {
    var library = element.library;
    if (library == null) {
      return null;
    }

    var definingSource = library.firstFragment.source;
    var uri = definingSource.uri;
    var analysisSession = _unit.declaredFragment?.element.session;

    String? libraryName, libraryPath;
    // for 'file:' URIs, use the path after the package root
    if (uri.isScheme('file') && analysisSession != null) {
      var pathContext = analysisSession.resourceProvider.pathContext;
      var libraryFilePath = pathContext.fromUri(uri);
      var contextRoot = analysisSession.analysisContext.contextRoot;
      var workspace = contextRoot.workspace;

      // Prefer using the root of the package that contains this library, since
      // the context root could now be a Pub Workspace. If we don't find one,
      // fall back to the context root.
      var package = workspace.packages.packageForPath(libraryFilePath);
      var packageRootDir = package?.rootFolder.path ?? contextRoot.root.path;

      var relativePath = pathContext.relative(
        libraryFilePath,
        from: packageRootDir,
      );
      if (pathContext.style == path.Style.windows) {
        var pathList = pathContext.split(relativePath);
        libraryName = pathList.join('/');
      } else {
        libraryName = relativePath;
      }
    } else {
      libraryName = uri.toString();
    }
    libraryPath = definingSource.fullName;

    return (libraryName: libraryName, libraryPath: libraryPath);
  }

  /// Returns the [SyntacticEntity] that should be used as the range for this
  /// hover.
  ///
  /// Returns `null` if there is no valid entity for this hover.
  SyntacticEntity? _locationEntity(AstNode node, int offset) {
    return switch (node) {
      BinaryExpression() => node.operator,
      ConditionalExpression()
          when offset >= node.question.offset && offset <= node.question.end =>
        node.question,
      ConditionalExpression()
          when offset >= node.colon.offset && offset <= node.colon.end =>
        node.colon,
      AssignmentExpression() => node.operator,
      PrefixExpression() => node.operator,
      PostfixExpression() => node.operator,
      CatchClauseParameter() => node.name,
      ClassDeclaration() => node.namePart.typeName,
      ConstructorDeclaration() =>
        node.name ?? node.typeName ?? node.newKeyword ?? node.factoryKeyword,
      DeclaredIdentifier() => node.name,
      EnumConstantDeclaration() => node.name,
      EnumDeclaration() => node.namePart.typeName,
      Expression() => node,
      ExtensionDeclaration() => node.name,
      ExtensionTypeDeclaration() => node.namePart.typeName,
      FormalParameter() => node.name,
      FunctionDeclaration() => node.name,
      ImportPrefixReference() => node.name,
      LibraryDirective() => node.libraryKeyword,
      MethodDeclaration() => node.name,
      NamedArgument() => node.name,
      MixinDeclaration() => node.name,
      NameWithTypeParameters() => node.typeName,
      NamedType() => node.name,
      PatternFieldName() => node.name,
      PrimaryConstructorBody() =>
        node.declaration?.constructorName ?? node.declaration?.typeName,
      PrimaryConstructorDeclaration() => node.constructorName ?? node.typeName,
      PrimaryConstructorName() => node.name,
      RecordLiteralNamedField() => node.name,
      TypeAlias() => node.name,
      VariableDeclaration() => node.name,
      VariablePattern() => node.name,
      WildcardPattern() => node.name,
      _ => null,
    };
  }

  /// Gets the display string for the type of the parameter.
  ///
  /// Returns `null` if the node doesn't correspond to a parameter.
  String? _parameterDisplayString(AstNode node) {
    FormalParameterElement? parameter;
    if (node is Argument) {
      parameter = node.correspondingParameter;
    }
    return switch (parameter?.enclosingElement) {
      // Expressions passed as arguments to setters and binary expressions
      // will have parameters here but we don't want them to show as such in
      // hovers because information about those functions are already available
      // by hovering over the function name or the operator.
      SetterElement() => null,
      MethodElement method when method.isOperator => null,
      _ => _elementDisplayString(node, parameter),
    };
  }

  /// Adjusts the target node for constructors.
  AstNode? _targetNode(AstNode node) {
    var parent = node.parent;
    var parent2 = parent?.parent;
    if (node is ClassNamePart) {
      return parent;
    } else if (parent is NamedType &&
        parent2 is ConstructorName &&
        parent2.parent is InstanceCreationExpression) {
      return parent2.parent;
    } else if (parent is ConstructorName &&
        parent2 is InstanceCreationExpression) {
      return parent2;
    } else if (node is SimpleIdentifier &&
        parent is ConstructorDeclaration &&
        parent.name != null) {
      return parent;
    } else if (node is SimpleIdentifier &&
        parent is DotShorthandConstructorInvocation) {
      return parent;
    }
    return node;
  }

  /// Returns information about the static type of [node].
  String? _typeDisplayString(AstNode node, Element? element) {
    var parent = node.parent;
    DartType? staticType;
    if (node is NamedArgument) {
      staticType = node.correspondingParameter?.type;
    } else if (node is Expression &&
        (element == null ||
            element is VariableElement ||
            element is GetterElement ||
            element is SetterElement)) {
      staticType = _getTypeOfDeclarationOrReference(node);
    } else if (element is VariableElement) {
      staticType = element.type;
    } else if (parent is MethodInvocation && parent.methodName == node) {
      staticType = parent.staticInvokeType;
      if (staticType != null && staticType is DynamicType) {
        staticType = null;
      }
    } else if (parent is PrefixedIdentifier && parent.identifier == node) {
      staticType = parent.identifier.staticType;
      if (staticType != null && staticType is DynamicType) {
        staticType = null;
      }
    } else if (parent is DotShorthandInvocation && parent.memberName == node) {
      staticType = parent.staticInvokeType;
      if (staticType != null && staticType is DynamicType) {
        staticType = null;
      }
    } else if (node is PatternFieldName && parent is PatternField) {
      staticType = parent.pattern.matchedValueType;
    } else if (node is DartPattern) {
      staticType = node.matchedValueType;
    }
    return staticType?.getDisplayString();
  }

  static DartType? _getTypeOfDeclarationOrReference(Expression node) {
    if (node is SimpleIdentifier) {
      var element = node.element;
      if (element is VariableElement) {
        if (node.inDeclarationContext()) {
          return element.type;
        }
      }
      var parent = node.parent;
      var parent2 = parent?.parent;

      if (parent is AssignmentExpression && parent.leftHandSide == node) {
        // Direct setter reference
        return parent.writeType;
      } else if (parent2 is AssignmentExpression &&
          parent2.leftHandSide == parent) {
        if (parent is PrefixedIdentifier && parent.identifier == node) {
          // Prefixed setter (`myInstance.foo =`)
          return parent2.writeType;
        } else if (parent is PropertyAccess && parent.propertyName == node) {
          // Expression prefix (`A<int>().foo =`)
          return parent2.writeType;
        }
      }
    }
    return node.staticType;
  }
}
