import clsx from 'clsx';
import React from 'react';

import ContextMenuContainer from './ContextMenuContainer';
import ContextMenuItem from './ContextMenuItem';
import NestedContextMenu from './NestedContextMenu';

// Configs
import { THEME_DARK, TRACKS_INFO_BY_TYPE } from './configs';
import OPTIONS_INFO from './options-info';

// Styles
import classes from '../styles/ContextMenu.module.scss';
import { isObject } from './utils/type-guards';

/** @import * as t from './types' */
/** @import { TrackRenderer } from './TrackRenderer' */

/**
 * @typedef ContextMenuHandler
 * @property {string} label
 * @property {(evt: unknown, onTrackOptionsChanged: (options: Record<string, unknown>) => void) => void} onClick
 */

/**
 * @param {unknown} x
 * @returns {x is { contextMenuItems: (trackLeft: number, trackRight: number) => Array<ContextMenuHandler> }}}
 */
function hasContextMenuItems(x) {
  return (
    isObject(x) &&
    'contextMenuItems' in x &&
    typeof x.contextMenuItems === 'function'
  );
}

/**
 *  We're going to get the track object to see if it has a
 *  context menu handler that will give use context menu items
 *  to display
 *
 * @param {t.TrackConfig} track The config for the track we're getting context menu
 *  items for
 * @param {TrackRenderer} trackRenderer The track renderer for the view
 *  containing this track. We'll use it to get the track's object
 * @param {{canvasLeft: number, canvasTop: number}} position The position of the track.
 *  Where the track starts relative to the canvas. This is important because all
 *  coordinates within a track are relative to left and top coordinates.
 * @returns {Array<ContextMenuHandler>}
 */
function findTrackContextMenuItems(track, trackRenderer, position) {
  let trackObj = trackRenderer.getTrackObject(track.uid);

  // The track may be a LeftTrackModifier track
  trackObj = trackObj?.originalTrack || trackObj;

  // See if the track will provide us with context menu items
  if (hasContextMenuItems(trackObj)) {
    let trackLeft = position.canvasLeft - trackObj.position[0];
    let trackTop = position.canvasTop - trackObj.position[1];

    if (trackObj.flipText) {
      // This is a left track modifier track so we need to swap the
      // left and right values
      const temp = trackLeft;
      trackLeft = trackTop;
      trackTop = temp;
    }

    const items = trackObj.contextMenuItems(trackLeft, trackTop);

    return items || [];
  }

  // The track doesn't have a contextMenuItems function so we it's
  // obviously not providing any items.
  return [];
}

/**
 * @typedef MenuItem
 * @property {string} name
 * @property {string} [value]
 * @property {Record<string, unknown>} [children]
 * @property {() => void} [handler]
 */

export default class SeriesListMenu extends ContextMenuContainer {
  /**
   * @param {unknown} position
   * @param {unknown} bbox
   * @param {{ type: string, options: Record<string, unknown>, uid: string, }} track
   */
  getConfigureSeriesMenu(position, bbox, track) {
    /** @type {Record<string, MenuItem>} */
    const menuItems = {};

    // plugin tracks can offer their own options
    // if they clash with the default higlass options
    // they will override them
    const pluginOptionsInfo =
      window.higlassTracksByType?.[track.type] &&
      window.higlassTracksByType[track.type].config &&
      window.higlassTracksByType[track.type].config.optionsInfo;

    if (pluginOptionsInfo) {
      for (const key of Object.keys(pluginOptionsInfo)) {
        // @ts-expect-error - extends OPTIONS_INFO with new data
        OPTIONS_INFO[key] = pluginOptionsInfo[key];
      }
    }

    const trackinfo = TRACKS_INFO_BY_TYPE[track.type];

    if (!trackinfo?.availableOptions) {
      return null;
    }

    for (const optionType of trackinfo.availableOptions) {
      if (optionType in OPTIONS_INFO) {
        const optionInfo =
          OPTIONS_INFO[/** @type {keyof typeof OPTIONS_INFO} */ (optionType)];
        menuItems[optionType] = { name: optionInfo.name };

        // can we dynamically generate some options?
        // should be used if the options depend on tileset info or other current state
        if ('generateOptions' in optionInfo) {
          const generatedOptions = optionInfo.generateOptions(track);

          if (!menuItems[optionType].children) {
            menuItems[optionType].children = {};
          }

          for (const generatedOption of generatedOptions) {
            const optionSelectorSettings = {
              name: generatedOption.name,
              value: generatedOption.value,
              handler: () => {
                track.options[optionType] = generatedOption.value;
                this.props.onTrackOptionsChanged(track.uid, track.options);
                this.props.closeMenu();
              },
            };

            menuItems[optionType].children[generatedOption.value] =
              optionSelectorSettings;
          }
        }

        if ('inlineOptions' in optionInfo) {
          // we can simply select this option from the menu
          for (const inlineOptionKey in optionInfo.inlineOptions) {
            /** @type {Record<string, Record<string, { name: string, value: unknown }>>} */
            const inlineOption = optionInfo.inlineOptions[inlineOptionKey];

            // check if there's already available options (e.g.
            // "Top right") for this option type (e.g. "Label
            // position")
            if (!menuItems[optionType].children) {
              menuItems[optionType].children = {};
            }

            const optionSelectorSettings = {
              name: inlineOption.name,
              value: inlineOption.value,
              // missing handler to be filled in below
              handler: () => {},
            };

            // is there a custom component available for picking this
            // option type value (e.g. 'custom' color scale)
            if (
              inlineOption.componentPickers &&
              track.type in inlineOption.componentPickers
            ) {
              optionSelectorSettings.handler = () => {
                this.props.onConfigureTrack(
                  track,
                  inlineOption.componentPickers[track.type],
                );
                this.props.closeMenu();
              };
            } else {
              // the menu option defines a potential value for this option
              // type (e.g. "top right")
              optionSelectorSettings.handler = () => {
                track.options[optionType] = inlineOption.value;
                this.props.onTrackOptionsChanged(track.uid, track.options);
                this.props.closeMenu();
              };
            }

            menuItems[optionType].children[inlineOptionKey] =
              optionSelectorSettings;
          }
          // @ts-expect-error - mutated from a plugin
        } else if (track.type in optionInfo.componentPickers) {
          // there's an option picker registered
          menuItems[optionType].handler = () => {
            this.props.onConfigureTrack(
              track,
              // @ts-expect-error - mutated from a plugin
              optionInfo.componentPickers[track.type],
            );
            this.props.closeMenu();
          };
        }
      }
    }

    return (
      <NestedContextMenu
        key="config-series-menu"
        closeMenu={this.props.closeMenu}
        menuItems={menuItems}
        orientation={this.state.orientation}
        parentBbox={bbox}
        position={position}
        theme={this.props.theme}
      />
    );
  }

  /**
   * Return a list of track types that can be used
   * with the data for this track
   *
   * @param {Object} position The position where to draw ths menu (e.g. {left: 42, top: 88})
   *
   * @param {Object} bbox
   *  The bounding box of the parent menu, used to determine whether
   *  to draw the child menu on the left or the right
   *
   * @param {{ uid: string, type: string, datatype: string }} track The track definition for this series (as in the viewconf)
   */
  getTrackTypeItems(position, bbox, track) {
    // if we've loaded external track types, list them here
    if (window.higlassTracksByType) {
      // Extend `TRACKS_INFO_BY_TYPE` with the configs of plugin tracks.
      for (const pluginTrackType of Object.keys(window.higlassTracksByType)) {
        TRACKS_INFO_BY_TYPE[pluginTrackType] =
          window.higlassTracksByType[pluginTrackType].config;
      }
    }

    let { datatype } = track;

    let orientation = null;
    // make sure that this is a valid track type before trying to
    // look up other tracks that can substitute for it
    if (track.type in TRACKS_INFO_BY_TYPE) {
      if (!datatype) {
        datatype = TRACKS_INFO_BY_TYPE[track.type].datatype[0];
      }
      ({ orientation } = TRACKS_INFO_BY_TYPE[track.type]);
    }

    // see which other tracks can display a similar datatype
    const availableTrackTypes = Object.values(TRACKS_INFO_BY_TYPE)
      .filter((x) => x.datatype)
      .filter((x) => x.orientation)
      .filter((x) => x.datatype.includes(datatype))
      .filter((x) => x.orientation === orientation)
      .map((x) => x.type);

    /** @type {Record<string, MenuItem>} */
    const menuItems = {};
    for (let i = 0; i < availableTrackTypes.length; i++) {
      menuItems[availableTrackTypes[i]] = {
        value: availableTrackTypes[i],
        name: availableTrackTypes[i],
        handler: () => {
          this.props.onChangeTrackType(track.uid, availableTrackTypes[i]);
        },
      };
    }

    return (
      <NestedContextMenu
        key="track-type-items"
        closeMenu={this.props.closeMenu}
        menuItems={menuItems}
        orientation={this.state.orientation}
        parentBbox={bbox}
        position={position}
        theme={this.props.theme}
      />
    );
  }

  getSubmenu() {
    if (this.state.submenuShown) {
      // the bounding box of the element which initiated the subMenu
      // necessary so that we can position the submenu next to the initiating
      // element
      /** @type {DOMRect} */
      // @ts-expect-error - parent class ContextMenuContainer requires typing
      const bbox = this.state.submenuSourceBbox;
      const position =
        this.state.orientation === 'left'
          ? {
              left: this.state.left,
              top: bbox.top,
            }
          : {
              left: this.state.left + bbox.width + 7,
              top: bbox.top,
            };

      // When a submenu is requested, the onMouseEnter handler of the
      // item that requested it provides a structure containing the option
      // picked as well as some data associated with it
      // e.g. {"option": "configure-series", data: track}
      /** @type {{option:  string, value: { uid: string, type: string, datatype: string, options: Record<string, unknown> }}} */
      const subMenuData = this.state.submenuShown;
      const track = subMenuData.value;

      if (subMenuData.option === 'track-type') {
        return this.getTrackTypeItems(position, bbox, track);
      }
      return this.getConfigureSeriesMenu(position, bbox, track);
    }

    return <div />;
  }

  getDivideByMenuItem() {
    if (this.props.series.data && this.props.series.data.type === 'divided') {
      const newData = {
        tilesetUid: this.props.series.data.children[0].tilesetUid,
        server: this.props.series.data.children[0].server,
      };

      // this track is already being divided
      return (
        <ContextMenuItem
          className={classes['context-menu-item']}
          onClick={() =>
            this.props.onChangeTrackData(this.props.series.uid, newData)
          }
          onMouseEnter={(e) => this.handleOtherMouseEnter()}
        >
          <span className={classes['context-menu-span']}>Remove divisor</span>
        </ContextMenuItem>
      );
    }

    return (
      <ContextMenuItem
        className={classes['context-menu-item']}
        onClick={() => this.props.onAddDivisor(this.props.series)}
        onMouseEnter={(e) => this.handleOtherMouseEnter()}
      >
        <span className={classes['context-menu-span']}>Divide by</span>
      </ContextMenuItem>
    );
  }

  componentWillUnmount() {}

  render() {
    let exportDataMenuItem = null;

    const trackContextMenuItems = findTrackContextMenuItems(
      this.props.track,
      this.props.trackRenderer,
      this.props.position,
    );

    if (
      TRACKS_INFO_BY_TYPE[this.props.series.type] &&
      TRACKS_INFO_BY_TYPE[this.props.series.type].exportable
    ) {
      exportDataMenuItem = (
        <ContextMenuItem
          className={classes['context-menu-item']}
          onClick={() =>
            this.props.onExportData(
              this.props.hostTrack.uid,
              this.props.track.uid,
            )
          }
          onMouseEnter={(e) => this.handleOtherMouseEnter()}
        >
          <span className={classes['context-menu-span']}>Export Data</span>
        </ContextMenuItem>
      );
    }

    // if a track can't be replaced, this.props.onAddSeries
    // will be null so we don't need to display the menu item
    const replaceSeriesItem = this.props.onAddSeries ? (
      <ContextMenuItem
        className={classes['context-menu-item']}
        onClick={() => {
          this.props.onCloseTrack(this.props.series.uid);
          this.props.onAddSeries(this.props.hostTrack.uid);
        }}
        onMouseEnter={(e) => this.handleOtherMouseEnter()}
      >
        <span className={classes['context-menu-span']}>Replace Series</span>
      </ContextMenuItem>
    ) : null;

    return (
      <div
        ref={(c) => {
          this.div = c;
        }}
        className={clsx(classes['context-menu'], {
          [classes['context-menu-dark']]: this.props.theme === THEME_DARK,
        })}
        data-menu-type="SeriesListMenu"
        onMouseLeave={this.props.handleMouseLeave}
        style={{
          left: this.state.left,
          top: this.state.top,
        }}
      >
        {trackContextMenuItems.map((x) => (
          <ContextMenuItem
            key={x.label}
            onClick={(evt) => {
              x.onClick(evt, (newOptions) => {
                // We're going to pass in a handler to that the track
                // can use to change its options
                this.props.onTrackOptionsChanged(this.props.track.uid, {
                  ...this.props.track.options,
                  ...newOptions,
                });
              });
              this.props.closeMenu();
            }}
            onMouseEnter={(e) => this.handleOtherMouseEnter()}
            className={classes['context-menu-item']}
          >
            <span className={classes['context-menu-span']}>{x.label}</span>
          </ContextMenuItem>
        ))}
        {trackContextMenuItems.length > 0 && (
          <hr className={classes['context-menu-hr']} />
        )}
        <ContextMenuItem
          onClick={() => {}}
          onMouseEnter={(e) =>
            this.handleItemMouseEnter(e, {
              option: 'configure-series',
              value: this.props.track,
            })
          }
          onMouseLeave={(e) => this.handleMouseLeave()}
        >
          Configure Series
          <svg className={classes['play-icon']}>
            <title>Play</title>
            <use xlinkHref="#play" />
          </svg>
        </ContextMenuItem>

        <ContextMenuItem
          className={classes['context-menu-item']}
          onClick={() => {}}
          onMouseEnter={(e) =>
            this.handleItemMouseEnter(e, {
              option: 'track-type',
              value: this.props.track,
            })
          }
          onMouseLeave={(e) => this.handleMouseLeave()}
        >
          <span className={classes['context-menu-span']}>
            Track Type
            <svg className={classes['play-icon']}>
              <title>Play</title>
              <use xlinkHref="#play" />
            </svg>
          </span>
        </ContextMenuItem>

        <ContextMenuItem
          className={classes['context-menu-item']}
          onClick={() => {
            this.props.apiPublish('datasetInfo', this.props.track);
            this.props.closeMenu();
          }}
          onMouseEnter={(e) => this.handleOtherMouseEnter()}
        >
          <span className={classes['context-menu-span']}>Dataset Info</span>
        </ContextMenuItem>

        {exportDataMenuItem}

        {this.getDivideByMenuItem()}

        <ContextMenuItem
          className={classes['context-menu-item']}
          onClick={this.props.onCloseTrack}
          onMouseEnter={(e) => this.handleOtherMouseEnter()}
        >
          <span className={classes['context-menu-span']}>Close Series</span>
        </ContextMenuItem>

        {replaceSeriesItem}

        {/*
          this.props.series.type === 'heatmap' ?
          <ContextMenuItem
            onClick={() => {
              this.props.onDivideSeries(this.props.series.uid);
              // this.props.onCloseTrack(this.props.series.uid);
              // this.props.onAddSeries(this.props.hostTrack.uid);
            }}
            onMouseEnter={e => this.handleOtherMouseEnter(e)}
            className={classes["context-menu-item"]}
          >
            <span className={classes["context-menu-span"]}>
              {'Divide Series By'}
            </span>
          </ContextMenuItem>
          : null
        */}

        {this.getSubmenu()}
      </div>
    );
  }
}
