"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.ResidualReactElementSerializer = undefined;

var _realm = require("../realm.js");

var _ResidualHeapSerializer = require("./ResidualHeapSerializer.js");

var _hoisting = require("../react/hoisting.js");

var _babelTypes = require("babel-types");

var t = _interopRequireWildcard(_babelTypes);

var _index = require("../values/index.js");

var _jsx = require("../react/jsx.js");

var _logger = require("../utils/logger.js");

var _invariant = require("../invariant.js");

var _invariant2 = _interopRequireDefault(_invariant);

var _errors = require("../errors");

var _utils = require("../react/utils.js");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

/**
 * Copyright (c) 2017-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

class ResidualReactElementSerializer {
  constructor(realm, residualHeapSerializer) {
    this.realm = realm;
    this.residualHeapSerializer = residualHeapSerializer;
    this.logger = residualHeapSerializer.logger;
    this.reactOutput = realm.react.output || "create-element";
    this._lazilyHoistedNodes = undefined;
  }

  _createReactElement(value) {
    return { attributes: [], children: [], declared: false, type: undefined, value };
  }

  _createReactElementAttribute() {
    return { expr: undefined, key: undefined, type: "PENDING" };
  }

  _createReactElementChild() {
    return { expr: undefined, type: "PENDING" };
  }

  _emitHoistedReactElement(id, reactElement, hoistedCreateElementIdentifier, originalCreateElementIdentifier) {
    // if the currentHoistedReactElements is not defined, we create it an emit the function call
    // this should only occur once per additional function
    if (this._lazilyHoistedNodes === undefined) {
      let funcId = t.identifier(this.residualHeapSerializer.functionNameGenerator.generate());
      this._lazilyHoistedNodes = {
        id: funcId,
        createElementIdentifier: hoistedCreateElementIdentifier,
        nodes: []
      };
      let statement = t.expressionStatement(t.logicalExpression("&&", t.binaryExpression("===", id, t.unaryExpression("void", t.numericLiteral(0), true)),
      // pass the createElementIdentifier if it's not null
      t.callExpression(funcId, originalCreateElementIdentifier ? [originalCreateElementIdentifier] : [])));
      this.residualHeapSerializer.emitter.emit(statement);
    }
    // we then push the reactElement and its id into our list of elements to process after
    // the current additional function has serialzied
    (0, _invariant2.default)(this._lazilyHoistedNodes !== undefined);
    (0, _invariant2.default)(Array.isArray(this._lazilyHoistedNodes.nodes));
    this._lazilyHoistedNodes.nodes.push({ id, astNode: reactElement });
  }

  _getReactLibraryValue() {
    let reactLibraryObject = this.realm.fbLibraries.react;
    // if there is no React library, then we should throw and error
    if (reactLibraryObject === undefined) {
      throw new _errors.FatalError("unable to find React library reference in scope");
    }
    return reactLibraryObject;
  }

  _getReactCreateElementValue() {
    let reactLibraryObject = this._getReactLibraryValue();
    return (0, _utils.getProperty)(this.realm, reactLibraryObject, "createElement");
  }

  _emitReactElement(reactElement) {
    let { value } = reactElement;
    let shouldHoist = this.residualHeapSerializer.isReferencedOnlyByAdditionalFunction(value) !== undefined && (0, _hoisting.canHoistReactElement)(this.realm, value);

    let id = this.residualHeapSerializer.getSerializeObjectIdentifier(value);
    // this identifier is used as the deafult, but also passed to the hoisted factory function
    let originalCreateElementIdentifier = null;
    // this name is used when hoisting, and is passed into the factory function, rather than the original
    let hoistedCreateElementIdentifier = null;
    let reactElementAstNode;

    this.residualHeapSerializer.emitter.emitNowOrAfterWaitingForDependencies([value], () => {
      if (this.reactOutput === "jsx") {
        reactElementAstNode = this._serializeReactElementToJSXElement(value, reactElement);
      } else if (this.reactOutput === "create-element") {
        let createElement = this._getReactCreateElementValue();
        originalCreateElementIdentifier = this.residualHeapSerializer.serializeValue(createElement);

        if (shouldHoist) {
          // if we haven't created a _lazilyHoistedNodes before, then this is the first time
          // so we only create the hoisted identifier once
          if (this._lazilyHoistedNodes === undefined) {
            // create a new unique instance
            hoistedCreateElementIdentifier = t.identifier(this.residualHeapSerializer.intrinsicNameGenerator.generate());
          } else {
            hoistedCreateElementIdentifier = this._lazilyHoistedNodes.createElementIdentifier;
          }
        }

        let createElementIdentifier = shouldHoist ? hoistedCreateElementIdentifier : originalCreateElementIdentifier;
        reactElementAstNode = this._serializeReactElementToCreateElement(value, reactElement, createElementIdentifier);
      } else {
        (0, _invariant2.default)(false, "Unknown reactOutput specified");
      }
      // if we are hoisting this React element, put the assignment in the body
      // also ensure we are in an additional function
      if (shouldHoist) {
        this._emitHoistedReactElement(id, reactElementAstNode, hoistedCreateElementIdentifier, originalCreateElementIdentifier);
      } else {
        if (reactElement.declared) {
          this.residualHeapSerializer.emitter.emit(t.expressionStatement(t.assignmentExpression("=", id, reactElementAstNode)));
        } else {
          reactElement.declared = true;
          this.residualHeapSerializer.emitter.emit(t.variableDeclaration("var", [t.variableDeclarator(id, reactElementAstNode)]));
        }
      }
    });
    return id;
  }

  _serializeNowOrAfterWaitingForDependencies(value, reactElement, func, shouldSerialize = true) {
    let reason = this.residualHeapSerializer.emitter.getReasonToWaitForDependencies(value);

    const serialize = () => {
      func();
    };

    if (reason) {
      this.residualHeapSerializer.emitter.emitAfterWaiting(reason, [value], () => {
        serialize();
        this._emitReactElement(reactElement);
      });
    } else {
      serialize();
    }
  }

  _serializeReactFragmentType(typeValue) {
    let reactLibraryObject = this._getReactLibraryValue();
    // we want to visit the Symbol type, but we don't want to serialize it
    // as this is a React internal
    this.residualHeapSerializer.serializedValues.add(typeValue);
    (0, _invariant2.default)(typeValue.$Description instanceof _index.StringValue);
    this.residualHeapSerializer.serializedValues.add(typeValue.$Description);
    return t.memberExpression(this.residualHeapSerializer.serializeValue(reactLibraryObject), t.identifier("Fragment"));
  }

  _serializeReactElementType(reactElement) {
    let { value } = reactElement;
    let typeValue = (0, _utils.getProperty)(this.realm, value, "type");

    this._serializeNowOrAfterWaitingForDependencies(typeValue, reactElement, () => {
      let expr;

      if (typeValue instanceof _index.SymbolValue && typeValue === (0, _utils.getReactSymbol)("react.fragment", this.realm)) {
        expr = this._serializeReactFragmentType(typeValue);
      } else {
        expr = this.residualHeapSerializer.serializeValue(typeValue);
      }
      reactElement.type = expr;
    });
  }

  _serializeReactElementAttributes(reactElement) {
    let { value } = reactElement;
    let keyValue = (0, _utils.getProperty)(this.realm, value, "key");
    let refValue = (0, _utils.getProperty)(this.realm, value, "ref");
    let propsValue = (0, _utils.getProperty)(this.realm, value, "props");

    if (keyValue !== this.realm.intrinsics.null && keyValue !== this.realm.intrinsics.undefined) {
      let reactElementKey = this._createReactElementAttribute();
      this._serializeNowOrAfterWaitingForDependencies(keyValue, reactElement, () => {
        let expr = this.residualHeapSerializer.serializeValue(keyValue);
        reactElementKey.expr = expr;
        reactElementKey.key = "key";
        reactElementKey.type = "PROPERTY";
      });
      reactElement.attributes.push(reactElementKey);
    }

    if (refValue !== this.realm.intrinsics.null && refValue !== this.realm.intrinsics.undefined) {
      let reactElementRef = this._createReactElementAttribute();
      this._serializeNowOrAfterWaitingForDependencies(refValue, reactElement, () => {
        let expr = this.residualHeapSerializer.serializeValue(refValue);
        reactElementRef.expr = expr;
        reactElementRef.key = "ref";
        reactElementRef.type = "PROPERTY";
      });
      reactElement.attributes.push(reactElementRef);
    }

    const assignPropsAsASpreadProp = () => {
      let reactElementSpread = this._createReactElementAttribute();
      this._serializeNowOrAfterWaitingForDependencies(propsValue, reactElement, () => {
        let expr = this.residualHeapSerializer.serializeValue(propsValue);
        reactElementSpread.expr = expr;
        reactElementSpread.type = "SPREAD";
      });
      reactElement.attributes.push(reactElementSpread);
    };

    // handle props
    if (propsValue instanceof _index.AbstractValue) {
      assignPropsAsASpreadProp();
    } else if (propsValue instanceof _index.ObjectValue) {
      if (propsValue.isPartialObject()) {
        assignPropsAsASpreadProp();
      } else {
        this.residualHeapSerializer.serializedValues.add(propsValue);
        for (let [propName, binding] of propsValue.properties) {
          if (binding.descriptor !== undefined && propName !== "children") {
            (0, _invariant2.default)(propName !== "key" && propName !== "ref", `"${propName}" is a reserved prop name`);
            let propValue = (0, _utils.getProperty)(this.realm, propsValue, propName);
            let reactElementAttribute = this._createReactElementAttribute();

            this._serializeNowOrAfterWaitingForDependencies(propValue, reactElement, () => {
              let expr = this.residualHeapSerializer.serializeValue(propValue);
              reactElementAttribute.expr = expr;
              reactElementAttribute.key = propName;
              reactElementAttribute.type = "PROPERTY";
            });
            reactElement.attributes.push(reactElementAttribute);
          }
        }
      }
    }
  }

  _serializeReactElementChildren(reactElement) {
    let { value } = reactElement;
    let propsValue = (0, _utils.getProperty)(this.realm, value, "props");
    if (!(propsValue instanceof _index.ObjectValue)) {
      return;
    }
    // handle children
    if (propsValue.properties.has("children")) {
      let childrenValue = (0, _utils.getProperty)(this.realm, propsValue, "children");
      this.residualHeapSerializer.serializedValues.add(childrenValue);

      if (childrenValue !== this.realm.intrinsics.undefined && childrenValue !== this.realm.intrinsics.null) {
        if (childrenValue instanceof _index.ArrayValue) {
          let childrenLength = (0, _utils.getProperty)(this.realm, childrenValue, "length");
          let childrenLengthValue = 0;
          if (childrenLength instanceof _index.NumberValue) {
            childrenLengthValue = childrenLength.value;
            for (let i = 0; i < childrenLengthValue; i++) {
              let child = (0, _utils.getProperty)(this.realm, childrenValue, "" + i);
              if (child instanceof _index.Value) {
                reactElement.children.push(this._serializeReactElementChild(child, reactElement));
              } else {
                this.logger.logError(value, `ReactElement "props.children[${i}]" failed to serialize due to a non-value`);
              }
            }
          }
        } else {
          reactElement.children.push(this._serializeReactElementChild(childrenValue, reactElement));
        }
      }
    }
  }

  serializeReactElement(val) {
    let reactElement = this._createReactElement(val);

    this._serializeReactElementType(reactElement);
    this._serializeReactElementAttributes(reactElement);
    this._serializeReactElementChildren(reactElement);

    return this._emitReactElement(reactElement);
  }

  _addSerializedValueToJSXAttriutes(prop, expr, attributes) {
    if (prop === null) {
      attributes.push(t.jSXSpreadAttribute(expr));
    } else {
      attributes.push((0, _jsx.convertKeyValueToJSXAttribute)(prop, expr));
    }
  }

  _serializeReactElementToCreateElement(val, reactElement, createElementIdentifier) {
    let { type, attributes, children } = reactElement;

    let createElementArguments = [type];
    // check if we need to add attributes
    if (attributes.length !== 0) {
      let astAttributes = [];
      for (let attribute of attributes) {
        let expr = attribute.expr;

        if (attribute.type === "SPREAD") {
          astAttributes.push(t.spreadProperty(expr));
        } else if (attribute.type === "PROPERTY") {
          let attributeKey = attribute.key;
          let key;

          (0, _invariant2.default)(typeof attributeKey === "string");
          if (attributeKey.includes("-")) {
            key = t.stringLiteral(attributeKey);
          } else {
            key = t.identifier(attributeKey);
          }
          astAttributes.push(t.objectProperty(key, expr));
        }
      }
      createElementArguments.push(t.objectExpression(astAttributes));
    }
    if (children.length !== 0) {
      if (attributes.length === 0) {
        createElementArguments.push(t.nullLiteral());
      }
      let astChildren = [];
      for (let child of children) {
        let expr = child.expr;

        if (child.type === "NORMAL") {
          astChildren.push(expr);
        }
      }
      createElementArguments.push(...astChildren);
    }
    // cast to any for createElementArguments as casting it to BabelNodeExpresion[] isn't working
    let createElementCall = t.callExpression(createElementIdentifier, createElementArguments);
    this._addBailOutMessageToBabelNode(val, createElementCall);
    return createElementCall;
  }

  _serializeReactElementToJSXElement(val, reactElement) {
    let { type, attributes, children } = reactElement;

    let jsxTypeIdentifer = (0, _jsx.convertExpressionToJSXIdentifier)(type, true);
    let astAttributes = [];
    for (let attribute of attributes) {
      let expr = attribute.expr;

      if (attribute.type === "SPREAD") {
        astAttributes.push(t.jSXSpreadAttribute(expr));
      } else if (attribute.type === "PROPERTY") {
        let attributeKey = attribute.key;
        (0, _invariant2.default)(typeof attributeKey === "string");
        astAttributes.push((0, _jsx.convertKeyValueToJSXAttribute)(attributeKey, expr));
      }
    }

    let astChildren = [];
    for (let child of children) {
      let expr = child.expr;

      if (child.type === "NORMAL") {
        if (t.isStringLiteral(expr) || t.isNumericLiteral(expr)) {
          astChildren.push(t.jSXText(expr.value + ""));
        } else if (t.isJSXElement(expr)) {
          astChildren.push(expr);
        } else {
          astChildren.push(t.jSXExpressionContainer(expr));
        }
      }
    }

    let openingElement = t.jSXOpeningElement(jsxTypeIdentifer, astAttributes, astChildren.length === 0);
    let closingElement = t.jSXClosingElement(jsxTypeIdentifer);
    let jsxElement = t.jSXElement(openingElement, closingElement, astChildren, astChildren.length === 0);
    this._addBailOutMessageToBabelNode(val, jsxElement);
    return jsxElement;
  }

  _addBailOutMessageToBabelNode(val, node) {
    // if there has been a bail-out, we create an inline BlockComment node before the JSX element
    if (val.$BailOutReason !== undefined) {
      // $BailOutReason contains an optional string of what to print out in the comment
      node.leadingComments = [{ type: "BlockComment", value: `${val.$BailOutReason}` }];
    }
  }

  _serializeReactElementChild(child, reactElement) {
    let reactElementChild = this._createReactElementChild();
    this._serializeNowOrAfterWaitingForDependencies(child, reactElement, () => {
      let expr = this.residualHeapSerializer.serializeValue(child);

      reactElementChild.expr = expr;
      reactElementChild.type = "NORMAL";
    });
    return reactElementChild;
  }

  serializeLazyHoistedNodes() {
    const entries = [];
    if (this._lazilyHoistedNodes !== undefined) {
      let { id, nodes, createElementIdentifier } = this._lazilyHoistedNodes;
      // create a function that initializes all the hoisted nodes
      let func = t.functionExpression(null,
      // use createElementIdentifier if it's not null
      createElementIdentifier ? [createElementIdentifier] : [], t.blockStatement(nodes.map(node => t.expressionStatement(t.assignmentExpression("=", node.id, node.astNode)))));
      // push it to the mainBody of the module
      entries.push(t.variableDeclaration("var", [t.variableDeclarator(id, func)]));
      // output all the empty variable declarations that will hold the nodes lazily
      entries.push(...nodes.map(node => t.variableDeclaration("var", [t.variableDeclarator(node.id)])));
      // reset the _lazilyHoistedNodes so other additional functions work
      this._lazilyHoistedNodes = undefined;
    }
    return entries;
  }
}
exports.ResidualReactElementSerializer = ResidualReactElementSerializer;
//# sourceMappingURL=ResidualReactElementSerializer.js.map