import { u } from 'unist-builder';
import { remark } from 'remark';
import mergeConfig from '../merge_config.js';
import toc from 'remark-toc';
import links from 'remark-reference-links';
import hljs from 'highlight.js';
import GithubSlugger from 'github-slugger';
import LinkerStack from './util/linker_stack.js';
import rerouteLinks from './util/reroute_links.js';
import _formatType from './util/format_type.js';

const DEFAULT_LANGUAGE = 'javascript';

/**
 * Given a hierarchy-nested set of comments, generate an remark-compatible
 * Abstract Syntax Tree usable for generating Markdown output
 *
 * @param  comments nested comment
 * @param {Object} args currently none accepted
 * @param {boolean} [args.markdownToc=true] whether to include a table of contents
 * in markdown output.
 * @param {Object} [args.hljs={}] config to be passed to highlightjs for code highlighting:
 * consult hljs.configure for the full list.
 * @returns {Promise<Object>} returns an eventual Markdown value
 */
export default function markdownAST(comments, args) {
  return mergeConfig(args).then(config => buildMarkdownAST(comments, config));
}

function buildMarkdownAST(comments, config) {
  // Configure code highlighting
  const hljsOptions = config.hljs || {};
  hljs.configure(hljsOptions);

  const linkerStack = new LinkerStack(config).namespaceResolver(
    comments,
    namespace => {
      const slugger = new GithubSlugger();
      return '#' + slugger.slug(namespace);
    }
  );

  const formatType = _formatType.bind(undefined, linkerStack.link);

  const generatorComment = [
    u(
      'html',
      '<!-- Generated by documentation.js. Update this documentation by updating the source code. -->'
    )
  ];

  const tableOfContentsHeading = [
    u('heading', { depth: 3 }, [u('text', 'Table of Contents')])
  ];

  /**
   * Generate an AST chunk for a comment at a given depth: this is
   * split from the main function to handle hierarchially nested comments
   *
   * @param {number} depth nesting of the comment, starting at 1
   * @param {Object} comment a single comment
   * @returns {Object} remark-compatible AST
   */
  function generate(depth, comment) {
    function typeSection(comment) {
      return (
        comment.type &&
        u('paragraph', [u('text', 'Type: ')].concat(formatType(comment.type)))
      );
    }

    function paramList(params) {
      if (params.length === 0) return [];
      return u(
        'list',
        { ordered: false, spread: false },
        params.map(param =>
          u(
            'listItem',
            [
              u(
                'paragraph',
                [
                  u('inlineCode', param.name),
                  u('text', ' '),
                  !!param.type && u('strong', formatType(param.type)),
                  u('text', ' ')
                ]
                  .concat(param.description ? param.description.children : [])
                  .concat([
                    !!param.default &&
                      u('paragraph', [
                        u('text', ' (optional, default '),
                        u('inlineCode', param.default),
                        u('text', ')')
                      ])
                  ])
                  .filter(Boolean)
              )
            ]
              .concat(param.properties && paramList(param.properties))
              .filter(Boolean)
          )
        )
      );
    }

    function paramSection(comment) {
      return (
        comment.params.length > 0 && [
          u('heading', { depth: depth + 1 }, [u('text', 'Parameters')]),
          paramList(comment.params)
        ]
      );
    }

    function propertySection(comment) {
      return (
        comment.properties.length > 0 && [
          u('heading', { depth: depth + 1 }, [u('text', 'Properties')]),
          propertyList(comment.properties)
        ]
      );
    }

    function propertyList(properties) {
      return u(
        'list',
        { ordered: false, spread: false },
        properties.map(property =>
          u(
            'listItem',
            [
              u(
                'paragraph',
                [
                  u('inlineCode', property.name),
                  u('text', ' '),
                  u('strong', formatType(property.type)),
                  u('text', ' ')
                ]
                  .concat(
                    property.description ? property.description.children : []
                  )
                  .filter(Boolean)
              ),
              property.properties && propertyList(property.properties)
            ].filter(Boolean)
          )
        )
      );
    }

    function examplesSection(comment) {
      return (
        comment.examples.length > 0 &&
        [u('heading', { depth: depth + 1 }, [u('text', 'Examples')])].concat(
          comment.examples.reduce(function (memo, example) {
            const language = hljsOptions.highlightAuto
              ? hljs.highlightAuto(example.description).language
              : DEFAULT_LANGUAGE;
            return memo
              .concat(
                example.caption
                  ? [u('paragraph', [u('emphasis', example.caption)])]
                  : []
              )
              .concat([u('code', { lang: language }, example.description)]);
          }, [])
        )
      );
    }

    function returnsSection(comment) {
      return (
        comment.returns.length > 0 &&
        comment.returns.map(returns =>
          u(
            'paragraph',
            [
              u('text', 'Returns '),
              u('strong', formatType(returns.type)),
              u('text', ' ')
            ].concat(returns.description ? returns.description.children : [])
          )
        )
      );
    }

    function throwsSection(comment) {
      return (
        comment.throws.length > 0 &&
        u(
          'list',
          { ordered: false, spread: false },
          comment.throws.map(returns =>
            u('listItem', [
              u(
                'paragraph',
                [
                  u('text', 'Throws '),
                  u('strong', formatType(returns.type)),
                  u('text', ' ')
                ].concat(
                  returns.description ? returns.description.children : []
                )
              )
            ])
          )
        )
      );
    }

    function augmentsLink(comment) {
      return (
        comment.augments.length > 0 &&
        u('paragraph', [
          u('strong', [
            u('text', 'Extends '),
            u('text', comment.augments.map(tag => tag.name).join(', '))
          ])
        ])
      );
    }

    function seeLink({ sees = [] }) {
      return (
        sees.length > 0 &&
        u(
          'list',
          { ordered: false, spread: false },
          sees.map(see =>
            u('listItem', [
              u(
                'paragraph',
                [u('strong', [u('text', 'See')]), u('text', ': ')].concat(
                  see.description
                )
              )
            ])
          )
        )
      );
    }

    function githubLink(comment) {
      return (
        comment.context &&
        comment.context.github &&
        u('paragraph', [
          u(
            'link',
            {
              title: 'Source code on GitHub',
              url: comment.context.github.url
            },
            [
              u(
                'text',
                comment.context.github.path +
                  ':' +
                  comment.context.loc.start.line +
                  '-' +
                  comment.context.loc.end.line
              )
            ]
          )
        ])
      );
    }

    function metaSection(comment) {
      const meta = [
        'version',
        'since',
        'copyright',
        'author',
        'license',
        'deprecated'
      ].filter(tag => comment[tag]);
      return (
        !!meta.length &&
        [u('strong', [u('text', 'Meta')])].concat(
          u(
            'list',
            { ordered: false, spread: false },
            meta.map(tag => {
              let metaContent;
              if (tag === 'copyright' || tag === 'deprecated') {
                metaContent = comment[tag];
              } else {
                metaContent = u('text', comment[tag]);
              }
              return u('listItem', [
                u('paragraph', [
                  u('strong', [u('text', tag)]),
                  u('text', ': '),
                  metaContent
                ])
              ]);
            })
          )
        )
      );
    }

    if (comment.kind === 'note') {
      return [u('heading', { depth }, [u('text', comment.name || '')])]
        .concat(comment.description)
        .concat(
          !!comment.members.static.length &&
            comment.members.static.reduce(
              (memo, child) => memo.concat(generate(depth + 1, child)),
              []
            )
        )
        .filter(Boolean);
    }

    return [u('heading', { depth }, [u('text', comment.name || '')])]
      .concat(githubLink(comment))
      .concat(augmentsLink(comment))
      .concat(seeLink(comment))
      .concat(comment.description ? comment.description.children : [])
      .concat(typeSection(comment))
      .concat(paramSection(comment))
      .concat(propertySection(comment))
      .concat(examplesSection(comment))
      .concat(throwsSection(comment))
      .concat(returnsSection(comment))
      .concat(metaSection(comment))
      .concat(
        !!comment.members.global.length &&
          comment.members.global.reduce(
            (memo, child) => memo.concat(generate(depth + 1, child)),
            []
          )
      )
      .concat(
        !!comment.members.instance.length &&
          comment.members.instance.reduce(
            (memo, child) => memo.concat(generate(depth + 1, child)),
            []
          )
      )
      .concat(
        !!comment.members.static.length &&
          comment.members.static.reduce(
            (memo, child) => memo.concat(generate(depth + 1, child)),
            []
          )
      )
      .concat(
        !!comment.members.inner.length &&
          comment.members.inner.reduce(
            (memo, child) => memo.concat(generate(depth + 1, child)),
            []
          )
      )
      .concat(
        !!comment.members.events.length &&
          comment.members.events.reduce(
            (memo, child) => memo.concat(generate(depth + 1, child)),
            []
          )
      )
      .filter(Boolean);
  }

  let root = rerouteLinks(
    linkerStack.link,
    u(
      'root',
      generatorComment
        .concat(config.markdownToc ? tableOfContentsHeading : [])
        .concat(
          comments.reduce(
            (memo, comment) => memo.concat(generate(2, comment)),
            []
          )
        )
    )
  );

  const pluginRemark = remark();
  if (config.markdownToc)
    pluginRemark.use(toc, {
      tight: true,
      maxDepth: config.markdownTocMaxDepth
    });
  if (config.noReferenceLinks !== true) pluginRemark.use(links);
  root = pluginRemark.run(root);

  return Promise.resolve(root);
}
