// 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 'exhaustive.dart';
import 'key.dart';
import 'path.dart';
import 'space.dart';
import 'static_type.dart';
import 'types.dart';

/// Interface implemented by analyze/CFE to support type operations need for the
/// shared [StaticType]s.
abstract class TypeOperations<Type extends Object> {
  /// Returns the type for `Object?`.
  Type get nullableObjectType;

  /// Returns the type for the non-nullable `Object`.
  Type get nonNullableObjectType;

  /// Returns `true` if [s] is a subtype of [t].
  bool isSubtypeOf(Type s, Type t);

  /// Returns a type that overapproximates the possible values of [type] by
  /// replacing all type variables with the default types.
  Type overapproximate(Type type);

  /// Returns `true` if [type] is a nullable type.
  bool isNullable(Type type);

  /// Returns `true` if [type] is a potentially nullable type.
  bool isPotentiallyNullable(Type type);

  /// Returns the non-nullable type corresponding to [type]. For instance
  /// `Foo` for `Foo?`. If [type] is already non-nullable, it itself is
  /// returned.
  Type getNonNullable(Type type);

  /// Returns `true` if [type] is the `Null` type.
  bool isNullType(Type type);

  /// Returns `true` if [type] is the `Never` type.
  bool isNeverType(Type type);

  /// Returns `true` if [type] is the `Object?` type.
  bool isNullableObject(Type type);

  /// Returns `true` if [type] is the `Object` type.
  bool isNonNullableObject(Type type);

  /// Returns `true` if [type] is the `dynamic` type.
  bool isDynamic(Type type);

  /// Returns `true` if [type] is the `bool` type.
  bool isBoolType(Type type);

  /// Returns the `bool` type.
  Type get boolType;

  /// Returns `true` if [type] is a record type.
  bool isRecordType(Type type);

  /// Returns `true` if [type] is a generic interface type.
  bool isGeneric(Type type);

  /// Returns the type `T` if [type] is `FutureOr<T>`. Returns `null` otherwise.
  Type? getFutureOrTypeArgument(Type type);

  /// Returns the non-nullable type `Future<T>` for [type] `T`.
  Type instantiateFuture(Type type);

  /// Returns a map of the field names and corresponding types available on
  /// [type]. For an interface type, these are the fields and getters, and for
  /// record types these are the record fields.
  Map<Key, Type> getFieldTypes(Type type);

  /// Returns the value type `V` if [type] implements `Map<K, V>` or `null`
  /// otherwise.
  Type? getMapValueType(Type type);

  /// Returns the element type `E` if [type] implements `List<E>` or `null`
  /// otherwise.
  Type? getListElementType(Type type);

  /// Returns the list type `List<E>` if [type] implements `List<E>` or `null`
  /// otherwise.
  Type? getListType(Type type);

  /// Returns the extension type erasure of [type].
  ///
  /// This is [type] in which all occurrences of extension types have been
  /// replaced with their representation type.
  Type getExtensionTypeErasure(Type type);

  /// Returns a human-readable representation of the [type].
  String typeToString(Type type);

  /// Returns `true` if [type] has a simple name that can be used as the type
  /// of an object pattern.
  bool hasSimpleName(Type type);

  /// Returns the bound of [type] if is a type variable or a promoted type
  /// variable. Otherwise returns `null`.
  Type? getTypeVariableBound(Type type);
}

/// Interface for looking up fields and their corresponding [StaticType]s of
/// a given type.
abstract class FieldLookup<Type extends Object> {
  /// Returns a map of the field names and corresponding [StaticType]s available
  /// on [type]. For an interface type, these are the fields and getters, and
  /// for record types these are the record fields.
  Map<Key, StaticType> getFieldTypes(Type type);

  StaticType? getAdditionalFieldType(Type type, Key key);
}

/// Cache used for computing [StaticType]s used for exhaustiveness checking.
///
/// This implementation is shared between analyzer and CFE, and implemented
/// using the analyzer/CFE implementations of [TypeOperations],
/// [EnumOperations], and [SealedClassOperations].
class ExhaustivenessCache<
        Type extends Object,
        Class extends Object,
        EnumClass extends Object,
        EnumElement extends Object,
        EnumElementValue extends Object>
    implements FieldLookup<Type>, ObjectPropertyLookup {
  final TypeOperations<Type> typeOperations;
  final EnumOperations<Type, EnumClass, EnumElement, EnumElementValue>
      enumOperations;
  final SealedClassOperations<Type, Class> _sealedClassOperations;

  /// Cache for [EnumInfo] for enum classes.
  Map<EnumClass, EnumInfo<Type, EnumClass, EnumElement, EnumElementValue>>
      _enumInfo = {};

  /// Cache for [SealedClassInfo] for sealed classes.
  Map<Class, SealedClassInfo<Type, Class>> _sealedClassInfo = {};

  /// Cache for unique [StaticType]s.
  Map<Object, StaticType> _uniqueTypeMap = {};

  /// Cache for the [StaticType] for `bool`.
  late BoolStaticType _boolStaticType =
      new BoolStaticType(typeOperations, this, typeOperations.boolType);

  /// Cache for [StaticType]s for fields available on a [Type].
  Map<Type, Map<Key, StaticType>> _fieldCache = {};

  ExhaustivenessCache(
      this.typeOperations, this.enumOperations, this._sealedClassOperations);

  /// Returns the [EnumInfo] for [enumClass].
  EnumInfo<Type, EnumClass, EnumElement, EnumElementValue> _getEnumInfo(
      EnumClass enumClass) {
    return _enumInfo[enumClass] ??=
        new EnumInfo(typeOperations, this, enumOperations, enumClass);
  }

  /// Returns the [SealedClassInfo] for [sealedClass].
  SealedClassInfo<Type, Class> _getSealedClassInfo(Class sealedClass) {
    return _sealedClassInfo[sealedClass] ??=
        new SealedClassInfo(_sealedClassOperations, sealedClass);
  }

  /// Returns the [StaticType] for the boolean [value].
  StaticType getBoolValueStaticType(bool value) {
    return value ? _boolStaticType.trueType : _boolStaticType.falseType;
  }

  /// Returns the [StaticType] for [type].
  StaticType getStaticType(Type type) {
    type = typeOperations.getExtensionTypeErasure(type);
    if (typeOperations.isNeverType(type)) {
      return StaticType.neverType;
    } else if (typeOperations.isNullType(type)) {
      return StaticType.nullType;
    } else if (typeOperations.isNonNullableObject(type)) {
      return StaticType.nonNullableObject;
    } else if (typeOperations.isNullableObject(type) ||
        typeOperations.isDynamic(type)) {
      return StaticType.nullableObject;
    }

    StaticType staticType;
    bool extractNull = typeOperations.isNullable(type);
    Type typeWithoutNull = type;
    if (extractNull) {
      // If [type] is nullable, we model the static type by creating the
      // non-nullable equivalent and then add `Null` afterwards.
      //
      // For instance we model `int?` as `int|Null`.
      typeWithoutNull = typeOperations.getNonNullable(type);
    }
    if (typeOperations.isBoolType(typeWithoutNull)) {
      staticType = _boolStaticType;
    } else if (typeOperations.isRecordType(typeWithoutNull)) {
      staticType = new RecordStaticType(typeOperations, this, typeWithoutNull);
    } else {
      Type? futureOrTypeArgument =
          typeOperations.getFutureOrTypeArgument(typeWithoutNull);
      if (futureOrTypeArgument != null) {
        StaticType typeArgument = getStaticType(futureOrTypeArgument);
        StaticType futureType = getStaticType(
            typeOperations.instantiateFuture(futureOrTypeArgument));
        bool isImplicitlyNullable =
            typeOperations.isNullable(futureOrTypeArgument);
        staticType = new FutureOrStaticType(
            typeOperations, this, typeWithoutNull, typeArgument, futureType,
            isImplicitlyNullable: isImplicitlyNullable);
      } else {
        EnumClass? enumClass = enumOperations.getEnumClass(typeWithoutNull);
        if (enumClass != null) {
          staticType = new EnumStaticType(
              typeOperations, this, typeWithoutNull, _getEnumInfo(enumClass));
        } else {
          Class? sealedClass =
              _sealedClassOperations.getSealedClass(typeWithoutNull);
          if (sealedClass != null) {
            staticType = new SealedClassStaticType(
                typeOperations,
                this,
                typeWithoutNull,
                this,
                _sealedClassOperations,
                _getSealedClassInfo(sealedClass));
          } else {
            Type? listType = typeOperations.getListType(typeWithoutNull);
            if (listType != null) {
              staticType =
                  new ListTypeStaticType(typeOperations, this, typeWithoutNull);
            } else {
              bool isImplicitlyNullable =
                  typeOperations.isNullable(typeWithoutNull);
              staticType = new TypeBasedStaticType(
                  typeOperations, this, typeWithoutNull,
                  isImplicitlyNullable: isImplicitlyNullable);
              Type? bound = typeOperations.getTypeVariableBound(type);
              if (bound != null) {
                staticType =
                    new WrappedStaticType(getStaticType(bound), staticType);
              }
            }
          }
        }
      }
    }
    if (extractNull) {
      // Include the `Null` which extracted from [type] into [typeWithoutNull`.
      staticType = staticType.nullable;
    }
    return staticType;
  }

  /// Returns the [StaticType] for the [enumElementValue] declared by
  /// [enumClass].
  StaticType getEnumElementStaticType(
      EnumClass enumClass, EnumElementValue enumElementValue) {
    return _getEnumInfo(enumClass).getEnumElement(enumElementValue);
  }

  /// Creates a new unique [StaticType].
  StaticType getUnknownStaticType() {
    // The unknown static type should be based on the nullable `Object`, since
    // even though it _might_ be `null`, using the nullable `Object` here would
    // mean that it _does_ include `null`, and we need this type to only cover
    // itself.
    return getUniqueStaticType<Object>(
        typeOperations.nonNullableObjectType, new Object(), '?');
  }

  /// Returns a [StaticType] of the given [type] with the given
  /// [textualRepresentation] that unique identifies the [uniqueValue].
  ///
  /// This is used for constants that are neither bool nor enum values.
  StaticType getUniqueStaticType<Identity extends Object>(
      Type type, Identity uniqueValue, String textualRepresentation) {
    Type nonNullable = typeOperations.getNonNullable(type);
    StaticType staticType = _uniqueTypeMap[uniqueValue] ??=
        new GeneralValueStaticType<Type, Identity>(
            typeOperations,
            this,
            nonNullable,
            new IdentityRestriction<Identity>(uniqueValue),
            textualRepresentation,
            uniqueValue);
    if (typeOperations.isNullable(type)) {
      staticType = staticType.nullable;
    }
    return staticType;
  }

  /// Returns a [StaticType] of the list [type] with the given [restriction] .
  StaticType getListStaticType(
      Type type, ListTypeRestriction<Type> restriction) {
    Type nonNullable = typeOperations.getNonNullable(type);
    StaticType staticType = new ListPatternStaticType(
        typeOperations, this, nonNullable, restriction, restriction.toString());
    if (typeOperations.isNullable(type)) {
      staticType = staticType.nullable;
    }
    return staticType;
  }

  /// Returns a [StaticType] of the map [type] with the given [restriction] .
  StaticType getMapStaticType(Type type, MapTypeRestriction<Type> restriction) {
    Type nonNullable = typeOperations.getNonNullable(type);
    StaticType staticType = new MapPatternStaticType(
        typeOperations, this, nonNullable, restriction, restriction.toString());
    if (typeOperations.isNullable(type)) {
      staticType = staticType.nullable;
    }
    return staticType;
  }

  @override
  Map<Key, StaticType> getFieldTypes(Type type) {
    Map<Key, StaticType>? fields = _fieldCache[type];
    if (fields == null) {
      _fieldCache[type] = fields = {};
      for (MapEntry<Key, Type> entry
          in typeOperations.getFieldTypes(type).entries) {
        fields[entry.key] = getStaticType(entry.value);
      }
    }
    return fields;
  }

  @override
  StaticType? getAdditionalFieldType(Type type, Key key) {
    if (key is MapKey) {
      Type? valueType = typeOperations.getMapValueType(type);
      if (valueType != null) {
        return getStaticType(valueType);
      }
    } else if (key is HeadKey || key is TailKey) {
      Type? elementType = typeOperations.getListElementType(type);
      if (elementType != null) {
        return getStaticType(elementType);
      }
    } else if (key is RestKey) {
      Type? listType = typeOperations.getListType(type);
      if (listType != null) {
        return getStaticType(listType);
      }
    } else {
      return getObjectFieldType(key);
    }
    return null;
  }

  @override
  StaticType? getObjectFieldType(Key key) {
    return getFieldTypes(typeOperations.nonNullableObjectType)[key];
  }
}

/// Mixin for creating [Space]s from [Pattern]s.
mixin SpaceCreator<Pattern extends Object, Type extends Object> {
  TypeOperations<Type> get typeOperations;

  ObjectPropertyLookup get objectFieldLookup;

  /// Returns `true` if the current library has version greater than or equal
  /// to `$major.$minor`.
  bool hasLanguageVersion(int major, int minor);

  /// Creates a [StaticType] for an unknown type.
  ///
  /// This is used when the type of the pattern is unknown or can't be
  /// represented as a [StaticType]. This type is unique and ensures that it
  /// is neither matches anything nor is matched by anything.
  StaticType createUnknownStaticType();

  /// Creates the [StaticType] for [type].
  StaticType createStaticType(Type type);

  /// Creates the [StaticType] for [type] restricted by the [contextType].
  /// If [nonNull] is `true`, the created type is non-nullable.
  StaticType _createStaticTypeWithContext(StaticType contextType, Type type,
      {required bool nonNull}) {
    StaticType staticType = createStaticType(type);
    if (contextType.isSubtypeOf(staticType)) {
      staticType = contextType;
    }
    if (nonNull) {
      staticType = staticType.nonNullable;
    }
    return staticType;
  }

  /// Creates the [StaticType] for the list [type] with the given [restriction].
  StaticType createListType(Type type, ListTypeRestriction<Type> restriction);

  /// Creates the [StaticType] for the map [type] with the given [restriction].
  StaticType createMapType(Type type, MapTypeRestriction<Type> restriction);

  /// Creates the [Space] for [pattern] at the given [path].
  ///
  /// The [contextType] is the [StaticType] in which the pattern match is
  /// performed. This is used to the restrict type of the created [Space] to
  /// the types allowed by the context. For instance `Object(:var hashCode)` is
  /// in itself unrestricted and would yield the top space for matching
  /// `var hashCode`. Using the [contextType] `int`, as given by the type of
  /// the `Object.hashCode`, the created space is all `int` values rather than
  /// all values.
  ///
  /// If [nonNull] is `true`, the space is implicitly non-nullable.
  Space dispatchPattern(Path path, StaticType contextType, Pattern pattern,
      {required bool nonNull});

  /// Creates the root space for [pattern].
  Space createRootSpace(StaticType contextType, Pattern pattern) {
    return dispatchPattern(const Path.root(), contextType, pattern,
        nonNull: false);
  }

  /// Creates the [Space] at [path] for a variable pattern of the declared
  /// [type].
  ///
  /// If [nonNull] is `true`, the space is implicitly non-nullable.
  Space createVariableSpace(Path path, StaticType contextType, Type type,
      {required bool nonNull}) {
    StaticType staticType =
        _createStaticTypeWithContext(contextType, type, nonNull: nonNull);
    return new Space(path, staticType);
  }

  /// Creates the [Space] at [path] for an object pattern of the required [type]
  /// and [fieldPatterns].
  ///
  /// If [nonNull] is `true`, the space is implicitly non-nullable.
  Space createObjectSpace(
      Path path,
      StaticType contextType,
      Type type,
      Map<String, Pattern> fieldPatterns,
      Map<String, Type> extensionPropertyTypes,
      {required bool nonNull}) {
    StaticType staticType =
        _createStaticTypeWithContext(contextType, type, nonNull: nonNull);
    Map<Key, Space> properties = <Key, Space>{};
    for (MapEntry<String, Pattern> entry in fieldPatterns.entries) {
      String name = entry.key;
      StaticType propertyType;
      Type? extensionPropertyType = extensionPropertyTypes[name];
      Key key;
      if (extensionPropertyType != null) {
        propertyType = createStaticType(extensionPropertyType);
        key = new ExtensionKey(createStaticType(type), name, propertyType);
      } else {
        key = new NameKey(name);
        propertyType = staticType.getPropertyType(objectFieldLookup, key) ??
            StaticType.nullableObject;
      }
      properties[key] = dispatchPattern(
          path.add(key), propertyType, entry.value,
          nonNull: false);
    }
    return new Space(path, staticType, properties: properties);
  }

  /// Creates the [Space] at [path] for a record pattern of the required
  /// [recordType], [positionalFields], and [namedFields].
  Space createRecordSpace(Path path, StaticType contextType, Type recordType,
      List<Pattern> positionalFields, Map<String, Pattern> namedFields) {
    StaticType staticType =
        _createStaticTypeWithContext(contextType, recordType, nonNull: true);
    Map<Key, Space> properties = <Key, Space>{};
    for (int index = 0; index < positionalFields.length; index++) {
      Key key = new RecordIndexKey(index);
      StaticType propertyType =
          staticType.getPropertyType(objectFieldLookup, key) ??
              StaticType.nullableObject;
      properties[key] = dispatchPattern(
          path.add(key), propertyType, positionalFields[index],
          nonNull: false);
    }
    for (MapEntry<String, Pattern> entry in namedFields.entries) {
      Key key = new RecordNameKey(entry.key);
      StaticType propertyType =
          staticType.getPropertyType(objectFieldLookup, key) ??
              StaticType.nullableObject;
      properties[key] = dispatchPattern(
          path.add(key), propertyType, entry.value,
          nonNull: false);
    }
    return new Space(path, staticType, properties: properties);
  }

  /// Creates the [Space] at [path] for a wildcard pattern with the declared
  /// [type].
  ///
  /// If [nonNull] is `true`, the space is implicitly non-nullable.
  Space createWildcardSpace(Path path, StaticType contextType, Type? type,
      {required bool nonNull}) {
    if (type == null) {
      StaticType staticType = contextType;
      if (nonNull) {
        staticType = staticType.nonNullable;
      }
      return new Space(path, staticType);
    } else {
      StaticType staticType =
          _createStaticTypeWithContext(contextType, type, nonNull: nonNull);
      return new Space(path, staticType);
    }
  }

  /// Creates the [Space] at [path] for a relational pattern.
  Space createRelationalSpace(Path path) {
    // This pattern do not add to the exhaustiveness coverage.
    return createUnknownSpace(path);
  }

  /// Returns `true` if [singleSpace] does not restrict the spaces of any of its
  /// properties. For instance the pattern `Object(:int hashCode)` is
  /// unrestricted but the pattern `Object(hashCode: 5)` is restricted.
  bool _isUnrestricted(SingleSpace singleSpace) {
    if (singleSpace.properties.isNotEmpty) {
      Map<Key, StaticType> fieldTypes = singleSpace.type.fields;
      for (MapEntry<Key, Space> entry in singleSpace.properties.entries) {
        Key key = entry.key;
        StaticType fieldType = fieldTypes[key] ?? StaticType.neverType;
        if (!_isContainedIn(fieldType, entry.value)) {
          return false;
        }
      }
    }
    if (singleSpace.additionalProperties.isNotEmpty) {
      for (MapEntry<Key, Space> entry
          in singleSpace.additionalProperties.entries) {
        Key key = entry.key;
        StaticType fieldType =
            singleSpace.type.getAdditionalPropertyType(key) ??
                StaticType.neverType;
        if (!_isContainedIn(fieldType, entry.value)) {
          return false;
        }
      }
    }
    return true;
  }

  /// Returns `true` if the space implied by [type], i.e. all values of [type]
  /// regardless of properties, is contained in [space].
  bool _isContainedIn(StaticType type, Space space) {
    Map<SingleSpace, bool> unrestrictedCache = {};
    if (space.singleSpaces.length == 1) {
      // Optimize for simple spaces to avoid unnecessary expansion of subtypes.
      SingleSpace singleSpace = space.singleSpaces.single;
      bool isUnrestricted =
          unrestrictedCache[singleSpace] ??= _isUnrestricted(singleSpace);
      if (isUnrestricted && type.isSubtypeOf(singleSpace.type)) {
        return true;
      }
    }
    // To handle case like:
    //
    //    sealed class M {}
    //    class A extends M {}
    //    class B extends M {}
    //    method(o) => switch (o) {
    //        (A() || B()) as M => 0,
    //      };
    //
    // we expand [type] into subtypes before determining for containment.
    List<StaticType> subtypes = expandSealedSubtypes(type, const {});
    for (StaticType subtype in subtypes) {
      bool found = false;
      for (SingleSpace singleSpace in space.singleSpaces) {
        bool isUnrestricted =
            unrestrictedCache[singleSpace] ??= _isUnrestricted(singleSpace);
        if (isUnrestricted && subtype.isSubtypeOf(singleSpace.type)) {
          found = true;
          break;
        }
      }
      if (!found) {
        return false;
      }
    }
    return true;
  }

  /// Creates the [Space] at [path] for a cast pattern with the given
  /// [subPattern].
  ///
  /// If [nonNull] is `true`, the space is implicitly non-nullable.
  Space createCastSpace(
      Path path, StaticType contextType, Type type, Pattern subPattern,
      {required bool nonNull}) {
    Space space =
        dispatchPattern(path, contextType, subPattern, nonNull: nonNull);
    StaticType castType = createStaticType(type);
    if (hasLanguageVersion(3, 3)) {
      if (_isContainedIn(castType, space)) {
        // If all values in [castType] are also in [space] then the complete
        // [contextType] is matched. For instance
        //
        //    method(var d) => switch (d) {
        //        final String value => value,
        //        // This covers everything either by matching as int or by
        //        // throwing.
        //        final value as int => value,
        //      };
        space = new Space(path, contextType);
      }
      if (!typeOperations.isPotentiallyNullable(type)) {
        // If `null` is _not_ included in [castType], we can include in the
        // generated [space].
        space = space.union(new Space(path, StaticType.nullType));
      }
    } else {
      // The following check assumes that the subpattern space is
      // unrestrictive.
      if (castType.isSubtypeOf(contextType) && contextType.isSealed) {
        for (StaticType subtype
            in expandSealedSubtypes(contextType, const {})) {
          // If [subtype] is a subtype of [castType] it will not throw and
          // must be handled by [subPattern]. For instance
          //
          //    sealed class S {}
          //    sealed class X extends S {}
          //    class A extends X {}
          //    method(S s) => switch (s) {
          //      A() as X => 0,
          //    }
          //
          // If [castType] is a subtype of [subtype] it might not throw but
          // still not handle all values of the [subtype].
          //
          //    sealed class S {}
          //    class A extends S {}
          //    class X extends A {
          //      int field;
          //      X(this.field);
          //    }
          //    method(S s) => switch (s) {
          //      X(field: 42) as X => 0,
          //    }
          //
          if (!castType.isSubtypeOf(subtype) &&
              !subtype.isSubtypeOf(castType)) {
            // Otherwise the cast implicitly handles [subtype].
            space = space.union(new Space(path, subtype));
          }
        }
      }
    }
    return space;
  }

  /// Creates the [Space] at [path] for a null check pattern with the given
  /// [subPattern].
  Space createNullCheckSpace(
      Path path, StaticType contextType, Pattern subPattern) {
    return dispatchPattern(path, contextType, subPattern, nonNull: true);
  }

  /// Creates the [Space] at [path] for a null assert pattern with the given
  /// [subPattern].
  Space createNullAssertSpace(
      Path path, StaticType contextType, Pattern subPattern) {
    Space space = dispatchPattern(path, contextType, subPattern, nonNull: true);
    return space.union(new Space(path, StaticType.nullType));
  }

  /// Creates the [Space] at [path] for a logical or pattern with the given
  /// [left] and [right] subpatterns.
  ///
  /// If [nonNull] is `true`, the space is implicitly non-nullable.
  Space createLogicalOrSpace(
      Path path, StaticType contextType, Pattern left, Pattern right,
      {required bool nonNull}) {
    Space aSpace = dispatchPattern(path, contextType, left, nonNull: nonNull);
    Space bSpace = dispatchPattern(path, contextType, right, nonNull: nonNull);
    return aSpace.union(bSpace);
  }

  /// Creates the [Space] at [path] for a logical and pattern with the given
  /// [left] and [right] subpatterns.
  ///
  /// If [nonNull] is `true`, the space is implicitly non-nullable.
  Space createLogicalAndSpace(
      Path path, StaticType contextType, Pattern left, Pattern right,
      {required bool nonNull}) {
    Space aSpace = dispatchPattern(path, contextType, left, nonNull: nonNull);
    Space bSpace = dispatchPattern(path, contextType, right, nonNull: nonNull);
    return _createSpaceIntersection(path, aSpace, bSpace);
  }

  /// Creates the [Space] at [path] for a list pattern.
  Space createListSpace(Path path,
      {required Type type,
      required Type elementType,
      required List<Pattern> headElements,
      required Pattern? restElement,
      required List<Pattern> tailElements,
      required bool hasRest,
      required bool hasExplicitTypeArgument}) {
    int headSize = headElements.length;
    int tailSize = tailElements.length;

    String typeArgumentText;
    if (hasExplicitTypeArgument) {
      StringBuffer sb = new StringBuffer();
      sb.write('<');
      sb.write(typeOperations.typeToString(elementType));
      sb.write('>');
      typeArgumentText = sb.toString();
    } else {
      typeArgumentText = '';
    }

    ListTypeRestriction<Type> identity = new ListTypeRestriction(
        elementType, typeArgumentText,
        size: headSize + tailSize, hasRest: hasRest);

    StaticType staticType = createListType(type, identity);

    Map<Key, Space> additionalProperties = {};
    for (int index = 0; index < headSize; index++) {
      Key key = new HeadKey(index);
      StaticType propertyType = staticType.getAdditionalPropertyType(key) ??
          StaticType.nullableObject;
      additionalProperties[key] = dispatchPattern(
          path.add(key), propertyType, headElements[index],
          nonNull: false);
    }
    if (hasRest) {
      Key key = new RestKey(headSize, tailSize);
      StaticType propertyType = staticType.getAdditionalPropertyType(key) ??
          StaticType.nullableObject;
      if (restElement != null) {
        additionalProperties[key] = dispatchPattern(
            path.add(key), propertyType, restElement,
            nonNull: false);
      } else {
        additionalProperties[key] = new Space(path.add(key), propertyType);
      }
    }
    for (int index = 0; index < tailSize; index++) {
      Key key = new TailKey(index);
      StaticType propertyType = staticType.getAdditionalPropertyType(key) ??
          StaticType.nullableObject;
      additionalProperties[key] = dispatchPattern(path.add(key), propertyType,
          tailElements[tailElements.length - index - 1],
          nonNull: false);
    }
    return new Space(path, staticType,
        additionalProperties: additionalProperties);
  }

  /// Creates the [Space] at [path] for a map pattern.
  Space createMapSpace(Path path,
      {required Type type,
      required Type keyType,
      required Type valueType,
      required Map<MapKey, Pattern> entries,
      required bool hasExplicitTypeArguments}) {
    String typeArgumentsText;
    if (hasExplicitTypeArguments) {
      StringBuffer sb = new StringBuffer();
      sb.write('<');
      sb.write(typeOperations.typeToString(keyType));
      sb.write(', ');
      sb.write(typeOperations.typeToString(valueType));
      sb.write('>');
      typeArgumentsText = sb.toString();
    } else {
      typeArgumentsText = '';
    }

    MapTypeRestriction<Type> identity = new MapTypeRestriction(
        keyType, valueType, entries.keys.toSet(), typeArgumentsText);
    StaticType staticType = createMapType(type, identity);

    Map<Key, Space> additionalProperties = {};
    for (MapEntry<Key, Pattern> entry in entries.entries) {
      Key key = entry.key;
      StaticType propertyType = staticType.getAdditionalPropertyType(key) ??
          StaticType.nullableObject;
      additionalProperties[key] = dispatchPattern(
          path.add(key), propertyType, entry.value,
          nonNull: false);
    }
    return new Space(path, staticType,
        additionalProperties: additionalProperties);
  }

  /// Creates the [Space] at [path] for a pattern with unknown space.
  ///
  /// This is used when the space of the pattern is unknown or can't be
  /// represented precisely as a union of [SingleSpace]s. This space is unique
  /// and ensures that it is neither matches anything nor is matched by
  /// anything.
  Space createUnknownSpace(Path path) {
    return new Space(path, createUnknownStaticType());
  }

  /// Creates an approximation of the intersection of the single spaces [a] and
  /// [b].
  SingleSpace? _createSingleSpaceIntersection(
      Path path, SingleSpace a, SingleSpace b) {
    StaticType? type;
    if (a.type.isSubtypeOf(b.type)) {
      type = a.type;
    } else if (b.type.isSubtypeOf(a.type)) {
      type = b.type;
    }
    if (type == null) {
      return null;
    }
    Map<Key, Space> properties = {};
    for (MapEntry<Key, Space> entry in a.properties.entries) {
      Key key = entry.key;
      Space aSpace = entry.value;
      Space? bSpace = b.properties[key];
      if (bSpace != null) {
        properties[key] =
            _createSpaceIntersection(path.add(key), aSpace, bSpace);
      } else {
        properties[key] = aSpace;
      }
    }
    for (MapEntry<Key, Space> entry in b.properties.entries) {
      Key key = entry.key;
      properties[key] ??= entry.value;
    }
    return new SingleSpace(type, properties: properties);
  }

  /// Creates an approximation of the intersection of spaces [a] and [b].
  Space _createSpaceIntersection(Path path, Space a, Space b) {
    assert(
        path == a.path, "Unexpected path. Expected $path, actual ${a.path}.");
    assert(
        path == b.path, "Unexpected path. Expected $path, actual ${b.path}.");
    List<SingleSpace> singleSpaces = [];
    bool hasUnknownSpace = false;
    for (SingleSpace aSingleSpace in a.singleSpaces) {
      for (SingleSpace bSingleSpace in b.singleSpaces) {
        SingleSpace? space =
            _createSingleSpaceIntersection(path, aSingleSpace, bSingleSpace);
        if (space != null) {
          singleSpaces.add(space);
        } else {
          hasUnknownSpace = true;
        }
      }
    }
    if (hasUnknownSpace) {
      singleSpaces.add(new SingleSpace(createUnknownStaticType()));
    }
    return new Space.fromSingleSpaces(path, singleSpaces);
  }
}
