// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
 * @packageDocumentation
 * @module help-extension
 */

import {
  ILayoutRestorer,
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';
import {
  CommandToolbarButton,
  Dialog,
  ICommandPalette,
  IFrame,
  MainAreaWidget,
  showDialog,
  Toolbar,
  WidgetTracker
} from '@jupyterlab/apputils';
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { IMainMenu } from '@jupyterlab/mainmenu';
import { KernelMessage } from '@jupyterlab/services';
import { ITranslator } from '@jupyterlab/translation';
import {
  copyrightIcon,
  jupyterIcon,
  jupyterlabWordmarkIcon,
  refreshIcon
} from '@jupyterlab/ui-components';
import { ReadonlyJSONObject } from '@lumino/coreutils';
import { Menu } from '@lumino/widgets';
import * as React from 'react';
import { Licenses } from './licenses';

/**
 * The command IDs used by the help plugin.
 */
namespace CommandIDs {
  export const open = 'help:open';

  export const about = 'help:about';

  export const activate = 'help:activate';

  export const close = 'help:close';

  export const show = 'help:show';

  export const hide = 'help:hide';

  export const launchClassic = 'help:launch-classic-notebook';

  export const jupyterForum = 'help:jupyter-forum';

  export const licenses = 'help:licenses';

  export const licenseReport = 'help:license-report';

  export const refreshLicenses = 'help:licenses-refresh';
}

/**
 * A flag denoting whether the application is loaded over HTTPS.
 */
const LAB_IS_SECURE = window.location.protocol === 'https:';

/**
 * The class name added to the help widget.
 */
const HELP_CLASS = 'jp-Help';

/**
 * Add a command to show an About dialog.
 */
const about: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/help-extension:about',
  autoStart: true,
  requires: [ITranslator],
  optional: [ICommandPalette],
  activate: (
    app: JupyterFrontEnd,
    translator: ITranslator,
    palette: ICommandPalette | null
  ): void => {
    const { commands } = app;
    const trans = translator.load('jupyterlab');
    const category = trans.__('Help');

    commands.addCommand(CommandIDs.about, {
      label: trans.__('About %1', app.name),
      execute: () => {
        // Create the header of the about dialog
        const versionNumber = trans.__('Version %1', app.version);
        const versionInfo = (
          <span className="jp-About-version-info">
            <span className="jp-About-version">{versionNumber}</span>
          </span>
        );
        const title = (
          <span className="jp-About-header">
            <jupyterIcon.react margin="7px 9.5px" height="auto" width="58px" />
            <div className="jp-About-header-info">
              <jupyterlabWordmarkIcon.react height="auto" width="196px" />
              {versionInfo}
            </div>
          </span>
        );

        // Create the body of the about dialog
        const jupyterURL = 'https://jupyter.org/about.html';
        const contributorsURL =
          'https://github.com/jupyterlab/jupyterlab/graphs/contributors';
        const externalLinks = (
          <span className="jp-About-externalLinks">
            <a
              href={contributorsURL}
              target="_blank"
              rel="noopener noreferrer"
              className="jp-Button-flat"
            >
              {trans.__('CONTRIBUTOR LIST')}
            </a>
            <a
              href={jupyterURL}
              target="_blank"
              rel="noopener noreferrer"
              className="jp-Button-flat"
            >
              {trans.__('ABOUT PROJECT JUPYTER')}
            </a>
          </span>
        );
        const copyright = (
          <span className="jp-About-copyright">
            {trans.__('© 2015-2022 Project Jupyter Contributors')}
          </span>
        );
        const body = (
          <div className="jp-About-body">
            {externalLinks}
            {copyright}
          </div>
        );

        return showDialog({
          title,
          body,
          buttons: [
            Dialog.createButton({
              label: trans.__('Dismiss'),
              className: 'jp-About-button jp-mod-reject jp-mod-styled'
            })
          ]
        });
      }
    });

    if (palette) {
      palette.addItem({ command: CommandIDs.about, category });
    }
  }
};

/**
 * A plugin to add a command to open the Classic Notebook interface.
 */
const launchClassic: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/help-extension:launch-classic',
  autoStart: true,
  requires: [ITranslator],
  optional: [ICommandPalette],
  activate: (
    app: JupyterFrontEnd,
    translator: ITranslator,
    palette: ICommandPalette | null
  ): void => {
    const { commands } = app;
    const trans = translator.load('jupyterlab');
    const category = trans.__('Help');

    commands.addCommand(CommandIDs.launchClassic, {
      label: trans.__('Launch Classic Notebook'),
      execute: () => {
        window.open(PageConfig.getBaseUrl() + 'tree');
      }
    });

    if (palette) {
      palette.addItem({ command: CommandIDs.launchClassic, category });
    }
  }
};

/**
 * A plugin to add a command to open the Jupyter Forum.
 */
const jupyterForum: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/help-extension:jupyter-forum',
  autoStart: true,
  requires: [ITranslator],
  optional: [ICommandPalette],
  activate: (
    app: JupyterFrontEnd,
    translator: ITranslator,
    palette: ICommandPalette | null
  ): void => {
    const { commands } = app;
    const trans = translator.load('jupyterlab');
    const category = trans.__('Help');

    commands.addCommand(CommandIDs.jupyterForum, {
      label: trans.__('Jupyter Forum'),
      execute: () => {
        window.open('https://discourse.jupyter.org/c/jupyterlab');
      }
    });

    if (palette) {
      palette.addItem({ command: CommandIDs.jupyterForum, category });
    }
  }
};

/**
 * A plugin to add a list of resources to the help menu.
 */
const resources: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/help-extension:resources',
  autoStart: true,
  requires: [IMainMenu, ITranslator],
  optional: [ICommandPalette, ILayoutRestorer],
  activate: (
    app: JupyterFrontEnd,
    mainMenu: IMainMenu,
    translator: ITranslator,
    palette: ICommandPalette | null,
    restorer: ILayoutRestorer | null
  ): void => {
    const trans = translator.load('jupyterlab');
    let counter = 0;
    const category = trans.__('Help');
    const namespace = 'help-doc';
    const { commands, shell, serviceManager } = app;
    const tracker = new WidgetTracker<MainAreaWidget<IFrame>>({ namespace });
    const resources = [
      {
        text: trans.__('JupyterLab Reference'),
        url: 'https://jupyterlab.readthedocs.io/en/3.4.x/'
      },
      {
        text: trans.__('JupyterLab FAQ'),
        url:
          'https://jupyterlab.readthedocs.io/en/3.4.x/getting_started/faq.html'
      },
      {
        text: trans.__('Jupyter Reference'),
        url: 'https://jupyter.org/documentation'
      },
      {
        text: trans.__('Markdown Reference'),
        url: 'https://commonmark.org/help/'
      }
    ];

    resources.sort((a: any, b: any) => {
      return a.text.localeCompare(b.text);
    });

    // Handle state restoration.
    if (restorer) {
      void restorer.restore(tracker, {
        command: CommandIDs.open,
        args: widget => ({
          url: widget.content.url,
          text: widget.content.title.label
        }),
        name: widget => widget.content.url
      });
    }

    /**
     * Create a new HelpWidget widget.
     */
    function newHelpWidget(url: string, text: string): MainAreaWidget<IFrame> {
      // Allow scripts and forms so that things like
      // readthedocs can use their search functionality.
      // We *don't* allow same origin requests, which
      // can prevent some content from being loaded onto the
      // help pages.
      const content = new IFrame({
        sandbox: ['allow-scripts', 'allow-forms']
      });
      content.url = url;
      content.addClass(HELP_CLASS);
      content.title.label = text;
      content.id = `${namespace}-${++counter}`;
      const widget = new MainAreaWidget({ content });
      widget.addClass('jp-Help');
      return widget;
    }

    // Populate the Help menu.
    const helpMenu = mainMenu.helpMenu;

    const resourcesGroup = resources.map(args => ({
      args,
      command: CommandIDs.open
    }));
    helpMenu.addGroup(resourcesGroup, 10);

    // Generate a cache of the kernel help links.
    const kernelInfoCache = new Map<
      string,
      KernelMessage.IInfoReplyMsg['content']
    >();
    serviceManager.sessions.runningChanged.connect((m, sessions) => {
      // If a new session has been added, it is at the back
      // of the session list. If one has changed or stopped,
      // it does not hurt to check it.
      if (!sessions.length) {
        return;
      }
      const sessionModel = sessions[sessions.length - 1];
      if (
        !sessionModel.kernel ||
        kernelInfoCache.has(sessionModel.kernel.name)
      ) {
        return;
      }
      const session = serviceManager.sessions.connectTo({
        model: sessionModel,
        kernelConnectionOptions: { handleComms: false }
      });

      void session.kernel?.info.then(kernelInfo => {
        const name = session.kernel!.name;

        // Check the cache second time so that, if two callbacks get scheduled,
        // they don't try to add the same commands.
        if (kernelInfoCache.has(name)) {
          return;
        }
        // Set the Kernel Info cache.
        kernelInfoCache.set(name, kernelInfo);

        // Utility function to check if the current widget
        // has registered itself with the help menu.
        const usesKernel = () => {
          let result = false;
          const widget = app.shell.currentWidget;
          if (!widget) {
            return result;
          }
          helpMenu.kernelUsers.forEach(u => {
            if (u.tracker.has(widget) && u.getKernel(widget)?.name === name) {
              result = true;
            }
          });
          return result;
        };

        // Add the kernel banner to the Help Menu.
        const bannerCommand = `help-menu-${name}:banner`;
        const spec = serviceManager.kernelspecs?.specs?.kernelspecs[name];
        if (!spec) {
          return;
        }
        const kernelName = spec.display_name;
        let kernelIconUrl = spec.resources['logo-64x64'];
        commands.addCommand(bannerCommand, {
          label: trans.__('About the %1 Kernel', kernelName),
          isVisible: usesKernel,
          isEnabled: usesKernel,
          execute: () => {
            // Create the header of the about dialog
            const headerLogo = <img src={kernelIconUrl} />;
            const title = (
              <span className="jp-About-header">
                {headerLogo}
                <div className="jp-About-header-info">{kernelName}</div>
              </span>
            );
            const banner = <pre>{kernelInfo.banner}</pre>;
            const body = <div className="jp-About-body">{banner}</div>;

            return showDialog({
              title,
              body,
              buttons: [
                Dialog.createButton({
                  label: trans.__('Dismiss'),
                  className: 'jp-About-button jp-mod-reject jp-mod-styled'
                })
              ]
            });
          }
        });
        helpMenu.addGroup([{ command: bannerCommand }], 20);

        // Add the kernel info help_links to the Help menu.
        const kernelGroup: Menu.IItemOptions[] = [];
        (kernelInfo.help_links || []).forEach(link => {
          const commandId = `help-menu-${name}:${link.text}`;
          commands.addCommand(commandId, {
            label: link.text,
            isVisible: usesKernel,
            isEnabled: usesKernel,
            execute: () => {
              return commands.execute(CommandIDs.open, link);
            }
          });
          kernelGroup.push({ command: commandId });
        });
        helpMenu.addGroup(kernelGroup, 21);

        // Dispose of the session object since we no longer need it.
        session.dispose();
      });
    });

    commands.addCommand(CommandIDs.open, {
      label: args => args['text'] as string,
      execute: args => {
        const url = args['url'] as string;
        const text = args['text'] as string;
        const newBrowserTab = (args['newBrowserTab'] as boolean) || false;

        // If help resource will generate a mixed content error, load externally.
        if (
          newBrowserTab ||
          (LAB_IS_SECURE && URLExt.parse(url).protocol !== 'https:')
        ) {
          window.open(url);
          return;
        }

        const widget = newHelpWidget(url, text);
        void tracker.add(widget);
        shell.add(widget, 'main');
        return widget;
      }
    });

    if (palette) {
      resources.forEach(args => {
        palette.addItem({ args, command: CommandIDs.open, category });
      });
      palette.addItem({
        args: { reload: true },
        command: 'apputils:reset',
        category
      });
    }
  }
};

/**
 * A plugin to add a licenses reporting tools.
 */
const licenses: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/help-extension:licenses',
  autoStart: true,
  requires: [ITranslator],
  optional: [IMainMenu, ICommandPalette, ILayoutRestorer],
  activate: (
    app: JupyterFrontEnd,
    translator: ITranslator,
    menu: IMainMenu | null,
    palette: ICommandPalette | null,
    restorer: ILayoutRestorer | null
  ) => {
    // bail if no license API is available from the server
    if (!PageConfig.getOption('licensesUrl')) {
      return;
    }

    const { commands, shell } = app;
    const trans = translator.load('jupyterlab');

    // translation strings
    const category = trans.__('Help');
    const downloadAsText = trans.__('Download All Licenses as');
    const licensesText = trans.__('Licenses');
    const refreshLicenses = trans.__('Refresh Licenses');

    // an incrementer for license widget ids
    let counter = 0;

    const licensesUrl =
      URLExt.join(
        PageConfig.getBaseUrl(),
        PageConfig.getOption('licensesUrl')
      ) + '/';

    const licensesNamespace = 'help-licenses';
    const licensesTracker = new WidgetTracker<MainAreaWidget<Licenses>>({
      namespace: licensesNamespace
    });

    /**
     * Return a full license report format based on a format name
     */
    function formatOrDefault(format: string): Licenses.IReportFormat {
      return (
        Licenses.REPORT_FORMATS[format] ||
        Licenses.REPORT_FORMATS[Licenses.DEFAULT_FORMAT]
      );
    }

    /**
     * Create a MainAreaWidget for a license viewer
     */
    function createLicenseWidget(args: Licenses.ICreateArgs) {
      const licensesModel = new Licenses.Model({
        ...args,
        licensesUrl,
        trans,
        serverSettings: app.serviceManager.serverSettings
      });
      const content = new Licenses({ model: licensesModel });
      content.id = `${licensesNamespace}-${++counter}`;
      content.title.label = licensesText;
      content.title.icon = copyrightIcon;
      const main = new MainAreaWidget({
        content,
        reveal: licensesModel.licensesReady
      });

      main.toolbar.addItem(
        'refresh-licenses',
        new CommandToolbarButton({
          id: CommandIDs.refreshLicenses,
          args: { noLabel: 1 },
          commands
        })
      );

      main.toolbar.addItem('spacer', Toolbar.createSpacerItem());

      for (const format of Object.keys(Licenses.REPORT_FORMATS)) {
        const button = new CommandToolbarButton({
          id: CommandIDs.licenseReport,
          args: { format, noLabel: 1 },
          commands
        });
        main.toolbar.addItem(`download-${format}`, button);
      }

      return main;
    }

    // register license-related commands
    commands.addCommand(CommandIDs.licenses, {
      label: licensesText,
      execute: (args: any) => {
        const licenseMain = createLicenseWidget(args as Licenses.ICreateArgs);
        shell.add(licenseMain, 'main');

        // add to tracker so it can be restored, and update when choices change
        void licensesTracker.add(licenseMain);
        licenseMain.content.model.trackerDataChanged.connect(() => {
          void licensesTracker.save(licenseMain);
        });
        return licenseMain;
      }
    });

    commands.addCommand(CommandIDs.refreshLicenses, {
      label: args => (args.noLabel ? '' : refreshLicenses),
      caption: refreshLicenses,
      icon: refreshIcon,
      execute: async () => {
        return licensesTracker.currentWidget?.content.model.initLicenses();
      }
    });

    commands.addCommand(CommandIDs.licenseReport, {
      label: args => {
        if (args.noLabel) {
          return '';
        }
        const format = formatOrDefault(`${args.format}`);
        return `${downloadAsText} ${format.title}`;
      },
      caption: args => {
        const format = formatOrDefault(`${args.format}`);
        return `${downloadAsText} ${format.title}`;
      },
      icon: args => {
        const format = formatOrDefault(`${args.format}`);
        return format.icon;
      },
      execute: async args => {
        const format = formatOrDefault(`${args.format}`);
        return await licensesTracker.currentWidget?.content.model.download({
          format: format.id
        });
      }
    });

    // handle optional integrations
    if (palette) {
      palette.addItem({ command: CommandIDs.licenses, category });
    }

    if (menu) {
      const helpMenu = menu.helpMenu;
      helpMenu.addGroup([{ command: CommandIDs.licenses }], 0);
    }

    if (restorer) {
      void restorer.restore(licensesTracker, {
        command: CommandIDs.licenses,
        name: widget => 'licenses',
        args: widget => {
          const {
            currentBundleName,
            currentPackageIndex,
            packageFilter
          } = widget.content.model;

          const args: Licenses.ICreateArgs = {
            currentBundleName,
            currentPackageIndex,
            packageFilter
          };
          return args as ReadonlyJSONObject;
        }
      });
    }
  }
};

const plugins: JupyterFrontEndPlugin<any>[] = [
  about,
  launchClassic,
  jupyterForum,
  resources,
  licenses
];

export default plugins;
