// 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_overrides.dart';
import 'package:analysis_server/src/utilities/documentation.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';

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

  DartUnitHoverComputer(this._unit, this._offset);

  /**
   * Returns the computed hover, maybe `null`.
   */
  HoverInformation compute() {
    AstNode node = new NodeLocator(_offset).searchWithin(_unit);
    if (node == null) {
      return null;
    }
    if (node.parent is TypeName &&
        node.parent.parent is ConstructorName &&
        node.parent.parent.parent is InstanceCreationExpression) {
      node = node.parent.parent.parent;
    }
    if (node.parent is ConstructorName &&
        node.parent.parent is InstanceCreationExpression) {
      node = node.parent.parent;
    }
    if (node is Expression) {
      Expression expression = node;
      HoverInformation hover =
          new HoverInformation(expression.offset, expression.length);
      // element
      Element element = ElementLocator.locate(expression);
      if (element != null) {
        // variable, if synthetic accessor
        if (element is PropertyAccessorElement) {
          PropertyAccessorElement accessor = element;
          if (accessor.isSynthetic) {
            element = accessor.variable;
          }
        }
        // description
        hover.elementDescription = element.toString();
        if (node is InstanceCreationExpression && node.keyword == null) {
          String prefix = node.isConst ? '(const) ' : '(new) ';
          hover.elementDescription = prefix + hover.elementDescription;
        }
        hover.elementKind = element.kind.displayName;
        hover.isDeprecated = element.hasDeprecated;
        // not local element
        if (element.enclosingElement is! ExecutableElement) {
          // containing class
          ClassElement containingClass =
              element.getAncestor((e) => e is ClassElement);
          if (containingClass != null) {
            hover.containingClassDescription = containingClass.displayName;
          }
          // containing library
          LibraryElement library = element.library;
          if (library != null) {
            hover.containingLibraryName = library.name;
            hover.containingLibraryPath = library.source.fullName;
          }
        }
        // documentation
        hover.dartdoc = computeDocumentation(element);
      }
      // parameter
      hover.parameter = _safeToString(expression.staticParameterElement);
      // types
      {
        AstNode parent = expression.parent;
        DartType staticType = null;
        if (element is ParameterElement) {
          staticType = element.type;
        } else if (element == null || element is VariableElement) {
          staticType = expression.staticType;
        }
        if (parent is MethodInvocation && parent.methodName == expression) {
          staticType = parent.staticInvokeType;
          if (staticType != null && staticType.isDynamic) {
            staticType = null;
          }
        }
        hover.staticType = _safeToString(staticType);
      }
      // done
      return hover;
    }
    // not an expression
    return null;
  }

  static String computeDocumentation(Element element) {
    // TODO(dantup) We're reusing this in parameter information - move it somewhere shared?
    if (element is FieldFormalParameterElement) {
      element = (element as FieldFormalParameterElement).field;
    }
    if (element is ParameterElement) {
      element = element.enclosingElement;
    }
    if (element == null) {
      // This can happen when the code is invalid, such as having a field formal
      // parameter for a field that does not exist.
      return null;
    }
    // The documentation of the element itself.
    if (element.documentationComment != null) {
      return removeDartDocDelimiters(element.documentationComment);
    }
    // Look for documentation comments of overridden members.
    OverriddenElements overridden = findOverriddenElements(element);
    for (Element superElement in []
      ..addAll(overridden.superElements)
      ..addAll(overridden.interfaceElements)) {
      String rawDoc = superElement.documentationComment;
      if (rawDoc != null) {
        Element interfaceClass = superElement.enclosingElement;
        return removeDartDocDelimiters(rawDoc) +
            '\n\nCopied from `${interfaceClass.displayName}`.';
      }
    }
    return null;
  }

  static String _safeToString(obj) => obj?.toString();
}
