/**
 * @license
 * Copyright 2020 Google LLC
 *
 * 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.
 */

// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import {
  FileSystem,
  NewlineKind,
  PackageName
} from '@rushstack/node-core-library';
import {
  DocSection,
  TSDocConfiguration,
  StringBuilder,
  DocNode,
  DocComment,
  DocParagraph,
  DocPlainText,
  DocNodeContainer,
  DocLinkTag,
  DocFencedCode
} from '@microsoft/tsdoc';
import {
  ApiModel,
  ApiItem,
  ApiItemKind,
  ApiReleaseTagMixin,
  ReleaseTag,
  ApiDocumentedItem,
  ApiDeclaredItem,
  ApiClass,
  ApiPropertyItem,
  ApiEnum,
  ApiInterface,
  ApiParameterListMixin,
  ApiReturnTypeMixin,
  Excerpt,
  ExcerptTokenKind,
  IResolveDeclarationReferenceResult,
  ApiPackage,
  ApiEntryPoint,
  ApiNamespace
} from 'api-extractor-model-me';

import { CustomDocNodes } from '../nodes/CustomDocNodeKind';
import { CustomMarkdownEmitter } from '../markdown/CustomMarkdownEmitter';
import { PluginLoader } from '../plugin/PluginLoader';
import {
  IMarkdownDocumenterFeatureOnBeforeWritePageArgs,
  MarkdownDocumenterFeatureContext
} from '../plugin/MarkdownDocumenterFeature';
import { DocumenterConfig } from './DocumenterConfig';
import { MarkdownDocumenterAccessor } from '../plugin/MarkdownDocumenterAccessor';
import {
  getLinkForApiItem,
  getFilenameForApiItem,
  createBetaWarning,
  createRemarksSection,
  createTitleCell,
  createModifiersCell,
  createDescriptionCell,
  createEnumTables,
  createThrowsSection,
  createEntryPointTitleCell,
  createExampleSection
} from './MarkdownDocumenterHelpers';
import * as path from 'path';
import { DocHeading } from '../nodes/DocHeading';
import { DocNoteBox } from '../nodes/DocNoteBox';
import { DocTable } from '../nodes/DocTable';
import { DocTableRow } from '../nodes/DocTableRow';
import { DocTableCell } from '../nodes/DocTableCell';
import { DocEmphasisSpan } from '../nodes/DocEmphasisSpan';

export interface IMarkdownDocumenterOptions {
  apiModel: ApiModel;
  documenterConfig: DocumenterConfig | undefined;
  outputFolder: string;
  addFileNameSuffix: boolean;
}

/**
 * Renders API documentation in the Markdown file format.
 * For more info:  https://en.wikipedia.org/wiki/Markdown
 */
export class MarkdownDocumenter {
  private readonly _apiModel: ApiModel;
  private readonly _documenterConfig: DocumenterConfig | undefined;
  private readonly _tsdocConfiguration: TSDocConfiguration;
  private readonly _markdownEmitter: CustomMarkdownEmitter;
  private readonly _outputFolder: string;
  private readonly _pluginLoader: PluginLoader;
  private readonly _addFileNameSuffix: boolean;

  public constructor(options: IMarkdownDocumenterOptions) {
    this._apiModel = options.apiModel;
    this._documenterConfig = options.documenterConfig;
    this._outputFolder = options.outputFolder;
    this._addFileNameSuffix = options.addFileNameSuffix;
    this._tsdocConfiguration = CustomDocNodes.configuration;
    this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel);

    this._pluginLoader = new PluginLoader();
  }

  public generateFiles(): void {
    if (this._documenterConfig) {
      this._pluginLoader.load(this._documenterConfig, () => {
        return new MarkdownDocumenterFeatureContext({
          apiModel: this._apiModel,
          outputFolder: this._outputFolder,
          documenter: new MarkdownDocumenterAccessor({
            getLinkForApiItem: (apiItem: ApiItem) => {
              return getLinkForApiItem(apiItem, this._addFileNameSuffix);
            }
          })
        });
      });
    }

    this._deleteOldOutputFiles();

    this._writeApiItemPage(this._apiModel);

    if (this._pluginLoader.markdownDocumenterFeature) {
      this._pluginLoader.markdownDocumenterFeature.onFinished({});
    }
  }

  _writeApiItemPage(apiItem: ApiItem): void {
    const output: DocSection = new DocSection({
      configuration: this._tsdocConfiguration
    });
    const nodes = this._createCompleteOutputForApiItem(apiItem);

    /**
     * Remove the heading of the page from md output. (the first item is always a DocHeading)
     * Later we will add the heading to the devsite header {% block title %}
     */
    const headingNode: DocHeading = nodes[0] as DocHeading;
    const pageWithoutHeading = nodes.slice(1);
    output.appendNodes(pageWithoutHeading);

    // write to file
    const filename: string = path.join(
      this._outputFolder,
      getFilenameForApiItem(apiItem, this._addFileNameSuffix)
    );
    const stringBuilder: StringBuilder = new StringBuilder();

    // devsite headers
    stringBuilder.append(
      '{% extends "_internal/templates/reference.html" %}\n'
    );
    stringBuilder.append(
      `{% block title %}${headingNode.title}{% endblock title %}\n`
    );
    stringBuilder.append('{% block body %}\n');

    this._markdownEmitter.emit(stringBuilder, output, {
      contextApiItem: apiItem,
      onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => {
        return getLinkForApiItem(apiItemForFilename, this._addFileNameSuffix);
      }
    });

    stringBuilder.append('{% endblock body %}\n');

    let pageContent: string = stringBuilder.toString();

    if (this._pluginLoader.markdownDocumenterFeature) {
      // Allow the plugin to customize the pageContent
      const eventArgs: IMarkdownDocumenterFeatureOnBeforeWritePageArgs = {
        apiItem: apiItem,
        outputFilename: filename,
        pageContent: pageContent
      };
      this._pluginLoader.markdownDocumenterFeature.onBeforeWritePage(eventArgs);
      pageContent = eventArgs.pageContent;
    }

    FileSystem.writeFile(filename, pageContent, {
      convertLineEndings: NewlineKind.Lf
    });
  }

  _createCompleteOutputForApiItem(apiItem: ApiItem): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const output: DocNode[] = [];
    const scopedName: string = apiItem.getScopedNameWithinPackage();

    switch (apiItem.kind) {
      case ApiItemKind.Class:
        output.push(
          new DocHeading({ configuration, title: `${scopedName} class` })
        );
        break;
      case ApiItemKind.Enum:
        output.push(new DocHeading({ configuration, title: `${scopedName}` }));
        break;
      case ApiItemKind.Interface:
        output.push(
          new DocHeading({ configuration, title: `${scopedName} interface` })
        );
        break;
      case ApiItemKind.Constructor:
      case ApiItemKind.ConstructSignature:
        output.push(new DocHeading({ configuration, title: scopedName }));
        break;
      case ApiItemKind.Method:
      case ApiItemKind.MethodSignature:
        output.push(new DocHeading({ configuration, title: `${scopedName}` }));
        break;
      case ApiItemKind.Function:
        output.push(new DocHeading({ configuration, title: `${scopedName}` }));
        break;
      case ApiItemKind.Model:
        output.push(new DocHeading({ configuration, title: `API Reference` }));
        break;
      case ApiItemKind.Namespace:
        output.push(
          new DocHeading({ configuration, title: `${scopedName} namespace` })
        );
        break;
      case ApiItemKind.Package:
        const unscopedPackageName: string = PackageName.getUnscopedName(
          apiItem.displayName
        );
        output.push(
          new DocHeading({
            configuration,
            title: `${unscopedPackageName} package`
          })
        );
        break;
      case ApiItemKind.EntryPoint:
        const packageName: string = apiItem.parent!.displayName;
        output.push(
          new DocHeading({
            configuration,
            title: `${packageName}${
              apiItem.displayName && '/' + apiItem.displayName
            }`
          })
        );
        break;
      case ApiItemKind.Property:
      case ApiItemKind.PropertySignature:
        output.push(new DocHeading({ configuration, title: `${scopedName}` }));
        break;
      case ApiItemKind.TypeAlias:
        output.push(new DocHeading({ configuration, title: `${scopedName}` }));
        break;
      case ApiItemKind.Variable:
        output.push(new DocHeading({ configuration, title: `${scopedName}` }));
        break;
      default:
        throw new Error('Unsupported API item kind:1 ' + apiItem.kind);
    }

    if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) {
      if (apiItem.releaseTag === ReleaseTag.Beta) {
        output.push(createBetaWarning(configuration));
      }
    }

    if (apiItem instanceof ApiDocumentedItem) {
      const tsdocComment: DocComment | undefined = apiItem.tsdocComment;

      if (tsdocComment) {
        if (tsdocComment.deprecatedBlock) {
          output.push(
            new DocNoteBox({ configuration }, [
              new DocParagraph({ configuration }, [
                new DocPlainText({
                  configuration,
                  text: 'Warning: This API is now obsolete. '
                })
              ]),
              ...tsdocComment.deprecatedBlock.content.nodes
            ])
          );
        }

        output.push(...tsdocComment.summarySection.nodes);
      }
    }

    // render remark sections
    output.push(...createRemarksSection(apiItem, configuration));

    if (apiItem instanceof ApiDeclaredItem) {
      output.push(...this._createSignatureSection(apiItem));
    }

    switch (apiItem.kind) {
      case ApiItemKind.Class:
        output.push(...this._createClassTables(apiItem as ApiClass));
        break;
      case ApiItemKind.Enum:
        output.push(...createEnumTables(apiItem as ApiEnum, configuration));
        break;
      case ApiItemKind.Interface:
        output.push(...this._createInterfaceTables(apiItem as ApiInterface));
        break;
      case ApiItemKind.Constructor:
      case ApiItemKind.ConstructSignature:
      case ApiItemKind.Method:
      case ApiItemKind.MethodSignature:
      case ApiItemKind.Function:
        output.push(
          ...this._createParameterTables(apiItem as ApiParameterListMixin)
        );
        output.push(...createThrowsSection(apiItem, configuration));
        break;
      case ApiItemKind.Namespace:
        output.push(
          ...this._createEntryPointOrNamespace(apiItem as ApiNamespace)
        );
        break;
      case ApiItemKind.Model:
        output.push(...this._createModelTable(apiItem as ApiModel));
        break;
      case ApiItemKind.Package:
        output.push(...this._createPackage(apiItem as ApiPackage));
        break;
      case ApiItemKind.EntryPoint:
        output.push(
          ...this._createEntryPointOrNamespace(apiItem as ApiEntryPoint)
        );
        break;
      case ApiItemKind.Property:
      case ApiItemKind.PropertySignature:
        break;
      case ApiItemKind.TypeAlias:
        break;
      case ApiItemKind.Variable:
        break;
      default:
        throw new Error('Unsupported API item kind:2 ' + apiItem.kind);
    }

    output.push(...createExampleSection(apiItem, configuration));

    return output;
  }

  /**
   * GENERATE PAGE: CLASS
   *
   * TODO: generate member references in the same page
   */
  private _createClassTables(apiClass: ApiClass): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const output: DocNode[] = [];
    const eventsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Property', 'Modifiers', 'Type', 'Description']
    });

    const constructorsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Constructor', 'Modifiers', 'Description']
    });

    const propertiesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Property', 'Modifiers', 'Type', 'Description']
    });

    const methodsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Method', 'Modifiers', 'Description']
    });

    const constructorsDefinitions: DocNode[] = [];
    const methodsDefinitions: DocNode[] = [];
    const propertiesDefinitions: DocNode[] = [];
    const eventsDefinitions: DocNode[] = [];

    for (const apiMember of apiClass.members) {
      switch (apiMember.kind) {
        case ApiItemKind.Constructor: {
          constructorsTable.addRow(
            new DocTableRow({ configuration }, [
              createTitleCell(
                apiMember,
                configuration,
                this._addFileNameSuffix
              ),
              createModifiersCell(apiMember, configuration),
              createDescriptionCell(apiMember, configuration)
            ])
          );

          constructorsDefinitions.push(
            ...this._createCompleteOutputForApiItem(apiMember)
          );
          break;
        }
        case ApiItemKind.Method: {
          methodsTable.addRow(
            new DocTableRow({ configuration }, [
              createTitleCell(
                apiMember,
                configuration,
                this._addFileNameSuffix
              ),
              createModifiersCell(apiMember, configuration),
              createDescriptionCell(apiMember, configuration)
            ])
          );

          methodsDefinitions.push(
            ...this._createCompleteOutputForApiItem(apiMember)
          );
          break;
        }
        case ApiItemKind.Property: {
          if ((apiMember as ApiPropertyItem).isEventProperty) {
            eventsTable.addRow(
              new DocTableRow({ configuration }, [
                createTitleCell(
                  apiMember,
                  configuration,
                  this._addFileNameSuffix
                ),
                createModifiersCell(apiMember, configuration),
                this._createPropertyTypeCell(apiMember),
                createDescriptionCell(apiMember, configuration)
              ])
            );

            eventsDefinitions.push(
              ...this._createCompleteOutputForApiItem(apiMember)
            );
          } else {
            propertiesTable.addRow(
              new DocTableRow({ configuration }, [
                createTitleCell(
                  apiMember,
                  configuration,
                  this._addFileNameSuffix
                ),
                createModifiersCell(apiMember, configuration),
                this._createPropertyTypeCell(apiMember),
                createDescriptionCell(apiMember, configuration)
              ])
            );
            propertiesDefinitions.push(
              ...this._createCompleteOutputForApiItem(apiMember)
            );
          }
          break;
        }
      }
    }

    if (eventsTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Events' }));
      output.push(eventsTable);
    }

    if (constructorsTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Constructors' }));
      output.push(constructorsTable);
    }

    if (propertiesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Properties' }));
      output.push(propertiesTable);
    }

    if (methodsTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Methods' }));
      output.push(methodsTable);
    }

    output.push(...eventsDefinitions);
    output.push(...constructorsDefinitions);
    output.push(...propertiesDefinitions);
    output.push(...methodsDefinitions);

    return output;
  }

  /**
   * GENERATE PAGE: INTERFACE
   */
  private _createInterfaceTables(apiClass: ApiInterface): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const output: DocNode[] = [];
    const eventsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Property', 'Type', 'Description']
    });

    const propertiesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Property', 'Type', 'Description']
    });

    const methodsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Method', 'Description']
    });

    const methodsDefinitions: DocNode[] = [];
    const propertiesDefinitions: DocNode[] = [];
    const eventsDefinitions: DocNode[] = [];

    for (const apiMember of apiClass.members) {
      switch (apiMember.kind) {
        case ApiItemKind.ConstructSignature:
        case ApiItemKind.MethodSignature: {
          methodsTable.addRow(
            new DocTableRow({ configuration }, [
              createTitleCell(
                apiMember,
                configuration,
                this._addFileNameSuffix
              ),
              createDescriptionCell(apiMember, configuration)
            ])
          );

          methodsDefinitions.push(
            ...this._createCompleteOutputForApiItem(apiMember)
          );
          break;
        }
        case ApiItemKind.PropertySignature: {
          if ((apiMember as ApiPropertyItem).isEventProperty) {
            eventsTable.addRow(
              new DocTableRow({ configuration }, [
                createTitleCell(
                  apiMember,
                  configuration,
                  this._addFileNameSuffix
                ),
                this._createPropertyTypeCell(apiMember),
                createDescriptionCell(apiMember, configuration)
              ])
            );
            eventsDefinitions.push(
              ...this._createCompleteOutputForApiItem(apiMember)
            );
          } else {
            propertiesTable.addRow(
              new DocTableRow({ configuration }, [
                createTitleCell(
                  apiMember,
                  configuration,
                  this._addFileNameSuffix
                ),
                this._createPropertyTypeCell(apiMember),
                createDescriptionCell(apiMember, configuration)
              ])
            );
            propertiesDefinitions.push(
              ...this._createCompleteOutputForApiItem(apiMember)
            );
          }
          break;
        }
      }
    }

    if (eventsTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Events' }));
      output.push(eventsTable);
    }

    if (propertiesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Properties' }));
      output.push(propertiesTable);
    }

    if (methodsTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Methods' }));
      output.push(methodsTable);
    }

    output.push(...eventsDefinitions);
    output.push(...propertiesDefinitions);
    output.push(...methodsDefinitions);

    return output;
  }

  /**
   * GENERATE PAGE: FUNCTION-LIKE
   */
  private _createParameterTables(
    apiParameterListMixin: ApiParameterListMixin
  ): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const output: DocNode[] = [];
    const parametersTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Parameter', 'Type', 'Description']
    });
    for (const apiParameter of apiParameterListMixin.parameters) {
      const parameterDescription: DocSection = new DocSection({
        configuration
      });
      if (apiParameter.tsdocParamBlock) {
        parameterDescription.appendNodes(
          apiParameter.tsdocParamBlock.content.nodes
        );
      }

      parametersTable.addRow(
        new DocTableRow({ configuration }, [
          new DocTableCell({ configuration }, [
            new DocParagraph({ configuration }, [
              new DocPlainText({ configuration, text: apiParameter.name })
            ])
          ]),
          new DocTableCell({ configuration }, [
            this._createParagraphForTypeExcerpt(
              apiParameter.parameterTypeExcerpt
            )
          ]),
          new DocTableCell({ configuration }, parameterDescription.nodes)
        ])
      );
    }

    if (parametersTable.rows.length > 0) {
      output.push(
        new DocHeading({ configuration, title: 'Parameters', level: 2 })
      );
      output.push(parametersTable);
    }

    if (ApiReturnTypeMixin.isBaseClassOf(apiParameterListMixin)) {
      const returnTypeExcerpt: Excerpt =
        apiParameterListMixin.returnTypeExcerpt;
      output.push(
        new DocParagraph({ configuration }, [
          new DocEmphasisSpan({ configuration, bold: true }, [
            new DocPlainText({ configuration, text: 'Returns:' })
          ])
        ])
      );

      output.push(this._createParagraphForTypeExcerpt(returnTypeExcerpt));

      if (apiParameterListMixin instanceof ApiDocumentedItem) {
        if (
          apiParameterListMixin.tsdocComment &&
          apiParameterListMixin.tsdocComment.returnsBlock
        ) {
          output.push(
            ...apiParameterListMixin.tsdocComment.returnsBlock.content.nodes
          );
        }
      }
    }

    return output;
  }

  private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph {
    const configuration = this._tsdocConfiguration;
    const paragraph: DocParagraph = new DocParagraph({ configuration });

    if (!excerpt.text.trim()) {
      paragraph.appendNode(
        new DocPlainText({ configuration, text: '(not declared)' })
      );
    } else {
      this._appendExcerptWithHyperlinks(paragraph, excerpt);
    }

    return paragraph;
  }

  private _appendExcerptWithHyperlinks(
    docNodeContainer: DocNodeContainer,
    excerpt: Excerpt
  ): void {
    const configuration = this._tsdocConfiguration;
    for (const token of excerpt.spannedTokens) {
      // Markdown doesn't provide a standardized syntax for hyperlinks inside code spans, so we will render
      // the type expression as DocPlainText.  Instead of creating multiple DocParagraphs, we can simply
      // discard any newlines and let the renderer do normal word-wrapping.
      const unwrappedTokenText: string = token.text.replace(/[\r\n]+/g, ' ');

      // If it's hyperlinkable, then append a DocLinkTag
      if (
        token.kind === ExcerptTokenKind.Reference &&
        token.canonicalReference
      ) {
        const apiItemResult: IResolveDeclarationReferenceResult =
          this._apiModel.resolveDeclarationReference(
            token.canonicalReference,
            undefined
          );

        if (apiItemResult.resolvedApiItem) {
          docNodeContainer.appendNode(
            new DocLinkTag({
              configuration,
              tagName: '@link',
              linkText: unwrappedTokenText,
              urlDestination: getLinkForApiItem(
                apiItemResult.resolvedApiItem,
                this._addFileNameSuffix
              )
            })
          );
          continue;
        }
      }

      // Otherwise append non-hyperlinked text
      docNodeContainer.appendNode(
        new DocPlainText({ configuration, text: unwrappedTokenText })
      );
    }
  }

  /**
   * GENERATE PAGE: MODEL
   */
  private _createModelTable(apiModel: ApiModel): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const output: DocNode[] = [];

    const packagesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Package', 'Description']
    });

    for (const apiMember of apiModel.members) {
      const row: DocTableRow = new DocTableRow({ configuration }, [
        createTitleCell(apiMember, configuration, this._addFileNameSuffix),
        createDescriptionCell(apiMember, configuration)
      ]);

      switch (apiMember.kind) {
        case ApiItemKind.Package:
          packagesTable.addRow(row);
          this._writeApiItemPage(apiMember);
          break;
      }
    }

    if (packagesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Packages' }));
      output.push(packagesTable);
    }

    return output;
  }

  /**´
   * Generate a table of entry points if there are more than one entry points.
   * Otherwise, generate the entry point directly in the package page.
   */
  private _createPackage(apiContainer: ApiPackage): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const output: DocNode[] = [];
    // If a package has a single entry point, generate entry point page in the package page directly
    if (apiContainer.entryPoints.length === 1) {
      return this._createEntryPointOrNamespace(
        apiContainer.members[0] as ApiEntryPoint
      );
    }

    const entryPointsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Entry Point', 'Description']
    });

    for (const entryPoint of apiContainer.entryPoints) {
      const row: DocTableRow = new DocTableRow({ configuration }, [
        createEntryPointTitleCell(
          entryPoint,
          configuration,
          this._addFileNameSuffix
        ),
        createDescriptionCell(entryPoint, configuration)
      ]);

      entryPointsTable.addRow(row);
    }

    output.push(entryPointsTable);

    // write entry point pages
    for (const entryPoint of apiContainer.entryPoints) {
      this._writeApiItemPage(entryPoint);
    }

    return output;
  }

  /**
   * GENERATE PAGE: ENTRYPOINT or NAMESPACE
   */
  private _createEntryPointOrNamespace(
    apiContainer: ApiEntryPoint | ApiNamespace
  ): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const output: DocNode[] = [];

    const classesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Class', 'Description']
    });

    const enumerationsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Enumeration', 'Description']
    });

    const functionsTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Function', 'Description']
    });

    const interfacesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Interface', 'Description']
    });

    const namespacesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Namespace', 'Description']
    });

    const variablesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Variable', 'Description']
    });

    const typeAliasesTable: DocTable = new DocTable({
      configuration,
      headerTitles: ['Type Alias', 'Description']
    });

    const functionsDefinitions: DocNode[] = [];
    const variablesDefinitions: DocNode[] = [];
    const typeAliasDefinitions: DocNode[] = [];
    const enumsDefinitions: DocNode[] = [];

    const apiMembers: ReadonlyArray<ApiItem> =
      apiContainer.kind === ApiItemKind.EntryPoint
        ? (apiContainer as ApiEntryPoint).members
        : (apiContainer as ApiNamespace).members;

    for (const apiMember of apiMembers) {
      const row: DocTableRow = new DocTableRow({ configuration }, [
        createTitleCell(apiMember, configuration, this._addFileNameSuffix),
        createDescriptionCell(apiMember, configuration)
      ]);

      switch (apiMember.kind) {
        case ApiItemKind.Class:
          classesTable.addRow(row);
          this._writeApiItemPage(apiMember);
          break;

        case ApiItemKind.Enum:
          enumerationsTable.addRow(row);
          enumsDefinitions.push(
            ...this._createCompleteOutputForApiItem(apiMember)
          );
          break;

        case ApiItemKind.Interface:
          interfacesTable.addRow(row);
          this._writeApiItemPage(apiMember);
          break;

        case ApiItemKind.Namespace:
          namespacesTable.addRow(row);
          this._writeApiItemPage(apiMember);
          break;

        case ApiItemKind.Function:
          functionsTable.addRow(row);
          functionsDefinitions.push(
            ...this._createCompleteOutputForApiItem(apiMember)
          );
          break;

        case ApiItemKind.TypeAlias:
          typeAliasesTable.addRow(row);
          typeAliasDefinitions.push(
            ...this._createCompleteOutputForApiItem(apiMember)
          );
          break;

        case ApiItemKind.Variable:
          variablesTable.addRow(row);
          variablesDefinitions.push(
            ...this._createCompleteOutputForApiItem(apiMember)
          );
          break;
      }
    }

    if (functionsTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Functions' }));
      output.push(functionsTable);
    }

    if (classesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Classes' }));
      output.push(classesTable);
    }

    if (enumerationsTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Enumerations' }));
      output.push(enumerationsTable);
    }

    if (interfacesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Interfaces' }));
      output.push(interfacesTable);
    }

    if (namespacesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Namespaces' }));
      output.push(namespacesTable);
    }

    if (variablesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Variables' }));
      output.push(variablesTable);
    }

    if (typeAliasesTable.rows.length > 0) {
      output.push(new DocHeading({ configuration, title: 'Type Aliases' }));
      output.push(typeAliasesTable);
    }

    if (functionsDefinitions.length > 0) {
      output.push(...functionsDefinitions);
    }

    if (variablesDefinitions.length > 0) {
      output.push(...variablesDefinitions);
    }

    if (typeAliasDefinitions.length > 0) {
      output.push(...typeAliasDefinitions);
    }

    if (enumsDefinitions.length > 0) {
      output.push(...enumsDefinitions);
    }

    return output;
  }

  private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell {
    const section: DocSection = new DocSection({
      configuration: this._tsdocConfiguration
    });

    if (apiItem instanceof ApiPropertyItem) {
      section.appendNode(
        this._createParagraphForTypeExcerpt(apiItem.propertyTypeExcerpt)
      );
    }

    return new DocTableCell(
      { configuration: this._tsdocConfiguration },
      section.nodes
    );
  }

  private _createSignatureSection(apiItem: ApiDeclaredItem): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const nodes: DocNode[] = [];
    if (apiItem.excerpt.text.length > 0) {
      nodes.push(
        new DocParagraph({ configuration }, [
          new DocEmphasisSpan({ configuration, bold: true }, [
            new DocPlainText({ configuration, text: 'Signature:' })
          ])
        ])
      );
      nodes.push(
        new DocFencedCode({
          configuration,
          code: apiItem.getExcerptWithModifiers(),
          language: 'typescript'
        })
      );
    }

    nodes.push(...this._writeHeritageTypes(apiItem));
    return nodes;
  }

  private _writeHeritageTypes(apiItem: ApiDeclaredItem): DocNode[] {
    const configuration = this._tsdocConfiguration;
    const nodes: DocNode[] = [];
    if (apiItem instanceof ApiClass) {
      if (apiItem.extendsType) {
        const extendsParagraph: DocParagraph = new DocParagraph(
          { configuration },
          [
            new DocEmphasisSpan({ configuration, bold: true }, [
              new DocPlainText({ configuration, text: 'Extends: ' })
            ])
          ]
        );
        this._appendExcerptWithHyperlinks(
          extendsParagraph,
          apiItem.extendsType.excerpt
        );
        nodes.push(extendsParagraph);
      }
      if (apiItem.implementsTypes.length > 0) {
        const implementsParagraph: DocParagraph = new DocParagraph(
          { configuration },
          [
            new DocEmphasisSpan({ configuration, bold: true }, [
              new DocPlainText({ configuration, text: 'Implements: ' })
            ])
          ]
        );
        let needsComma: boolean = false;
        for (const implementsType of apiItem.implementsTypes) {
          if (needsComma) {
            implementsParagraph.appendNode(
              new DocPlainText({ configuration, text: ', ' })
            );
          }
          this._appendExcerptWithHyperlinks(
            implementsParagraph,
            implementsType.excerpt
          );
          needsComma = true;
        }
        nodes.push(implementsParagraph);
      }
    }

    if (apiItem instanceof ApiInterface) {
      if (apiItem.extendsTypes.length > 0) {
        const extendsParagraph: DocParagraph = new DocParagraph(
          { configuration },
          [
            new DocEmphasisSpan({ configuration, bold: true }, [
              new DocPlainText({ configuration, text: 'Extends: ' })
            ])
          ]
        );
        let needsComma: boolean = false;
        for (const extendsType of apiItem.extendsTypes) {
          if (needsComma) {
            extendsParagraph.appendNode(
              new DocPlainText({ configuration, text: ', ' })
            );
          }
          this._appendExcerptWithHyperlinks(
            extendsParagraph,
            extendsType.excerpt
          );
          needsComma = true;
        }
        nodes.push(extendsParagraph);
      }
    }

    return nodes;
  }

  private _deleteOldOutputFiles(): void {
    console.log('Deleting old output from ' + this._outputFolder);
    FileSystem.ensureEmptyFolder(this._outputFolder);
  }
}
