// 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 'package:_fe_analyzer_shared/src/exhaustiveness/key.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/shared.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/types.dart';

class TestEnvironment implements ObjectPropertyLookup {
  late final _TypeOperations _typeOperations = new _TypeOperations(this);
  late final _EnumOperations _enumOperations = new _EnumOperations();
  late final _SealedClassOperations _sealedClassOperations =
      new _SealedClassOperations(this);
  late final _ExhaustivenessCache _exhaustivenessCache =
      new _ExhaustivenessCache(
          _typeOperations, _enumOperations, _sealedClassOperations);

  TestEnvironment() {
    _addClass(_Class.Object);
    _addClass(_Class.Never);
    _addClass(_Class.Bool);
  }

  Map<String, _Class> _classes = {};
  Map<_Class, Map<Key, _Type>> _fields = {};
  Map<_Class, Set<_InterfaceType>> _supertypes = {};
  Map<_Class, Set<_InterfaceType>> _subtypes = {};

  void _addClass(_Class cls) {
    assert(!_classes.containsKey(cls.name), "Duplicate class '${cls.name}'");
    _classes[cls.name] = cls;
  }

  void _addSupertype(_InterfaceType type, _InterfaceType supertype) {
    (_supertypes[type.cls] ??= {}).add(supertype);
    (_subtypes[supertype.cls] ??= {}).add(type);
  }

  _Type _typeFromStaticType(StaticType type) {
    if (type is TypeBasedStaticType<_Type>) {
      return type.typeForTesting;
    } else if (type is NullableStaticType) {
      return new _NullableType(_typeFromStaticType(type.underlying));
    } else if (type == StaticType.nullType) {
      return _Type.Null;
    } else if (type == StaticType.nonNullableObject) {
      return _Type.Object;
    } else if (type == StaticType.nullableObject) {
      return _Type.NullableObject;
    } else if (type == StaticType.neverType) {
      return _Type.Never;
    }
    throw new UnsupportedError(
        "Unexpected StaticType $type (${type.runtimeType}).");
  }

  Set<_InterfaceType> _getSupertypes(_Class cls) {
    return _supertypes[cls] ?? const {};
  }

  Set<_InterfaceType> _getSubtypes(_Class cls) {
    return _subtypes[cls] ?? const {};
  }

  Map<Key, _Type> _getFields(_Class cls) {
    return _fields[cls] ?? const {};
  }

  StaticType createClass(String name,
      {bool isSealed = false,
      List<StaticType> inherits = const [],
      Map<String, StaticType> fields = const {}}) {
    _Class cls = new _Class(name, isSealed: isSealed);
    _addClass(cls);
    _InterfaceType type = new _InterfaceType(cls);
    _addSupertype(type, _Type.Object);
    for (StaticType inherit in inherits) {
      _Type supertype = _typeFromStaticType(inherit);
      if (supertype is _InterfaceType) {
        _addSupertype(type, supertype);
      } else {
        throw new UnsupportedError(
            "Unexpected supertype $supertype (${supertype.runtimeType}).");
      }
    }

    if (fields.isNotEmpty) {
      Map<Key, _Type> fieldMap = _fields[cls] ??= {};
      for (MapEntry<String, StaticType> entry in fields.entries) {
        // TODO(srawlins): Look into fixing this code. Right now we get:
        // "The argument type 'String' isn't related to 'Key'."
        // ignore: collection_methods_unrelated_type
        assert(!fieldMap.containsKey(entry.key),
            "Duplicate field '${entry.key}' in $cls.");
        fieldMap[new NameKey(entry.key)] = _typeFromStaticType(entry.value);
      }
    }

    int sealed = 0;
    for (_InterfaceType supertype in _getSupertypes(cls)) {
      if (supertype.cls.isSealed) sealed++;
    }

    // We don't allow a sealed type's subtypes to be shared with some other
    // sibling supertype, as in D here:
    //
    //   (A) (B)
    //   / \ / \
    //  C   D   E
    //
    // We could remove this restriction but doing so will require
    // expandTypes() to be more complex. In the example here, if we subtract
    // E from A, the result should be C|D. That requires knowing that B should
    // be expanded, which expandTypes() doesn't currently handle.
    if (sealed > 1) {
      throw new ArgumentError('Can only have one sealed supertype.');
    }

    return _exhaustivenessCache.getStaticType(type);
  }

  StaticType createRecordType(Map<String, StaticType> named) {
    Map<Key, _Type> namedTypes = {};
    for (MapEntry<String, StaticType> entry in named.entries) {
      namedTypes[new RecordNameKey(entry.key)] =
          _typeFromStaticType(entry.value);
    }
    _Type type = new _RecordType([], namedTypes);
    return _exhaustivenessCache.getStaticType(type);
  }

  @override
  StaticType? getObjectFieldType(Key key) {
    return _exhaustivenessCache.getObjectFieldType(key);
  }
}

class _Type {
  static const _InterfaceType Object = _InterfaceType(_Class.Object);
  static const _Type NullableObject = _NullableType(_Type.Object);
  static const _InterfaceType Never = _InterfaceType(_Class.Never);
  static const _InterfaceType Bool = _InterfaceType(_Class.Bool);
  static const _Type Null = _NullableType(_Type.Never);
}

class _Class {
  final String name;
  final bool isSealed;

  const _Class(this.name, {this.isSealed = false});

  @override
  String toString() => name;

  static const _Class Object = const _Class('Object');
  static const _Class Bool = const _Class('bool');
  static const _Class Never = const _Class('Never');
}

class _EnumClass extends _Class {
  _EnumClass(super.name, {required super.isSealed});
}

// TODO(johnniwinther): Support testing of enums elements.
typedef _EnumElement = Object;

// TODO(johnniwinther): Support testing of enums element values.
typedef _EnumElementValue = Object;

class _InterfaceType implements _Type {
  final _Class cls;

  const _InterfaceType(this.cls);

  @override
  int get hashCode => cls.hashCode;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    return other is _InterfaceType && cls == other.cls;
  }

  @override
  String toString() => cls.name;
}

class _NullableType implements _Type {
  final _Type type;

  const _NullableType(this.type);

  @override
  int get hashCode => type.hashCode;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    return other is _NullableType && type == other.type;
  }

  @override
  String toString() => identical(this, _Type.Null) ? 'Null' : '$type?';
}

class _RecordType implements _Type {
  final List<_Type> positional;
  final Map<Key, _Type> named;

  _RecordType(this.positional, this.named);

  @override
  int get hashCode => Object.hash(
      Object.hashAll(positional),
      Object.hashAllUnordered(named.keys),
      Object.hashAllUnordered(named.values));

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! _RecordType) return false;
    if (positional.length != other.positional.length) return false;
    if (named.length != other.named.length) return false;
    for (int i = 0; i < positional.length; i++) {
      if (positional[i] != other.positional[i]) {
        return false;
      }
    }
    for (MapEntry<Key, _Type> entry in named.entries) {
      if (entry.value != other.named[entry.key]) return false;
    }
    return true;
  }

  @override
  String toString() {
    StringBuffer sb = new StringBuffer();
    sb.write('(');
    String comma = '';
    for (_Type type in positional) {
      sb.write(comma);
      sb.write(type);
      comma = ', ';
    }
    if (named.isNotEmpty) {
      sb.write(comma);
      sb.write('{');
      comma = '';
      for (MapEntry<Key, _Type> entry in named.entries) {
        sb.write(comma);
        sb.write(entry.key.name);
        sb.write(': ');
        sb.write(entry.value);
        comma = ', ';
      }
      sb.write('}');
    }
    sb.write(')');
    return sb.toString();
  }
}

class _TypeOperations implements TypeOperations<_Type> {
  final TestEnvironment env;

  _TypeOperations(this.env);

  @override
  _Type get boolType => _Type.Bool;

  @override
  Map<Key, _Type> getFieldTypes(_Type type) {
    if (type is _InterfaceType) {
      Map<Key, _Type> fields = {};
      for (_InterfaceType supertype in env._getSupertypes(type.cls)) {
        fields.addAll(getFieldTypes(supertype));
      }
      fields.addAll(env._getFields(type.cls));
      return fields;
    } else if (type is _RecordType) {
      Map<Key, _Type> fields = {};
      fields.addAll(getFieldTypes(_Type.Object));
      for (int i = 0; i < type.positional.length; i++) {
        fields[new RecordIndexKey(i)] = type.positional[i];
      }
      fields.addAll(type.named.map(
          (key, value) => new MapEntry(new RecordNameKey(key.name), value)));
      return fields;
    } else {
      return getFieldTypes(_Type.Object);
    }
  }

  @override
  _Type getNonNullable(_Type type) {
    if (type is _NullableType) {
      return type.type;
    }
    return type;
  }

  @override
  bool isBoolType(_Type type) {
    return type == _Type.Bool;
  }

  @override
  bool isNeverType(_Type type) {
    return type == _Type.Never;
  }

  @override
  bool isNonNullableObject(_Type type) {
    return type == _Type.Object;
  }

  @override
  bool isDynamic(_Type type) {
    return type == _Type.NullableObject;
  }

  @override
  bool isNullType(_Type type) {
    return type == _Type.Null;
  }

  @override
  bool isNullable(_Type type) {
    return type is _NullableType;
  }

  @override
  bool isPotentiallyNullable(_Type type) {
    // TODO(johnniwinther): Support type variables.
    return type is _NullableType;
  }

  @override
  bool isNullableObject(_Type type) {
    return type == _Type.NullableObject;
  }

  @override
  bool isSubtypeOf(_Type s, _Type t) {
    if (s == t) return true;
    if (t == _Type.NullableObject) return true;
    if (s == _Type.Never) return true;
    if (s == _Type.Null && t is _NullableType) return true;
    if (s is _NullableType) {
      if (t is _NullableType) {
        return isSubtypeOf(s.type, t.type);
      }
      return false;
    } else {
      if (t is _NullableType) {
        return isSubtypeOf(s, t.type);
      } else {
        if (s is _InterfaceType && t is _InterfaceType) {
          if (t.cls == _Class.Object) return true;
          for (_InterfaceType supertype in env._getSupertypes(s.cls)) {
            if (isSubtypeOf(supertype, t)) {
              return true;
            }
          }
        }
        return false;
      }
    }
  }

  @override
  _Type get nonNullableObjectType => _Type.Object;

  @override
  _Type get nullableObjectType => _Type.NullableObject;

  @override
  String typeToString(_Type type) {
    return type.toString();
  }

  @override
  bool isRecordType(_Type type) {
    return type is _RecordType;
  }

  @override
  _Type overapproximate(_Type type) {
    // TODO(johnniwinther): Support generic types in testing.
    return type;
  }

  @override
  bool isGeneric(_Type type) {
    // TODO(johnniwinther): Support generic types in testing.
    return false;
  }

  @override
  _Type instantiateFuture(_Type type) {
    throw new UnimplementedError('_TypeOperations.getFutureOrFutureType');
  }

  @override
  _Type? getFutureOrTypeArgument(_Type type) {
    // TODO(johnniwinther): Support future or types in testing.
    return null;
  }

  @override
  _Type? getListElementType(_Type type) {
    // TODO(johnniwinther): Support list types in testing.
    return null;
  }

  @override
  _Type? getListType(_Type type) {
    // TODO(johnniwinther): Support list types in testing.
    return null;
  }

  @override
  _Type? getMapValueType(_Type type) {
    // TODO(johnniwinther): Support map types in testing.
    return null;
  }

  @override
  bool hasSimpleName(_Type type) {
    return type is _InterfaceType;
  }

  @override
  _Type? getTypeVariableBound(_Type type) {
    // TODO(johnniwinther): Support type variable bounds in testing.
    return null;
  }

  @override
  _Type getExtensionTypeErasure(_Type type) {
    // TODO(johnniwinther): Support extension types in testing.
    return type;
  }
}

class _EnumOperations
    implements
        EnumOperations<_Type, _EnumClass, _EnumElement, _EnumElementValue> {
  @override
  _EnumClass? getEnumClass(_Type type) {
    if (type is _InterfaceType) {
      _Class cls = type.cls;
      if (cls is _EnumClass) {
        return cls;
      }
    }
    return null;
  }

  @override
  String getEnumElementName(_EnumElement enumElement) {
    // TODO(johnniwinther): Support testing of enums.
    throw new UnimplementedError('_EnumOperations.getEnumElementName');
  }

  @override
  _Type getEnumElementType(_EnumElement enumElement) {
    // TODO(johnniwinther): Support testing of enums.
    throw new UnimplementedError('_EnumOperations.getEnumElementType');
  }

  @override
  _EnumElementValue getEnumElementValue(_EnumElement enumElement) {
    // TODO(johnniwinther): Support testing of enums.
    throw new UnimplementedError('_EnumOperations.getEnumElementValue');
  }

  @override
  Iterable<_EnumElement> getEnumElements(_EnumClass enumClass) {
    // TODO(johnniwinther): Support testing of enums.
    throw new UnimplementedError('_EnumOperations.getEnumElements');
  }
}

class _SealedClassOperations implements SealedClassOperations<_Type, _Class> {
  final TestEnvironment env;

  _SealedClassOperations(this.env);

  @override
  List<_Class> getDirectSubclasses(_Class sealedClass) {
    List<_Class> classes = [];
    for (_InterfaceType subtype in env._getSubtypes(sealedClass)) {
      classes.add(subtype.cls);
    }
    return classes;
  }

  @override
  _Class? getSealedClass(_Type type) {
    if (type is _InterfaceType) {
      _Class cls = type.cls;
      if (cls.isSealed) {
        return cls;
      }
    }
    return null;
  }

  @override
  _Type? getSubclassAsInstanceOf(_Class subClass, _Type sealedClassType) {
    return new _InterfaceType(subClass);
  }
}

class _ExhaustivenessCache extends ExhaustivenessCache<_Type, _Class,
    _EnumClass, _EnumElement, _EnumElementValue> {
  _ExhaustivenessCache(
      super.typeOperations, super.enumOperations, super.sealedClassOperations);
}
