// Copyright (c) 2015, 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 'dart:convert';

import 'package:analysis_server/src/status/utilities/tree_writer.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/element/element.dart';

/// A visitor that will produce an HTML representation of an element structure.
class ElementWriter with TreeWriter {
  @override
  final StringBuffer buffer;

  /// Initialize a newly created element writer to write the HTML representation
  /// of visited elements on the given [buffer].
  new(this.buffer);

  void write(Element element) {
    _writeElement(element);
    writeProperties(_computeProperties(element));
    _writeFragments(element);
    indentLevel++;
    try {
      for (var child in element.children) {
        write(child);
      }
    } finally {
      indentLevel--;
    }
  }

  /// Returns a representation Map of the properties of the given [element].
  Map<String, Object> _computeProperties(Element element) {
    return {
      'annotations': element.metadata.annotations,
      if (element is InterfaceElement) ...{
        'interfaces': element.interfaces,
        'isEnum': element is EnumElement,
        'mixins': element.mixins,
        'supertype': ?element.supertype,
        if (element is ClassElement) ...{
          'hasNonFinalField': element.hasNonFinalField,
          'isAbstract': element.isAbstract,
          'isMixinApplication': element.isMixinApplication,
          'isValidMixin': element.isValidMixin,
        },
      },
      'evaluationResult': ?switch (element) {
        FieldElementImpl() => element.evaluationResult,
        LocalVariableElementImpl(constantInitializer: Expression()) =>
          element.evaluationResult,
        TopLevelVariableElementImpl() => element.evaluationResult,
        _ => null,
      },
      if (element is ConstructorElement) ...{
        'isConst': element.isConst,
        'isDefaultConstructor': element.isDefaultConstructor,
        'isFactory': element.isFactory,
        'redirectedConstructor': ?element.redirectedConstructor,
      },
      if (element is ExecutableElement) ...{
        'hasImplicitReturnType': element.hasImplicitReturnType,
        'isAbstract': element.isAbstract,
        'isExternal': element.isExternal,
        if (element is MethodElement) 'isOperator': element.isOperator,
        'isStatic': element.isStatic,
        'returnType': element.returnType,
        'type': element.type,
      },
      if (element is FieldElement) 'isEnumConstant': element.isEnumConstant,
      if (element is FieldFormalParameterElement) 'field': ?element.field,
      if (element is TopLevelFunctionElement)
        'isEntryPoint': element.isEntryPoint,
      if (element is FunctionTypedElement) ...{
        'returnType': element.returnType,
        'type': element.type,
      },
      if (element is LibraryElement) ...{
        'entryPoint': ?element.entryPoint,
        'isDartAsync': element.isDartAsync,
        'isDartCore': element.isDartCore,
        'isInSdk': element.isInSdk,
      },
      if (element is FormalParameterElement) ...{
        'defaultValueCode': ?element.defaultValueCode,
        'isInitializingFormal': element is FieldFormalParameterElement,
        if (element.isRequiredPositional)
          'parameterKind': 'required-positional'
        else if (element.isRequiredNamed)
          'parameterKind': 'required-named'
        else if (element.isOptionalPositional)
          'parameterKind': 'optional-positional'
        else if (element.isOptionalNamed)
          'parameterKind': 'optional-named'
        else
          'parameterKind': 'unknown kind',
      },
      if (element is PropertyInducingElement) 'isStatic': element.isStatic,
      if (element is TypeParameterElement) 'bound': ?element.bound,
      if (element is TypeParameterizedElement)
        'typeParameters': element.typeParameters,
      if (element is VariableElement) ...{
        'hasImplicitType': element.hasImplicitType,
        'isConst': element.isConst,
        'isFinal': element.isFinal,
        'isStatic': element.isStatic,
        'type': element.type,
      },
    };
  }

  /// Write a representation of the given [element] to the buffer.
  void _writeElement(Element element) {
    indent();

    var showItalic = switch (element) {
      ConstructorElement() => !element.isOriginDeclaration,
      PropertyAccessorElement() => !element.isOriginDeclaration,
      PropertyInducingElement() => !element.isOriginDeclaration,
      _ => false,
    };

    if (showItalic) {
      buffer.write('<i>');
    }
    buffer.write(htmlEscape.convert(element.toString()));
    if (showItalic) {
      buffer.write('</i>');
    }
    buffer.write(' <span style="color:gray">(');
    buffer.write(element.runtimeType);
    buffer.write(')</span>');
    buffer.write('<br>');
  }

  /// Write a representation of the given [fragment] to the buffer.
  void _writeFragment(Fragment fragment, int index) {
    indent();
    buffer.write('fragments[$index]: ');
    buffer.write(fragment.name);
    buffer.write(' <span style="color:gray">(');
    buffer.write(fragment.runtimeType);
    buffer.write(')</span>');
    buffer.write('<br>');
    var properties = {
      if (fragment is LibraryFragment) ...{
        'source': fragment.source,
        'imports': {
          for (var import in fragment.libraryImports)
            {
              'combinators': import.combinators,
              if (import.prefix != null) 'prefix': import.prefix?.name,
              'isDeferred': import.prefix?.isDeferred ?? false,
              'library': import.importedLibrary,
            },
          for (var export in fragment.libraryExports)
            {
              'combinators': export.combinators,
              'library': export.exportedLibrary,
            },
        },
      },
      'nameOffset': ?fragment.nameOffset,
      if (fragment is ExecutableFragment) ...{
        'isAsynchronous': fragment.isAsynchronous,
        'isGenerator': fragment.isGenerator,
        'isSynchronous': fragment.isSynchronous,
      },
    };
    writeProperties(properties);
  }

  void _writeFragments(Element element) {
    indentLevel++;
    try {
      var index = 0;
      Fragment? fragment = element.firstFragment;
      while (fragment != null) {
        _writeFragment(fragment, index++);
        fragment = fragment.nextFragment;
      }
    } finally {
      indentLevel--;
    }
  }
}
