// Copyright (c) 2017, 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 '../messages/codes.dart' as codes;
import '../scanner/token.dart'
    show Keyword, Token, TokenIsAExtension, TokenType;
import 'formal_parameter_kind.dart';
import 'member_kind.dart' show MemberKind;
import 'parser_impl.dart' show Parser;

bool isModifier(Token token) {
  if (!token.isModifier) {
    return false;
  } else if (token.type.isBuiltIn) {
    // A built-in keyword can only be a modifier as long as it is
    // followed by another keyword or an identifier. Otherwise, it is the
    // identifier.
    //
    // For example, `external` is a modifier in this declaration:
    //   external Foo foo();
    // but is the identifier in this declaration
    //   external() => true;
    // and in
    //   for (final external in list) { }
    Token next = token.next!;
    Keyword? keyword = next.keyword;
    if (keyword == null && !next.isIdentifier || keyword == Keyword.IN) {
      // Record type is a possibility.
      if (next.isA(TokenType.OPEN_PAREN)) {
        Token afterGroup = next.endGroup!.next!;
        if (afterGroup.isIdentifier || _thisOrSuperWithDot(afterGroup)) {
          // We've seen either
          // [modifier] [record type] [identifier], or
          // [modifier] [record type] `this` `.`, or
          // [modifier] [record type] `super` `.`
          return true;
        } else if (afterGroup.isA(TokenType.QUESTION) &&
            (afterGroup.next!.isIdentifier ||
                _thisOrSuperWithDot(afterGroup.next!))) {
          // We've seen either
          // [modifier] [record type] `?` [identifier], or
          // [modifier] [record type] `?` `this` `.`, or
          // [modifier] [record type] `?` `super` `.`
          return true;
        }
      }
      return false;
    }
  }
  return true;
}

bool _thisOrSuperWithDot(Token token) {
  if (token.isA(Keyword.THIS) || token.isA(Keyword.SUPER)) {
    return token.next!.isA(TokenType.PERIOD);
  }
  return false;
}

/// This class is used to parse modifiers in most locations where modifiers
/// can occur, but does not call handleModifier or handleModifiers.
class ModifierContext {
  final Parser parser;
  Token? abstractToken;
  Token? augmentToken;
  Token? constToken;
  Token? covariantToken;
  Token? externalToken;
  Token? finalToken;
  Token? lateToken;
  Token? requiredToken;
  Token? staticToken;
  Token? varToken;

  // Set `true` when parsing modifiers after the `factory` token.
  bool _afterFactory = false;

  ModifierContext(this.parser);

  set staticOrCovariant(Token? staticOrCovariant) {
    if (staticOrCovariant == null) {
      covariantToken = null;
      staticToken = null;
    } else if (staticOrCovariant.isA(Keyword.COVARIANT)) {
      covariantToken = staticOrCovariant;
      staticToken = null;
    } else if (staticOrCovariant.isA(Keyword.STATIC)) {
      covariantToken = null;
      staticToken = staticOrCovariant;
    } else {
      throw "Internal error: "
          "Unexpected staticOrCovariant '$staticOrCovariant'.";
    }
  }

  Token? get varFinalOrConst => varToken ?? finalToken ?? constToken;

  set varFinalOrConst(Token? varFinalOrConst) {
    if (varFinalOrConst == null) {
      varToken = null;
      finalToken = null;
      constToken = null;
    } else if (varFinalOrConst.isA(Keyword.VAR)) {
      varToken = varFinalOrConst;
      finalToken = null;
      constToken = null;
    } else if (varFinalOrConst.isA(Keyword.FINAL)) {
      varToken = null;
      finalToken = varFinalOrConst;
      constToken = null;
    } else if (varFinalOrConst.isA(Keyword.CONST)) {
      varToken = null;
      finalToken = null;
      constToken = varFinalOrConst;
    } else {
      throw "Internal error: Unexpected varFinalOrConst '$varFinalOrConst'.";
    }
  }

  /// Parse modifiers for class declarations.
  Token parseClassModifiers(Token token, Token keyword) {
    token = _parseModifiers(token);
    if (constToken != null) {
      reportTopLevelModifierError(constToken!, keyword);
    }
    if (externalToken != null) {
      reportTopLevelModifierError(externalToken!, keyword);
    }
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(varToken);
    return token;
  }

  /// Parse modifiers for enum declarations.
  Token parseEnumModifiers(Token token, Token keyword) {
    token = _parseModifiers(token);
    reportTopLevelModifierError(constToken, keyword);
    reportTopLevelModifierError(externalToken, keyword);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(varToken);
    return token;
  }

  /// Parse modifiers for extension declarations.
  Token parseExtensionModifiers(Token token, Token keyword) {
    token = _parseModifiers(token);
    reportTopLevelModifierError(constToken, keyword);
    reportTopLevelModifierError(externalToken, keyword);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(varToken);
    return token;
  }

  /// Parse modifiers for mixin declarations.
  Token parseMixinModifiers(Token token, Token keyword) {
    token = _parseModifiers(token);
    reportTopLevelModifierError(constToken, keyword);
    reportTopLevelModifierError(externalToken, keyword);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(varToken);
    return token;
  }

  /// Parse modifiers for library, import, export, part (of) directives and
  /// typedef and extension declarations.
  Token parseTopLevelKeywordModifiers(Token token, Token keyword) {
    token = _parseModifiers(token);
    reportTopLevelModifierError(constToken, keyword);
    reportTopLevelModifierError(externalToken, keyword);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(augmentToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(finalToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(varToken);
    return token;
  }

  /// Parse modifiers for class methods and fields.
  Token parseClassMemberModifiers(Token token) {
    token = _parseModifiers(token);
    reportExtraneousModifier(requiredToken);
    return token;
  }

  /// Parse modifiers for formal parameters.
  Token parseFormalParameterModifiers(
    Token token,
    FormalParameterKind parameterKind,
    MemberKind memberKind,
  ) {
    token = _parseModifiers(token);

    if (parameterKind != FormalParameterKind.optionalNamed) {
      reportExtraneousModifier(requiredToken);
    }
    switch (memberKind) {
      case MemberKind.StaticMethod:
      case MemberKind.TopLevelMethod:
        reportExtraneousModifier(this.covariantToken);
      case MemberKind.ExtensionNonStaticMethod:
      case MemberKind.ExtensionStaticMethod:
        reportExtraneousModifierInExtension(this.covariantToken);
      case MemberKind.ExtensionTypeNonStaticMethod:
      case MemberKind.ExtensionTypeStaticMethod:
        reportExtraneousModifierInExtensionType(this.covariantToken);
      case MemberKind.PrimaryConstructor:
        reportExtraneousModifierInPrimaryConstructor(this.covariantToken);
      case MemberKind.Catch:
      case MemberKind.Factory:
      case MemberKind.FunctionTypeAlias:
      case MemberKind.FunctionTypedParameter:
      case MemberKind.GeneralizedFunctionType:
      case MemberKind.Local:
      case MemberKind.NonStaticMethod:
      case MemberKind.NonStaticField:
      case MemberKind.StaticField:
      case MemberKind.TopLevelField:
    }
    if (constToken != null) {
      reportExtraneousModifier(constToken);
    } else if (memberKind == MemberKind.GeneralizedFunctionType) {
      if (varFinalOrConst != null) {
        parser.reportRecoverableError(
          varFinalOrConst!,
          codes.codeFunctionTypedParameterVar,
        );
      }
    }
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(externalToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(staticToken);
    return token;
  }

  /// Parse modifiers for library directives.
  Token parseLibraryDirectiveModifiers(Token token, Token keyword) {
    token = _parseModifiers(token);
    reportTopLevelModifierError(constToken, keyword);
    reportTopLevelModifierError(externalToken, keyword);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(finalToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(varToken);
    return token;
  }

  /// Parse modifiers after the `factory` token.
  Token parseModifiersAfterFactory(Token token) {
    _afterFactory = true;
    token = _parseModifiers(token);
    if (abstractToken != null) {
      parser.reportRecoverableError(
        abstractToken!,
        codes.codeAbstractClassMember,
      );
    }
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    return token;
  }

  /// Parse modifiers for top level functions and fields.
  Token parseTopLevelMemberModifiers(Token token) {
    token = _parseModifiers(token);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    return token;
  }

  Token parseTypedefModifiers(Token token, Token keyword) {
    token = _parseModifiers(token);
    reportTopLevelModifierError(constToken, keyword);
    reportTopLevelModifierError(externalToken, keyword);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(finalToken);
    reportExtraneousModifier(lateToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(varToken);
    return token;
  }

  /// Parse modifiers for variable declarations.
  Token parseVariableDeclarationModifiers(Token token) {
    token = _parseModifiers(token);
    reportExtraneousModifier(abstractToken);
    reportExtraneousModifier(covariantToken);
    reportExtraneousModifier(externalToken);
    reportExtraneousModifier(requiredToken);
    reportExtraneousModifier(staticToken);
    reportExtraneousModifier(augmentToken);
    return token;
  }

  /// Parse modifiers during recovery when modifiers are out of order
  /// or invalid. Typically clients call methods like
  /// [parseClassMemberModifiers] which in turn calls this method,
  /// rather than calling this method directly.
  ///
  /// The various modifier token parameters represent tokens of modifiers
  /// that have already been parsed prior to recovery. The [staticOrCovariant]
  /// parameter is for convenience if caller has a token that may be either
  /// `static` or `covariant`. The first non-null parameter of
  /// [staticOrCovariant], [staticToken], or [covariantToken] will be used,
  /// in that order, and the others ignored.
  Token _parseModifiers(Token token) {
    // Process invalid and out-of-order modifiers
    Token next = token.next!;
    while (true) {
      final String? value = next.stringValue;
      if (isModifier(next)) {
        if (identical('abstract', value)) {
          token = _parseAbstract(token);
        } else if (identical('augment', value)) {
          token = _parseAugment(token);
        } else if (identical('const', value)) {
          token = _parseConst(token);
        } else if (identical('covariant', value)) {
          token = _parseCovariant(token);
        } else if (identical('external', value)) {
          token = _parseExternal(token);
        } else if (identical('final', value)) {
          token = _parseFinal(token);
        } else if (identical('late', value)) {
          token = _parseLate(token);
        } else if (identical('required', value)) {
          token = _parseRequired(token);
        } else if (identical('static', value)) {
          token = _parseStatic(token);
        } else if (identical('var', value)) {
          token = _parseVar(token);
        } else {
          throw 'Internal Error: Unhandled modifier: $value';
        }
      } else if (_afterFactory && identical('factory', value)) {
        parser.reportRecoverableErrorWithToken(
          next,
          codes.codeDuplicatedModifier,
        );
        token = next;
      } else {
        break;
      }
      next = token.next!;
    }
    return token;
  }

  Token _parseAbstract(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.ABSTRACT));
    if (abstractToken == null) {
      abstractToken = next;

      if (varFinalOrConst != null) {
        reportModifierOutOfOrder(next, varFinalOrConst!.lexeme);
      } else if (covariantToken != null) {
        reportModifierOutOfOrder(next, covariantToken!.lexeme);
      }
      return next;
    }

    // Recovery
    parser.reportRecoverableErrorWithToken(next, codes.codeDuplicatedModifier);
    return next;
  }

  Token _parseAugment(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.AUGMENT));
    if (augmentToken == null) {
      augmentToken = next;

      if (varFinalOrConst != null) {
        reportModifierOutOfOrder(next, varFinalOrConst!.lexeme);
      } else if (abstractToken != null) {
        reportModifierOutOfOrder(next, abstractToken!.lexeme);
      } else if (constToken != null) {
        reportModifierOutOfOrder(next, constToken!.lexeme);
      } else if (covariantToken != null) {
        reportModifierOutOfOrder(next, covariantToken!.lexeme);
      } else if (finalToken != null) {
        reportModifierOutOfOrder(next, finalToken!.lexeme);
      } else if (lateToken != null) {
        reportModifierOutOfOrder(next, lateToken!.lexeme);
      } else if (staticToken != null) {
        reportModifierOutOfOrder(next, staticToken!.lexeme);
      } else if (externalToken != null) {
        reportConflictingModifiers(next, externalToken!);
      }
      return next;
    }

    // Recovery
    parser.reportRecoverableErrorWithToken(next, codes.codeDuplicatedModifier);
    return next;
  }

  Token _parseConst(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.CONST));
    if (varFinalOrConst == null && covariantToken == null) {
      constToken = next;

      if (_afterFactory) {
        reportModifierOutOfOrder(next, 'factory');
      } else if (lateToken != null) {
        reportConflictingModifiers(next, lateToken!);
      }
      return next;
    }

    // Recovery
    if (constToken != null) {
      parser.reportRecoverableErrorWithToken(
        next,
        codes.codeDuplicatedModifier,
      );
    } else if (covariantToken != null) {
      reportConflictingModifiers(next, covariantToken!);
    } else if (finalToken != null) {
      parser.reportRecoverableError(next, codes.codeConstAndFinal);
    } else if (varToken != null) {
      reportConflictingModifiers(next, varToken!);
    } else {
      throw 'Internal Error: Unexpected varFinalOrConst: $varFinalOrConst';
    }
    return next;
  }

  Token _parseCovariant(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.COVARIANT));
    if (constToken == null &&
        covariantToken == null &&
        staticToken == null &&
        !_afterFactory) {
      covariantToken = next;

      if (varToken != null) {
        reportModifierOutOfOrder(next, varToken!.lexeme);
      } else if (finalToken != null) {
        reportModifierOutOfOrder(next, finalToken!.lexeme);
      } else if (lateToken != null) {
        reportModifierOutOfOrder(next, lateToken!.lexeme);
      }
      return next;
    }

    // Recovery
    if (covariantToken != null) {
      parser.reportRecoverableErrorWithToken(
        next,
        codes.codeDuplicatedModifier,
      );
    } else if (_afterFactory) {
      reportExtraneousModifier(next);
    } else if (constToken != null) {
      reportConflictingModifiers(next, constToken!);
    } else if (staticToken != null) {
      parser.reportRecoverableError(next, codes.codeCovariantAndStatic);
    } else {
      throw 'Internal Error: Unhandled recovery: $next';
    }
    return next;
  }

  Token _parseExternal(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.EXTERNAL));
    if (externalToken == null) {
      externalToken = next;

      if (_afterFactory) {
        reportModifierOutOfOrder(next, 'factory');
      } else if (constToken != null) {
        reportModifierOutOfOrder(next, constToken!.lexeme);
      } else if (staticToken != null) {
        reportModifierOutOfOrder(next, staticToken!.lexeme);
      } else if (lateToken != null) {
        reportModifierOutOfOrder(next, lateToken!.lexeme);
      } else if (varFinalOrConst != null) {
        reportModifierOutOfOrder(next, varFinalOrConst!.lexeme);
      } else if (covariantToken != null) {
        reportModifierOutOfOrder(next, covariantToken!.lexeme);
      } else if (augmentToken != null) {
        reportConflictingModifiers(next, augmentToken!);
      }
      return next;
    }

    // Recovery
    parser.reportRecoverableErrorWithToken(next, codes.codeDuplicatedModifier);
    return next;
  }

  Token _parseFinal(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.FINAL));
    if (varFinalOrConst == null && !_afterFactory) {
      finalToken = next;
      return next;
    }

    // Recovery
    if (finalToken != null) {
      parser.reportRecoverableErrorWithToken(
        next,
        codes.codeDuplicatedModifier,
      );
    } else if (_afterFactory) {
      reportExtraneousModifier(next);
    } else if (constToken != null) {
      parser.reportRecoverableError(next, codes.codeConstAndFinal);
    } else if (varToken != null) {
      parser.reportRecoverableError(next, codes.codeFinalAndVar);
    } else if (lateToken != null) {
      reportModifierOutOfOrder(next, lateToken!.lexeme);
    } else {
      throw 'Internal Error: Unexpected varFinalOrConst: $varFinalOrConst';
    }
    return next;
  }

  Token _parseLate(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.LATE));
    if (lateToken == null) {
      lateToken = next;

      if (constToken != null) {
        reportConflictingModifiers(next, constToken!);
      } else if (varToken != null) {
        reportModifierOutOfOrder(next, varToken!.lexeme);
      } else if (finalToken != null) {
        reportModifierOutOfOrder(next, finalToken!.lexeme);
      }
      return next;
    }

    // Recovery
    parser.reportRecoverableErrorWithToken(next, codes.codeDuplicatedModifier);
    return next;
  }

  Token _parseRequired(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.REQUIRED));
    if (requiredToken == null) {
      requiredToken = next;

      if (constToken != null) {
        reportModifierOutOfOrder(requiredToken!, constToken!.lexeme);
      } else if (covariantToken != null) {
        reportModifierOutOfOrder(requiredToken!, covariantToken!.lexeme);
      } else if (finalToken != null) {
        reportModifierOutOfOrder(requiredToken!, finalToken!.lexeme);
      } else if (varToken != null) {
        reportModifierOutOfOrder(requiredToken!, varToken!.lexeme);
      }
      return next;
    }

    // Recovery
    parser.reportRecoverableErrorWithToken(next, codes.codeDuplicatedModifier);
    return next;
  }

  Token _parseStatic(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.STATIC));
    if (covariantToken == null && staticToken == null && !_afterFactory) {
      staticToken = next;

      if (constToken != null) {
        reportModifierOutOfOrder(next, constToken!.lexeme);
      } else if (finalToken != null) {
        reportModifierOutOfOrder(next, finalToken!.lexeme);
      } else if (varToken != null) {
        reportModifierOutOfOrder(next, varToken!.lexeme);
      } else if (lateToken != null) {
        reportModifierOutOfOrder(next, lateToken!.lexeme);
      }
      return next;
    }

    // Recovery
    if (covariantToken != null) {
      parser.reportRecoverableError(next, codes.codeCovariantAndStatic);
    } else if (staticToken != null) {
      parser.reportRecoverableErrorWithToken(
        next,
        codes.codeDuplicatedModifier,
      );
    } else if (_afterFactory) {
      reportExtraneousModifier(next);
    } else {
      throw 'Internal Error: Unhandled recovery: $next';
    }
    return next;
  }

  Token _parseVar(Token token) {
    Token next = token.next!;
    assert(next.isA(Keyword.VAR));
    if (varFinalOrConst == null && !_afterFactory) {
      varToken = next;
      return next;
    }

    // Recovery
    if (varToken != null) {
      parser.reportRecoverableErrorWithToken(
        next,
        codes.codeDuplicatedModifier,
      );
    } else if (_afterFactory) {
      reportExtraneousModifier(next);
    } else if (constToken != null) {
      reportConflictingModifiers(next, constToken!);
    } else if (finalToken != null) {
      parser.reportRecoverableError(next, codes.codeFinalAndVar);
    } else {
      throw 'Internal Error: Unexpected varFinalOrConst: $varFinalOrConst';
    }
    return next;
  }

  void reportConflictingModifiers(Token modifier, Token earlierModifier) {
    parser.reportRecoverableError(
      modifier,
      codes.codeConflictingModifiers.withArgumentsOld(
        modifier.lexeme,
        earlierModifier.lexeme,
      ),
    );
  }

  void reportExtraneousModifier(Token? modifier) {
    if (modifier != null) {
      parser.reportRecoverableErrorWithToken(
        modifier,
        codes.codeExtraneousModifier,
      );
    }
  }

  // Report an error for the given modifier preceding a top level keyword
  // such as `import` or `class`.
  void reportTopLevelModifierError(Token? modifier, Token afterModifiers) {
    if (modifier != null) {
      if (modifier.isA(Keyword.CONST) && afterModifiers.isA(Keyword.CLASS)) {
        parser.reportRecoverableError(modifier, codes.codeConstClass);
      } else if (modifier.isA(Keyword.EXTERNAL)) {
        if (afterModifiers.isA(Keyword.CLASS)) {
          parser.reportRecoverableError(modifier, codes.codeExternalClass);
        } else if (afterModifiers.isA(Keyword.ENUM)) {
          parser.reportRecoverableError(modifier, codes.codeExternalEnum);
        } else if (afterModifiers.isA(Keyword.TYPEDEF)) {
          parser.reportRecoverableError(modifier, codes.codeExternalTypedef);
        } else {
          parser.reportRecoverableErrorWithToken(
            modifier,
            codes.codeExtraneousModifier,
          );
        }
      } else {
        parser.reportRecoverableErrorWithToken(
          modifier,
          codes.codeExtraneousModifier,
        );
      }
    }
  }

  void reportExtraneousModifierInExtension(Token? modifier) {
    if (modifier != null) {
      parser.reportRecoverableErrorWithToken(
        modifier,
        codes.codeExtraneousModifierInExtension,
      );
    }
  }

  void reportExtraneousModifierInExtensionType(Token? modifier) {
    if (modifier != null) {
      parser.reportRecoverableErrorWithToken(
        modifier,
        codes.codeExtraneousModifierInExtensionType,
      );
    }
  }

  void reportExtraneousModifierInPrimaryConstructor(Token? modifier) {
    if (modifier != null) {
      parser.reportRecoverableErrorWithToken(
        modifier,
        codes.codeExtraneousModifierInPrimaryConstructor,
      );
    }
  }

  void reportModifierOutOfOrder(Token modifier, String beforeModifier) {
    parser.reportRecoverableError(
      modifier,
      codes.codeModifierOutOfOrder.withArgumentsOld(
        modifier.lexeme,
        beforeModifier,
      ),
    );
  }
}
