/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
import {Element, Expansion, ExpansionCase, Node, Text, visitAll} from '@angular/compiler';

import {BaseVisitor} from '../base_visitor';
import {TranslationParseError} from '../translation_parsers/translation_parse_error';
import {getAttribute, getAttrOrThrow} from '../translation_parsers/translation_utils';

import {MessageRenderer} from './message_renderer';

interface MessageSerializerConfig {
  inlineElements: string[];
  placeholder?: {elementName: string; nameAttribute: string; bodyAttribute?: string;};
  placeholderContainer?: {elementName: string; startAttribute: string; endAttribute: string;};
}

/**
 * This visitor will walk over a set of XML nodes, which represent an i18n message, and serialize
 * them into a message object of type `T`.
 * The type of the serialized message is controlled by the
 */
export class MessageSerializer<T> extends BaseVisitor {
  constructor(private renderer: MessageRenderer<T>, private config: MessageSerializerConfig) {
    super();
  }

  serialize(nodes: Node[]): T {
    this.renderer.startRender();
    visitAll(this, nodes);
    this.renderer.endRender();
    return this.renderer.message;
  }

  visitElement(element: Element): void {
    if (this.config.placeholder && element.name === this.config.placeholder.elementName) {
      const name = getAttrOrThrow(element, this.config.placeholder.nameAttribute);
      const body = this.config.placeholder.bodyAttribute &&
          getAttribute(element, this.config.placeholder.bodyAttribute);
      this.visitPlaceholder(name, body);
    } else if (
        this.config.placeholderContainer &&
        element.name === this.config.placeholderContainer.elementName) {
      const start = getAttrOrThrow(element, this.config.placeholderContainer.startAttribute);
      const end = getAttrOrThrow(element, this.config.placeholderContainer.endAttribute);
      this.visitPlaceholderContainer(start, element.children, end);
    } else if (this.config.inlineElements.indexOf(element.name) !== -1) {
      visitAll(this, element.children);
    } else {
      throw new TranslationParseError(element.sourceSpan, `Invalid element found in message.`);
    }
  }

  visitText(text: Text): void {
    this.renderer.text(text.value);
  }

  visitExpansion(expansion: Expansion): void {
    this.renderer.startIcu();
    this.renderer.text(`${expansion.switchValue}, ${expansion.type},`);
    visitAll(this, expansion.cases);
    this.renderer.endIcu();
  }

  visitExpansionCase(expansionCase: ExpansionCase): void {
    this.renderer.text(` ${expansionCase.value} {`);
    this.renderer.startContainer();
    visitAll(this, expansionCase.expression);
    this.renderer.closeContainer();
    this.renderer.text(`}`);
  }

  visitContainedNodes(nodes: Node[]): void {
    const length = nodes.length;
    let index = 0;
    while (index < length) {
      if (!this.isPlaceholderContainer(nodes[index])) {
        const startOfContainedNodes = index;
        while (index < length - 1) {
          index++;
          if (this.isPlaceholderContainer(nodes[index])) {
            break;
          }
        }
        if (index - startOfContainedNodes > 1) {
          // Only create a container if there are two or more contained Nodes in a row
          this.renderer.startContainer();
          visitAll(this, nodes.slice(startOfContainedNodes, index - 1));
          this.renderer.closeContainer();
        }
      }
      if (index < length) {
        nodes[index].visit(this, undefined);
      }
      index++;
    }
  }

  visitPlaceholder(name: string, body: string|undefined): void {
    this.renderer.placeholder(name, body);
  }

  visitPlaceholderContainer(startName: string, children: Node[], closeName: string): void {
    this.renderer.startPlaceholder(startName);
    this.visitContainedNodes(children);
    this.renderer.closePlaceholder(closeName);
  }

  private isPlaceholderContainer(node: Node): boolean {
    return node instanceof Element && node.name === this.config.placeholderContainer!.elementName;
  }
}
