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

part of dart2js.js_emitter;

class ClassEmitter extends CodeEmitterHelper {
  /**
   * Documentation wanted -- johnniwinther
   *
   * Invariant: [classElement] must be a declaration element.
   */
  void generateClass(ClassElement classElement,
                     ClassBuilder properties,
                     Map<String, jsAst.Expression> additionalProperties) {
    final onlyForRti =
        task.typeTestEmitter.rtiNeededClasses.contains(classElement);

    assert(invariant(classElement, classElement.isDeclaration));
    assert(invariant(classElement, !classElement.isNative || onlyForRti));

    task.needsDefineClass = true;
    String className = namer.getNameOfClass(classElement);

    ClassElement superclass = classElement.superclass;
    String superName = "";
    if (superclass != null) {
      superName = namer.getNameOfClass(superclass);
    }

    if (classElement.isMixinApplication) {
      String mixinName = namer.getNameOfClass(computeMixinClass(classElement));
      superName = '$superName+$mixinName';
      task.needsMixinSupport = true;
    }

    ClassBuilder builder = new ClassBuilder(namer);
    emitClassConstructor(classElement, builder, onlyForRti: onlyForRti);
    emitFields(classElement, builder, superName, onlyForRti: onlyForRti);
    emitClassGettersSetters(classElement, builder, onlyForRti: onlyForRti);
    emitInstanceMembers(classElement, builder, onlyForRti: onlyForRti);
    task.typeTestEmitter.emitIsTests(classElement, builder);
    if (additionalProperties != null) {
      additionalProperties.forEach(builder.addProperty);
    }

    if (classElement == compiler.closureClass) {
      // We add a special getter here to allow for tearing off a closure from
      // itself.
      String name = namer.getMappedInstanceName(Compiler.CALL_OPERATOR_NAME);
      jsAst.Fun function = js('function() { return this; }');
      builder.addProperty(namer.getterNameFromAccessorName(name), function);
    }

    emitTypeVariableReaders(classElement, builder);

    emitClassBuilderWithReflectionData(
        className, classElement, builder, properties);
  }

  void emitClassConstructor(ClassElement classElement,
                            ClassBuilder builder,
                            {bool onlyForRti: false}) {
    List<String> fields = <String>[];
    if (!onlyForRti && !classElement.isNative) {
      visitFields(classElement, false,
                  (Element member,
                   String name,
                   String accessorName,
                   bool needsGetter,
                   bool needsSetter,
                   bool needsCheckedSetter) {
        fields.add(name);
      });
    }
    String constructorName = namer.getNameOfClass(classElement);

    // TODO(sra): Implement placeholders in VariableDeclaration position:
    //     task.precompiledFunction.add(js.statement('function #(#) { #; }',
    //        [ constructorName, fields,
    //            fields.map(
    //                (name) => js('this.# = #', [name, name]))]));
    task.precompiledFunction.add(
        new jsAst.FunctionDeclaration(
            new jsAst.VariableDeclaration(constructorName),
            js('function(#) { #; }',
                [fields,
                 fields.map((name) => js('this.# = #', [name, name]))])));
    // TODO(floitsch): do we actually need the name field?
    // TODO(floitsch): these should all go through the namer.

    task.precompiledFunction.add(
        js.statement(r'''{
          #.builtin$cls = #;
          if (!"name" in #)
              #.name = #;
          $desc=$collectedClasses.#;
          if ($desc instanceof Array) $desc = $desc[1];
          #.prototype = $desc;
        }''',
            [   constructorName, js.string(constructorName),
                constructorName,
                constructorName, js.string(constructorName),
                constructorName,
                constructorName
             ]));

    task.precompiledConstructorNames.add(js('#', constructorName));
  }

  /// Returns `true` if fields added.
  bool emitFields(Element element,
                  ClassBuilder builder,
                  String superName,
                  { bool classIsNative: false,
                    bool emitStatics: false,
                    bool onlyForRti: false }) {
    assert(!emitStatics || !onlyForRti);
    if (element.isLibrary) {
      assert(invariant(element, emitStatics));
    } else if (!element.isClass) {
      throw new SpannableAssertionFailure(
          element, 'Must be a ClassElement or a LibraryElement');
    }
    if (emitStatics) {
      assert(invariant(element, superName == null, message: superName));
    } else {
      assert(invariant(element, superName != null));
      builder.superName = superName;
    }
    var fieldMetadata = [];
    bool hasMetadata = false;
    bool fieldsAdded = false;

    if (!onlyForRti) {
      visitFields(element, emitStatics,
                  (VariableElement field,
                   String name,
                   String accessorName,
                   bool needsGetter,
                   bool needsSetter,
                   bool needsCheckedSetter) {
        // Ignore needsCheckedSetter - that is handled below.
        bool needsAccessor = (needsGetter || needsSetter);
        // We need to output the fields for non-native classes so we can auto-
        // generate the constructor.  For native classes there are no
        // constructors, so we don't need the fields unless we are generating
        // accessors at runtime.
        if (!classIsNative || needsAccessor) {
          var metadata = task.metadataEmitter.buildMetadataFunction(field);
          if (metadata != null) {
            hasMetadata = true;
          } else {
            metadata = new jsAst.LiteralNull();
          }
          fieldMetadata.add(metadata);
          recordMangledField(field, accessorName, field.name);
          String fieldName = name;
          String fieldCode = '';
          String reflectionMarker = '';
          if (!needsAccessor) {
            // Emit field for constructor generation.
            assert(!classIsNative);
          } else {
            // Emit (possibly renaming) field name so we can add accessors at
            // runtime.
            if (name != accessorName) {
              fieldName = '$accessorName:$name';
            }

            int getterCode = 0;
            if (needsAccessor && backend.fieldHasInterceptedGetter(field)) {
              task.interceptorEmitter.interceptorInvocationNames.add(
                  namer.getterName(field));
            }
            if (needsAccessor && backend.fieldHasInterceptedGetter(field)) {
              task.interceptorEmitter.interceptorInvocationNames.add(
                  namer.setterName(field));
            }
            if (needsGetter) {
              if (field.isInstanceMember) {
                // 01:  function() { return this.field; }
                // 10:  function(receiver) { return receiver.field; }
                // 11:  function(receiver) { return this.field; }
                bool isIntercepted = backend.fieldHasInterceptedGetter(field);
                getterCode += isIntercepted ? 2 : 0;
                getterCode += backend.isInterceptorClass(element) ? 0 : 1;
                // TODO(sra): 'isInterceptorClass' might not be the correct test
                // for methods forced to use the interceptor convention because
                // the method's class was elsewhere mixed-in to an interceptor.
                assert(!field.isInstanceMember || getterCode != 0);
                if (isIntercepted) {
                  task.interceptorEmitter.interceptorInvocationNames.add(
                      namer.getterName(field));
                }
              } else {
                getterCode = 1;
              }
            }
            int setterCode = 0;
            if (needsSetter) {
              if (field.isInstanceMember) {
                // 01:  function(value) { this.field = value; }
                // 10:  function(receiver, value) { receiver.field = value; }
                // 11:  function(receiver, value) { this.field = value; }
                bool isIntercepted = backend.fieldHasInterceptedSetter(field);
                setterCode += isIntercepted ? 2 : 0;
                setterCode += backend.isInterceptorClass(element) ? 0 : 1;
                assert(!field.isInstanceMember || setterCode != 0);
                if (isIntercepted) {
                  task.interceptorEmitter.interceptorInvocationNames.add(
                      namer.setterName(field));
                }
              } else {
                setterCode = 1;
              }
            }
            int code = getterCode + (setterCode << 2);
            if (code == 0) {
              compiler.internalError(field,
                  'Field code is 0 ($element/$field).');
            } else {
              fieldCode = FIELD_CODE_CHARACTERS[code - FIRST_FIELD_CODE];
            }
          }
          if (backend.isAccessibleByReflection(field)) {
            reflectionMarker = '-';
            if (backend.isNeededForReflection(field)) {
              DartType type = field.type;
              reflectionMarker = '-${task.metadataEmitter.reifyType(type)}';
            }
          }
          builder.addField('$fieldName$fieldCode$reflectionMarker');
          fieldsAdded = true;
        }
      });
    }

    if (hasMetadata) {
      builder.fieldMetadata = fieldMetadata;
    }
    return fieldsAdded;
  }

  void emitClassGettersSetters(ClassElement classElement,
                               ClassBuilder builder,
                               {bool onlyForRti: false}) {
    if (onlyForRti) return;

    visitFields(classElement, false,
                (VariableElement member,
                 String name,
                 String accessorName,
                 bool needsGetter,
                 bool needsSetter,
                 bool needsCheckedSetter) {
      compiler.withCurrentElement(member, () {
        if (needsCheckedSetter) {
          assert(!needsSetter);
          generateCheckedSetter(member, name, accessorName, builder);
        }
        if (needsGetter) {
          generateGetter(member, name, accessorName, builder);
        }
        if (needsSetter) {
          generateSetter(member, name, accessorName, builder);
        }
      });
    });
  }

  /**
   * Documentation wanted -- johnniwinther
   *
   * Invariant: [classElement] must be a declaration element.
   */
  void emitInstanceMembers(ClassElement classElement,
                           ClassBuilder builder,
                           {bool onlyForRti: false}) {
    assert(invariant(classElement, classElement.isDeclaration));

    if (onlyForRti || classElement.isMixinApplication) return;

    void visitMember(ClassElement enclosing, Element member) {
      assert(invariant(classElement, member.isDeclaration));
      if (member.isInstanceMember) {
        task.containerBuilder.addMember(member, builder);
      }
    }

    classElement.implementation.forEachMember(
        visitMember,
        includeBackendMembers: true);

    if (identical(classElement, compiler.objectClass)
        && compiler.enabledNoSuchMethod) {
      // Emit the noSuchMethod handlers on the Object prototype now,
      // so that the code in the dynamicFunction helper can find
      // them. Note that this helper is invoked before analyzing the
      // full JS script.
      if (!task.nativeEmitter.handleNoSuchMethod) {
        task.nsmEmitter.emitNoSuchMethodHandlers(builder.addProperty);
      }
    }
  }

  void emitClassBuilderWithReflectionData(String className,
                                          ClassElement classElement,
                                          ClassBuilder classBuilder,
                                          ClassBuilder enclosingBuilder) {
    var metadata = task.metadataEmitter.buildMetadataFunction(classElement);
    if (metadata != null) {
      classBuilder.addProperty("@", metadata);
    }

    if (backend.isNeededForReflection(classElement)) {
      Link typeVars = classElement.typeVariables;
      Iterable typeVariableProperties = task.typeVariableHandler
          .typeVariablesOf(classElement).map(js.number);

      ClassElement superclass = classElement.superclass;
      bool hasSuper = superclass != null;
      if ((!typeVariableProperties.isEmpty && !hasSuper) ||
          (hasSuper && superclass.typeVariables != typeVars)) {
        classBuilder.addProperty('<>',
            new jsAst.ArrayInitializer.from(typeVariableProperties));
      }
    }

    List<jsAst.Property> statics = new List<jsAst.Property>();
    ClassBuilder staticsBuilder = new ClassBuilder(namer);
    if (emitFields(classElement, staticsBuilder, null, emitStatics: true)) {
      statics.add(staticsBuilder.toObjectInitializer().properties.single);
    }

    Map<OutputUnit, ClassBuilder> classPropertyLists =
        task.elementDescriptors.remove(classElement);
    if (classPropertyLists != null) {
      for (ClassBuilder classProperties in classPropertyLists.values) {
        // TODO(sigurdm): What about deferred?
        if (classProperties != null) {
          statics.addAll(classProperties.properties);
        }
      }
    }

    if (!statics.isEmpty) {
      classBuilder.addProperty('static', new jsAst.ObjectInitializer(statics));
    }

    // TODO(ahe): This method (generateClass) should return a jsAst.Expression.
    enclosingBuilder.addProperty(className, classBuilder.toObjectInitializer());

    String reflectionName = task.getReflectionName(classElement, className);
    if (reflectionName != null) {
      if (!backend.isNeededForReflection(classElement)) {
        enclosingBuilder.addProperty("+$reflectionName", js.number(0));
      } else {
        List<int> types = <int>[];
        if (classElement.supertype != null) {
          types.add(task.metadataEmitter.reifyType(classElement.supertype));
        }
        for (DartType interface in classElement.interfaces) {
          types.add(task.metadataEmitter.reifyType(interface));
        }
        enclosingBuilder.addProperty("+$reflectionName",
            new jsAst.ArrayInitializer.from(types.map(js.number)));
      }
    }
  }

  /**
   * Calls [addField] for each of the fields of [element].
   *
   * [element] must be a [ClassElement] or a [LibraryElement].
   *
   * If [element] is a [ClassElement], the static fields of the class are
   * visited if [visitStatics] is true and the instance fields are visited if
   * [visitStatics] is false.
   *
   * If [element] is a [LibraryElement], [visitStatics] must be true.
   *
   * When visiting the instance fields of a class, the fields of its superclass
   * are also visited if the class is instantiated.
   *
   * Invariant: [element] must be a declaration element.
   */
  void visitFields(Element element, bool visitStatics, AcceptField f) {
    assert(invariant(element, element.isDeclaration));

    bool isClass = false;
    bool isLibrary = false;
    if (element.isClass) {
      isClass = true;
    } else if (element.isLibrary) {
      isLibrary = true;
      assert(invariant(element, visitStatics));
    } else {
      throw new SpannableAssertionFailure(
          element, 'Expected a ClassElement or a LibraryElement.');
    }

    // If the class is never instantiated we still need to set it up for
    // inheritance purposes, but we can simplify its JavaScript constructor.
    bool isInstantiated =
        compiler.codegenWorld.instantiatedClasses.contains(element);

    void visitField(Element holder, VariableElement field) {
      assert(invariant(element, field.isDeclaration));
      String name = field.name;

      // Keep track of whether or not we're dealing with a field mixin
      // into a native class.
      bool isMixinNativeField =
          isClass && element.isNative && holder.isMixinApplication;

      // See if we can dynamically create getters and setters.
      // We can only generate getters and setters for [element] since
      // the fields of super classes could be overwritten with getters or
      // setters.
      bool needsGetter = false;
      bool needsSetter = false;
      if (isLibrary || isMixinNativeField || holder == element) {
        needsGetter = fieldNeedsGetter(field);
        needsSetter = fieldNeedsSetter(field);
      }

      if ((isInstantiated && !holder.isNative)
          || needsGetter
          || needsSetter) {
        String accessorName = namer.fieldAccessorName(field);
        String fieldName = namer.fieldPropertyName(field);
        bool needsCheckedSetter = false;
        if (compiler.enableTypeAssertions
            && needsSetter
            && !canAvoidGeneratedCheckedSetter(field)) {
          needsCheckedSetter = true;
          needsSetter = false;
        }
        // Getters and setters with suffixes will be generated dynamically.
        f(field, fieldName, accessorName, needsGetter, needsSetter,
          needsCheckedSetter);
      }
    }

    if (isLibrary) {
      LibraryElement library = element;
      library.implementation.forEachLocalMember((Element member) {
        if (member.isField) visitField(library, member);
      });
    } else if (visitStatics) {
      ClassElement cls = element;
      cls.implementation.forEachStaticField(visitField);
    } else {
      ClassElement cls = element;
      // TODO(kasperl): We should make sure to only emit one version of
      // overridden fields. Right now, we rely on the ordering so the
      // fields pulled in from mixins are replaced with the fields from
      // the class definition.

      // If a class is not instantiated then we add the field just so we can
      // generate the field getter/setter dynamically. Since this is only
      // allowed on fields that are in [element] we don't need to visit
      // superclasses for non-instantiated classes.
      cls.implementation.forEachInstanceField(
          visitField, includeSuperAndInjectedMembers: isInstantiated);
    }
  }

  void recordMangledField(Element member,
                          String accessorName,
                          String memberName) {
    if (!backend.shouldRetainGetter(member)) return;
    String previousName;
    if (member.isInstanceMember) {
      previousName = task.mangledFieldNames.putIfAbsent(
          '${namer.getterPrefix}$accessorName',
          () => memberName);
    } else {
      previousName = task.mangledGlobalFieldNames.putIfAbsent(
          accessorName,
          () => memberName);
    }
    assert(invariant(member, previousName == memberName,
                     message: '$previousName != ${memberName}'));
  }

  bool fieldNeedsGetter(VariableElement field) {
    assert(field.isField);
    if (fieldAccessNeverThrows(field)) return false;
    return backend.shouldRetainGetter(field)
        || compiler.codegenWorld.hasInvokedGetter(field, compiler);
  }

  bool fieldNeedsSetter(VariableElement field) {
    assert(field.isField);
    if (fieldAccessNeverThrows(field)) return false;
    return (!field.isFinal && !field.isConst)
        && (backend.shouldRetainSetter(field)
            || compiler.codegenWorld.hasInvokedSetter(field, compiler));
  }

  // We never access a field in a closure (a captured variable) without knowing
  // that it is there.  Therefore we don't need to use a getter (that will throw
  // if the getter method is missing), but can always access the field directly.
  static bool fieldAccessNeverThrows(VariableElement field) {
    return field is ClosureFieldElement;
  }

  bool canAvoidGeneratedCheckedSetter(VariableElement member) {
    // We never generate accessors for top-level/static fields.
    if (!member.isInstanceMember) return true;
    DartType type = member.type;
    return type.treatAsDynamic || (type.element == compiler.objectClass);
  }

  void generateCheckedSetter(Element member,
                             String fieldName,
                             String accessorName,
                             ClassBuilder builder) {
    jsAst.Expression code = backend.generatedCode[member];
    assert(code != null);
    String setterName = namer.setterNameFromAccessorName(accessorName);
    builder.addProperty(setterName, code);
    generateReflectionDataForFieldGetterOrSetter(
        member, setterName, builder, isGetter: false);
  }

  void generateGetter(Element member, String fieldName, String accessorName,
                      ClassBuilder builder) {
    String getterName = namer.getterNameFromAccessorName(accessorName);
    ClassElement cls = member.enclosingClass;
    String className = namer.getNameOfClass(cls);
    String receiver = backend.isInterceptorClass(cls) ? 'receiver' : 'this';
    List<String> args = backend.isInterceptedMethod(member) ? ['receiver'] : [];
    task.precompiledFunction.add(
        js('#.prototype.# = function(#) { return #.# }',
           [className, getterName, args, receiver, fieldName]));
    if (backend.isNeededForReflection(member)) {
      task.precompiledFunction.add(
          js('#.prototype.#.${namer.reflectableField} = 1',
              [className, getterName]));
    }
  }

  void generateSetter(Element member, String fieldName, String accessorName,
                      ClassBuilder builder) {
    String setterName = namer.setterNameFromAccessorName(accessorName);
    ClassElement cls = member.enclosingClass;
    String className = namer.getNameOfClass(cls);
    String receiver = backend.isInterceptorClass(cls) ? 'receiver' : 'this';
    List<String> args = backend.isInterceptedMethod(member) ? ['receiver'] : [];
    task.precompiledFunction.add(
        // TODO: remove 'return'?
        js('#.prototype.# = function(#, v) { return #.# = v; }',
            [className, setterName, args, receiver, fieldName]));
    if (backend.isNeededForReflection(member)) {
      task.precompiledFunction.add(
          js('#.prototype.#.${namer.reflectableField} = 1',
              [className, setterName]));
    }
  }

  void generateReflectionDataForFieldGetterOrSetter(Element member,
                                                    String name,
                                                    ClassBuilder builder,
                                                    {bool isGetter}) {
    Selector selector = isGetter
        ? new Selector.getter(member.name, member.library)
        : new Selector.setter(member.name, member.library);
    String reflectionName = task.getReflectionName(selector, name);
    if (reflectionName != null) {
      var reflectable =
          js(backend.isAccessibleByReflection(member) ? '1' : '0');
      builder.addProperty('+$reflectionName', reflectable);
    }
  }

  void emitTypeVariableReaders(ClassElement cls, ClassBuilder builder) {
    List typeVariables = [];
    ClassElement superclass = cls;
    while (superclass != null) {
      for (TypeVariableType parameter in superclass.typeVariables) {
        if (task.readTypeVariables.contains(parameter.element)) {
          emitTypeVariableReader(cls, builder, parameter.element);
        }
      }
      superclass = superclass.superclass;
    }
  }

  void emitTypeVariableReader(ClassElement cls,
                              ClassBuilder builder,
                              TypeVariableElement element) {
    String name = namer.readTypeVariableName(element);
    jsAst.Expression index =
        js.number(RuntimeTypes.getTypeVariableIndex(element));
    jsAst.Expression computeTypeVariable;

    Substitution substitution =
        backend.rti.computeSubstitution(
            cls, element.enclosingElement, alwaysGenerateFunction: true);
    if (substitution != null) {
      jsAst.Expression typeArguments =
          js(r'#.apply(null, this.$builtinTypeInfo)',
              substitution.getCode(backend.rti, true));
      computeTypeVariable = js('#[#]', [typeArguments, index]);
    } else {
      // TODO(ahe): These can be generated dynamically.
      computeTypeVariable =
          js(r'this.$builtinTypeInfo && this.$builtinTypeInfo[#]', index);
    }
    jsAst.Expression convertRtiToRuntimeType =
        namer.elementAccess(compiler.findHelper('convertRtiToRuntimeType'));
    builder.addProperty(name,
        js('function () { return #(#) }',
            [convertRtiToRuntimeType, computeTypeVariable]));
  }
}
