// 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 'dart:collection';

import 'package:analyzer/dart/ast/ast.dart' show AstNode;
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:front_end/src/fasta/fasta_codes.dart' show Message;
import 'package:source_span/source_span.dart';

/**
 * An object that listen for [AnalysisError]s being produced by the analysis
 * engine.
 */
abstract class AnalysisErrorListener {
  /**
   * An error listener that ignores errors that are reported to it.
   */
  static final AnalysisErrorListener NULL_LISTENER = new _NullErrorListener();

  /**
   * This method is invoked when an [error] has been found by the analysis
   * engine.
   */
  void onError(AnalysisError error);
}

/**
 * An [AnalysisErrorListener] that keeps track of whether any error has been
 * reported to it.
 */
class BooleanErrorListener implements AnalysisErrorListener {
  /**
   * A flag indicating whether an error has been reported to this listener.
   */
  bool _errorReported = false;

  /**
   * Return `true` if an error has been reported to this listener.
   */
  bool get errorReported => _errorReported;

  @override
  void onError(AnalysisError error) {
    _errorReported = true;
  }
}

/**
 * An object used to create analysis errors and report then to an error
 * listener.
 */
class ErrorReporter {
  /**
   * The error listener to which errors will be reported.
   */
  final AnalysisErrorListener _errorListener;

  /**
   * The default source to be used when reporting errors.
   */
  final Source _defaultSource;

  /**
   * The source to be used when reporting errors.
   */
  Source _source;

  /**
   * Initialize a newly created error reporter that will report errors to the
   * given [_errorListener]. Errors will be reported against the
   * [_defaultSource] unless another source is provided later.
   */
  ErrorReporter(this._errorListener, this._defaultSource) {
    if (_errorListener == null) {
      throw new ArgumentError("An error listener must be provided");
    } else if (_defaultSource == null) {
      throw new ArgumentError("A default source must be provided");
    }
    this._source = _defaultSource;
  }

  Source get source => _source;

  /**
   * Set the source to be used when reporting errors to the given [source].
   * Setting the source to `null` will cause the default source to be used.
   */
  void set source(Source source) {
    this._source = source ?? _defaultSource;
  }

  /**
   * Report the given [error].
   */
  void reportError(AnalysisError error) {
    _errorListener.onError(error);
  }

  /**
   * Report an error with the given [errorCode] and [arguments]. The [element]
   * is used to compute the location of the error.
   */
  void reportErrorForElement(ErrorCode errorCode, Element element,
      [List<Object> arguments]) {
    int length = 0;
    if (element is ImportElement) {
      length = 6; // 'import'.length
    } else if (element is ExportElement) {
      length = 6; // 'export'.length
    } else {
      length = element.nameLength;
    }
    reportErrorForOffset(errorCode, element.nameOffset, length, arguments);
  }

  /**
   * Report an error with the given [errorCode] and [arguments].
   * The [node] is used to compute the location of the error.
   *
   * If the arguments contain the names of two or more types, the method
   * [reportTypeErrorForNode] should be used and the types
   * themselves (rather than their names) should be passed as arguments.
   */
  void reportErrorForNode(ErrorCode errorCode, AstNode node,
      [List<Object> arguments]) {
    reportErrorForOffset(errorCode, node.offset, node.length, arguments);
  }

  /**
   * Report an error with the given [errorCode] and [arguments]. The location of
   * the error is specified by the given [offset] and [length].
   */
  void reportErrorForOffset(ErrorCode errorCode, int offset, int length,
      [List<Object> arguments]) {
    _errorListener.onError(
        new AnalysisError(_source, offset, length, errorCode, arguments));
  }

  /**
   * Report an error with the given [errorCode] and [arguments]. The location of
   * the error is specified by the given [span].
   */
  void reportErrorForSpan(ErrorCode errorCode, SourceSpan span,
      [List<Object> arguments]) {
    reportErrorForOffset(errorCode, span.start.offset, span.length, arguments);
  }

  /**
   * Report an error with the given [errorCode] and [arguments]. The [token] is
   * used to compute the location of the error.
   */
  void reportErrorForToken(ErrorCode errorCode, Token token,
      [List<Object> arguments]) {
    reportErrorForOffset(errorCode, token.offset, token.length, arguments);
  }

  /**
   * Report an error with the given [errorCode] and [message]. The location of
   * the error is specified by the given [offset] and [length].
   */
  void reportErrorMessage(
      ErrorCode errorCode, int offset, int length, Message message) {
    _errorListener.onError(new AnalysisError.forValues(
        _source, offset, length, errorCode, message.message, message.tip));
  }

  /**
   * Report an error with the given [errorCode] and [arguments]. The [node] is
   * used to compute the location of the error. The arguments are expected to
   * contain two or more types. Convert the types into strings by using the
   * display names of the types, unless there are two or more types with the
   * same names, in which case the extended display names of the types will be
   * used in order to clarify the message.
   *
   * If there are not two or more types in the argument list, the method
   * [reportErrorForNode] should be used instead.
   */
  void reportTypeErrorForNode(
      ErrorCode errorCode, AstNode node, List<Object> arguments) {
    _convertTypeNames(arguments);
    reportErrorForOffset(errorCode, node.offset, node.length, arguments);
  }

  /**
   * Given an array of [arguments] that is expected to contain two or more
   * types, convert the types into strings by using the display names of the
   * types, unless there are two or more types with the same names, in which
   * case the extended display names of the types will be used in order to
   * clarify the message.
   */
  void _convertTypeNames(List<Object> arguments) {
    String displayName(DartType type) {
      if (type is FunctionType) {
        String name = type.name;
        if (name != null && name.length > 0) {
          StringBuffer buffer = new StringBuffer();
          buffer.write(name);
          (type as TypeImpl).appendTo(buffer, new Set.identity());
          return buffer.toString();
        }
      }
      return type.displayName;
    }

    if (_hasEqualTypeNames(arguments)) {
      int count = arguments.length;
      for (int i = 0; i < count; i++) {
        Object argument = arguments[i];
        if (argument is DartType) {
          Element element = argument.element;
          if (element == null) {
            arguments[i] = displayName(argument);
          } else {
            arguments[i] =
                element.getExtendedDisplayName(displayName(argument));
          }
        }
      }
    } else {
      int count = arguments.length;
      for (int i = 0; i < count; i++) {
        Object argument = arguments[i];
        if (argument is DartType) {
          arguments[i] = displayName(argument);
        }
      }
    }
  }

  /**
   * Return `true` if the given array of [arguments] contains two or more types
   * with the same display name.
   */
  bool _hasEqualTypeNames(List<Object> arguments) {
    int count = arguments.length;
    HashSet<String> typeNames = new HashSet<String>();
    for (int i = 0; i < count; i++) {
      Object argument = arguments[i];
      if (argument is DartType && !typeNames.add(argument.displayName)) {
        return true;
      }
    }
    return false;
  }
}

/**
 * An error listener that will record the errors that are reported to it in a
 * way that is appropriate for caching those errors within an analysis context.
 */
class RecordingErrorListener implements AnalysisErrorListener {
  /**
   * A map of sets containing the errors that were collected, keyed by each
   * source.
   */
  Map<Source, HashSet<AnalysisError>> _errors =
      new HashMap<Source, HashSet<AnalysisError>>();

  /**
   * Return the errors collected by the listener.
   */
  List<AnalysisError> get errors {
    int numEntries = _errors.length;
    if (numEntries == 0) {
      return AnalysisError.NO_ERRORS;
    }
    List<AnalysisError> resultList = new List<AnalysisError>();
    for (HashSet<AnalysisError> errors in _errors.values) {
      resultList.addAll(errors);
    }
    return resultList;
  }

  /**
   * Add all of the errors recorded by the given [listener] to this listener.
   */
  void addAll(RecordingErrorListener listener) {
    for (AnalysisError error in listener.errors) {
      onError(error);
    }
  }

  /**
   * Return the errors collected by the listener for the given [source].
   */
  List<AnalysisError> getErrorsForSource(Source source) {
    HashSet<AnalysisError> errorsForSource = _errors[source];
    if (errorsForSource == null) {
      return AnalysisError.NO_ERRORS;
    } else {
      return new List.from(errorsForSource);
    }
  }

  @override
  void onError(AnalysisError error) {
    Source source = error.source;
    HashSet<AnalysisError> errorsForSource = _errors[source];
    if (_errors[source] == null) {
      errorsForSource = new HashSet<AnalysisError>();
      _errors[source] = errorsForSource;
    }
    errorsForSource.add(error);
  }
}

/**
 * An [AnalysisErrorListener] that ignores error.
 */
class _NullErrorListener implements AnalysisErrorListener {
  @override
  void onError(AnalysisError event) {
    // Ignore errors
  }
}
