import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/error.dart';
import 'package:build/build.dart';
import 'package:source_span/source_span.dart';
import 'package:angular_compiler/v1/src/compiler/compile_metadata.dart';
import 'package:angular_compiler/v1/src/source_gen/common/annotation_matcher.dart';
import 'package:angular_compiler/v1/cli.dart';

class IndexedAnnotation<T extends Element> {
  final T element;
  final ElementAnnotation annotation;
  final int annotationIndex;

  IndexedAnnotation(this.element, this.annotation, this.annotationIndex);
}

class ComponentVisitorExceptionHandler {
  final List<AsyncBuildError> _errors = [];
  final List<AsyncBuildError> _warnings = [];

  void handle(AsyncBuildError error) {
    _errors.add(error);
  }

  void handleWarning(AsyncBuildError warning) {
    _warnings.add(warning);
  }

  Future<void> maybeReportErrors(Resolver resolver) async {
    if (_warnings.isNotEmpty) {
      final buildWarnings = await Future.wait(
          _warnings.map((warning) => warning.resolve(resolver)));
      buildWarnings.forEach((buildWarning) => logWarning(buildWarning.message));
    }
    if (_errors.isEmpty) {
      return;
    }
    final buildErrors =
        await Future.wait(_errors.map((error) => error.resolve(resolver)));
    throw BuildError(
        buildErrors.map((buildError) => buildError.message).join('\n\n'));
  }
}

Future<ElementDeclarationResult> _resolvedClassResult(
    Resolver resolver, Element element) async {
  var newLibraryElement =
      await resolver.libraryFor(await resolver.assetIdForElement(element));
  var libraryResult = await newLibraryElement.session
      .getResolvedLibraryByElement(newLibraryElement);
  if (libraryResult.state == ResultState.NOT_A_FILE) {
    // We don't have access to source information in summarized libraries,
    // but another build step will likely emit the root cause errors.
    throw BuildError('Analysis errors in summarized library '
        '${newLibraryElement.source.fullName}');
  }
  return libraryResult.getElementDeclaration(element);
}

class AsyncBuildError extends BuildError {
  AsyncBuildError([String message]) : super(message);

  Future<BuildError> resolve(Resolver resolver) => Future.value(this);
}

class AngularAnalysisError extends AsyncBuildError {
  final List<AnalysisError> constantEvaluationErrors;
  final IndexedAnnotation<Element> indexedAnnotation;

  AngularAnalysisError(this.constantEvaluationErrors, this.indexedAnnotation);

  @override
  Future<BuildError> resolve(Resolver resolver) async {
    final annotationSource = indexedAnnotation.annotation.toSource();

    var hasOffsetInformation =
        constantEvaluationErrors.any((error) => error.offset != 0);
    // If this code is called from a tool using [AnalysisResolvers], then
    //   1) [constantEvaluationErrors] already has source location information
    //   2) [ResolvedLibraryResultImpl] will NOT have the errors.
    // So, we return immediately with the information in
    // [constantEvaluationErrors].
    if (hasOffsetInformation) {
      return Future.value(_buildErrorForAnalysisErrors(constantEvaluationErrors,
          indexedAnnotation.element, annotationSource));
    }

    ElementDeclarationResult result;
    try {
      result = await _resolvedClassResult(resolver, indexedAnnotation.element);
    } on BuildError catch (buildError) {
      return BuildError(
          '${buildError.message} while compiling $annotationSource');
    }

    var resolvedMetadata =
        _metadataWithWorkaround(result.node, indexedAnnotation.element);

    if (resolvedMetadata == null) {
      return BuildError.forElement(indexedAnnotation.element, message);
    }

    var resolvedAnnotation =
        resolvedMetadata[indexedAnnotation.annotationIndex];

    // Only include the errors that are inside the annotation.
    return _buildErrorForAnalysisErrors(
        result.resolvedUnit.errors.where((error) =>
            error.offset >= resolvedAnnotation.offset &&
            error.offset <= resolvedAnnotation.end),
        indexedAnnotation.element,
        annotationSource);
  }

  BuildError _buildErrorForAnalysisErrors(Iterable<AnalysisError> errors,
      Element element, String annnotationSouce) {
    String reason;
    if (element is ClassElement && annnotationSouce.startsWith('@Component')) {
      reason = ''
          'Compiling @Component-annotated class "${element.name}" '
          'failed.\n\n${messages.analysisFailureReasons}';
    } else {
      reason =
          'Compiling annotation $annnotationSouce on element $element failed.';
    }

    return BuildError(
      messages.unresolvedSource(
        errors.map((e) {
          final sourceUrl = e.source.uri;
          final sourceContent = e.source.contents.data;

          if (sourceContent.isEmpty) {
            return SourceSpanMessageTuple(
                SourceSpan(
                    SourceLocation(0, sourceUrl: sourceUrl, line: 0, column: 0),
                    SourceLocation(0, sourceUrl: sourceUrl, line: 0, column: 0),
                    ''),
                '${e.message} [with offsets into source file missing]');
          }

          return SourceSpanMessageTuple(
            sourceSpanWithLineInfo(
              e.offset,
              e.length,
              sourceContent,
              sourceUrl,
            ),
            e.message,
          );
        }),
        reason: reason,
      ),
    );
  }
}

class UnresolvedExpressionError extends AsyncBuildError {
  final Iterable<AstNode> expressions;
  final ClassElement componentType;
  final CompilationUnitElement compilationUnit;

  UnresolvedExpressionError(
      this.expressions, this.componentType, this.compilationUnit);

  @override
  Future<BuildError> resolve(Resolver resolver) =>
      Future.value(_buildErrorForUnresolvedExpressions(
          expressions, componentType, compilationUnit));

  // TODO(deboer): Since we are checking ElementAnnotation.constantValueErrors,
  // all code paths that call this function are unreachable.
  // If we don't see any errors in the wild, delete this code.
  BuildError _buildErrorForUnresolvedExpressions(Iterable<AstNode> expressions,
      ClassElement componentType, CompilationUnitElement compilationUnit) {
    return BuildError(
      messages.unresolvedSource(
        expressions.map((e) {
          return SourceSpanMessageTuple(
            sourceSpanWithLineInfo(
              e.offset,
              e.length,
              componentType.source.contents.data,
              componentType.source.uri,
            ),
            'This argument *may* have not been resolved',
          );
        }),
        reason: ''
            'Compiling @Component annotated class "${componentType.name}" '
            'failed.\n'
            'NOTE: Your build triggered an error in the Angular error reporting\n'
            'code. Please report a bug: ${messages.urlFileBugs}\n'
            '\n\n${messages.analysisFailureReasons}',
      ),
    );
  }
}

class UnusedDirectiveTypeError extends ErrorMessageForAnnotation {
  final ClassElement element;
  final CompileTypedMetadata directiveType;

  static IndexedAnnotation firstComponentAnnotation(ClassElement element) {
    final index = element.metadata.indexWhere(isComponent);
    if (index == -1) {
      throw ArgumentError('[element] must have a @Component annotation');
    }
    return IndexedAnnotation(element, element.metadata[index], index);
  }

  UnusedDirectiveTypeError(this.element, this.directiveType)
      : super(
            firstComponentAnnotation(element),
            'Entry in "directiveTypes" missing corresponding entry in '
            '"directives" for "${directiveType.name}".\n\n'
            'If you recently removed "${directiveType.name}" from "directives", '
            'please also remove its corresponding entry from "directiveTypes".');
}

/// Find the ancestor node that should have the metadata and return
/// that metadata.
/// Angular only looks at metadata on class declarations,
/// class members and formal parameters.
List<Annotation> _metadataFromAncestry(AstNode node) {
// NOTE: We check for [ClassMember] or [ClassDeclaration] explicitly
// as some [AnnotatedNode]s in the ancestor chain do not have
// the metadata we are looking for.  See
// 519_missing_query_selector_test.dart for an example of this condition.
  if (node is ClassMember ||
      node is ClassDeclaration ||
      node is EnumDeclaration ||
      node is FunctionDeclaration) {
    return (node as AnnotatedNode).metadata;
  } else if (node is FormalParameter) {
    return node.metadata;
  }
  return _metadataFromAncestry(node.parent);
}

List<Annotation> _metadataWithWorkaround(
    AstNode node, Element originalElement) {
  var resolvedMetadata = _metadataFromAncestry(node);

  // TODO(b/124524319): Remove this check when the Analyzer is fixed.
  if (resolvedMetadata.isEmpty &&
      originalElement is ParameterElement &&
      node is ConstructorDeclaration) {
    return null;
  }
  return resolvedMetadata;
}

class ErrorMessageForAnnotation extends AsyncBuildError {
  final IndexedAnnotation indexedAnnotation;

  ErrorMessageForAnnotation(
    this.indexedAnnotation,
    String message,
  ) : super(message);

  @override
  Future<BuildError> resolve(Resolver resolver) async {
    final annotationIndex = indexedAnnotation.annotationIndex;

    ElementDeclarationResult result;
    try {
      result = await _resolvedClassResult(resolver, indexedAnnotation.element);
    } on BuildError catch (buildError) {
      return buildError;
    }

    var resolvedMetadata =
        _metadataWithWorkaround(result.node, indexedAnnotation.element);

    if (resolvedMetadata == null) {
      return BuildError.forElement(indexedAnnotation.element, message);
    }

    var resolvedAnnotation =
        resolvedMetadata[annotationIndex].elementAnnotation;
    return BuildError.forAnnotation(resolvedAnnotation, message);
  }
}

class ErrorMessageForElement extends AsyncBuildError {
  final Element element;

  ErrorMessageForElement(this.element, String message) : super(message);

  @override
  Future<BuildError> resolve(Resolver resolver) =>
      Future.value(BuildError.forElement(element, message));
}
