let {
  Link,
  TextField,
  CommandBar,
  IContextualMenuProps,
  IContextualMenuItem,
  DirectionalHint,
  ContextualMenu,
  CheckboxVisibility,
  ColumnActionsMode,
  ConstrainMode,
  DetailsList,
  DetailsListLayoutMode,
  IColumn,
  IGroup,
  Selection,
  SelectionMode,
  buildColumns,
  IDetailsColumnProps,
  memoizeFunction,
  getTheme,
  mergeStyles,
  mergeStyleSets,
  IGroup,
  Fabric
} = window.Fabric;

const theme = getTheme();
const classNames = mergeStyleSets({
  headerDivider: {
    display: 'inline-block',
    height: '100%'
  },
  headerDividerBar: {
    display: 'none',
    background: theme.palette.themePrimary,
    position: 'absolute',
    top: 0,
    bottom: 0,
    width: '1px',
    zIndex: 5
  },
  linkField: {
    display: 'block',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    maxWidth: '100%'
  }
});
const rootClass = mergeStyles({
  selectors: {
    [`.${classNames.headerDivider}:hover + .${classNames.headerDividerBar}`]: {
      display: 'inline'
    }
  }
});

const DEFAULT_ITEM_LIMIT = 5;
const PAGING_SIZE = 10;
const PAGING_DELAY = 2000;
const ITEMS_COUNT = 5000;

interface IDetailsListAdvancedExampleState {
  canResizeColumns?: boolean;
  checkboxVisibility?: CheckboxVisibility;
  columns: IColumn[];
  constrainMode?: ConstrainMode;
  contextualMenuProps?: IContextualMenuProps;
  groupItemLimit?: number;
  groups?: IGroup[];
  isHeaderVisible?: boolean;
  isLazyLoaded?: boolean;
  isSortedDescending?: boolean;
  items: IExampleItem[];
  layoutMode?: DetailsListLayoutMode;
  selectionMode?: SelectionMode;
  sortedColumnKey?: string;
  selectionCount: number;
}

class DetailsListAdvancedExample extends React.Component<{}, IDetailsListAdvancedExampleState> {
  private _isFetchingItems: boolean;
  private _selection: Selection;
  private _allItems: IExampleItem[];

  constructor(props: {}) {
    super(props);

    this._getCommandItems = memoizeFunction(this._getCommandItems);

    this._allItems = createListItems(ITEMS_COUNT);
    this._selection = new Selection({
      onSelectionChanged: this._onItemsSelectionChanged
    });
    this._selection.setItems(this._allItems, false);

    this.state = {
      items: this._allItems,
      selectionCount: 0,
      groups: undefined,
      groupItemLimit: DEFAULT_ITEM_LIMIT,
      layoutMode: DetailsListLayoutMode.justified,
      constrainMode: ConstrainMode.horizontalConstrained,
      selectionMode: SelectionMode.multiple,
      canResizeColumns: true,
      checkboxVisibility: CheckboxVisibility.onHover,
      columns: this._buildColumns(
        this._allItems,
        true,
        this._onColumnClick,
        '',
        undefined,
        undefined,
        this._onColumnContextMenu
      ),
      contextualMenuProps: undefined,
      sortedColumnKey: 'name',
      isSortedDescending: false,
      isLazyLoaded: false,
      isHeaderVisible: true
    };
  }

  public render(): JSX.Element {
    const {
      canResizeColumns,
      checkboxVisibility,
      columns,
      constrainMode,
      contextualMenuProps,
      groupItemLimit,
      groups,
      isHeaderVisible,
      isLazyLoaded,
      items,
      layoutMode,
      selectionMode
    } = this.state;

    const isGrouped = groups && groups.length > 0;
    const groupProps = {
      getGroupItemLimit: (group: IGroup) => {
        if (group) {
          return group.isShowingAll ? group.count : Math.min(group.count, groupItemLimit as number);
        } else {
          return items.length;
        }
      },
      footerProps: {
        showAllLinkText: 'Show all'
      }
    };

    return (
      <div className={rootClass}>
        <CommandBar
          styles={{ root: { marginBottom: '40px' } }}
          items={this._getCommandItems(
            canResizeColumns,
            checkboxVisibility,
            constrainMode,
            isHeaderVisible,
            isLazyLoaded,
            layoutMode,
            selectionMode
          )}
          farItems={[{ key: 'count', text: `${this.state.selectionCount} selected` }]}
        />

        {isGrouped ? <TextField label="Group item limit" onChange={this._onItemLimitChanged} /> : null}

        <DetailsList
          setKey="items"
          items={items}
          selection={this._selection}
          groups={groups}
          columns={columns}
          checkboxVisibility={checkboxVisibility}
          layoutMode={layoutMode}
          isHeaderVisible={isHeaderVisible}
          selectionMode={selectionMode}
          constrainMode={constrainMode}
          groupProps={groupProps}
          enterModalSelectionOnTouch={true}
          onItemInvoked={this._onItemInvoked}
          onItemContextMenu={this._onItemContextMenu}
          selectionZoneProps={{
            selection: this._selection,
            disableAutoSelectOnInputElements: true,
            selectionMode: selectionMode
          }}
          ariaLabelForListHeader="Column headers. Click to sort."
          ariaLabelForSelectAllCheckbox="Toggle selection for all items"
          ariaLabelForSelectionColumn="Toggle selection"
          onRenderMissingItem={this._onRenderMissingItem}
        />

        {contextualMenuProps && <ContextualMenu {...contextualMenuProps} />}
      </div>
    );
  }

  private _onRenderDivider = (
    columnProps: IDetailsColumnProps,
    defaultRenderer: (props?: IDetailsColumnProps) => JSX.Element | null
  ): JSX.Element => {
    const { columnIndex } = columnProps;
    return (
      <React.Fragment key={`divider-wrapper-${columnIndex}`}>
        <span className={classNames.headerDivider}>{defaultRenderer(columnProps)}</span>
        <span className={classNames.headerDividerBar} />
      </React.Fragment>
    );
  };

  private _onDataMiss(index: number): void {
    index = Math.floor(index / PAGING_SIZE) * PAGING_SIZE;

    if (!this._isFetchingItems) {
      this._isFetchingItems = true;

      setTimeout(() => {
        this._isFetchingItems = false;
        const itemsCopy = [...this.state.items];

        itemsCopy.splice(index, PAGING_SIZE).concat(this._allItems.slice(index, index + PAGING_SIZE));

        this.setState({
          items: itemsCopy
        });
      }, PAGING_DELAY);
    }
  }

  private _onRenderMissingItem = (index: number): null => {
    this._onDataMiss(index);
    return null;
  };

  private _onToggleLazyLoad = (): void => {
    let { isLazyLoaded } = this.state;

    isLazyLoaded = !isLazyLoaded;

    this.setState({
      isLazyLoaded: isLazyLoaded,
      items: isLazyLoaded
        ? this._allItems.slice(0, PAGING_SIZE).concat(new Array(ITEMS_COUNT - PAGING_SIZE))
        : this._allItems
    });
  };

  private _onToggleHeaderVisible = (): void => {
    this.setState({ isHeaderVisible: !this.state.isHeaderVisible });
  };

  private _onToggleResizing = (): void => {
    const { items, sortedColumnKey, isSortedDescending } = this.state;
    let { canResizeColumns } = this.state;

    canResizeColumns = !canResizeColumns;

    this.setState({
      canResizeColumns: canResizeColumns,
      columns: this._buildColumns(items, canResizeColumns, this._onColumnClick, sortedColumnKey, isSortedDescending)
    });
  };

  private _onCheckboxVisibilityChanged = (ev: React.MouseEvent<HTMLElement>, menuItem: IContextualMenuItem): void => {
    this.setState({ checkboxVisibility: menuItem.data });
  };

  private _onLayoutChanged = (ev: React.MouseEvent<HTMLElement>, menuItem: IContextualMenuItem): void => {
    this.setState({ layoutMode: menuItem.data });
  };

  private _onConstrainModeChanged = (ev: React.MouseEvent<HTMLElement>, menuItem: IContextualMenuItem): void => {
    this.setState({ constrainMode: menuItem.data });
  };

  private _onSelectionChanged = (ev: React.MouseEvent<HTMLElement>, menuItem: IContextualMenuItem): void => {
    this.setState({ selectionMode: menuItem.data });
  };

  private _onItemLimitChanged = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, value: string): void => {
    let newValue = parseInt(value, 10);
    if (isNaN(newValue)) {
      newValue = DEFAULT_ITEM_LIMIT;
    }
    this.setState({ groupItemLimit: newValue });
  };

  private _getCommandItems = (
    canResizeColumns?: boolean,
    checkboxVisibility?: CheckboxVisibility,
    constrainMode?: ConstrainMode,
    isHeaderVisible?: boolean,
    isLazyLoaded?: boolean,
    layoutMode?: DetailsListLayoutMode,
    selectionMode?: SelectionMode
  ): IContextualMenuItem[] => {
    return [
      {
        key: 'addRow',
        text: 'Insert row',
        iconProps: { iconName: 'Add' },
        onClick: this._onAddRow
      },
      {
        key: 'deleteRow',
        text: 'Delete row',
        iconProps: { iconName: 'Delete' },
        onClick: this._onDeleteRow
      },
      {
        key: 'configure',
        text: 'Configure',
        iconProps: { iconName: 'Settings' },
        subMenuProps: {
          items: [
            {
              key: 'resizing',
              text: 'Allow column resizing',
              canCheck: true,
              checked: canResizeColumns,
              onClick: this._onToggleResizing
            },
            {
              key: 'headerVisible',
              text: 'Is header visible',
              canCheck: true,
              checked: isHeaderVisible,
              onClick: this._onToggleHeaderVisible
            },
            {
              key: 'lazyload',
              text: 'Simulate async loading',
              canCheck: true,
              checked: isLazyLoaded,
              onClick: this._onToggleLazyLoad
            },
            {
              key: 'dash',
              text: '-'
            },
            {
              key: 'checkboxVisibility',
              text: 'Checkbox visibility',
              subMenuProps: {
                items: [
                  {
                    key: 'checkboxVisibility.always',
                    text: 'Always',
                    canCheck: true,
                    isChecked: checkboxVisibility === CheckboxVisibility.always,
                    onClick: this._onCheckboxVisibilityChanged,
                    data: CheckboxVisibility.always
                  },
                  {
                    key: 'checkboxVisibility.onHover',
                    text: 'On hover',
                    canCheck: true,
                    isChecked: checkboxVisibility === CheckboxVisibility.onHover,
                    onClick: this._onCheckboxVisibilityChanged,
                    data: CheckboxVisibility.onHover
                  },
                  {
                    key: 'checkboxVisibility.hidden',
                    text: 'Hidden',
                    canCheck: true,
                    isChecked: checkboxVisibility === CheckboxVisibility.hidden,
                    onClick: this._onCheckboxVisibilityChanged,
                    data: CheckboxVisibility.hidden
                  }
                ]
              }
            },
            {
              key: 'layoutMode',
              text: 'Layout mode',
              subMenuProps: {
                items: [
                  {
                    key: DetailsListLayoutMode[DetailsListLayoutMode.fixedColumns],
                    text: 'Fixed columns',
                    canCheck: true,
                    checked: layoutMode === DetailsListLayoutMode.fixedColumns,
                    onClick: this._onLayoutChanged,
                    data: DetailsListLayoutMode.fixedColumns
                  },
                  {
                    key: DetailsListLayoutMode[DetailsListLayoutMode.justified],
                    text: 'Justified columns',
                    canCheck: true,
                    checked: layoutMode === DetailsListLayoutMode.justified,
                    onClick: this._onLayoutChanged,
                    data: DetailsListLayoutMode.justified
                  }
                ]
              }
            },
            {
              key: 'selectionMode',
              text: 'Selection mode',
              subMenuProps: {
                items: [
                  {
                    key: SelectionMode[SelectionMode.none],
                    text: 'None',
                    canCheck: true,
                    checked: selectionMode === SelectionMode.none,
                    onClick: this._onSelectionChanged,
                    data: SelectionMode.none
                  },
                  {
                    key: SelectionMode[SelectionMode.single],
                    text: 'Single select',
                    canCheck: true,
                    checked: selectionMode === SelectionMode.single,
                    onClick: this._onSelectionChanged,
                    data: SelectionMode.single
                  },
                  {
                    key: SelectionMode[SelectionMode.multiple],
                    text: 'Multi select',
                    canCheck: true,
                    checked: selectionMode === SelectionMode.multiple,
                    onClick: this._onSelectionChanged,
                    data: SelectionMode.multiple
                  }
                ]
              }
            },
            {
              key: 'constrainMode',
              text: 'Constrain mode',
              subMenuProps: {
                items: [
                  {
                    key: ConstrainMode[ConstrainMode.unconstrained],
                    text: 'Unconstrained',
                    canCheck: true,
                    checked: constrainMode === ConstrainMode.unconstrained,
                    onClick: this._onConstrainModeChanged,
                    data: ConstrainMode.unconstrained
                  },
                  {
                    key: ConstrainMode[ConstrainMode.horizontalConstrained],
                    text: 'Horizontal constrained',
                    canCheck: true,
                    checked: constrainMode === ConstrainMode.horizontalConstrained,
                    onClick: this._onConstrainModeChanged,
                    data: ConstrainMode.horizontalConstrained
                  }
                ]
              }
            }
          ]
        }
      }
    ];
  };

  private _getContextualMenuProps(ev: React.MouseEvent<HTMLElement>, column: IColumn): IContextualMenuProps {
    const items = [
      {
        key: 'aToZ',
        name: 'A to Z',
        iconProps: { iconName: 'SortUp' },
        canCheck: true,
        checked: column.isSorted && !column.isSortedDescending,
        onClick: () => this._onSortColumn(column.key, false)
      },
      {
        key: 'zToA',
        name: 'Z to A',
        iconProps: { iconName: 'SortDown' },
        canCheck: true,
        checked: column.isSorted && column.isSortedDescending,
        onClick: () => this._onSortColumn(column.key, true)
      }
    ];
    if (isGroupable(column.key)) {
      items.push({
        key: 'groupBy',
        name: 'Group by ' + column.name,
        iconProps: { iconName: 'GroupedDescending' },
        canCheck: true,
        checked: column.isGrouped,
        onClick: () => this._onGroupByColumn(column)
      });
    }
    return {
      items: items,
      target: ev.currentTarget as HTMLElement,
      directionalHint: DirectionalHint.bottomLeftEdge,
      gapSpace: 10,
      isBeakVisible: true,
      onDismiss: this._onContextualMenuDismissed
    };
  }

  private _onItemInvoked = (item: IExampleItem, index: number): void => {
    console.log('Item invoked', item, index);
  };

  private _onItemContextMenu = (item: IExampleItem, index: number, ev: MouseEvent): boolean => {
    const contextualMenuProps: IContextualMenuProps = {
      target: ev.target as HTMLElement,
      items: [
        {
          key: 'text',
          name: `${this._selection.getSelectedCount()} selected`
        }
      ],
      onDismiss: () => {
        this.setState({
          contextualMenuProps: undefined
        });
      }
    };

    if (index > -1) {
      this.setState({
        contextualMenuProps: contextualMenuProps
      });
    }

    return false;
  };

  private _onColumnClick = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
    if (column.columnActionsMode !== ColumnActionsMode.disabled) {
      this.setState({
        contextualMenuProps: this._getContextualMenuProps(ev, column)
      });
    }
  };

  private _onColumnContextMenu = (column: IColumn, ev: React.MouseEvent<HTMLElement>): void => {
    if (column.columnActionsMode !== ColumnActionsMode.disabled) {
      this.setState({
        contextualMenuProps: this._getContextualMenuProps(ev, column)
      });
    }
  };

  private _onContextualMenuDismissed = (): void => {
    this.setState({
      contextualMenuProps: undefined
    });
  };

  private _onSortColumn = (columnKey: string, isSortedDescending: boolean): void => {
    const sortedItems = _copyAndSort(this._allItems, columnKey, isSortedDescending);

    this.setState({
      items: sortedItems,
      groups: undefined,
      columns: this._buildColumns(
        sortedItems,
        true,
        this._onColumnClick,
        columnKey,
        isSortedDescending,
        undefined,
        this._onColumnContextMenu
      ),
      isSortedDescending: isSortedDescending,
      sortedColumnKey: columnKey
    });
  };

  private _onGroupByColumn = (column: IColumn): void => {
    const { key, isGrouped } = column;
    const { sortedColumnKey, isSortedDescending, groups, items, columns } = this.state;

    if (isGrouped) {
      // ungroup
      this._onSortColumn(sortedColumnKey!, !!isSortedDescending);
    } else {
      let groupedItems = [];
      let newGroups: IGroup[];
      if (groups) {
        newGroups = [...groups];
        groupedItems = this._groupByKey(newGroups, items, key as keyof IExampleItem);
      } else {
        groupedItems = _copyAndSort(items, key);
        newGroups = this._getGroups(groupedItems, key as keyof IExampleItem);
      }

      for (const c of columns) {
        if (c.key === key) {
          c.isGrouped = true;
          break;
        }
      }
      this.setState({
        items: groupedItems,
        columns: [...columns],
        groups: newGroups
      });
    }
  };

  private _groupByKey(groups: IGroup[], items: IExampleItem[], key: keyof IExampleItem): IExampleItem[] {
    let groupedItems: IExampleItem[] = [];
    if (groups) {
      for (const group of groups) {
        if (group.children && group.children.length > 0) {
          const childGroupedItems = this._groupByKey(group.children, items, key);
          groupedItems = groupedItems.concat(childGroupedItems);
        } else {
          const itemsInGroup = items.slice(group.startIndex, group.startIndex + group.count);
          const nextLevelGroupedItems = _copyAndSort(itemsInGroup, key);
          groupedItems = groupedItems.concat(nextLevelGroupedItems);
          group.children = this._getGroups(nextLevelGroupedItems, key, group);
        }
      }
    }
    return groupedItems;
  }

  private _getGroups(groupedItems: IExampleItem[], key: keyof IExampleItem, parentGroup?: IGroup): IGroup[] {
    const separator = '-';
    const groups = groupedItems.reduce(
      (current: IGroup[], item: IExampleItem, index: number) => {
        const currentGroup = current[current.length - 1];
        const itemColumnValue = item[key];

        if (!currentGroup || this._getLeafGroupKey(currentGroup.key, separator) !== itemColumnValue) {
          current.push({
            key: (parentGroup ? parentGroup.key + separator : '') + itemColumnValue,
            name: key + ': ' + itemColumnValue,
            startIndex: parentGroup ? parentGroup.startIndex + index : index,
            count: 1,
            level: parentGroup ? parentGroup.level! + 1 : 0
          });
        } else {
          currentGroup.count++;
        }
        return current;
      },
      [] as IGroup[]
    );

    return groups;
  }

  private _getLeafGroupKey(key: string, separator: string): string {
    let leafKey = key;
    if (key.indexOf(separator) !== -1) {
      const arrKeys = key.split(separator);
      leafKey = arrKeys[arrKeys.length - 1];
    }
    return leafKey;
  }

  private _onAddRow = (): void => {
    this.setState({
      items: createListItems(1).concat(this.state.items)
    });
  };

  private _onDeleteRow = (): void => {
    if (this._selection.getSelectedCount() > 0) {
      this.setState((previousState: IDetailsListAdvancedExampleState) => {
        return {
          items: previousState.items.filter((item, index) => !this._selection.isIndexSelected(index))
        };
      });
    } else {
      this.setState({
        items: this.state.items.slice(1)
      });
    }
  };

  private _onItemsSelectionChanged = () => {
    this.setState({
      selectionCount: this._selection.getSelectedCount()
    });
  };

  private _buildColumns(
    items: IExampleItem[],
    canResizeColumns?: boolean,
    onColumnClick?: (ev: React.MouseEvent<HTMLElement>, column: IColumn) => any,
    sortedColumnKey?: string,
    isSortedDescending?: boolean,
    groupedColumnKey?: string,
    onColumnContextMenu?: (column: IColumn, ev: React.MouseEvent<HTMLElement>) => any
  ) {
    const columns = buildColumns(
      items,
      canResizeColumns,
      onColumnClick,
      sortedColumnKey,
      isSortedDescending,
      groupedColumnKey
    );

    columns.forEach(column => {
      column.onRenderDivider = this._onRenderDivider;
      column.onColumnContextMenu = onColumnContextMenu;
      column.ariaLabel = `Operations for ${column.name}`;
      if (column.key === 'thumbnail') {
        column.iconName = 'Picture';
        column.isIconOnly = true;
      } else if (column.key === 'description') {
        column.isMultiline = true;
        column.minWidth = 200;
      } else if (column.key === 'name') {
        column.onRender = (item: IExampleItem) => <Link data-selection-invoke={true}>{item.name}</Link>;
      } else if (column.key === 'key') {
        column.columnActionsMode = ColumnActionsMode.disabled;
        column.onRender = (item: IExampleItem) => (
          <Link className={classNames.linkField} href="https://microsoft.com" target="_blank" rel="noopener">
            {item.key}
          </Link>
        );
        column.minWidth = 90;
        column.maxWidth = 90;
      }
    });

    return columns;
  }
}

function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
  const key = columnKey as keyof T;
  return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
}

const LOREM_IPSUM = (
  'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut ' +
  'labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' +
  'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore ' +
  'eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt ' +
  'mollit anim id est laborum'
).split(' ');

const DATA = {
  color: ['red', 'blue', 'green', 'yellow'],
  shape: ['circle', 'square', 'triangle'],
  location: ['Seattle', 'New York', 'Chicago', 'Los Angeles', 'Portland']
};

interface IExampleItem {
  thumbnail: string;
  key: string;
  name: string;
  description: string;
  color: string;
  shape: string;
  location: string;
  width: number;
  height: number;
}

function createListItems(count: number, startIndex: number = 0): IExampleItem[] {
  return Array.apply(null, Array(count)).map((item: number, index: number) => {
    const size = 150 + Math.round(Math.random() * 100);

    return {
      thumbnail: `//placehold.it/${size}x${size}`,
      key: 'item-' + (index + startIndex) + ' ' + lorem(4),
      name: lorem(5),
      description: lorem(10 + Math.round(Math.random() * 50)),
      color: _randWord(DATA.color),
      shape: _randWord(DATA.shape),
      location: _randWord(DATA.location),
      width: size,
      height: size
    };
  });
}

function createGroups(
  groupCount: number,
  groupDepth: number,
  startIndex: number,
  itemsPerGroup: number,
  level: number = 0,
  key: string = '',
  isCollapsed?: boolean
): IGroup[] {
  if (key !== '') {
    key = key + '-';
  }
  const count = Math.pow(itemsPerGroup, groupDepth);
  return Array.apply(null, Array(groupCount)).map((value: number, index: number) => {
    return {
      count: count,
      key: 'group' + key + index,
      name: 'group ' + key + index,
      startIndex: index * count + startIndex,
      level: level,
      isCollapsed: isCollapsed,
      children:
        groupDepth > 1
          ? createGroups(groupCount, groupDepth - 1, index * count + startIndex, itemsPerGroup, level + 1, key + index)
          : []
    };
  });
}

function lorem(wordCount: number): string {
  return Array.apply(null, Array(wordCount))
    .map((item: number, idx: number) => {
      return LOREM_IPSUM[idx % LOREM_IPSUM.length];
    })
    .join(' ');
}

function isGroupable(key: string): boolean {
  return key === 'color' || key === 'shape' || key === 'location';
}

function _randWord(array: string[]): string {
  const index = Math.floor(Math.random() * array.length);
  return array[index];
}

ReactDOM.render(<DetailsListAdvancedExample />, document.getElementById('content'));
