// Copyright (c) 2024, 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 'arguments.dart';
import 'expressions.dart';
import 'references.dart';
import 'scope.dart';
import 'type_annotations.dart';
import 'util.dart';

/// A [Proto] represents a parsed substructure before it can be determined
/// whether is is an expression, a type annotation, or something else.
///
/// For instance when parsing `Foo` we cannot determine what it means before
/// we have parsed what comes after `Foo`. If it is followed by `.bar` it could
/// be the class name prefix of a static field access `Foo.bar` and if it is
/// followed by `,` then it could be a type literal `Foo` as an expression.
sealed class Proto {
  /// Creates the [Proto] corresponding to this [Proto] followed by [send].
  ///
  /// If [send] is `null`, this corresponds to this [Proto] on its own.
  /// Otherwise this corresponds to this [Proto] followed by `.` or `?.`, an
  /// identifier, and optionally type arguments and/or arguments.
  Proto apply(IdentifierProto? send, {bool isNullAware = false}) {
    if (send == null) {
      return this;
    } else if (isNullAware) {
      return new InstanceAccessProto(this, send.text, isNullAware: true)
          .instantiate(send.typeArguments)
          .invoke(send.arguments);
    } else {
      return access(send.text)
          .instantiate(send.typeArguments)
          .invoke(send.arguments);
    }
  }

  /// Returns the [Proto] corresponding to accessing the property [name].
  ///
  /// If [name] is `null`, the [Proto] itself is returned. This functionality
  /// is supported for ease of use.
  Proto access(String? name);

  /// Returns the [Proto] corresponding applying [typeArguments] to it.
  ///
  /// If [typeArguments] is `null`, the [Proto] itself is returned. This
  /// functionality is supported for ease of use.
  Proto instantiate(List<TypeAnnotation>? typeArguments);

  /// Returns the [Proto] corresponding invoke it with [arguments].
  ///
  /// If [arguments] is `null`, the [Proto] itself is returned. This
  /// functionality is supported for ease of use.
  Proto invoke(List<Argument>? arguments);

  /// Creates the [Expression] corresponding to this [Proto].
  ///
  /// This corresponding to this [Proto] occurring in an expression context
  /// on its own.
  Expression toExpression();

  /// Creates the [TypeAnnotation] corresponding to this [Proto].
  ///
  /// This corresponding to this [Proto] occurring in a type annotation context
  /// on its own.
  TypeAnnotation toTypeAnnotation();

  /// Returns the [Proto] corresponding to this [Proto] in which all
  /// [UnresolvedIdentifier]s have been resolved within their scope.
  ///
  /// If this didn't create a new [Proto], `null` is returned.
  Proto? resolve();
}

/// An unresolved part of a proto, expression or type annotation.
sealed class Unresolved {
  /// Returns this unresolved part as an [Expression] if it can be resolved.
  Expression? resolveAsExpression();

  /// Returns this unresolved part as a [TypeAnnotation] if it can be resolved.
  TypeAnnotation? resolveAsTypeAnnotation();
}

/// The unresolved identifier [name] occurring in [scope].
class UnresolvedIdentifier extends Proto implements Unresolved {
  final Scope scope;
  final String name;

  UnresolvedIdentifier(this.scope, this.name);

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    return new UnresolvedAccess(this, name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    if (typeArguments == null) {
      return this;
    }
    return new UnresolvedInstantiate(this, typeArguments);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    if (arguments == null) {
      return this;
    }
    return new UnresolvedInvoke(this, arguments);
  }

  @override
  Expression toExpression() {
    return new UnresolvedExpression(this);
  }

  @override
  TypeAnnotation toTypeAnnotation() {
    return new UnresolvedTypeAnnotation(this);
  }

  @override
  String toString() => 'UnresolvedIdentifier($name)';

  @override
  Expression? resolveAsExpression() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toExpression();
  }

  @override
  TypeAnnotation? resolveAsTypeAnnotation() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toTypeAnnotation();
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is UnresolvedIdentifier &&
          runtimeType == other.runtimeType &&
          name == other.name;

  @override
  int get hashCode => name.hashCode;

  @override
  Proto? resolve() {
    Proto resolved = scope.lookup(name);
    return this == resolved ? null : resolved;
  }
}

/// The unresolved access to [name] on [prefix].
class UnresolvedAccess extends Proto implements Unresolved {
  final Proto prefix;
  final String name;

  UnresolvedAccess(this.prefix, this.name);

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    return new UnresolvedAccess(this, name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    if (typeArguments == null) {
      return this;
    }
    return new UnresolvedInstantiate(this, typeArguments);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    if (arguments == null) {
      return this;
    }
    return new UnresolvedInvoke(this, arguments);
  }

  @override
  Expression toExpression() {
    return new UnresolvedExpression(this);
  }

  @override
  TypeAnnotation toTypeAnnotation() {
    return new UnresolvedTypeAnnotation(this);
  }

  @override
  String toString() => 'UnresolvedAccess($prefix,$name)';

  @override
  Expression? resolveAsExpression() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toExpression();
  }

  @override
  TypeAnnotation? resolveAsTypeAnnotation() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toTypeAnnotation();
  }

  @override
  Proto? resolve() {
    Proto? newPrefix = prefix.resolve();
    return newPrefix == null ? null : newPrefix.access(name);
  }
}

/// The application of [typeArguments] on [prefix].
///
/// The [prefix] is (partially) unresolved, so it cannot yet determined what
/// the application of [typeArguments] means. For instance `unresolved<int>`
/// could be instantiation of the generic class `unresolved` or the
/// instantiation of the generic method `unresolved`.
class UnresolvedInstantiate extends Proto implements Unresolved {
  final Proto prefix;
  final List<TypeAnnotation> typeArguments;

  UnresolvedInstantiate(this.prefix, this.typeArguments);

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    return new UnresolvedAccess(this, name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    if (typeArguments == null) {
      return this;
    }
    return new UnresolvedInstantiate(this, typeArguments);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    if (arguments == null) {
      return this;
    }
    return new UnresolvedInvoke(this, arguments);
  }

  @override
  Expression toExpression() {
    return new UnresolvedExpression(this);
  }

  @override
  TypeAnnotation toTypeAnnotation() {
    return new UnresolvedTypeAnnotation(this);
  }

  @override
  String toString() => 'UnresolvedInstantiate($prefix,$typeArguments)';

  @override
  Expression? resolveAsExpression() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toExpression();
  }

  @override
  TypeAnnotation? resolveAsTypeAnnotation() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toTypeAnnotation();
  }

  @override
  Proto? resolve() {
    Proto? newPrefix = prefix.resolve();
    List<TypeAnnotation>? newTypeArguments =
        typeArguments.resolve((t) => t.resolve());

    return newPrefix == null && newTypeArguments == null
        ? null
        : (newPrefix ?? prefix).instantiate(newTypeArguments ?? typeArguments);
  }
}

/// The invocation of [arguments] on [prefix].
///
/// The [prefix] is (partially) unresolved, so it cannot yet determined what
/// the invocation of [arguments] means. For instance `unresolved(0)`
/// could be the constructor invocation of the unnamed constructor of the class
/// `unresolved` or the invocation of the method `unresolved`.
class UnresolvedInvoke extends Proto implements Unresolved {
  final Proto prefix;
  final List<Argument> arguments;

  UnresolvedInvoke(this.prefix, this.arguments);

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    return new UnresolvedAccess(this, name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    if (typeArguments == null) {
      return this;
    }
    return new UnresolvedInstantiate(this, typeArguments);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    if (arguments == null) {
      return this;
    }
    return new UnresolvedInvoke(this, arguments);
  }

  @override
  Expression toExpression() {
    return new UnresolvedExpression(this);
  }

  @override
  TypeAnnotation toTypeAnnotation() {
    return new UnresolvedTypeAnnotation(this);
  }

  @override
  String toString() => 'UnresolvedInvoke($prefix,$arguments)';

  @override
  Expression? resolveAsExpression() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toExpression();
  }

  @override
  TypeAnnotation? resolveAsTypeAnnotation() {
    Proto? resolved = resolve();
    return resolved == null ? null : resolved.toTypeAnnotation();
  }

  @override
  Proto? resolve() {
    Proto? newPrefix = prefix.resolve();
    List<Argument>? newArguments = arguments.resolve((a) => a.resolve());
    return newPrefix == null && newArguments == null
        ? null
        : (newPrefix ?? prefix).invoke(newArguments ?? arguments);
  }
}

/// A [reference] to a class.
///
/// The [Proto] includes the [scope] of the class, which is used to resolve
/// access to constructors and static members on the class.
class ClassProto extends Proto {
  final ClassReference reference;
  final TypeDeclarationScope scope;

  ClassProto(this.reference, this.scope);

  @override
  String toString() => 'ClassProto($reference)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new GenericClassProto(reference, scope, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() => new NamedTypeAnnotation(reference);

  @override
  Proto? resolve() => null;
}

/// A [reference] to a class instantiated with [typeArguments].
///
/// The [Proto] includes the [scope] of the class, which is used to resolve
/// access to constructors on the class.
class GenericClassProto extends Proto {
  final ClassReference reference;
  final TypeDeclarationScope scope;
  final List<TypeAnnotation> typeArguments;

  GenericClassProto(this.reference, this.scope, this.typeArguments);

  @override
  String toString() => 'GenericClassProto($reference,$typeArguments)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name, typeArguments);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? throw new UnimplementedError('GenericClassProto.instantiate')
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() =>
      new NamedTypeAnnotation(reference, typeArguments);

  @override
  Proto? resolve() {
    List<TypeAnnotation>? newTypeArguments =
        typeArguments.resolve((a) => a.resolve());
    return newTypeArguments == null
        ? null
        : new GenericClassProto(reference, scope, newTypeArguments);
  }
}

/// A [reference] to an extension
///
/// The [Proto] includes the [scope] of the extension, which is used to resolve
/// access to static members on the extension.
class ExtensionProto extends Proto {
  final ExtensionReference reference;
  final TypeDeclarationScope scope;

  ExtensionProto(this.reference, this.scope);

  @override
  String toString() => 'ExtensionProto($reference)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new InvalidInstantiationProto(this, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new InvalidInvocationProto(this, const [], arguments)
        : this;
  }

  @override
  Expression toExpression() {
    return new InvalidExpression();
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  Proto? resolve() => null;
}

/// A [reference] to an enum
///
/// The [Proto] includes the [scope] of the enum, which is used to resolve
/// access to static members on the enum.
class EnumProto extends Proto {
  final EnumReference reference;
  final TypeDeclarationScope scope;

  EnumProto(this.reference, this.scope);

  @override
  String toString() => 'EnumProto($reference)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new GenericEnumProto(reference, scope, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new InvalidInvocationProto(this, const [], arguments)
        : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() => new NamedTypeAnnotation(reference);

  @override
  Proto? resolve() => null;
}

/// A [reference] to an enum instantiated with [typeArguments].
///
/// The [Proto] includes the [scope] of the enum, which is used to resolve
/// access to constructors on the enum.
class GenericEnumProto extends Proto {
  final EnumReference reference;
  final TypeDeclarationScope scope;
  final List<TypeAnnotation> typeArguments;

  GenericEnumProto(this.reference, this.scope, this.typeArguments);

  @override
  String toString() => 'GenericEnumProto($reference,$typeArguments)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name, typeArguments);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? throw new UnimplementedError('$this.instantiate')
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() =>
      new NamedTypeAnnotation(reference, typeArguments);

  @override
  Proto? resolve() {
    List<TypeAnnotation>? newTypeArguments =
        typeArguments.resolve((a) => a.resolve());
    return newTypeArguments == null
        ? null
        : new GenericEnumProto(reference, scope, newTypeArguments);
  }
}

/// A [reference] to a mixin
///
/// The [Proto] includes the [scope] of the mixin, which is used to resolve
/// access to static members on the mixin.
class MixinProto extends Proto {
  final MixinReference reference;
  final TypeDeclarationScope scope;

  MixinProto(this.reference, this.scope);

  @override
  String toString() => 'MixinProto($reference)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new GenericMixinProto(reference, scope, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new InvalidInvocationProto(this, const [], arguments)
        : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() => new NamedTypeAnnotation(reference);

  @override
  Proto? resolve() => null;
}

/// A [reference] to an enum instantiated with [typeArguments].
///
/// The [Proto] includes the [scope] of the enum, which is used to resolve
/// access to constructors on the enum.
class GenericMixinProto extends Proto {
  final MixinReference reference;
  final TypeDeclarationScope scope;
  final List<TypeAnnotation> typeArguments;

  GenericMixinProto(this.reference, this.scope, this.typeArguments);

  @override
  String toString() => 'GenericMixinProto($reference,$typeArguments)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name, typeArguments);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? throw new UnimplementedError('$this.instantiate')
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() =>
      new NamedTypeAnnotation(reference, typeArguments);

  @override
  Proto? resolve() {
    List<TypeAnnotation>? newTypeArguments =
        typeArguments.resolve((a) => a.resolve());
    return newTypeArguments == null
        ? null
        : new GenericMixinProto(reference, scope, newTypeArguments);
  }
}

/// A [reference] to an extension type.
///
/// The [Proto] includes the [scope] of the extension type, which is used to
/// resolve access to constructors and static members on the extension type.
class ExtensionTypeProto extends Proto {
  final ExtensionTypeReference reference;
  final TypeDeclarationScope scope;

  ExtensionTypeProto(this.reference, this.scope);

  @override
  String toString() => 'ExtensionTypeProto($reference)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new GenericExtensionTypeProto(reference, scope, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() => new NamedTypeAnnotation(reference);

  @override
  Proto? resolve() => null;
}

/// A [reference] to an extension type instantiated with [typeArguments].
///
/// The [Proto] includes the [scope] of the extension type, which is used to
/// resolve access to constructors on the class.
class GenericExtensionTypeProto extends Proto {
  final ExtensionTypeReference reference;
  final TypeDeclarationScope scope;
  final List<TypeAnnotation> typeArguments;

  GenericExtensionTypeProto(this.reference, this.scope, this.typeArguments);

  @override
  String toString() => 'GenericExtensionTypeProto($reference,$typeArguments)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name, typeArguments);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? throw new UnimplementedError('GenericExtensionTypeProto.instantiate')
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() =>
      new NamedTypeAnnotation(reference, typeArguments);

  @override
  Proto? resolve() {
    List<TypeAnnotation>? newTypeArguments =
        typeArguments.resolve((a) => a.resolve());
    return newTypeArguments == null
        ? null
        : new GenericExtensionTypeProto(reference, scope, newTypeArguments);
  }
}

/// A [reference] to a typedef.
///
/// The [Proto] includes the [scope] of the typedef, which is used to resolve
/// access to constructors through the typedef.
class TypedefProto extends Proto {
  final TypedefReference reference;
  final TypeDeclarationScope scope;

  TypedefProto(this.reference, this.scope);

  @override
  String toString() => 'TypedefProto($reference)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new GenericTypedefProto(reference, scope, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() => new NamedTypeAnnotation(reference);

  @override
  Proto? resolve() => null;
}

/// A [reference] to a typedef instantiated with [typeArguments].
///
/// The [Proto] includes the [scope] of the typedef, which is used to resolve
/// access to constructors through the typedef.
class GenericTypedefProto extends Proto {
  final TypedefReference reference;
  final TypeDeclarationScope scope;
  final List<TypeAnnotation> typeArguments;

  GenericTypedefProto(this.reference, this.scope, this.typeArguments);

  @override
  String toString() => 'GenericTypedefProto($reference,$typeArguments)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    if (name == 'new') {
      name = '';
    }
    return scope.lookup(name, typeArguments);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? throw new UnimplementedError('GenericTypedefProto.instantiate')
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null ? access('new').invoke(arguments) : this;
  }

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() =>
      new NamedTypeAnnotation(reference, typeArguments);

  @override
  Proto? resolve() {
    List<TypeAnnotation>? newTypeArguments =
        typeArguments.resolve((a) => a.resolve());
    return newTypeArguments == null
        ? null
        : new GenericTypedefProto(reference, scope, newTypeArguments);
  }
}

/// A [reference] to a `void`.
class VoidProto extends Proto {
  final Reference reference;

  VoidProto(this.reference);

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() => new VoidTypeAnnotation(reference);

  @override
  Proto access(String? name) {
    return name == null ? this : new InvalidAccessProto(this, name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments == null
        ? this
        : new InvalidInstantiationProto(this, typeArguments);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments == null
        ? this
        : new InvalidInvocationProto(this, const [], arguments);
  }

  @override
  String toString() => 'VoidProto()';

  @override
  Proto? resolve() => null;
}

/// A [reference] to a `dynamic`.
class DynamicProto extends Proto {
  final Reference reference;

  DynamicProto(this.reference);

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() => new DynamicTypeAnnotation(reference);

  @override
  Proto access(String? name) {
    return name == null ? this : new InvalidAccessProto(this, name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments == null
        ? this
        : new InvalidInstantiationProto(this, typeArguments);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments == null
        ? this
        : new InvalidInvocationProto(this, const [], arguments);
  }

  @override
  String toString() => 'DynamicProto()';

  @override
  Proto? resolve() => null;
}

/// A [reference] to a constructor, including a reference to the [type] on which
/// it was accessed, as well as the [typeArguments] applied to [type].
class ConstructorProto extends Proto {
  final Reference type;
  final List<TypeAnnotation> typeArguments;
  final ConstructorReference reference;

  ConstructorProto(this.type, this.typeArguments, this.reference);

  @override
  String toString() => 'ConstructorProto($type,$typeArguments,$reference)';

  @override
  Proto access(String? name) {
    return name != null ? new InvalidAccessProto(this, name) : this;
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new ExpressionInstantiationProto(this, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new ExpressionProto(new ConstructorInvocation(
            new NamedTypeAnnotation(type, typeArguments), reference, arguments))
        : this;
  }

  @override
  Expression toExpression() {
    return new ConstructorTearOff(
        new NamedTypeAnnotation(type, typeArguments), reference);
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  Proto? resolve() => null;
}

/// A [reference] to a static or top level field.
class FieldProto extends Proto {
  final FieldReference reference;

  FieldProto(this.reference);

  @override
  String toString() => 'FieldProto($reference)';

  @override
  Proto access(String? name) {
    return name != null ? new InstanceAccessProto(this, name) : this;
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new ExpressionInstantiationProto(this, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new InstanceInvocationProto(this, const [], arguments)
        : this;
  }

  @override
  Expression toExpression() {
    return new StaticGet(reference);
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  Proto? resolve() => null;
}

/// A [reference] to a static or top level method.
class FunctionProto extends Proto {
  final FunctionReference reference;

  FunctionProto(this.reference);

  @override
  String toString() => 'FunctionProto($reference)';

  @override
  Proto access(String? name) {
    return name != null ? new InstanceAccessProto(this, name) : this;
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new FunctionInstantiationProto(reference, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new ExpressionProto(
            new StaticInvocation(reference, const [], arguments))
        : this;
  }

  @override
  Expression toExpression() {
    return new FunctionTearOff(reference);
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  Proto? resolve() => null;
}

/// A [reference] to a static or top level method instantiated with
/// [typeArguments].
class FunctionInstantiationProto extends Proto {
  final FunctionReference reference;
  final List<TypeAnnotation> typeArguments;

  FunctionInstantiationProto(this.reference, this.typeArguments);

  @override
  String toString() => 'FunctionInstantiationProto($reference,$typeArguments)';

  @override
  Proto access(String? name) {
    return name != null ? new InstanceAccessProto(this, name) : this;
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new ExpressionInstantiationProto(this, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new ExpressionProto(
            new StaticInvocation(reference, typeArguments, arguments))
        : this;
  }

  @override
  Expression toExpression() {
    return new Instantiation(new FunctionTearOff(reference), typeArguments);
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  Proto? resolve() => null;
}

/// A reference to the [prefix].
///
/// The [Proto] includes the [scope] of the prefix, which is used to resolve
/// access to imported members and types through the prefix.
class PrefixProto extends Proto {
  final String prefix;
  final Scope scope;

  PrefixProto(this.prefix, this.scope);

  @override
  String toString() => 'PrefixProto($prefix)';

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    return scope.lookup(name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new InvalidInstantiationProto(this, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new InvalidInvocationProto(this, const [], arguments)
        : this;
  }

  @override
  Expression toExpression() {
    return new InvalidExpression();
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  Proto? resolve() => null;
}

/// The suffix of an access to the property [text] including applied
/// [typeArguments] and [arguments], if any.
///
/// This is not really a [Proto] but just a sequence of information needed to
/// call [Proto.access], [Proto.instantiate], and [Proto.invoke]. Unfortunately
/// the parser currently provides these as a "send" which is not the ideal model
/// for parsing Dart.
class IdentifierProto extends Proto {
  final String text;
  final List<TypeAnnotation>? typeArguments;
  final List<Argument>? arguments;

  IdentifierProto(String text) : this._(text, null, null);

  IdentifierProto._(this.text, this.typeArguments, this.arguments);

  @override
  Proto access(String? name) {
    return name == null
        ? this
        : throw new UnimplementedError('IdentifierProto.access');
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments == null
        ? this
        : new IdentifierProto._(text, typeArguments, null);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments == null
        ? this
        : new IdentifierProto._(text, typeArguments, arguments);
  }

  @override
  Expression toExpression() {
    throw new UnimplementedError('IdentifierProto.toExpression');
  }

  @override
  TypeAnnotation toTypeAnnotation() {
    throw new UnimplementedError('IdentifierProto.toTypeAnnotation');
  }

  @override
  String toString() => 'IdentifierProto($text)';

  @override
  Proto? resolve() => null;
}

/// An access to the [text] property on [receiver].
///
/// If [isNullAware] is `true`, this is a `?.` access, otherwise it is a '.'
/// access.
///
/// This is used for the when the [text] property is known to be an instance
/// member of [receiver]. Either because the [receiver] cannot be the prefix
/// of a static access or if [isNullAware] is `true`.
class InstanceAccessProto extends Proto {
  final Proto receiver;
  final String text;
  final bool isNullAware;

  InstanceAccessProto(this.receiver, this.text, {this.isNullAware = false});

  @override
  Proto access(String? name) {
    if (name == null) {
      return this;
    }
    throw new UnimplementedError('$this.access');
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    if (typeArguments == null) {
      return this;
    }
    throw new UnimplementedError('$this.instantiate');
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    if (arguments == null) {
      return this;
    }
    throw new UnimplementedError('$this.invoke');
  }

  @override
  Expression toExpression() {
    return isNullAware
        ? new NullAwarePropertyGet(receiver.toExpression(), text)
        : new PropertyGet(receiver.toExpression(), text);
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  String toString() => 'InstanceAccessProto($receiver,$text)';

  @override
  Proto? resolve() => null;
}

/// The application of [typeArguments] on [receiver] which is known to be an
/// expression.
class ExpressionInstantiationProto extends Proto {
  final Proto receiver;
  final List<TypeAnnotation> typeArguments;

  ExpressionInstantiationProto(this.receiver, this.typeArguments);

  @override
  Proto access(String? name) {
    return name != null
        ? new ExpressionProto(new PropertyGet(receiver.toExpression(), name))
        : this;
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    throw new UnimplementedError('InstanceInstantiationProto.instantiate');
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new ExpressionProto(new ImplicitInvocation(
            receiver.toExpression(), const [], arguments))
        : this;
  }

  @override
  Expression toExpression() {
    return new Instantiation(receiver.toExpression(), typeArguments);
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  String toString() => 'InstanceInstantiationProto($receiver,$typeArguments)';

  @override
  Proto? resolve() => null;
}

/// The application of [typeArguments] and [arguments] on [receiver] which is
/// known to be an expression.
class InstanceInvocationProto extends Proto {
  final Proto receiver;
  final List<TypeAnnotation> typeArguments;
  final List<Argument> arguments;

  InstanceInvocationProto(this.receiver, this.typeArguments, this.arguments);

  @override
  Proto access(String? name) {
    throw new UnimplementedError('InstanceInvocationProto.access');
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    throw new UnimplementedError('InstanceInvocationProto.instantiate');
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    throw new UnimplementedError('InstanceInvocationProto.invoke');
  }

  @override
  Expression toExpression() {
    return new ImplicitInvocation(
        receiver.toExpression(), typeArguments, arguments);
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  String toString() =>
      'InstanceInvocationProto($receiver,$typeArguments,$arguments)';

  @override
  Proto? resolve() => null;
}

/// The access of [text] on [receiver] when this is known to be an invalid
/// construct.
// TODO(johnniwinther): This is not valid for non-const expressions. Expand
// this if used for non-const expressions.
class InvalidAccessProto extends Proto {
  final Proto receiver;
  final String text;

  InvalidAccessProto(this.receiver, this.text);

  @override
  Proto access(String? name) {
    throw new UnimplementedError('InvalidAccessProto.access');
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    throw new UnimplementedError('InvalidAccessProto.instantiate');
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    throw new UnimplementedError('InvalidAccessProto.invoke');
  }

  @override
  Expression toExpression() {
    return new InvalidExpression();
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  String toString() => 'InvalidAccessProto($receiver,$text)';

  @override
  Proto? resolve() => null;
}

/// The application of [typeArguments] on [receiver] when this is known to be an
/// invalid construct.
// TODO(johnniwinther): This might not be valid for non-const expressions.
//  Expand this if used for non-const expressions.
class InvalidInstantiationProto extends Proto {
  final Proto receiver;
  final List<TypeAnnotation> typeArguments;

  InvalidInstantiationProto(this.receiver, this.typeArguments);

  @override
  Proto access(String? name) {
    throw new UnimplementedError('InvalidInstantiationProto.access');
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    throw new UnimplementedError('InvalidInstantiationProto.instantiate');
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    throw new UnimplementedError('InvalidInstantiationProto.invoke');
  }

  @override
  Expression toExpression() {
    return new InvalidExpression();
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  String toString() => 'InvalidInstantiationProto($receiver,$typeArguments)';

  @override
  Proto? resolve() => null;
}

/// The application of [typeArguments] and [arguments] on [receiver] when this
/// is known to be an invalid construct.
// TODO(johnniwinther): This might not be valid for non-const expressions.
//  Expand this if used for non-const expressions.
class InvalidInvocationProto extends Proto {
  final Proto receiver;
  final List<TypeAnnotation> typeArguments;
  final List<Argument> arguments;

  InvalidInvocationProto(this.receiver, this.typeArguments, this.arguments);

  @override
  Proto access(String? name) {
    throw new UnimplementedError('InvalidInvocationProto.access');
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    throw new UnimplementedError('InvalidInvocationProto.instantiate');
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    throw new UnimplementedError('InvalidInvocationProto.invoke');
  }

  @override
  Expression toExpression() {
    return new InvalidExpression();
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  String toString() =>
      'InvalidInvocationProto($receiver,$typeArguments,$arguments)';

  @override
  Proto? resolve() => null;
}

/// An [expression] occurring as a [Proto].
class ExpressionProto extends Proto {
  final Expression expression;

  ExpressionProto(this.expression);

  @override
  Proto access(String? name) {
    return name != null ? new InstanceAccessProto(this, name) : this;
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments != null
        ? new ExpressionInstantiationProto(this, typeArguments)
        : this;
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new ExpressionProto(
            new ImplicitInvocation(expression, const [], arguments))
        : this;
  }

  @override
  Expression toExpression() {
    return expression;
  }

  @override
  TypeAnnotation toTypeAnnotation() => new InvalidTypeAnnotation();

  @override
  String toString() => 'ExpressionProto($expression)';

  @override
  Proto? resolve() => null;
}

/// A reference to a [functionTypeParameter].
class FunctionTypeParameterProto extends Proto {
  final FunctionTypeParameter functionTypeParameter;

  FunctionTypeParameterProto(this.functionTypeParameter);

  @override
  Proto access(String? name) {
    return name == null ? this : new InvalidAccessProto(this, name);
  }

  @override
  Proto instantiate(List<TypeAnnotation>? typeArguments) {
    return typeArguments == null
        ? this
        : new ExpressionInstantiationProto(this, typeArguments);
  }

  @override
  Proto invoke(List<Argument>? arguments) {
    return arguments != null
        ? new ExpressionProto(
            new ImplicitInvocation(toExpression(), const [], arguments))
        : this;
  }

  @override
  Proto? resolve() => null;

  @override
  Expression toExpression() {
    return new TypeLiteral(toTypeAnnotation());
  }

  @override
  TypeAnnotation toTypeAnnotation() {
    return new FunctionTypeParameterType(functionTypeParameter);
  }
}
