// Copyright (c) 2016, 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.

library kernel.transformations.reify.transformation.transformer;

import '../analysis/program_analysis.dart';
import 'package:kernel/ast.dart';
import 'binding.dart' show RuntimeLibrary;
import 'builder.dart' show RuntimeTypeSupportBuilder;
import 'dart:collection' show LinkedHashMap;
import '../asts.dart';

export 'binding.dart' show RuntimeLibrary;
export 'builder.dart' show RuntimeTypeSupportBuilder;

enum RuntimeTypeStorage {
  none,
  inheritedField,
  field,
  getter,
}

class TransformationContext {
  /// Describes how the runtime type is stored on the object.
  RuntimeTypeStorage runtimeTypeStorage;

  /// Field added to store the runtime type if [runtimeType] is
  /// [RuntimeTypeStorage.field].
  Field runtimeTypeField;

  /// The parameter for the type information introduced to the constructor or
  /// to static initializers.
  VariableDeclaration parameter;

  /// A ordered collection of fields together with their initializers rewritten
  /// to static initializer functions that can be used in the constructor's
  /// initializer list.
  /// The order is important because of possible side-effects in the
  /// initializers.
  LinkedHashMap<Field, Procedure> initializers;

  // `true` if the visitor currently is in a field initializer, a initializer
  // list of a constructor, or the body of a factory method. In these cases,
  // type argument access is different than in an instance context, since `this`
  // is not available.
  bool inInitializer = false;

  String toString() => "s: ${runtimeTypeStorage} f: $runtimeTypeField,"
      " p: $parameter, i: $inInitializer";
}

abstract class DebugTrace {
  static const bool debugTrace = false;

  static const int lineLength = 80;

  TransformationContext get context;

  String getNodeLevel(TreeNode node) {
    String level = "";
    while (node != null && node is! Library) {
      level = " $level";
      node = node.parent;
    }
    return level;
  }

  String shorten(String s) {
    return s.length > lineLength ? s.substring(0, lineLength) : s;
  }

  void trace(TreeNode node) {
    if (debugTrace) {
      String nodeText = node.toString().replaceAll("\n", " ");
      print(shorten("trace:${getNodeLevel(node)}$context"
          " [${node.runtimeType}] $nodeText"));
    }
  }
}

/// Rewrites a tree to remove generic types and runtime type checks and replace
/// them with Dart objects.
///
/// Runtime types are stored in a field/getter called [runtimeTypeName] on the
/// object, which for parameterized classes is initialized in the constructor.
//  TODO(karlklose):
//  - add a scoped namer
//  - rewrite types (supertypes, implemented types)
//  - rewrite as
class ReifyVisitor extends Transformer with DebugTrace {
  final RuntimeLibrary rtiLibrary;
  final RuntimeTypeSupportBuilder builder;
  final ProgramKnowledge knowledge;

  ReifyVisitor(this.rtiLibrary, this.builder, this.knowledge,
      [this.libraryToTransform]);

  /// If not null, the transformation will only be applied to classes declared
  /// in this library.
  final Library libraryToTransform;

  // TODO(karlklose): find a way to get rid of this state in the visitor.
  TransformationContext context;

  static const String genericMethodTypeParametersName = r"$typeParameters";

  bool libraryShouldBeTransformed(Library library) {
    return libraryToTransform == null || libraryToTransform == library;
  }

  bool needsTypeInformation(Class cls) {
    return !isObject(cls) &&
        !rtiLibrary.contains(cls) &&
        libraryShouldBeTransformed(cls.enclosingLibrary);
  }

  bool usesTypeGetter(Class cls) {
    return cls.typeParameters.isEmpty;
  }

  bool isObject(Class cls) {
    // TODO(karlklose): use [CoreTypes].
    return "$cls" == 'dart.core::Object';
  }

  Initializer addTypeAsArgument(initializer) {
    assert(initializer is SuperInitializer ||
        initializer is RedirectingInitializer);
    Class cls = getEnclosingClass(initializer.target);
    if (needsTypeInformation(cls) && !usesTypeGetter(cls)) {
      // If the current class uses a getter for type information, we did not add
      // a parameter to the constructor, but we can pass `null` as the value to
      // initialize the type field, since it will be shadowed by the getter.
      Expression type = (context.parameter != null)
          ? new VariableGet(context.parameter)
          : new NullLiteral();
      builder.insertAsFirstArgument(initializer.arguments, type);
    }
    return initializer;
  }

  Expression interceptInstantiation(
      InvocationExpression invocation, Member target) {
    Class targetClass = target.parent;
    Library targetLibrary = targetClass.parent;
    Library currentLibrary = getEnclosingLibrary(invocation);
    if (libraryShouldBeTransformed(currentLibrary) &&
        !libraryShouldBeTransformed(targetLibrary) &&
        !rtiLibrary.contains(target)) {
      return builder.attachTypeToConstructorInvocation(invocation, target);
    }
    return invocation;
  }

  Expression createRuntimeType(DartType type) {
    if (context?.inInitializer == true) {
      // In initializer context, the instance type is provided in
      // `context.parameter` as there is no `this`.
      return builder.createRuntimeType(type, typeContext: context.parameter);
    } else {
      return builder.createRuntimeType(type);
    }
  }

  TreeNode defaultTreeNode(TreeNode node) {
    trace(node);
    return super.defaultTreeNode(node);
  }

  Expression visitStaticInvocation(StaticInvocation invocation) {
    trace(invocation);

    invocation.transformChildren(this);

    Procedure target = invocation.target;
    if (target == rtiLibrary.reifyFunction) {
      /// Rewrite calls to reify(TypeLiteral) to a reified type.
      TypeLiteral literal = invocation.arguments.positional.single;
      return createRuntimeType(literal.type);
    } else if (target.kind == ProcedureKind.Factory) {
      // Intercept calls to factories of classes we do not transform
      return interceptInstantiation(invocation, target);
    }

    addTypeArgumentToGenericInvocation(invocation);

    return invocation;
  }

  Library visitLibrary(Library library) {
    trace(library);

    if (libraryShouldBeTransformed(library)) {
      library.transformChildren(this);
    }
    return library;
  }

  Expression visitConstructorInvocation(ConstructorInvocation invocation) {
    invocation.transformChildren(this);
    return interceptInstantiation(invocation, invocation.target);
  }

  Member getStaticInvocationTarget(InvocationExpression invocation) {
    if (invocation is ConstructorInvocation) {
      return invocation.target;
    } else if (invocation is StaticInvocation) {
      return invocation.target;
    } else {
      throw "Unexpected InvocationExpression $invocation.";
    }
  }

  bool isInstantiation(TreeNode invocation) {
    return invocation is ConstructorInvocation ||
        invocation is StaticInvocation &&
            invocation.target.kind == ProcedureKind.Factory;
  }

  bool isTypeVariable(DartType type) => type is TypeParameterType;

  /// Add the runtime type as an extra argument to constructor invocations.
  Arguments visitArguments(Arguments arguments) {
    trace(arguments);

    arguments.transformChildren(this);
    TreeNode parent = arguments.parent;
    if (isInstantiation(parent)) {
      Class targetClass = getEnclosingClass(getStaticInvocationTarget(parent));
      // Do not add the extra argument if the class does not need a type member
      // or if it can be implemented as a getter.
      if (!needsTypeInformation(targetClass) || usesTypeGetter(targetClass)) {
        return arguments;
      }

      List<DartType> typeArguments = arguments.types;

      Expression type =
          createRuntimeType(new InterfaceType(targetClass, typeArguments));

      builder.insertAsFirstArgument(arguments, type);
    }
    return arguments;
  }

  Field visitField(Field field) {
    trace(field);

    visitDartType(field.type);
    for (Expression annotation in field.annotations) {
      annotation.accept(this);
    }
    // Do not visit initializers, they have already been transformed when the
    // class was handled.
    return field;
  }

  /// Go through all initializers of fields and record a static initializer
  /// function, if necessary.
  void rewriteFieldInitializers(Class cls) {
    assert(context != null);
    context.initializers = new LinkedHashMap<Field, Procedure>();
    List<Field> fields = cls.fields;
    bool initializerRewritten = false;
    for (Field field in fields) {
      if (!initializerRewritten && knowledge.usedParameters(field).isEmpty) {
        // This field needs no static initializer.
        continue;
      }

      Expression initializer = field.initializer;
      if (initializer == null || field.isStatic) continue;
      // Declare a new variable that holds the type information and can be
      // used to access type variables in initializer context.
      // TODO(karlklose): some fields do not need the parameter.
      VariableDeclaration typeObject = new VariableDeclaration(r"$type");
      context.parameter = typeObject;
      context.inInitializer = true;
      // Translate the initializer while keeping track of whether there was
      // already an initializers that required type information in
      // [typeVariableUsedInInitializer].
      initializer = initializer.accept(this);
      context.parameter = null;
      context.inInitializer = false;
      // Create a static initializer function from the translated initializer
      // expression and record it.
      Name name = new Name("\$init\$${field.name.name}");
      Statement body = new ReturnStatement(initializer);
      Procedure staticInitializer = new Procedure(
          name,
          ProcedureKind.Method,
          new FunctionNode(body,
              positionalParameters: <VariableDeclaration>[typeObject]),
          isStatic: true,
          fileUri: cls.fileUri);
      context.initializers[field] = staticInitializer;
      // Finally, remove the initializer from the field.
      field.initializer = null;
    }
  }

  bool inheritsTypeProperty(Class cls) {
    assert(needsTypeInformation(cls));
    Class superclass = cls.superclass;
    return needsTypeInformation(superclass);
  }

  Class visitClass(Class cls) {
    trace(cls);

    if (needsTypeInformation(cls)) {
      context = new TransformationContext();
      List<TypeParameter> typeParameters = cls.typeParameters;
      if (usesTypeGetter(cls)) {
        assert(typeParameters.isEmpty);
        context.runtimeTypeStorage = RuntimeTypeStorage.getter;
        Member getter = builder.createGetter(rtiLibrary.runtimeTypeName,
            createRuntimeType(cls.rawType), cls, rtiLibrary.typeType);
        cls.addMember(getter);
      } else if (!inheritsTypeProperty(cls)) {
        context.runtimeTypeStorage = RuntimeTypeStorage.field;
        // TODO(karlklose): should we add the field to [Object]?
        context.runtimeTypeField = new Field(rtiLibrary.runtimeTypeName,
            fileUri: cls.fileUri, isFinal: true, type: rtiLibrary.typeType);
        cls.addMember(context.runtimeTypeField);
      } else {
        context.runtimeTypeStorage = RuntimeTypeStorage.inheritedField;
      }

      for (int i = 0; i < typeParameters.length; ++i) {
        TypeParameter variable = typeParameters[i];
        cls.addMember(builder.createTypeVariableGetter(cls, variable, i));
      }

      // Tag the class as supporting the runtime type getter.
      InterfaceType interfaceTypeForSupertype =
          new InterfaceType(rtiLibrary.markerClass);
      cls.implementedTypes.add(new Supertype(
          interfaceTypeForSupertype.classNode,
          interfaceTypeForSupertype.typeArguments));

      // Before transforming the parts of the class declaration, rewrite field
      // initializers that use type variables (or that would be called after one
      // that does) to static functions that can be used from constructors.
      rewriteFieldInitializers(cls);

      // Add properties for declaration tests.
      for (Class test in knowledge.classTests) {
        if (test == rtiLibrary.markerClass) continue;

        Procedure tag = builder.createGetter(
            builder.getTypeTestTagName(test),
            new BoolLiteral(isSuperClass(test, cls)),
            cls,
            builder.coreTypes.boolClass.rawType);
        cls.addMember(tag);
      }

      // Add a runtimeType getter.
      if (!usesTypeGetter(cls) && !inheritsTypeProperty(cls)) {
        cls.addMember(new Procedure(
            new Name("runtimeType"),
            ProcedureKind.Getter,
            new FunctionNode(
                new ReturnStatement(new DirectPropertyGet(
                    new ThisExpression(), context.runtimeTypeField)),
                returnType: builder.coreTypes.typeClass.rawType),
            fileUri: cls.fileUri));
      }
    }

    cls.transformChildren(this);

    // Add the static initializer functions. They have already been transformed.
    if (context?.initializers != null) {
      context.initializers.forEach((_, Procedure initializer) {
        cls.addMember(initializer);
      });
    }

    // TODO(karlklose): clear type arguments later, the order of class
    // transformations otherwise influences the result.
    // cls.typeParameters.clear();
    context = null;
    return cls;
  }

  // TODO(karlklose): replace with a structure that can answer also the question
  // which tags must be overriden due to different values.
  /// Returns `true` if [a] is a declaration used in a supertype of [b].
  bool isSuperClass(Class a, Class b) {
    if (b == null) return false;
    if (a == b) return true;

    if (isSuperClass(a, b.superclass)) {
      return true;
    }

    Iterable<Class> interfaceClasses = b.implementedTypes
        .map((Supertype type) => type.classNode)
        .where((Class cls) => cls != rtiLibrary.markerClass);
    return interfaceClasses
        .any((Class declaration) => isSuperClass(a, declaration));
  }

  bool isConstructorOrFactory(TreeNode node) {
    return isFactory(node) || node is Constructor;
  }

  bool isFactory(TreeNode node) {
    return node is Procedure && node.kind == ProcedureKind.Factory;
  }

  bool needsParameterForRuntimeType(TreeNode node) {
    if (!isConstructorOrFactory(node)) return false;

    RuntimeTypeStorage access = context.runtimeTypeStorage;
    assert(access != RuntimeTypeStorage.none);
    return access == RuntimeTypeStorage.field ||
        access == RuntimeTypeStorage.inheritedField;
  }

  FunctionNode visitFunctionNode(FunctionNode node) {
    trace(node);

    addTypeArgumentToGenericDeclaration(node);

    // If we have a [TransformationContext] with a runtime type field and we
    // translate a constructor or factory, we need a parameter that the code of
    // initializers or the factory body can use to access type arguments.
    // The parameter field in the context will be reset in the visit-method of
    // the parent.
    if (context != null && needsParameterForRuntimeType(node.parent)) {
      assert(context.parameter == null);
      // Create the parameter and insert it as the function's first parameter.
      context.parameter = new VariableDeclaration(
          rtiLibrary.runtimeTypeName.name,
          type: rtiLibrary.typeType);
      context.parameter.parent = node;
      node.positionalParameters.insert(0, context.parameter);
      node.requiredParameterCount++;
    }
    node.transformChildren(this);
    return node;
  }

  SuperInitializer visitSuperInitializer(SuperInitializer initializer) {
    initializer.transformChildren(this);
    return addTypeAsArgument(initializer);
  }

  RedirectingInitializer visitRedirectingInitializer(
      RedirectingInitializer initializer) {
    initializer.transformChildren(this);
    return addTypeAsArgument(initializer);
  }

  Procedure visitProcedure(Procedure procedure) {
    trace(procedure);

    transformList(procedure.annotations, this, procedure.parent);
    // Visit the function body in a initializing context, if it is a factory.
    context?.inInitializer = isFactory(procedure);
    procedure.function?.accept(this);
    context?.inInitializer = false;

    context?.parameter = null;
    return procedure;
  }

  Constructor visitConstructor(Constructor constructor) {
    trace(constructor);

    transformList(constructor.annotations, this, constructor);
    if (constructor.function != null) {
      constructor.function = constructor.function.accept(this);
      constructor.function?.parent = constructor;
    }

    context?.inInitializer = true;
    transformList(constructor.initializers, this, constructor);
    context?.inInitializer = false;

    if (context != null) {
      if (context.runtimeTypeStorage == RuntimeTypeStorage.field) {
        // Initialize the runtime type field with value given in the additional
        // constructor parameter.
        assert(context.parameter != null);
        Initializer initializer = new FieldInitializer(
            context.runtimeTypeField, new VariableGet(context.parameter));
        initializer.parent = constructor;
        constructor.initializers.insert(0, initializer);
      }
      if (context.initializers != null) {
        // For each field that needed a static initializer function, initialize
        // the field by calling the function.
        List<Initializer> fieldInitializers = <Initializer>[];
        context.initializers.forEach((Field field, Procedure initializer) {
          assert(context.parameter != null);
          Arguments argument =
              new Arguments(<Expression>[new VariableGet(context.parameter)]);
          fieldInitializers.add(new FieldInitializer(
              field, new StaticInvocation(initializer, argument)));
        });
        constructor.initializers.insertAll(0, fieldInitializers);
      }
      context.parameter = null;
    }

    return constructor;
  }

  /// Returns `true` if the given type can be tested using type test tags.
  ///
  /// This implies that there are no subtypes of the [type] that are not
  /// transformed.
  bool typeSupportsTagTest(InterfaceType type) {
    return needsTypeInformation(type.classNode);
  }

  Expression visitIsExpression(IsExpression expression) {
    trace(expression);

    expression.transformChildren(this);

    if (getEnclosingLibrary(expression) == rtiLibrary.interceptorsLibrary) {
      // In the interceptor library we need actual is-checks at the moment.
      return expression;
    }

    Expression target = expression.operand;
    DartType type = expression.type;

    if (type is InterfaceType && typeSupportsTagTest(type)) {
      assert(knowledge.classTests.contains(type.classNode));
      bool checkArguments =
          type.typeArguments.any((DartType type) => type is! DynamicType);
      Class declaration = type.classNode;
      VariableDeclaration typeExpression =
          new VariableDeclaration(null, initializer: createRuntimeType(type));
      VariableDeclaration targetValue =
          new VariableDeclaration(null, initializer: target);
      Expression markerClassTest = new IsExpression(
          new VariableGet(targetValue), rtiLibrary.markerClass.rawType);
      Expression tagCheck = new PropertyGet(new VariableGet(targetValue),
          builder.getTypeTestTagName(declaration));
      Expression check = new LogicalExpression(markerClassTest, "&&", tagCheck);
      if (checkArguments) {
        // TODO(karlklose): support a direct argument check, we already checked
        // the declaration.
        Expression uninterceptedCheck = new Let(
            typeExpression,
            builder.createIsSubtypeOf(
                new VariableGet(targetValue), new VariableGet(typeExpression),
                targetHasTypeProperty: true));
        check = new LogicalExpression(check, "&&", uninterceptedCheck);
      }
      return new Let(targetValue, check);
    } else {
      return builder.createIsSubtypeOf(target, createRuntimeType(type));
    }
  }

  Expression visitListLiteral(ListLiteral node) {
    trace(node);
    node.transformChildren(this);
    return builder.callAttachType(
        node,
        new InterfaceType(
            builder.coreTypes.listClass, <DartType>[node.typeArgument]));
  }

  Expression visitMapLiteral(MapLiteral node) {
    trace(node);
    node.transformChildren(this);
    return builder.callAttachType(
        node,
        new InterfaceType(builder.coreTypes.mapClass,
            <DartType>[node.keyType, node.valueType]));
  }

  Expression visitMethodInvocation(MethodInvocation node) {
    node.transformChildren(this);
    addTypeArgumentToGenericInvocation(node);
    return node;
  }

  bool isGenericMethod(FunctionNode node) {
    if (node.parent is Member) {
      Member member = node.parent;
      if (member is Constructor ||
          member is Procedure && member.kind == ProcedureKind.Factory) {
        return member.enclosingClass.typeParameters.length <
            node.typeParameters.length;
      }
    }
    return node.typeParameters.isNotEmpty;
  }

  void addTypeArgumentToGenericInvocation(InvocationExpression expression) {
    if (expression.arguments.types.length > 0) {
      ListLiteral genericMethodTypeParameters = new ListLiteral(
          expression.arguments.types
              .map(createRuntimeType)
              .toList(growable: false),
          typeArgument: rtiLibrary.typeType);
      expression.arguments.named.add(new NamedExpression(
          genericMethodTypeParametersName, genericMethodTypeParameters)
        ..parent = expression.arguments);
    }
  }

  void addTypeArgumentToGenericDeclaration(FunctionNode node) {
    if (isGenericMethod(node)) {
      VariableDeclaration genericMethodTypeParameters = new VariableDeclaration(
          genericMethodTypeParametersName,
          type: new InterfaceType(
              builder.coreTypes.listClass, <DartType>[rtiLibrary.typeType]));
      genericMethodTypeParameters.parent = node;
      node.namedParameters.insert(0, genericMethodTypeParameters);
    }
  }
}
