// Copyright (c) 2020, 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/element/nullability_suffix.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/extensions.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/element/type_schema.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';

class GreatestLowerBoundHelper {
  final TypeSystemImpl _typeSystem;

  GreatestLowerBoundHelper(this._typeSystem);

  InterfaceTypeImpl get _nullNone => _typeSystem.nullNone;

  TypeProviderImpl get _typeProvider => _typeSystem.typeProvider;

  /// Computes the greatest lower bound of [T1] and [T2].
  ///
  /// https://github.com/dart-lang/language
  /// See `resources/type-system/upper-lower-bounds.md`
  TypeImpl getGreatestLowerBound(TypeImpl T1, TypeImpl T2) {
    // DOWN(T, T) = T
    if (identical(T1, T2)) {
      return T1;
    }

    // For any type T, DOWN(?, T) == T.
    if (identical(T1, UnknownInferredType.instance)) {
      return T2;
    }
    if (identical(T2, UnknownInferredType.instance)) {
      return T1;
    }

    var T1_isTop = _typeSystem.isTop(T1);
    var T2_isTop = _typeSystem.isTop(T2);

    // DOWN(T1, T2) where TOP(T1) and TOP(T2)
    if (T1_isTop && T2_isTop) {
      // * T1 if MORETOP(T2, T1)
      // * T2 otherwise
      if (_typeSystem.isMoreTop(T2, T1)) {
        return T1;
      } else {
        return T2;
      }
    }

    // DOWN(T1, T2) = T2 if TOP(T1)
    if (T1_isTop) {
      return T2;
    }

    // DOWN(T1, T2) = T1 if TOP(T2)
    if (T2_isTop) {
      return T1;
    }

    var T1_isBottom = T1.isBottom;
    var T2_isBottom = T2.isBottom;

    // DOWN(T1, T2) where BOTTOM(T1) and BOTTOM(T2)
    if (T1_isBottom && T2_isBottom) {
      // * T1 if MOREBOTTOM(T1, T2)
      // * T2 otherwise
      if (_typeSystem.isMoreBottom(T1, T2)) {
        return T1;
      } else {
        return T2;
      }
    }

    // DOWN(T1, T2) = T1 if BOTTOM(T1)
    if (T1_isBottom) {
      return T1;
    }

    // DOWN(T1, T2) = T2 if BOTTOM(T2)
    if (T2_isBottom) {
      return T2;
    }

    var T1_isNull = _typeSystem.isNull(T1);
    var T2_isNull = _typeSystem.isNull(T2);

    // DOWN(T1, T2) where NULL(T1) and NULL(T2)
    if (T1_isNull && T2_isNull) {
      // * T1 if MOREBOTTOM(T1, T2)
      // * T2 otherwise
      if (_typeSystem.isMoreBottom(T1, T2)) {
        return T1;
      } else {
        return T2;
      }
    }

    var T1_nullability = T1.nullabilitySuffix;
    var T2_nullability = T2.nullabilitySuffix;

    // DOWN(Null, T2)
    if (T1_nullability == NullabilitySuffix.none && T1.isDartCoreNull) {
      // * Null if Null <: T2
      // * Never otherwise
      if (_typeSystem.isSubtypeOf(_nullNone, T2)) {
        return _nullNone;
      } else {
        return NeverTypeImpl.instance;
      }
    }

    // DOWN(T1, Null)
    if (T2_nullability == NullabilitySuffix.none && T2.isDartCoreNull) {
      // * Null if Null <: T1
      // * Never otherwise
      if (_typeSystem.isSubtypeOf(_nullNone, T1)) {
        return _nullNone;
      } else {
        return NeverTypeImpl.instance;
      }
    }

    var T1_isObject = _typeSystem.isObject(T1);
    var T2_isObject = _typeSystem.isObject(T2);

    // DOWN(T1, T2) where OBJECT(T1) and OBJECT(T2)
    if (T1_isObject && T2_isObject) {
      // * T1 if MORETOP(T2, T1)
      // * T2 otherwise
      if (_typeSystem.isMoreTop(T2, T1)) {
        return T1;
      } else {
        return T2;
      }
    }

    // DOWN(T1, T2) where OBJECT(T1)
    if (T1_isObject) {
      // * T2 if T2 is non-nullable
      if (_typeSystem.isNonNullable(T2)) {
        return T2;
      }

      // * NonNull(T2) if NonNull(T2) is non-nullable
      var T2_nonNull = _typeSystem.promoteToNonNull(T2);
      if (_typeSystem.isNonNullable(T2_nonNull)) {
        return T2_nonNull;
      }

      // * Never otherwise
      return NeverTypeImpl.instance;
    }

    // DOWN(T1, T2) where OBJECT(T2)
    if (T2_isObject) {
      // * T1 if T1 is non-nullable
      if (_typeSystem.isNonNullable(T1)) {
        return T1;
      }

      // * NonNull(T1) if NonNull(T1) is non-nullable
      var T1_nonNull = _typeSystem.promoteToNonNull(T1);
      if (_typeSystem.isNonNullable(T1_nonNull)) {
        return T1_nonNull;
      }

      // * Never otherwise
      return NeverTypeImpl.instance;
    }

    // DOWN(T1?, T2?) = S? where S is DOWN(T1, T2)
    // DOWN(T1?, T2) = S where S is DOWN(T1, T2)
    // DOWN(T1, T2?) = S where S is DOWN(T1, T2)
    if (T1_nullability != NullabilitySuffix.none ||
        T2_nullability != NullabilitySuffix.none) {
      var T1_none = T1.withNullability(NullabilitySuffix.none);
      var T2_none = T2.withNullability(NullabilitySuffix.none);
      var S = getGreatestLowerBound(T1_none, T2_none);
      if (T1_nullability == NullabilitySuffix.question &&
          T2_nullability == NullabilitySuffix.question) {
        return S.withNullability(NullabilitySuffix.question);
      }
      return S;
    }

    assert(T1_nullability == NullabilitySuffix.none);
    assert(T2_nullability == NullabilitySuffix.none);

    if (T1 is FunctionTypeImpl && T2 is FunctionTypeImpl) {
      return _functionType(T1, T2);
    }

    if (T1 is RecordTypeImpl && T2 is RecordTypeImpl) {
      return _recordType(T1, T2);
    }

    // DOWN(T1, T2) = T1 if T1 <: T2
    if (_typeSystem.isSubtypeOf(T1, T2)) {
      return T1;
    }

    // DOWN(T1, T2) = T2 if T2 <: T1
    if (_typeSystem.isSubtypeOf(T2, T1)) {
      return T2;
    }

    // FutureOr<S1>
    if (T1 is InterfaceTypeImpl && T1.isDartAsyncFutureOr) {
      var S1 = T1.typeArguments[0];
      // DOWN(FutureOr<S1>, FutureOr<S2>) = FutureOr(S)
      //   S = DOWN(S1, S2)
      if (T2 is InterfaceTypeImpl && T2.isDartAsyncFutureOr) {
        var S2 = T2.typeArguments[0];
        var S = getGreatestLowerBound(S1, S2);
        return _typeProvider.futureOrType(S);
      }
      // DOWN(FutureOr<S1>, Future<S2>) = Future(S)
      //   S = DOWN(S1, S2)
      if (T2 is InterfaceTypeImpl && T2.isDartAsyncFuture) {
        var S2 = T2.typeArguments[0];
        var S = getGreatestLowerBound(S1, S2);
        return _typeProvider.futureType(S);
      }
      // DOWN(FutureOr<S1>, T2) = DOWN(S1, T2)
      return getGreatestLowerBound(S1, T2);
    }

    // FutureOr<S2>
    if (T2 is InterfaceTypeImpl && T2.isDartAsyncFutureOr) {
      var S2 = T2.typeArguments[0];
      // DOWN(Future<S1>, FutureOr<S2>) = Future<S>
      //   S = DOWN(S1, S2)
      if (T1 is InterfaceTypeImpl && T1.isDartAsyncFuture) {
        var S1 = T1.typeArguments[0];
        var S = getGreatestLowerBound(S1, S2);
        return _typeProvider.futureType(S);
      }
      // DOWN(T1, FutureOr<S2>) = DOWN(T1, S2)
      return getGreatestLowerBound(T1, S2);
    }

    // DOWN(T1, T2) = Never otherwise
    return NeverTypeImpl.instance;
  }

  /// Compute the greatest lower bound of function types [f] and [g].
  ///
  /// https://github.com/dart-lang/language
  /// See `resources/type-system/upper-lower-bounds.md`
  TypeImpl _functionType(FunctionTypeImpl f, FunctionTypeImpl g) {
    var fTypeFormals = f.typeFormals;
    var gTypeFormals = g.typeFormals;

    // The number of type parameters must be the same.
    // Otherwise the result is `Never`.
    if (fTypeFormals.length != gTypeFormals.length) {
      return NeverTypeImpl.instance;
    }

    // The bounds of type parameters must be equal.
    // Otherwise the result is `Never`.
    var fresh = _typeSystem.relateTypeParameters2(
      f.typeParameters,
      g.typeParameters,
    );
    if (fresh == null) {
      return NeverTypeImpl.instance;
    }

    f = f.instantiate(fresh.typeParameterTypes);
    g = g.instantiate(fresh.typeParameterTypes);

    var fParameters = f.formalParameters;
    var gParameters = g.formalParameters;

    var parameters = <FormalParameterElementMixin>[];
    var fIndex = 0;
    var gIndex = 0;
    while (fIndex < fParameters.length && gIndex < gParameters.length) {
      var fParameter = fParameters[fIndex];
      var gParameter = gParameters[gIndex];
      if (fParameter.isPositional) {
        if (gParameter.isPositional) {
          fIndex++;
          gIndex++;
          parameters.add(
            fParameter.copyWith(
              type: _typeSystem.leastUpperBound(
                fParameter.type,
                gParameter.type,
              ),
              kind: fParameter.isOptional || gParameter.isOptional
                  ? ParameterKind.POSITIONAL
                  : ParameterKind.REQUIRED,
            ),
          );
        } else {
          return NeverTypeImpl.instance;
        }
      } else if (fParameter.isNamed) {
        if (gParameter.isNamed) {
          var fName = fParameter.name3;
          var gName = gParameter.name3;
          if (fName == null || gName == null) {
            return NeverTypeImpl.instance;
          }

          var compareNames = fName.compareTo(gName);
          if (compareNames == 0) {
            fIndex++;
            gIndex++;
            parameters.add(
              fParameter.copyWith(
                type: _typeSystem.leastUpperBound(
                  fParameter.type,
                  gParameter.type,
                ),
                kind: fParameter.isRequiredNamed && gParameter.isRequiredNamed
                    ? ParameterKind.NAMED_REQUIRED
                    : ParameterKind.NAMED,
              ),
            );
          } else if (compareNames < 0) {
            fIndex++;
            parameters.add(
              fParameter.copyWith(kind: ParameterKind.NAMED),
            );
          } else {
            assert(compareNames > 0);
            gIndex++;
            parameters.add(
              gParameter.copyWith(kind: ParameterKind.NAMED),
            );
          }
        } else {
          return NeverTypeImpl.instance;
        }
      }
    }

    while (fIndex < fParameters.length) {
      var fParameter = fParameters[fIndex++];
      if (fParameter.isPositional) {
        parameters.add(
          fParameter.copyWith(kind: ParameterKind.POSITIONAL),
        );
      } else {
        assert(fParameter.isNamed);
        parameters.add(
          fParameter.copyWith(kind: ParameterKind.NAMED),
        );
      }
    }

    while (gIndex < gParameters.length) {
      var gParameter = gParameters[gIndex++];
      if (gParameter.isPositional) {
        parameters.add(
          gParameter.copyWith(kind: ParameterKind.POSITIONAL),
        );
      } else {
        assert(gParameter.isNamed);
        parameters.add(
          gParameter.copyWith(kind: ParameterKind.NAMED),
        );
      }
    }

    var returnType = getGreatestLowerBound(f.returnType, g.returnType);

    return FunctionTypeImpl.v2(
      typeParameters: fresh.typeParameters,
      formalParameters: parameters,
      returnType: returnType,
      nullabilitySuffix: NullabilitySuffix.none,
    );
  }

  TypeImpl _recordType(RecordTypeImpl T1, RecordTypeImpl T2) {
    var positional1 = T1.positionalFields;
    var positional2 = T2.positionalFields;
    if (positional1.length != positional2.length) {
      return _typeSystem.typeProvider.neverType;
    }

    var named1 = T1.namedFields;
    var named2 = T2.namedFields;
    if (named1.length != named2.length) {
      return _typeSystem.typeProvider.neverType;
    }

    var positionalFields = <RecordTypePositionalFieldImpl>[];
    for (var i = 0; i < positional1.length; i++) {
      var field1 = positional1[i];
      var field2 = positional2[i];
      var type = getGreatestLowerBound(field1.type, field2.type);
      positionalFields.add(
        RecordTypePositionalFieldImpl(
          type: type,
        ),
      );
    }

    var namedFields = <RecordTypeNamedFieldImpl>[];
    for (var i = 0; i < named1.length; i++) {
      var field1 = named1[i];
      var field2 = named2[i];
      if (field1.name != field2.name) {
        return _typeSystem.typeProvider.neverType;
      }
      var type = getGreatestLowerBound(field1.type, field2.type);
      namedFields.add(
        RecordTypeNamedFieldImpl(
          name: field1.name,
          type: type,
        ),
      );
    }

    return RecordTypeImpl(
      positionalFields: positionalFields,
      namedFields: namedFields,
      nullabilitySuffix: NullabilitySuffix.none,
    );
  }
}
