// 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 'class_hierarchy.dart';
import 'core_types.dart';
import 'kernel.dart';
import 'type_checker.dart' as type_checker;
import 'type_algebra.dart';

abstract class FailureListener {
  void reportFailure(TreeNode node, String message);
  void reportNotAssignable(TreeNode node, DartType first, DartType second);
  void reportInvalidOverride(Member member, Member inherited, String message);
}

class StrongModeTypeChecker extends type_checker.TypeChecker {
  final FailureListener failures;

  StrongModeTypeChecker(FailureListener failures, Component component,
      {bool ignoreSdk: false})
      : this._(
            failures,
            new CoreTypes(component),
            new ClassHierarchy(component,
                onAmbiguousSupertypes: (Class cls, Supertype s0, Supertype s1) {
              failures.reportFailure(
                  cls, "$cls can't implement both $s1 and $s1");
            }),
            ignoreSdk);

  StrongModeTypeChecker._(this.failures, CoreTypes coreTypes,
      ClassHierarchy hierarchy, bool ignoreSdk)
      : super(coreTypes, hierarchy, strongMode: true, ignoreSdk: ignoreSdk);

  // TODO(vegorov) this only gets called for immediate overrides which leads
  // to less strict checking that Dart 2.0 specification demands for covariant
  // parameters.
  @override
  void checkOverride(
      Class host, Member ownMember, Member superMember, bool isSetter) {
    final ownMemberIsFieldOrAccessor =
        ownMember is Field || (ownMember as Procedure).isAccessor;
    final superMemberIsFieldOrAccessor =
        superMember is Field || (superMember as Procedure).isAccessor;

    // TODO: move to error reporting code
    String _memberKind(Member m) {
      if (m is Field) {
        return 'field';
      } else {
        final p = m as Procedure;
        if (p.isGetter) {
          return 'getter';
        } else if (p.isSetter) {
          return 'setter';
        } else {
          return 'method';
        }
      }
    }

    // First check if we are overriding field/accessor with a normal method
    // or other way around.
    if (ownMemberIsFieldOrAccessor != superMemberIsFieldOrAccessor) {
      return failures.reportInvalidOverride(ownMember, superMember, '''
${ownMember} is a ${_memberKind(ownMember)}
${superMember} is a ${_memberKind(superMember)}
''');
    }

    if (ownMemberIsFieldOrAccessor) {
      if (isSetter) {
        final DartType ownType = setterType(host, ownMember);
        final DartType superType = setterType(host, superMember);
        final isCovariant = ownMember is Field
            ? ownMember.isCovariant
            : ownMember.function.positionalParameters[0].isCovariant;
        if (!_isValidParameterOverride(isCovariant, ownType, superType)) {
          if (isCovariant) {
            return failures.reportInvalidOverride(ownMember, superMember, '''
${ownType} is neither a subtype nor supertype of ${superType}
''');
          } else {
            return failures.reportInvalidOverride(ownMember, superMember, '''
${ownType} is not a subtype of ${superType}
''');
          }
        }
      } else {
        final DartType ownType = getterType(host, ownMember);
        final DartType superType = getterType(host, superMember);
        if (!environment.isSubtypeOf(ownType, superType)) {
          return failures.reportInvalidOverride(ownMember, superMember, '''
${ownType} is not a subtype of ${superType}
''');
        }
      }
    } else {
      final msg = _checkFunctionOverride(host, ownMember, superMember);
      if (msg != null) {
        return failures.reportInvalidOverride(ownMember, superMember, msg);
      }
    }
  }

  /// Check if [subtype] is subtype of [supertype] after applying
  /// type parameter [substitution].
  bool _isSubtypeOf(DartType subtype, DartType supertype) =>
      environment.isSubtypeOf(subtype, supertype);

  Substitution _makeSubstitutionForMember(Class host, Member member) {
    final hostType =
        hierarchy.getClassAsInstanceOf(host, member.enclosingClass);
    return Substitution.fromSupertype(hostType);
  }

  /// Check if function node [ownMember] is a valid override for [superMember].
  /// Returns [null] if override is valid or an error message.
  ///
  /// Note: this function is a copy of [SubtypeTester._isFunctionSubtypeOf]
  /// but it additionally accounts for parameter covariance.
  String _checkFunctionOverride(
      Class host, Member ownMember, Member superMember) {
    final FunctionNode ownFunction = ownMember.function;
    final FunctionNode superFunction = superMember.function;
    Substitution ownSubstitution = _makeSubstitutionForMember(host, ownMember);
    final Substitution superSubstitution =
        _makeSubstitutionForMember(host, superMember);

    if (ownFunction.requiredParameterCount >
        superFunction.requiredParameterCount) {
      return 'override has more required parameters';
    }
    if (ownFunction.positionalParameters.length <
        superFunction.positionalParameters.length) {
      return 'super method has more positional parameters';
    }
    if (ownFunction.typeParameters.length !=
        superFunction.typeParameters.length) {
      return 'methods have different type parameters counts';
    }

    if (ownFunction.typeParameters.isNotEmpty) {
      final typeParameterMap = <TypeParameter, DartType>{};
      for (int i = 0; i < ownFunction.typeParameters.length; ++i) {
        var subParameter = ownFunction.typeParameters[i];
        var superParameter = superFunction.typeParameters[i];
        typeParameterMap[subParameter] = new TypeParameterType(superParameter);
      }

      ownSubstitution = Substitution.combine(
          ownSubstitution, Substitution.fromMap(typeParameterMap));
      for (int i = 0; i < ownFunction.typeParameters.length; ++i) {
        var subParameter = ownFunction.typeParameters[i];
        var superParameter = superFunction.typeParameters[i];
        var subBound = ownSubstitution.substituteType(subParameter.bound);
        if (!_isSubtypeOf(
            superSubstitution.substituteType(superParameter.bound), subBound)) {
          return 'type parameters have incompatible bounds';
        }
      }
    }

    if (!_isSubtypeOf(ownSubstitution.substituteType(ownFunction.returnType),
        superSubstitution.substituteType(superFunction.returnType))) {
      return 'return type of override ${ownFunction
          .returnType} is not a subtype'
          ' of ${superFunction.returnType}';
    }

    for (int i = 0; i < superFunction.positionalParameters.length; ++i) {
      final ownParameter = ownFunction.positionalParameters[i];
      final superParameter = superFunction.positionalParameters[i];
      if (!_isValidParameterOverride(
          ownParameter.isCovariant,
          ownSubstitution.substituteType(ownParameter.type),
          superSubstitution.substituteType(superParameter.type))) {
        return '''
type of parameter ${ownParameter.name} is incompatible
override declares ${ownParameter.type}
super method declares ${superParameter.type}
''';
      }
    }

    if (superFunction.namedParameters.isEmpty) {
      return null;
    }

    // Note: FunctionNode.namedParameters are not sorted so we convert them
    // to map to make lookup faster.
    final ownParameters = new Map<String, VariableDeclaration>.fromIterable(
        ownFunction.namedParameters,
        key: (v) => v.name);
    for (VariableDeclaration superParameter in superFunction.namedParameters) {
      final ownParameter = ownParameters[superParameter.name];
      if (ownParameter == null) {
        return 'override is missing ${superParameter.name} parameter';
      }

      if (!_isValidParameterOverride(
          ownParameter.isCovariant,
          ownSubstitution.substituteType(ownParameter.type),
          superSubstitution.substituteType(superParameter.type))) {
        return '''
type of parameter ${ownParameter.name} is incompatible
override declares ${ownParameter.type}
super method declares ${superParameter.type}
''';
      }
    }

    return null;
  }

  /// Checks whether parameter with [ownParameterType] type is a valid override
  /// for parameter with [superParameterType] type taking into account its
  /// covariance and applying type parameter [substitution] if necessary.
  bool _isValidParameterOverride(bool isCovariant, DartType ownParameterType,
      DartType superParameterType) {
    if (_isSubtypeOf(superParameterType, ownParameterType)) {
      return true;
    } else if (isCovariant &&
        _isSubtypeOf(ownParameterType, superParameterType)) {
      return true;
    } else {
      return false;
    }
  }

  @override
  void checkAssignable(TreeNode where, DartType from, DartType to) {
    // Note: we permit implicit downcasts.
    if (from != to &&
        !environment.isSubtypeOf(from, to) &&
        !environment.isSubtypeOf(to, from)) {
      failures.reportNotAssignable(where, from, to);
    }
  }

  @override
  void checkUnresolvedInvocation(DartType receiver, TreeNode where) {
    if (receiver is DynamicType) {
      return;
    }

    // Permit any invocation on Function type.
    if (receiver == environment.rawFunctionType &&
        where is MethodInvocation &&
        where.name.name == 'call') {
      return;
    }

    fail(where, 'Unresolved method invocation');
  }

  @override
  void fail(TreeNode where, String message) {
    failures.reportFailure(where, message);
  }
}
