/* tslint:disable:no-any */
import {
  BACKSPACE,
  DOWN_ARROW,
  ENTER,
  ESCAPE,
  LEFT_ARROW,
  RIGHT_ARROW,
  UP_ARROW,
} from '@angular/cdk/keycodes';
import { ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay';
import {
  forwardRef,
  AfterViewInit,
  ChangeDetectorRef,
  Component,
  ElementRef,
  EventEmitter,
  HostBinding,
  HostListener,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Output,
  Renderer2,
  SimpleChanges,
  TemplateRef,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const ESC = 27;

import { dropDownAnimation } from '../core/animation/dropdown-animations';
import { toBoolean } from '../util/convert';

// TODO: remove this as used nowhere
const noop = () => null;

function toArray<T>(value: T | T[]): T[] {
  let ret: T[];
  if (value == null) {
    ret = [];
  } else if (!Array.isArray(value)) {
    ret = [value];
  } else {
    ret = value;
  }
  return ret;
}

function arrayEquals<T>(array1: T[], array2: T[]): boolean {
  if (!array1 || !array2 || array1.length !== array2.length) {
      return false;
  }

  const len = array1.length;
  for (let i = 0; i < len; i++) {
      if (array1[i] !== array2[i]) {
          return false;
      }
  }
  return true;
}

const defaultDisplayRender = label => label.join(' / ');

export type CascaderExpandTrigger = 'click' | 'hover';
export type CascaderTriggerType = 'click' | 'hover';

export interface CascaderOption {
  value?: string;
  label?: string;
  title?: string;
  disabled?: boolean;
  loading?: boolean;
  isLeaf?: boolean;
  parent?: CascaderOption;
  children?: CascaderOption[];
  [key: string]: any;
}

export interface DisplayLabelContext {
  labels?: string[];
  selectedOptions?: CascaderOption[];
}

@Component({
  selector     : 'nz-cascader',
  encapsulation: ViewEncapsulation.None,
  animations   : [
    dropDownAnimation
  ],
  template       : `
    <div
      cdkOverlayOrigin
      #origin="cdkOverlayOrigin"
      #trigger
    >
      <div *ngIf="nzShowInput">
        <input #input
          nz-input
          [attr.autoComplete]="'off'"
          [attr.placeholder]="_hasInput() || _hasSelection() ? null : nzPlaceHolder"
          [readonly]="!nzShowSearch"
          [disabled]="nzDisabled"
          [nzSize]="nzSize"
          [ngClass]="_inputCls"
          [(ngModel)]="_inputValue"
          (blur)="_handleInputBlur($event)"
          (focus)="_handleInputFocus($event)"
          (keydown)="_handleInputKeyDown($event)"
          (change)="_handlerInputChange($event)"
        >
        <i *ngIf="_showClearIcon"
          [class]="'anticon anticon-cross-circle'"
          [ngClass]="_clearCls"
          [attr.title]="nzClearText"
          (click)="_clearSelection($event)"></i>
        <i *ngIf="nzShowArrow"
          class="anticon anticon-down"
          [ngClass]="_arrowCls"></i>
        <span [ngClass]="_pickerLabelCls">
          <ng-container *ngIf="_displayLabelIsTemplate">
            <ng-container *ngTemplateOutlet="_displayLabel; context: _displayLabelContext"></ng-container>
          </ng-container>
          <ng-container *ngIf="!_displayLabelIsTemplate">{{ _displayLabel }}</ng-container>
        </span>
      </div>
      <ng-content></ng-content>
    </div>
    <ng-template
      cdkConnectedOverlay
      cdkConnectedOverlayHasBackdrop
      [cdkConnectedOverlayOrigin]="origin"
      (backdropClick)="_closeMenu()"
      (detach)="_closeMenu()"
      (positionChange)="onPositionChange($event)"
      [cdkConnectedOverlayOpen]="_popupVisible"
    >
      <div #menu
        [ngClass]="_menuCls"
        [@dropDownAnimation]="_dropDownPosition"
      >
        <ul *ngFor="let options of _nzColumns; let i = index;"
          [ngClass]="_columnCls"
          [attr.data-key]="i">
          <li *ngFor="let option of options"
            [attr.data-key]="_getOptionValue(option)"
            [attr.title]="option.title || _getOptionLabel(option)"
            [ngClass]="_getOptionCls(option, i)"
            (mouseenter)="_onOptionMouseEnter(option, i, $event)"
            (mouseleave)="_onOptionMouseLeave(option, i, $event)"
            (click)="_onOptionClick(option, i, $event)"
          >
            {{ _getOptionLabel(option) }}
          </li>
        </ul>
      </div>
    </ng-template>
  `,
  providers    : [
    {
      provide    : NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => NzCascaderComponent),
      multi      : true
    }
  ],
  styleUrls    : [
    './style/index.less',
    './style/patch.less'
  ]
})
export class NzCascaderComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit, ControlValueAccessor {
  private _allowClear = true;
  private _disabled = false;
  private _enableCache = true;
  private _showArrow = true;
  private _showInput = true;
  private _showSearch = false;
  private _changeOnSelect = false;
  _el: HTMLElement;
  _prefixCls = 'ant-cascader';
  _inputPrefixCls = 'ant-input';
  _dropDownPosition = 'bottom';
  _focused = false;
  _popupVisible = false;

  _displayLabel: string | TemplateRef<void>;
  _displayLabelIsTemplate = false;
  _displayLabelContext: DisplayLabelContext = {};

  /* tslint:disable-next-line:variable-name */
  __inputValue = '';
  get _inputValue(): string {
    return this.__inputValue;
  }
  set _inputValue(inputValue: string) {
    this.__inputValue = inputValue;
    if (inputValue.length) {
      this._addHostClass(`${this._prefixCls}-picker-with-value`);
    } else {
      this._removeHostClass(`${this._prefixCls}-picker-with-value`);
    }
  }

  // check if change happened
  _lastValue: any[];
  // selection will trigger value change
  _selectedOptions: CascaderOption[] = [];
  // activaction will not triiger value change
  _activatedOptions: CascaderOption[] = [];
  // all data columns
  _nzColumns: CascaderOption[][] = [];

  // 点击Document的事件（一般用于点击后隐藏菜单）
  private _clickOutsideHandler: () => void;
  private _touchOutsideHandler: () => void;
  private _delayTimer: any;

  // ngModel Access
  onChange: any = Function.prototype;
  onTouched: any = Function.prototype;

  /** Whether is disabled */
  @Input()
  set nzDisabled(value: boolean) {
    this._disabled = toBoolean(value);
  }

  get nzDisabled(): boolean {
    return this._disabled;
  }

  /** Input size, one of `large` `default` `small` */
  @Input() nzSize: 'large' | 'default' | 'small' = 'default';

  /** Input placeholder */
  @Input() nzPlaceHolder = 'Please select';

  /** Whether show input box. Defaults to `true`. */
  @Input()
  set nzShowInput(value: boolean) {
    this._showInput = toBoolean(value);
  }

  get nzShowInput(): boolean {
    return this._showInput;
  }

  /** Whether can search. Defaults to `false`. */
  @Input()
  set nzShowSearch(value: boolean) {
    this._showSearch = toBoolean(value);
  }

  get nzShowSearch(): boolean {
    return this._showSearch;
  }

  /** Whether allow clear. Defaults to `true`. */
  @Input()
  set nzAllowClear(value: boolean) {
    this._allowClear = toBoolean(value);
  }

  get nzAllowClear(): boolean {
    return this._allowClear;
  }

  /** Hover text for the clear icon */
  @Input() nzClearText = 'Clear';

  /** Whether to show arrow */
  @Input()
  set nzShowArrow(value: boolean) {
    this._showArrow = toBoolean(value);
  }

  get nzShowArrow(): boolean {
    return this._showArrow;
  }

  /** Specify content to show when no result matches. */
  @Input() nzNotFoundContent = 'Not Found';

  /** Additional className of popup overlay */
  @Input() nzPopupClassName: string;

  /** Additional className of popup overlay column */
  @Input() nzColumnClassName: string;

  /** Options for first column, sub column will be load async */
  @Input() nzOptions: CascaderOption[];

  /** Whether cache children when they were loaded asych */
  @Input()
  set nzEnableCache(value: boolean) {
    this._enableCache = toBoolean(value);
  }

  get nzEnableCache(): boolean {
    return this._enableCache;
  }

  /** Expand column item when click or hover, one of 'click' 'hover' */
  @Input() nzExpandTrigger: CascaderExpandTrigger = 'click';

  /** Change value on each selection if set to true */
  @Input()
  set nzChangeOnSelect(value: boolean) {
    this._changeOnSelect = toBoolean(value);
  }

  get nzChangeOnSelect(): boolean {
    return this._changeOnSelect;
  }

  /** Change value on selection only if this function returns `true` */
  @Input() nzChangeOn: (option: CascaderOption, level: number) => boolean;

  /** Delay time to show when mouse enter, when `nzExpandTrigger` is `hover`. */
  @Input() nzMouseEnterDelay = 150; // ms

  /** Delay time to hide when mouse enter, when `nzExpandTrigger` is `hover`. */
  @Input() nzMouseLeaveDelay = 150; // ms

  /** Triggering mode: can be Array<'click'|'hover'> */
  @Input() nzTriggerAction: CascaderTriggerType | CascaderTriggerType[] = ['click'];

  /** Render function of displaying selected options */
  @Input() nzDisplayRender: (label: string[], selectedOptions: CascaderOption[]) => string | TemplateRef<any>;

  /** Property name for getting `value` in the option */
  @Input() nzValueProperty = 'value';

  /** Property name for getting `label` in the option */
  @Input() nzLabelProperty = 'label';

  @ViewChild('menu') menu: ElementRef;

  @HostBinding('attr.tabIndex') tabIndex = '0';

  /** Event: emit on popup show or hide */
  @Output() nzVisibleChange = new EventEmitter<boolean>();

  /** Event: emit on values changed */
  @Output() nzChange = new EventEmitter<any[]>();

  /** Event: emit on values and selection changed */
  @Output() nzSelectionChange = new EventEmitter<CascaderOption[]>();

  /**
   * Event: emit on option selected, event data：{option: any, index: number}
   */
  @Output() nzSelect = new EventEmitter<{
    option: CascaderOption,
    index: number
  }>();

  /**
   * Event: emit before loading children. event data：{option: any|null, index: number, resolve, reject}
   */
  @Output() nzLoad = new EventEmitter<{
    option: CascaderOption,
    index: number,
    resolve(children: CascaderOption[]): void,
    reject(): void
  }>();

  /** Event: emit on the clear button clicked */
  @Output() nzClear = new EventEmitter<any>();

  onPositionChange(position: ConnectedOverlayPositionChange): void {
    const _position = position.connectionPair.originY === 'bottom' ? 'bottom' : 'top';
    if (this._dropDownPosition !== _position) {
      this._dropDownPosition = _position;
      this._cdr.detectChanges();
    }
  }

  nzFocus(): void {
    this._focused = true;
    this._addHostClass(`${this._prefixCls}-focused`);
  }

  nzBlur(): void {
    this._focused = false;
    this._removeHostClass(`${this._prefixCls}-focused`);
  }

  get _pickerLabelCls(): any {
    return {
      [`${this._prefixCls}-picker-label`]: true
    };
  }

  get _arrowCls(): any {
    return {
      [`${this._prefixCls}-picker-arrow`]       : true,
      [`${this._prefixCls}-picker-arrow-expand`]: this._popupVisible
    };
  }

  get _clearCls(): any {
    return {
      [`${this._prefixCls}-picker-clear`]: true
    };
  }

  get _inputCls(): any {
    return {
      [`${this._prefixCls}-input`]        : 1,
      [`${this._inputPrefixCls}-disabled`]: this.nzDisabled,
      [`${this._inputPrefixCls}-lg`]      : this.nzSize === 'large',
      [`${this._inputPrefixCls}-sm`]      : this.nzSize === 'small',
    };
  }

  get _menuCls(): any {
    return {
      [`${this._prefixCls}-menus`]       : true,
      [`${this._prefixCls}-menus-hidden`]: !this._popupVisible,
      [`${this.nzPopupClassName}`]       : this.nzPopupClassName
    };
  }

  /** 获取菜单中列的样式 */
  get _columnCls(): any {
    return {
      [`${this._prefixCls}-menu`]  : true,
      [`${this.nzColumnClassName}`]: this.nzColumnClassName
    };
  }

  /** 获取列中Option的样式 */
  _getOptionCls(option: CascaderOption, index: number): any {
    return {
      [`${this._prefixCls}-menu-item`]         : true,
      [`${this._prefixCls}-menu-item-expand`]  : !option.isLeaf,
      [`${this._prefixCls}-menu-item-active`]  : this._isActiveOption(option, index),
      [`${this._prefixCls}-menu-item-disabled`]: option.disabled,
      [`${this._prefixCls}-menu-item-loading`] : option.loading
    };
  }

  _getLabel(): string | TemplateRef<void> {
    return this._displayLabelIsTemplate ? '' : this._displayLabel;
  }

  /** prevent input change event */
  _handlerInputChange(event: Event): void {
    event.stopPropagation();
  }

  /** input blur */
  _handleInputBlur(event: Event): void {
    if (!this.nzShowSearch) {
      return;
    }
    if (this._popupVisible) {
      this.nzFocus();
    } else {
      this.nzBlur();
    }
  }

  /** input focus */
  _handleInputFocus(event: Event): void {
    if (!this.nzShowSearch) {
      return;
    }
    this.nzFocus();
  }

  /** input key down */
  _handleInputKeyDown(event: KeyboardEvent): void { }

  _setInputValue(inputValue: any, fireSearch: boolean = true): void {
    if (inputValue !== this._inputValue) {
      this._inputValue = inputValue;
    }
  }

  _hasInput(): boolean {
    return this._inputValue.length > 0;
  }

  _hasSelection(): boolean {
    return this._selectedOptions.length > 0;
  }

  /** Whether the clear button is visible */
  get _showClearIcon(): boolean {
    const isSelected = this._hasSelection();
    const isHasInput = this._hasInput();
    return this.nzAllowClear && !this.nzDisabled && (isSelected || isHasInput);
  }

  /** clear the input box and selected options */
  _clearSelection(event: Event): void {
    if (event) {
      event.preventDefault();
      event.stopPropagation();
    }

    this._displayLabel = '';
    this._displayLabelIsTemplate = false;
    this._selectedOptions = [];
    this._activatedOptions = [];
    this._setInputValue('', false);
    this.setPopupVisible(false);

    // trigger change event
    this._onValueChange();
  }

  get _displayRender(): (label: string[], selectedOptions: CascaderOption[]) => string | TemplateRef<any> {
    return this.nzDisplayRender || defaultDisplayRender;
  }

  _buildDisplayLabel(): void {
    const selectedOptions = this._selectedOptions;
    const labels: string[] = selectedOptions.map(o => o[this.nzLabelProperty || 'label']);
    // 设置当前控件的显示值
    this._displayLabel = this._displayRender.call(this, labels, selectedOptions);
    this._displayLabelIsTemplate = !(typeof this._displayLabel === 'string');
    this._displayLabelContext = {labels, selectedOptions};
  }

  /** 由用户来定义点击后是否变更 */
  _isChangeOn(option: CascaderOption, index: number): boolean {
    if (typeof this.nzChangeOn === 'function') {
      return this.nzChangeOn(option, index) === true;
    }
    return false;
  }

  @HostListener('keydown', ['$event'])
  onKeyDown(event: KeyboardEvent): void {
    const keyCode = event.keyCode;
    if (keyCode !== DOWN_ARROW &&
      keyCode !== UP_ARROW &&
      keyCode !== LEFT_ARROW &&
      keyCode !== RIGHT_ARROW &&
      keyCode !== ENTER &&
      keyCode !== BACKSPACE &&
      keyCode !== ESC) {
      return;
    }

    // Press any keys above to reopen menu
    if (!this._isPopupVisible() &&
      keyCode !== BACKSPACE &&
      keyCode !== ESC) {
      this.setPopupVisible(true);
      return;
    }
    // Press ESC to close menu
    if (keyCode === ESC) {
      this.setPopupVisible(false);
      return;
    }

    if (this._isPopupVisible()) {
      event.preventDefault();
      if (keyCode === DOWN_ARROW) {
        this._moveDown();
      }
      if (keyCode === UP_ARROW) {
        this._moveUp();
      }
      if (keyCode === LEFT_ARROW) {
        this._moveLeft();
      }
      if (keyCode === RIGHT_ARROW) {
        this._moveRight();
      }
    }
  }

  @HostListener('click', ['$event'])
  _onTriggerClick(event: MouseEvent): void {
    if (this.nzDisabled) {
      return;
    }
    this.onTouched(); // set your control to 'touched'

    if (this._isClickTiggerAction()) {
      this._delaySetPopupVisible(!this._popupVisible, 100);
    }
  }

  @HostListener('mouseenter', ['$event'])
  _onTriggerMouseEnter(event: MouseEvent): void {
    if (this.nzDisabled) {
      return;
    }
    if (this._isPointerTiggerAction()) {
      this._delaySetPopupVisible(true, this.nzMouseEnterDelay);
    }
  }

  @HostListener('mouseleave', ['$event'])
  _onTriggerMouseLeave(event: MouseEvent): void {
      if (this.nzDisabled) {
        return;
      }
      if (!this._isPopupVisible()) {
        return;
      }
      if (this._isPointerTiggerAction()) {
        const currEl = this._el;
        const popupEl = this.menu && this.menu.nativeElement as HTMLElement;
        if (currEl.contains(event.target as Node) ||
           (popupEl && popupEl.contains(event.target as Node))) {
          return; // 还在菜单内部
        }
        this._delaySetPopupVisible(false, this.nzMouseLeaveDelay);
      }
  }

  _isClickTiggerAction(): boolean {
    if (typeof this.nzTriggerAction === 'string') {
      return this.nzTriggerAction === 'click';
    }
    return this.nzTriggerAction.indexOf('click') !== -1;
  }

  _isPointerTiggerAction(): boolean {
    if (typeof this.nzTriggerAction === 'string') {
      return this.nzTriggerAction === 'hover';
    }
    return this.nzTriggerAction.indexOf('hover') !== -1;
  }

  _closeMenu(): void {
    this._clearDelayTimer();
    this.setPopupVisible(false);
  }

  /**
   * 显示或者隐藏菜单
   *
   * @param visible true-显示，false-隐藏
   * @param delay 延迟时间
   */
  _delaySetPopupVisible(visible: boolean, delay: number): void {
    this._clearDelayTimer();
    if (delay) {
      this._delayTimer = setTimeout(() => {
        this.setPopupVisible(visible);
        this._clearDelayTimer();
      }, delay);
    } else {
      this.setPopupVisible(visible);
    }
  }

  _isPopupVisible(): boolean {
    return this._popupVisible;
  }

  setPopupVisible(popupVisible: boolean): void {
    if (this.nzDisabled) {
      return;
    }

    if (this._popupVisible !== popupVisible) {
      this._popupVisible = popupVisible;

      // We must listen to `mousedown` or `touchstart`, edge case:
      // https://github.com/ant-design/ant-design/issues/5804
      // https://github.com/react-component/calendar/issues/250
      // https://github.com/react-component/trigger/issues/50
      if (popupVisible) {
        if (!this._clickOutsideHandler) {
          this._clickOutsideHandler = this._render.listen('document', 'mousedown', this._onDocumentClick.bind(this));
        }
        // always hide on mobile
        if (!this._touchOutsideHandler) {
          this._touchOutsideHandler = this._render.listen('document', 'touchstart', this._onDocumentClick.bind(this));
        }
      }
      if (!popupVisible) {
        this._clearOutsideHandler();
      }

      if (popupVisible) {
        this._beforeVisible();
      }
      this.nzVisibleChange.emit(popupVisible);
    }
  }

  /** load init data if necessary */
  _beforeVisible(): void {
    if (!this._nzColumns.length) {
      new Promise((resolve, reject) => {
        this.nzLoad.emit({
          option: null,
          index: -1,
          resolve,
          reject
        });
      }).then((children: CascaderOption[]) => {
        this._setColumnData(children, 0);
      }, (reason: any) => {
        // should not be here
      });
    }
  }

  _onDocumentClick(event: MouseEvent): void {
    const target = event.target as Node;
    const popupEl = this.menu && this.menu.nativeElement as HTMLElement;
    if (!this._el.contains(target) && !popupEl.contains(target)) {
      this.setPopupVisible(false);
    }
  }

  _clearOutsideHandler(): void {
    if (this._clickOutsideHandler) {
      this._clickOutsideHandler(); // Removes "listen" listener
      this._clickOutsideHandler = null;
    }

    if (this._touchOutsideHandler) {
      this._touchOutsideHandler(); // Removes "listen" listener
      this._touchOutsideHandler = null;
    }
  }

  _clearDelayTimer(): void {
    if (this._delayTimer) {
      clearTimeout(this._delayTimer);
      this._delayTimer = null;
    }
  }

  /** 获取Option的值，例如，可以指定labelProperty="name"来取Name */
  _getOptionLabel(option: CascaderOption): any {
    return option[this.nzLabelProperty || 'label'];
  }

  /** 获取Option的值，例如，可以指定valueProperty="id"来取ID */
  _getOptionValue(option: CascaderOption): any {
    return option[this.nzValueProperty || 'value'];
  }

  _isActiveOption(option: CascaderOption, index: number): boolean {
    const activeOpt = this._activatedOptions[index];
    if (activeOpt === option) {
      return true;
    }
    if (activeOpt && this._getOptionValue(activeOpt) === this._getOptionValue(option)) {
      return true;
    }
    return false;
  }

  /**
   * 设置某列的激活的菜单选项
   *
   * @param option 菜单选项
   * @param index  选项所在的列组的索引
   */
  _setActiveOption(option: CascaderOption, index: number): void {
    if (!option || option.disabled) {
      return;
    }

    this._activatedOptions[index] = option;

    // 当直接选择最后一级时，前面的选项要补全。例如，选择“城市”，则自动补全“国家”、“省份”
    for (let i = index - 1; i >= 0; i--) {
      if (!this._activatedOptions[i]) {
        this._activatedOptions[i] = this._activatedOptions[i + 1].parent;
      }
    }
    // 截断多余的选项，如选择“省份”，则只会有“国家”、“省份”，去掉“城市”、“区县”
    if (index < this._activatedOptions.length - 1) {
      this._activatedOptions = this._activatedOptions.slice(0, index + 1);
    }

    // trigger select event, and display label
    this._onSelectOption(option, index);
  }

  _onSelectOption(option: CascaderOption, index: number): void {
    // trigger `nzSelect` event
    this.nzSelect.emit({
      option,
      index
    });

    // load children directly
    if (option.children && option.children.length) {
      option.isLeaf = false;
      option.children.forEach(child => child.parent = option);
      this._setColumnData(option.children, index + 1);
    } else if (!option.isLeaf) {
      // load children async
      new Promise((resolve, reject) => {
        this.nzLoad.emit({
          option,
          index,
          resolve,
          reject
        });
      }).then((children: CascaderOption[]) => {
        children.forEach(child => child.parent = option);
        this._setColumnData(children, index + 1);
        if (this.nzEnableCache) {
          option.children = children; // next time we load children directly
        }
      }, (reason: any) => {
        option.isLeaf = true;
      });
    } else {
      // clicking leaf node will remove any children columns
      if (index < this._nzColumns.length - 1) {
        this._nzColumns = this._nzColumns.slice(0, index + 1);
      }
    }

    // 生成显示
    if (option.isLeaf || this.nzChangeOnSelect || this._isChangeOn(option, index)) {
      this._selectedOptions = this._activatedOptions;
      // 设置当前控件的显示值
      this._buildDisplayLabel();
      // 触发变更事件
      this._onValueChange();
    }

    // close menu if click on leaf
    if (option.isLeaf) {
      this._delaySetPopupVisible(false, this.nzMouseLeaveDelay);
    }
  }

  _setColumnData(options: CascaderOption[], index: number): void {
    if (!arrayEquals(this._nzColumns[index], options)) {
      this._nzColumns[index] = options;
      if (index < this._nzColumns.length - 1) {
        this._nzColumns = this._nzColumns.slice(0, index + 1);
      }
    }
  }

  /**
   * 鼠标点击选项
   *
   * @param option 菜单选项
   * @param index 选项所在的列组的索引
   * @param event 鼠标事件
   */
  _onOptionClick(option: CascaderOption, index: number, event: Event): void {
    event.preventDefault();

    // Keep focused state for keyboard support
    this._el.focus();

    if (option && option.disabled) {
      return;
    }
    this._setActiveOption(option, index);
  }

  /**
   * press `up` or `down` arrow to select the sibling option.
   */
  _moveUpOrDown(isUp: boolean): void {
    const columnIndex = Math.max(this._activatedOptions.length - 1, 0);
    // 该组中已经被激活的选项
    const activeOption = this._activatedOptions[columnIndex];
    // 该组所有的选项，用于遍历获取下一个被激活的选项
    const options = this._nzColumns[columnIndex];
    if (!options || !options.length) {
      return;
    }

    const length = options.length;
    let nextOptIndex = -1;
    if (!activeOption) { // 该列还没有选中的选项
      nextOptIndex = isUp ? length : -1;
    } else {
      nextOptIndex = options.indexOf(activeOption);
    }

    while (true) {
      nextOptIndex = isUp ? nextOptIndex - 1 : nextOptIndex + 1;
      if (nextOptIndex < 0 || nextOptIndex >= length) {
        break;
      }
      const nextOption = options[nextOptIndex];
      if (!nextOption || nextOption.disabled) {
        continue;
      }
      this._setActiveOption(nextOption, columnIndex);
      break;
    }
  }

  _moveUp(): void {
    this._moveUpOrDown(true);
  }
  _moveDown(): void {
    this._moveUpOrDown(false);
  }

  /**
   * press `left` arrow to remove the last selected option.
   * If there is no option selected, emit `nzClear` event.
   */
  _moveLeft(): void {
    const options = this._selectedOptions;
    if (options.length) {
      options.pop(); // Remove the last one
      const len = options.length;
      if (len) {
        this._setActiveOption(options[len - 1],  len - 1);
      } else {
        this.nzClear.emit();
      }
    }
  }

  /**
   * press `right` arrow to select the next column option.
   */
  _moveRight(): void {
    const columns = this._nzColumns;
    const length = this._selectedOptions.length;
    if (length === 0) {
      return;
    }

    const nextColIndex = length;
    const options = columns.length > nextColIndex ? columns[nextColIndex] : null;
    if (options) { // 存在`下级选项`
      const len = options.length;
      for (let i = 0; i < len; i++) {
        const activeOpt = options[i];
        if (activeOpt && !activeOpt.disabled) {
          this._setActiveOption(activeOpt, nextColIndex);
          return;
        }
      }
    }
  }

  /**
   * 鼠标划入选项
   *
   * @param option 菜单选项
   * @param index 选项所在的列组的索引
   * @param event 鼠标事件
   */
  _onOptionMouseEnter(option: CascaderOption, index: number, event: Event): void {
    event.preventDefault();
    if (this.nzExpandTrigger === 'hover' && !option.isLeaf) {
      this._delaySelect(option, index, true);
    }
  }

  /**
   * 鼠标划出选项
   *
   * @param option 菜单选项
   * @param index 选项所在的列组的索引
   * @param event 鼠标事件
   */
  _onOptionMouseLeave(option: CascaderOption, index: number, event: Event): void {
    event.preventDefault();
    if (this.nzExpandTrigger === 'hover' && !option.isLeaf) {
      this._delaySelect(option, index, false);
    }
  }

  _delaySelect(option: CascaderOption, index: number, doSelect: boolean): void {
    if (this._delayTimer) {
      clearTimeout(this._delayTimer);
      this._delayTimer = null;
    }
    if (doSelect) {
      this._delayTimer = setTimeout(() => {
        this._setActiveOption(option, index);
        this._delayTimer = null;
      }, 150);
    }
  }

  _getSubmitValue(): any[] {
    const values: any[] = [];
    this._selectedOptions.forEach(option => {
      values.push(this._getOptionValue(option));
    });
    return values;
  }

  _onValueChange(): void {
    const value = this._getSubmitValue();
    if (!arrayEquals(this._lastValue, value)) {
      this._lastValue = value;
      this.onChange(value); // Angular need this
      if (value.length === 0) {
        this.nzClear.emit(); // first trigger `clear` and then `change`
      }
      this.nzSelectionChange.emit(this._selectedOptions);
      this.nzChange.emit(value);
    }
  }

  constructor(
    private _elementRef: ElementRef,
    private _render: Renderer2,
    private _cdr: ChangeDetectorRef
  ) {
    this._el = this._elementRef.nativeElement;
  }

  _addHostClass(classname: string): void {
    this._render.addClass(this._el, classname);
  }

  _removeHostClass(classname: string): void {
    this._render.removeClass(this._el, classname);
  }

  /**
   * Write a new value to the element.
   *
   * @Override (From ControlValueAccessor interface)
   */
  writeValue(value: any): void {
    const array: any[] = [];
    toArray(value).forEach((v: any, index: number) => {
      if (typeof v !== 'object') {
        const obj = {};
        obj[this.nzValueProperty] = v;
        obj[this.nzLabelProperty] = v;
        array[index] = obj;
      } else {
        array[index] = v;
      }
    });
    this._activatedOptions = array;
    this._selectedOptions = array;
    this._buildDisplayLabel();
  }

  registerOnChange(fn: (_: any) => {}): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => {}): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    if (isDisabled) {
      this._closeMenu();
      this._addHostClass(`${this._prefixCls}-picker-disabled`);
    } else {
      this._removeHostClass(`${this._prefixCls}-picker-disabled`);
    }
    this.nzDisabled = isDisabled;
  }

  ngOnInit(): void {
    // 设置第一列
    if (this.nzOptions && this.nzOptions.length) {
      this._nzColumns.push(this.nzOptions);
    }
  }

  ngOnDestroy(): void {
    if (this._delayTimer) {
      clearTimeout(this._delayTimer);
      this._delayTimer = null;
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    const nzDisabled = changes.nzDisabled;
    if (nzDisabled) {
      if (nzDisabled.currentValue) {
        this._addHostClass(`${this._prefixCls}-picker-disabled`);
      } else {
        this._removeHostClass(`${this._prefixCls}-picker-disabled`);
      }
    }

    const nzOptions = changes.nzOptions;
    if (nzOptions && !nzOptions.isFirstChange()) {
      this._nzColumns.splice(0);
      const newOptions: CascaderOption[] = nzOptions.currentValue;
      if (newOptions && newOptions.length) {
        this._nzColumns.push(newOptions);
        this._clearSelection(null);
      }
    }
  }

  ngAfterViewInit(): void {
    this._addHostClass(this._prefixCls);
    this._addHostClass(`${this._prefixCls}-picker`);
  }
}
