import { fs, value as valueUtil } from '../common';
import {
  IMarkdown,
  IMarkdownSection,
  IParseResponse,
  IManifestMarkdown,
} from './types';
import { parseYaml } from './util.parse';

export type IMarkdownResponse = IParseResponse<IMarkdown>;

/**
 * See:
 *    https://unified.js.org/
 *    https://remark.js.org/
 */
const unified = require('unified');
const parse = require('remark-parse');
const html = require('remark-html');

const processor = unified()
  .use(parse, { commonmark: true })
  .use(html);

/**
 * Parses a markdown body and packages it as a manifest return type.
 */
export async function toManifestMarkdown(
  path: string,
  body?: string,
): Promise<IManifestMarkdown> {
  const res = body ? await parseMarkdown(body) : undefined;
  const markdown = res ? res.data : undefined;
  const error = res ? res.error : undefined;
  const result: IManifestMarkdown = {
    type: 'FILE/markdown',
    path,
    markdown,
    error,
  };
  return valueUtil.deleteUndefined(result);
}

/**
 * Loads a markdown file and parses it.
 */
export async function parseMarkdownFile(
  path: string,
): Promise<IMarkdownResponse> {
  try {
    const text = (await fs.readFile(path)).toString();
    return parseMarkdown(text);
  } catch (err) {
    let message = `Failed to parse markdown while loading file.`;
    message = `${message} ${err.message}`;
    return { data: undefined, error: { message } };
  }
}

/**
 * Parses markdown into structure, converting YAML blocks
 * into usable data.
 */
export async function parseMarkdown(text: string): Promise<IMarkdownResponse> {
  const result: IMarkdownResponse = {};

  type IMdAst = {
    type: string;
    value: string;
    lang?: string;
    meta?: string;
    depth?: number;
  };

  try {
    const ast = (await processor.parse(text)) as { children: IMdAst[] };

    // Convert to raw AST parts.
    const parts = ast.children.map((child, index) => {
      const { type, lang, value, depth } = child;
      const meta = child.meta ? child.meta : undefined;
      const html = processor.stringify(child);
      let data: object | undefined;
      let error: IMarkdownResponse['error'] | undefined;
      if (type === 'code' && lang === 'yaml' && value) {
        const res = parseYaml(value);
        data = res.data;
        error = res.error;
      }
      return { type, depth, html, data, meta, error };
    });

    // State-machine to convert into flat list of sections.
    let title: string | undefined;
    let sections: IMarkdownSection[] = [];

    const addHtml = (section: IMarkdownSection, html: string) =>
      `${section.html || ''}\n${html || ''}`.trim();

    parts.forEach(part => {
      const isHeading = part.type === 'heading';
      if (isHeading || sections.length === 0) {
        const depth = part.depth || 0;
        sections = [...sections, { index: -1, depth, data: [] }];
      }
      const section = { ...sections[sections.length - 1] };
      if (isHeading) {
        section.titleHtml = part.html;
        section.title = stripTags(part.html);
      }

      if (part.data) {
        const { meta, data: value } = part;
        section.data = [...section.data, { meta, value }];
      }

      if (!part.data && !isHeading) {
        section.html = addHtml(section, part.html);
      }

      if (part.data && part.meta && part.meta.includes('RENDER')) {
        section.html = addHtml(section, part.html);
      }

      sections = [
        ...sections.slice(0, sections.length - 1),
        valueUtil.deleteEmpty(section),
      ];

      if (!title && isHeading && part.depth === 1) {
        title = stripTags(part.html);
      }
    });

    // Store data.
    sections = sections.map((s, index) => ({ ...s, index }));
    result.data = { title, sections };
  } catch (err) {
    let message = `Failed to parse markdown.`;
    message = `${message} ${err.message}`;
    result.error = { message };
  }

  return valueUtil.deleteUndefined(result);
}

/**
 * HELPERS
 */
function stripTags(text: string) {
  return text
    .trim()
    .replace(/^\<[a-z1-9]+\>/, '')
    .replace(/\<\/[a-z1-9]+\>$/, '');
}
