// Copyright (c) 2018, 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/src/dart/ast/token.dart';
import 'package:analyzer/src/summary/api_signature.dart';

/// Return the bytes of the unlinked API signature of the given [unit].
///
/// If API signatures of two units are different, they may have different APIs.
List<int> computeUnlinkedApiSignature(CompilationUnit unit) {
  var computer = new _UnitApiSignatureComputer();
  computer.compute(unit);
  return computer.signature.toByteList();
}

class _UnitApiSignatureComputer {
  final signature = new ApiSignature();

  void addClassOrMixin(ClassOrMixinDeclaration node) {
    addTokens(node.beginToken, node.leftBracket);

    bool hasConstConstructor = node.members
        .any((m) => m is ConstructorDeclaration && m.constKeyword != null);

    signature.addInt(node.members.length);
    for (var member in node.members) {
      if (member is ConstructorDeclaration) {
        var lastInitializer = member.constKeyword != null &&
                member.initializers != null &&
                member.initializers.isNotEmpty
            ? member.initializers.last
            : null;
        addTokens(
          member.beginToken,
          (lastInitializer ?? member.parameters ?? member.name).endToken,
        );
      } else if (member is FieldDeclaration) {
        var variableList = member.fields;
        addVariables(
          member,
          variableList,
          !member.isStatic && variableList.isFinal && hasConstConstructor,
        );
      } else if (member is MethodDeclaration) {
        addTokens(
          member.beginToken,
          (member.parameters ?? member.name).endToken,
        );
      } else {
        addNode(member);
      }
    }

    addToken(node.rightBracket);
  }

  void addNode(AstNode node) {
    addTokens(node.beginToken, node.endToken);
  }

  void addToken(Token token) {
    signature.addString(token.lexeme);
  }

  /// Appends tokens from [begin] (including), to [end] (also including).
  void addTokens(Token begin, Token end) {
    if (begin is CommentToken) {
      begin = (begin as CommentToken).parent;
    }
    Token token = begin;
    while (token != null) {
      addToken(token);
      if (token == end) {
        break;
      }
      token = token.next;
    }
  }

  void addVariables(
    AstNode node,
    VariableDeclarationList variableList,
    bool includeInitializers,
  ) {
    if (variableList.type == null ||
        variableList.isConst ||
        includeInitializers) {
      addTokens(node.beginToken, node.endToken);
    } else {
      addTokens(node.beginToken, variableList.type.endToken);

      signature.addInt(variableList.variables.length);
      for (var variable in variableList.variables) {
        addTokens(variable.beginToken, variable.name.endToken);
        addToken(variable.endToken.next); // `,` or `;`
      }
    }
  }

  void compute(CompilationUnit unit) {
    signature.addInt(unit.directives.length);
    unit.directives.forEach(addNode);

    signature.addInt(unit.declarations.length);
    for (var declaration in unit.declarations) {
      if (declaration is ClassOrMixinDeclaration) {
        addClassOrMixin(declaration);
      } else if (declaration is FunctionDeclaration) {
        var parameters = declaration.functionExpression.parameters;
        addTokens(
          declaration.beginToken,
          (parameters ?? declaration.name).endToken,
        );
      } else if (declaration is TopLevelVariableDeclaration) {
        addVariables(declaration, declaration.variables, false);
      } else {
        addNode(declaration);
      }
    }
  }
}
