// Copyright (c) 2023, 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:math' as math;

import 'package:analyzer/dart/ast/doc_comment.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/error/codes.g.dart';

/// Verifies various data parsed in doc comments.
class DocCommentVerifier {
  final ErrorReporter _errorReporter;

  DocCommentVerifier(this._errorReporter);

  void docDirective(DocDirective docDirective) {
    switch (docDirective) {
      case SimpleDocDirective():
        docDirectiveTag(docDirective.tag);
      case BlockDocDirective(:var openingTag, :var closingTag):
        docDirectiveTag(openingTag);
        if (closingTag != null) {
          docDirectiveTag(closingTag);
        }
    }
  }

  void docDirectiveTag(DocDirectiveTag tag) {
    validateArgumentCount(tag);
    validateArgumentFormat(tag);
  }

  /// Verifies doc imports, written as `@docImport`.
  void docImport(DocImport docImport) {
    var deferredKeyword = docImport.import.deferredKeyword;
    if (deferredKeyword != null) {
      _errorReporter.atToken(
        deferredKeyword,
        WarningCode.DOC_IMPORT_CANNOT_BE_DEFERRED,
      );
    }
    var configurations = docImport.import.configurations;
    if (configurations.isNotEmpty) {
      _errorReporter.atOffset(
        offset: configurations.first.offset,
        length: configurations.last.end - configurations.first.offset,
        errorCode: WarningCode.DOC_IMPORT_CANNOT_HAVE_CONFIGURATIONS,
      );
    }

    // TODO(srawlins): Support combinators.
    var combinators = docImport.import.combinators;
    if (combinators.isNotEmpty) {
      _errorReporter.atOffset(
        offset: combinators.first.offset,
        length: combinators.last.end - combinators.first.offset,
        errorCode: WarningCode.DOC_IMPORT_CANNOT_HAVE_COMBINATORS,
      );
    }

    // TODO(srawlins): Support prefixes. This was done temporarily with
    // https://dart-review.googlesource.com/c/sdk/+/387861, but this was
    // reverted as it increased memory usage.
    var prefix = docImport.import.prefix;
    if (prefix != null) {
      _errorReporter.atOffset(
        offset: prefix.offset,
        length: prefix.end - prefix.offset,
        errorCode: WarningCode.DOC_IMPORT_CANNOT_HAVE_PREFIX,
      );
    }
  }

  void validateArgumentCount(DocDirectiveTag tag) {
    var positionalArgumentCount = tag.positionalArguments.length;
    var required = tag.type.positionalParameters;
    var requiredCount = tag.type.positionalParameters.length;

    if (positionalArgumentCount < requiredCount) {
      var gap = requiredCount - positionalArgumentCount;
      if (gap == 1) {
        _errorReporter.atOffset(
          offset: tag.offset,
          length: tag.end - tag.offset,
          errorCode: WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT,
          arguments: [tag.type.name, required.last.name],
        );
      } else if (gap == 2) {
        var missingArguments = [
          required[required.length - 2].name,
          required.last.name,
        ];
        _errorReporter.atOffset(
          offset: tag.offset,
          length: tag.end - tag.offset,
          errorCode: WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS,
          arguments: [tag.type.name, ...missingArguments],
        );
      } else if (gap == 3) {
        var missingArguments = [
          required[required.length - 3].name,
          required[required.length - 2].name,
          required.last.name,
        ];
        _errorReporter.atOffset(
          offset: tag.offset,
          length: tag.end - tag.offset,
          errorCode: WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS,
          arguments: [tag.type.name, ...missingArguments],
        );
      }
    }

    if (tag.type.restParametersAllowed) {
      // TODO(srawlins): We probably want to enforce that at least one argument
      // is given, particularly for 'category' and 'subCategory'.
      return;
    }

    if (positionalArgumentCount > requiredCount) {
      var errorOffset = tag.positionalArguments[requiredCount].offset;
      var errorLength = tag.positionalArguments.last.end - errorOffset;
      _errorReporter.atOffset(
        offset: errorOffset,
        length: errorLength,
        errorCode: WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS,
        arguments: [tag.type.name, positionalArgumentCount, requiredCount],
      );
    }

    for (var namedArgument in tag.namedArguments) {
      if (!tag.type.namedParameters.containsNamed(namedArgument.name)) {
        _errorReporter.atOffset(
          offset: namedArgument.offset,
          length: namedArgument.end - namedArgument.offset,
          errorCode: WarningCode.DOC_DIRECTIVE_HAS_UNEXPECTED_NAMED_ARGUMENT,
          arguments: [tag.type.name, namedArgument.name],
        );
      }
    }
  }

  void validateArgumentFormat(DocDirectiveTag tag) {
    var required = tag.type.positionalParameters;
    var positionalArgumentCount =
        math.min(tag.positionalArguments.length, required.length);
    for (var i = 0; i < positionalArgumentCount; i++) {
      var parameter = required[i];
      var argument = tag.positionalArguments[i];

      void reportWrongFormat() {
        _errorReporter.atOffset(
          offset: argument.offset,
          length: argument.end - argument.offset,
          errorCode: WarningCode.DOC_DIRECTIVE_ARGUMENT_WRONG_FORMAT,
          arguments: [parameter.name, parameter.expectedFormat.displayString],
        );
      }

      switch (parameter.expectedFormat) {
        case DocDirectiveParameterFormat.any:
          continue;
        case DocDirectiveParameterFormat.integer:
          if (int.tryParse(argument.value) == null) {
            reportWrongFormat();
          }
        case DocDirectiveParameterFormat.uri:
          if (Uri.tryParse(argument.value) == null) {
            reportWrongFormat();
          }
        case DocDirectiveParameterFormat.youtubeUrl:
          if (Uri.tryParse(argument.value) == null ||
              !argument.value
                  .startsWith(DocDirectiveParameterFormat.youtubeUrlPrefix)) {
            reportWrongFormat();
          }
      }
    }
  }
}

extension on List<DocDirectiveParameter> {
  bool containsNamed(String name) => any((p) => p.name == name);
}
