/**
 * @license
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../gr-linked-text/gr-linked-text';
import '../../../styles/shared-styles';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {customElement, property} from '@polymer/decorators';
import {htmlTemplate} from './gr-formatted-text_html';
import {CommentLinks} from '../../../types/common';

const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;

interface Block {
  type: string;
  text?: string;
  blocks?: Block[];
  items?: string[];
}

declare global {
  interface HTMLElementTagNameMap {
    'gr-formatted-text': GrFormattedText;
  }
}

export interface GrFormattedText {
  $: {
    container: HTMLElement;
  };
}

@customElement('gr-formatted-text')
export class GrFormattedText extends GestureEventListeners(
  LegacyElementMixin(PolymerElement)
) {
  static get template() {
    return htmlTemplate;
  }

  @property({type: String, observer: '_contentChanged'})
  content?: string;

  @property({type: Object})
  config?: CommentLinks;

  @property({type: Boolean})
  noTrailingMargin = false;

  static get observers() {
    return ['_contentOrConfigChanged(content, config)'];
  }

  /** @override */
  ready() {
    super.ready();
    if (this.noTrailingMargin) {
      this.classList.add('noTrailingMargin');
    }
  }

  _contentChanged(content: string) {
    // In the case where the config may not be set (perhaps due to the
    // request for it still being in flight), set the content anyway to
    // prevent waiting on the config to display the text.
    if (this.config) return;
    this._contentOrConfigChanged(content);
  }

  /**
   * Given a source string, update the DOM inside #container.
   */
  _contentOrConfigChanged(content?: string) {
    const container = this.$.container;

    // Remove existing content.
    while (container.firstChild) {
      container.removeChild(container.firstChild);
    }

    // Add new content.
    for (const node of this._computeNodes(this._computeBlocks(content))) {
      if (node) container.appendChild(node);
    }
  }

  /**
   * Given a source string, parse into an array of block objects. Each block
   * has a `type` property which takes any of the following values.
   * * 'paragraph'
   * * 'quote' (Block quote.)
   * * 'pre' (Pre-formatted text.)
   * * 'list' (Unordered list.)
   * * 'code' (code blocks.)
   *
   * For blocks of type 'paragraph', 'pre' and 'code' there is a `text`
   * property that maps to a string of the block's content.
   *
   * For blocks of type 'list', there is an `items` property that maps to a
   * list of strings representing the list items.
   *
   * For blocks of type 'quote', there is a `blocks` property that maps to a
   * list of blocks contained in the quote.
   *
   * NOTE: Strings appearing in all block objects are NOT escaped.
   */
  _computeBlocks(content?: string): Block[] {
    if (!content) return [];

    const result = [];
    const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');

    for (let i = 0; i < lines.length; i++) {
      if (!lines[i].length) {
        continue;
      }

      if (this._isCodeMarkLine(lines[i])) {
        // handle multi-line code
        let nextI = i + 1;
        while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
          nextI++;
        }

        if (this._isCodeMarkLine(lines[nextI])) {
          result.push({
            type: 'code',
            text: lines.slice(i + 1, nextI).join('\n'),
          });
          i = nextI;
          continue;
        }

        // otherwise treat it as regular line and continue
        // check for other cases
      }

      if (this._isSingleLineCode(lines[i])) {
        // no guard check as _isSingleLineCode tested on the pattern
        const codeContent = lines[i].match(CODE_MARKER_PATTERN)![2];
        result.push({type: 'code', text: codeContent});
      } else if (this._isList(lines[i])) {
        let nextI = i + 1;
        while (this._isList(lines[nextI])) {
          nextI++;
        }
        result.push(this._makeList(lines.slice(i, nextI)));
        i = nextI - 1;
      } else if (this._isQuote(lines[i])) {
        let nextI = i + 1;
        while (this._isQuote(lines[nextI])) {
          nextI++;
        }
        const blockLines = lines
          .slice(i, nextI)
          .map(l => l.replace(/^[ ]?>[ ]?/, ''));
        result.push({
          type: 'quote',
          blocks: this._computeBlocks(blockLines.join('\n')),
        });
        i = nextI - 1;
      } else if (this._isPreFormat(lines[i])) {
        let nextI = i + 1;
        // include pre or all regular lines but stop at next new line
        while (
          this._isPreFormat(lines[nextI]) ||
          (this._isRegularLine(lines[nextI]) && lines[nextI].length)
        ) {
          nextI++;
        }
        result.push({
          type: 'pre',
          text: lines.slice(i, nextI).join('\n'),
        });
        i = nextI - 1;
      } else {
        let nextI = i + 1;
        while (this._isRegularLine(lines[nextI])) {
          nextI++;
        }
        result.push({
          type: 'paragraph',
          text: lines.slice(i, nextI).join('\n'),
        });
        i = nextI - 1;
      }
    }

    return result;
  }

  /**
   * Take a block of comment text that contains a list, generate appropriate
   * block objects and append them to the output list.
   *
   * * Item one.
   * * Item two.
   * * item three.
   *
   * TODO(taoalpha): maybe we should also support nested list
   *
   * @param lines The block containing the list.
   */
  _makeList(lines: string[]) {
    const items = [];
    for (let i = 0; i < lines.length; i++) {
      let line = lines[i];
      line = line.substring(1).trim();
      items.push(line);
    }
    return {type: 'list', items};
  }

  _isRegularLine(line: string) {
    // line can not be recognized by existing patterns
    if (line === undefined) return false;
    return (
      !this._isQuote(line) &&
      !this._isCodeMarkLine(line) &&
      !this._isSingleLineCode(line) &&
      !this._isList(line) &&
      !this._isPreFormat(line)
    );
  }

  _isQuote(line: string) {
    return line && (line.startsWith('> ') || line.startsWith(' > '));
  }

  _isCodeMarkLine(line: string) {
    return line && line.trim() === '```';
  }

  _isSingleLineCode(line: string) {
    return line && CODE_MARKER_PATTERN.test(line);
  }

  _isPreFormat(line: string) {
    return line && /^[ \t]/.test(line);
  }

  _isList(line: string) {
    return line && /^[-*] /.test(line);
  }

  _makeLinkedText(content = '', isPre?: boolean) {
    const text = document.createElement('gr-linked-text');
    text.config = this.config;
    text.content = content;
    text.pre = true;
    if (isPre) {
      text.classList.add('pre');
    }
    return text;
  }

  /**
   * Map an array of block objects to an array of DOM nodes.
   */
  _computeNodes(blocks: Block[]): HTMLElement[] {
    return blocks.map(block => {
      if (block.type === 'paragraph') {
        const p = document.createElement('p');
        p.appendChild(this._makeLinkedText(block.text));
        return p;
      }

      if (block.type === 'quote') {
        const bq = document.createElement('blockquote');
        for (const node of this._computeNodes(block.blocks || [])) {
          if (node) bq.appendChild(node);
        }
        return bq;
      }

      if (block.type === 'code') {
        const code = document.createElement('code');
        code.textContent = block.text || '';
        return code;
      }

      if (block.type === 'pre') {
        return this._makeLinkedText(block.text, true);
      }

      if (block.type === 'list') {
        const ul = document.createElement('ul');
        const items = block.items || [];
        for (const item of items) {
          const li = document.createElement('li');
          li.appendChild(this._makeLinkedText(item));
          ul.appendChild(li);
        }
        return ul;
      }

      console.warn('Unrecognized type.');
      return document.createElement('span');
    });
  }
}
