/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Provides a soy renderer that allows registration of
 * injected data ("globals") that will be passed into the rendered
 * templates.
 */

goog.module('goog.soy.Renderer');
goog.module.declareLegacyNamespace();

const InjectedDataSupplier = goog.requireType('goog.soy.InjectedDataSupplier');
const SafeHtml = goog.requireType('goog.html.SafeHtml');
const SafeStyleSheet = goog.requireType('goog.html.SafeStyleSheet');
const SanitizedContent = goog.require('goog.soy.data.SanitizedContent');
const SanitizedContentKind = goog.require('goog.soy.data.SanitizedContentKind');
const SanitizedCss = goog.requireType('goog.soy.data.SanitizedCss');
const SanitizedHtml = goog.requireType('goog.soy.data.SanitizedHtml');
const SanitizedUri = goog.requireType('goog.soy.data.SanitizedUri');
const asserts = goog.require('goog.asserts');
const dom = goog.require('goog.dom');
const soy = goog.require('goog.soy');

/**
 * Creates a new soy renderer. Note that the renderer will only be
 * guaranteed to work correctly within the document scope provided in
 * the DOM helper.
 */
class Renderer {
  /**
   * @param {?InjectedDataSupplier=} injectedDataSupplier A supplier that
   *     provides an injected data.
   * @param {?dom.DomHelper=} domHelper Optional DOM helper; defaults to that
   *     provided by `dom.getDomHelper()`.
   */
  constructor(injectedDataSupplier = undefined, domHelper = undefined) {
    /** @private @const {!dom.DomHelper} */
    this.dom_ = domHelper || dom.getDomHelper();

    /** @private @const {?InjectedDataSupplier} */
    this.supplier_ = injectedDataSupplier || null;
  }

  /**
   * Renders a Soy template into a single node or a document fragment.
   * Delegates to `soy.renderAsFragment`.
   * @param {function(ARG_TYPES, ?soy.CompatibleIj=): *} template The Soy
   *     template defining the element's content.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {!Node} The resulting node or document fragment.
   * @template ARG_TYPES
   */
  renderAsFragment(template, templateData = undefined) {
    const node = soy.renderAsFragment(
        template, templateData, this.getInjectedData_(), this.dom_);
    this.handleRender(node, SanitizedContentKind.HTML);
    return node;
  }

  /**
   * Renders a Soy template into a single node. If the rendered HTML
   * string represents a single node, then that node is returned.
   * Otherwise, a DIV element is returned containing the rendered nodes.
   * Delegates to `soy.renderAsElement`.
   * @param {function(ARG_TYPES, ?soy.CompatibleIj=): *} template The Soy
   *     template defining the element's content.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {!Element} Rendered template contents, wrapped in a parent DIV
   *     element if necessary.
   * @template ARG_TYPES
   */
  renderAsElement(template, templateData = undefined) {
    const element = soy.renderAsElement(
        template, templateData, this.getInjectedData_(), this.dom_);
    this.handleRender(element, SanitizedContentKind.HTML);
    return element;
  }

  /**
   * Renders a Soy template and then set the output string as the
   * innerHTML of the given element. Delegates to `soy.renderElement`.
   * @param {?Element} element The element whose content we are rendering.
   * @param {function(ARG_TYPES, ?soy.CompatibleIj=): *} template The Soy
   *     template defining the element's content.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @template ARG_TYPES
   */
  renderElement(element, template, templateData = undefined) {
    soy.renderElement(element, template, templateData, this.getInjectedData_());
    this.handleRender(element, SanitizedContentKind.HTML);
  }

  /**
   * Renders a Soy template and returns the output string.
   * If the template is strict, it must be of kind HTML. To render strict
   * templates of other kinds, use `renderText` (for `kind="text"`) or
   * `renderStrictOfKind`.
   * @param {function(ARG_TYPES, ?Object<string, *>=): *} template The Soy
   *     template to render.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {string} The return value of rendering the template directly.
   * @template ARG_TYPES
   */
  render(template, templateData = undefined) {
    const result = template(templateData || {}, this.getInjectedData_());
    asserts.assert(
        !(result instanceof SanitizedContent) ||
            result.contentKind === SanitizedContentKind.HTML,
        'render was called with a strict template of kind other than "html"' +
            ' (consider using renderText or renderStrict)');
    const contentKind =
        result instanceof SanitizedContent ? result.contentKind : null;
    this.handleRender(null /* node */, contentKind);
    return String(result);
  }

  /**
   * Renders a strict Soy template of kind="text" and returns the output string.
   * It is an error to use renderText on templates of kinds other than "text".
   * @param {function(ARG_TYPES, ?Object<string,*>=): string} template The Soy
   *     template to render.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {string} The return value of rendering the template directly.
   * @template ARG_TYPES
   */
  renderText(template, templateData = undefined) {
    return soy.renderAsText(template, templateData, this.getInjectedData_());
  }

  /**
   * Renders a strict Soy HTML template and returns the output SanitizedHtml
   * object.
   * @param {function(ARG_TYPES, ?Object<string,*>=):
   *     !SanitizedHtml} template The Soy template to render.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {!SanitizedHtml}
   * @template ARG_TYPES
   */
  renderStrict(template, templateData = undefined) {
    return this.renderStrictOfKind(
        template, templateData, SanitizedContentKind.HTML);
  }

  /**
   * Renders a strict Soy template and returns the output SanitizedUri object.
   * @param {function(ARG_TYPES, ?Object<string, *>=):
   *     !SanitizedUri} template The Soy template to render.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {!SanitizedUri}
   * @template ARG_TYPES
   */
  renderStrictUri(template, templateData = undefined) {
    return this.renderStrictOfKind(
        template, templateData, SanitizedContentKind.URI);
  }

  /**
   * Renders a strict Soy template and returns the output SanitizedContent
   * object.
   * @param {function(ARG_TYPES, ?Object<string, *>=): RETURN_TYPE} template The
   *     Soy template to render.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @param {?SanitizedContentKind=} kind The output kind to assert. If null,
   *     the template must be of kind="html" (i.e., kind defaults to
   *     SanitizedContentKind.HTML).
   * @return {RETURN_TYPE} The SanitizedContent object. This return type is
   *     generic based on the return type of the template, such as
   *     soy.data.SanitizedHtml.
   * @template ARG_TYPES, RETURN_TYPE
   */
  renderStrictOfKind(template, templateData = undefined, kind = undefined) {
    const result = template(templateData || {}, this.getInjectedData_());
    asserts.assertInstanceof(
        result, SanitizedContent,
        'renderStrict cannot be called on a text soy template');
    asserts.assert(
        result.contentKind === (kind || SanitizedContentKind.HTML),
        'renderStrict was called with the wrong kind of template');
    this.handleRender(null /* node */, result.contentKind);
    return result;
  }

  /**
   * Renders a strict Soy template of kind="html" and returns the result as
   * a SafeHtml object.
   * Rendering a template that is not a strict template of kind="html" results
   * in a runtime error.
   * @param {function(ARG_TYPES, ?Object<string, *>=):
   *     !SanitizedHtml} template The Soy template to render.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {!SafeHtml}
   * @template ARG_TYPES
   */
  renderSafeHtml(template, templateData = undefined) {
    const result = this.renderStrict(template, templateData);
    // Convert from SanitizedHtml to SafeHtml.
    return result.toSafeHtml();
  }

  /**
   * Renders a strict Soy template of kind="css" and returns the result as
   * a SafeStyleSheet object.
   * Rendering a template that is not a strict template of kind="css" results in
   * a runtime and compile-time error.
   * @param {function(ARG_TYPES, ?Object<string, *>=):
   *     !SanitizedCss} template The Soy template to render.
   * @param {ARG_TYPES=} templateData The data for the template.
   * @return {!SafeStyleSheet}
   * @template ARG_TYPES
   */
  renderSafeStyleSheet(template, templateData = undefined) {
    const result = this.renderStrictOfKind(
        template, templateData, SanitizedContentKind.CSS);
    return result.toSafeStyleSheet();
  }

  /**
   * @return {!dom.DomHelper}
   * @protected
   */
  getDom() {
    return this.dom_;
  }

  /**
   * Observes rendering of non-text templates by this renderer.
   * @param {?Node} node Relevant node, if available. The node may or may not be
   *     in the document, depending on whether Soy is creating an element or
   *     writing into an existing one.
   * @param {?SanitizedContentKind} kind of the template, or null if it was not
   *     strict.
   * @protected
   */
  handleRender(node, kind) {}

  /**
   * Creates the injectedParams map if necessary and calls the configuration
   * service to prepopulate it.
   * @return {?} The injected params.
   * @private
   */
  getInjectedData_() {
    return this.supplier_ ? this.supplier_.getData() : {};
  }
}

exports = Renderer;
