import { _Renderer } from './Renderer';
import { _TextRenderer } from './TextRenderer';
import { _Slugger } from './Slugger';
import { _defaults } from './defaults';
import {
  unescape
} from './helpers';
import type { Token, Tokens } from './Tokens';
import type { MarkedOptions } from './MarkedOptions';

/**
 * Parsing & Compiling
 */
export class _Parser {
  options: MarkedOptions;
  renderer: _Renderer;
  textRenderer: _TextRenderer;
  slugger: _Slugger;
  constructor(options?: MarkedOptions) {
    this.options = options || _defaults;
    this.options.renderer = this.options.renderer || new _Renderer();
    this.renderer = this.options.renderer;
    this.renderer.options = this.options;
    this.textRenderer = new _TextRenderer();
    this.slugger = new _Slugger();
  }

  /**
   * Static Parse Method
   */
  static parse(tokens: Token[], options?: MarkedOptions) {
    const parser = new _Parser(options);
    return parser.parse(tokens);
  }

  /**
   * Static Parse Inline Method
   */
  static parseInline(tokens: Token[], options?: MarkedOptions) {
    const parser = new _Parser(options);
    return parser.parseInline(tokens);
  }

  /**
   * Parse Loop
   */
  parse(tokens: Token[], top = true): string {
    let out = '';

    for (let i = 0; i < tokens.length; i++) {
      const token = tokens[i];

      // Run any renderer extensions
      if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) {
        const genericToken = token as Tokens.Generic;
        const ret = this.options.extensions.renderers[genericToken.type].call({ parser: this }, genericToken);
        if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'paragraph', 'text'].includes(genericToken.type)) {
          out += ret || '';
          continue;
        }
      }

      switch (token.type) {
        case 'space': {
          continue;
        }
        case 'hr': {
          out += this.renderer.hr();
          continue;
        }
        case 'heading': {
          const headingToken = token as Tokens.Heading;
          out += this.renderer.heading(
            this.parseInline(headingToken.tokens),
            headingToken.depth,
            unescape(this.parseInline(headingToken.tokens, this.textRenderer)),
            this.slugger);
          continue;
        }
        case 'code': {
          const codeToken = token as Tokens.Code;
          out += this.renderer.code(codeToken.text,
            codeToken.lang,
            !!codeToken.escaped);
          continue;
        }
        case 'table': {
          const tableToken = token as Tokens.Table;
          let header = '';

          // header
          let cell = '';
          for (let j = 0; j < tableToken.header.length; j++) {
            cell += this.renderer.tablecell(
              this.parseInline(tableToken.header[j].tokens),
              { header: true, align: tableToken.align[j] }
            );
          }
          header += this.renderer.tablerow(cell);

          let body = '';
          for (let j = 0; j < tableToken.rows.length; j++) {
            const row = tableToken.rows[j];

            cell = '';
            for (let k = 0; k < row.length; k++) {
              cell += this.renderer.tablecell(
                this.parseInline(row[k].tokens),
                { header: false, align: tableToken.align[k] }
              );
            }

            body += this.renderer.tablerow(cell);
          }
          out += this.renderer.table(header, body);
          continue;
        }
        case 'blockquote': {
          const blockquoteToken = token as Tokens.Blockquote;
          const body = this.parse(blockquoteToken.tokens);
          out += this.renderer.blockquote(body);
          continue;
        }
        case 'list': {
          const listToken = token as Tokens.List;
          const ordered = listToken.ordered;
          const start = listToken.start;
          const loose = listToken.loose;

          let body = '';
          for (let j = 0; j < listToken.items.length; j++) {
            const item = listToken.items[j];
            const checked = item.checked;
            const task = item.task;

            let itemBody = '';
            if (item.task) {
              const checkbox = this.renderer.checkbox(!!checked);
              if (loose) {
                if (item.tokens.length > 0 && item.tokens[0].type === 'paragraph') {
                  item.tokens[0].text = checkbox + ' ' + item.tokens[0].text;
                  if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') {
                    item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text;
                  }
                } else {
                  item.tokens.unshift({
                    type: 'text',
                    text: checkbox
                  } as Tokens.Text);
                }
              } else {
                itemBody += checkbox;
              }
            }

            itemBody += this.parse(item.tokens, loose);
            body += this.renderer.listitem(itemBody, task, !!checked);
          }

          out += this.renderer.list(body, ordered, start);
          continue;
        }
        case 'html': {
          const htmlToken = token as Tokens.HTML;
          out += this.renderer.html(htmlToken.text, htmlToken.block);
          continue;
        }
        case 'paragraph': {
          const paragraphToken = token as Tokens.Paragraph;
          out += this.renderer.paragraph(this.parseInline(paragraphToken.tokens));
          continue;
        }
        case 'text': {
          let textToken = token as Tokens.Text;
          let body = textToken.tokens ? this.parseInline(textToken.tokens) : textToken.text;
          while (i + 1 < tokens.length && tokens[i + 1].type === 'text') {
            textToken = tokens[++i] as Tokens.Text;
            body += '\n' + (textToken.tokens ? this.parseInline(textToken.tokens) : textToken.text);
          }
          out += top ? this.renderer.paragraph(body) : body;
          continue;
        }

        default: {
          const errMsg = 'Token with "' + token.type + '" type was not found.';
          if (this.options.silent) {
            console.error(errMsg);
            return '';
          } else {
            throw new Error(errMsg);
          }
        }
      }
    }

    return out;
  }

  /**
   * Parse Inline Tokens
   */
  parseInline(tokens: Token[], renderer?: _Renderer | _TextRenderer): string {
    renderer = renderer || this.renderer;
    let out = '';

    for (let i = 0; i < tokens.length; i++) {
      const token = tokens[i];

      // Run any renderer extensions
      if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) {
        const ret = this.options.extensions.renderers[token.type].call({ parser: this }, token);
        if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(token.type)) {
          out += ret || '';
          continue;
        }
      }

      switch (token.type) {
        case 'escape': {
          const escapeToken = token as Tokens.Escape;
          out += renderer.text(escapeToken.text);
          break;
        }
        case 'html': {
          const tagToken = token as Tokens.Tag;
          out += renderer.html(tagToken.text);
          break;
        }
        case 'link': {
          const linkToken = token as Tokens.Link;
          out += renderer.link(linkToken.href, linkToken.title, this.parseInline(linkToken.tokens, renderer));
          break;
        }
        case 'image': {
          const imageToken = token as Tokens.Image;
          out += renderer.image(imageToken.href, imageToken.title, imageToken.text);
          break;
        }
        case 'strong': {
          const strongToken = token as Tokens.Strong;
          out += renderer.strong(this.parseInline(strongToken.tokens, renderer));
          break;
        }
        case 'em': {
          const emToken = token as Tokens.Em;
          out += renderer.em(this.parseInline(emToken.tokens, renderer));
          break;
        }
        case 'codespan': {
          const codespanToken = token as Tokens.Codespan;
          out += renderer.codespan(codespanToken.text);
          break;
        }
        case 'br': {
          out += renderer.br();
          break;
        }
        case 'del': {
          const delToken = token as Tokens.Del;
          out += renderer.del(this.parseInline(delToken.tokens, renderer));
          break;
        }
        case 'text': {
          const textToken = token as Tokens.Text;
          out += renderer.text(textToken.text);
          break;
        }
        default: {
          const errMsg = 'Token with "' + token.type + '" type was not found.';
          if (this.options.silent) {
            console.error(errMsg);
            return '';
          } else {
            throw new Error(errMsg);
          }
        }
      }
    }
    return out;
  }
}
