import React, { Component, CSSProperties } from 'react';
import { findDOMNode } from 'react-dom';
import classNames from 'classnames';
import shallowequal from 'shallowequal';
import omit from 'lodash/omit';
import noop from 'lodash/noop';
import getScroll from '../_util/getScroll';
import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame';
import addEventListener from '../_util/addEventListener';
import ConfigContext, { ConfigContextValue } from '../config-provider/ConfigContext';

function getTargetRect(target: HTMLElement | Window | null): ClientRect {
  return target !== window
    ? (target as HTMLElement).getBoundingClientRect()
    : ({ top: 0, left: 0, bottom: 0 } as ClientRect);
}

function getOffset(element: HTMLElement, target: HTMLElement | Window | null) {
  const elemRect = element.getBoundingClientRect();
  const targetRect = getTargetRect(target);

  const scrollTop = getScroll(target, true);
  const scrollLeft = getScroll(target, false);

  const docElem = window.document.body;
  const clientTop = docElem.clientTop || 0;
  const clientLeft = docElem.clientLeft || 0;

  return {
    top: elemRect.top - targetRect.top + scrollTop - clientTop,
    left: elemRect.left - targetRect.left + scrollLeft - clientLeft,
    width: elemRect.width,
    height: elemRect.height,
  };
}

function getDefaultTarget() {
  return typeof window !== 'undefined' ? window : null;
}

// Affix
export interface AffixProps {
  /**
   * 距离窗口顶部达到指定偏移量后触发
   */
  offsetTop?: number;
  offset?: number;
  /** 距离窗口底部达到指定偏移量后触发 */
  offsetBottom?: number;
  style?: CSSProperties;
  /** 固定状态改变时触发的回调函数 */
  onChange?: (affixed?: boolean) => void;
  /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */
  target?: () => Window | HTMLElement | null;
  prefixCls?: string;
  className?: string;
}

export interface AffixState {
  affixStyle: CSSProperties | undefined;
  placeholderStyle: CSSProperties | undefined;
}

export default class Affix extends Component<AffixProps, AffixState> {
  static displayName = 'Affix';

  static get contextType(): typeof ConfigContext {
    return ConfigContext;
  }

  context: ConfigContextValue;

  state: AffixState = {
    affixStyle: undefined,
    placeholderStyle: undefined,
  };

  private timeout: number;

  private eventHandlers: Record<string, any> = {};

  private fixedNode: HTMLElement;

  private placeholderNode: HTMLElement;

  private readonly events = [
    'resize',
    'scroll',
    'touchstart',
    'touchmove',
    'touchend',
    'pageshow',
    'load',
  ];

  setAffixStyle(e: Event, affixStyle: CSSProperties | null) {
    const { onChange = noop, target = getDefaultTarget } = this.props;
    const { affixStyle: originalAffixStyle } = this.state;
    const isWindow = target() === window;
    if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) {
      return;
    }
    if (shallowequal(affixStyle, originalAffixStyle)) {
      return;
    }

    this.setState({ affixStyle: affixStyle as React.CSSProperties }, () => {
      if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) {
        const { state } = this;
        onChange(!!state.affixStyle);
      }
    });
  }

  setPlaceholderStyle(placeholderStyle: CSSProperties | null) {
    const { placeholderStyle: originalPlaceholderStyle } = this.state;
    if (shallowequal(placeholderStyle, originalPlaceholderStyle)) {
      return;
    }
    this.setState({ placeholderStyle: placeholderStyle as CSSProperties });
  }

  syncPlaceholderStyle(e: Event) {
    const { affixStyle } = this.state;
    if (!affixStyle) {
      return;
    }
    this.placeholderNode.style.cssText = '';
    this.setAffixStyle(e, {
      ...affixStyle,
      width: this.placeholderNode.offsetWidth,
    });
    this.setPlaceholderStyle({
      width: this.placeholderNode.offsetWidth,
    });
  }

  @throttleByAnimationFrameDecorator()
  updatePosition(e: Event) {
    const { offsetBottom, offset, target = getDefaultTarget } = this.props;
    let { offsetTop } = this.props;
    const targetNode = target();

    // Backwards support
    offsetTop = typeof offsetTop === 'undefined' ? offset : offsetTop;
    const scrollTop = getScroll(targetNode, true);
    const affixNode = findDOMNode(this) as HTMLElement;
    const elemOffset = getOffset(affixNode, targetNode);
    const elemSize = {
      width: this.fixedNode.offsetWidth,
      height: this.fixedNode.offsetHeight,
    };

    const offsetMode = {
      top: false,
      bottom: false,
    };
    // Default to `offsetTop=0`.
    if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') {
      offsetMode.top = true;
      offsetTop = 0;
    } else {
      offsetMode.top = typeof offsetTop === 'number';
      offsetMode.bottom = typeof offsetBottom === 'number';
    }

    const targetRect = getTargetRect(targetNode);
    const targetInnerHeight =
      (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight;
    if (scrollTop > elemOffset.top - (offsetTop as number) && offsetMode.top) {
      // Fixed Top
      const width = elemOffset.width;
      const top = targetRect.top + (offsetTop as number);
      this.setAffixStyle(e, {
        position: 'fixed',
        top,
        left: targetRect.left + elemOffset.left,
        width,
      });
      this.setPlaceholderStyle({
        width,
        height: elemSize.height,
      });
    } else if (
      scrollTop <
        elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight &&
      offsetMode.bottom
    ) {
      // Fixed Bottom
      const targetBottomOffet = targetNode === window ? 0 : window.innerHeight - targetRect.bottom;
      const width = elemOffset.width;
      this.setAffixStyle(e, {
        position: 'fixed',
        bottom: targetBottomOffet + (offsetBottom as number),
        left: targetRect.left + elemOffset.left,
        width,
      });
      this.setPlaceholderStyle({
        width,
        height: elemOffset.height,
      });
    } else {
      const { affixStyle } = this.state;
      if (
        e.type === 'resize' &&
        affixStyle &&
        affixStyle.position === 'fixed' &&
        affixNode.offsetWidth
      ) {
        this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth });
      } else {
        this.setAffixStyle(e, null);
      }
      this.setPlaceholderStyle(null);
    }

    if (e.type === 'resize') {
      this.syncPlaceholderStyle(e);
    }
  }

  componentDidMount() {
    const { props } = this;
    const target = props.target || getDefaultTarget;
    // Wait for parent component ref has its value
    this.timeout = window.setTimeout(() => {
      this.setTargetEventListeners(target);
      // Mock Event object.
      this.updatePosition({} as Event);
    });
  }

  componentWillReceiveProps(nextProps: AffixProps) {
    const { offsetTop, offsetBottom, target } = this.props;
    if (target !== nextProps.target) {
      this.clearEventListeners();
      this.setTargetEventListeners(nextProps.target!);

      // Mock Event object.
      this.updatePosition({} as Event);
    }
    if (offsetTop !== nextProps.offsetTop || offsetBottom !== nextProps.offsetBottom) {
      this.updatePosition({} as Event);
    }
  }

  componentWillUnmount() {
    this.clearEventListeners();
    clearTimeout(this.timeout);
    (this.updatePosition as any).cancel();
  }

  setTargetEventListeners(getTarget: () => HTMLElement | Window | null) {
    const target = getTarget();
    if (!target) {
      return;
    }
    this.clearEventListeners();

    this.events.forEach(eventName => {
      this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition);
    });
  }

  clearEventListeners() {
    this.events.forEach(eventName => {
      const handler = this.eventHandlers[eventName];
      if (handler && handler.remove) {
        handler.remove();
      }
    });
  }

  saveFixedNode = (node: HTMLDivElement) => {
    this.fixedNode = node;
  };

  savePlaceholderNode = (node: HTMLDivElement) => {
    this.placeholderNode = node;
  };

  render() {
    const { prefixCls, style, children } = this.props;
    const { affixStyle, placeholderStyle } = this.state;
    const { getPrefixCls } = this.context;
    const className = classNames({
      [getPrefixCls('affix', prefixCls)]: affixStyle,
    });

    const props = omit<AffixProps, keyof AffixProps>(this.props, [
      'prefixCls',
      'offsetTop',
      'offsetBottom',
      'target',
      'onChange',
    ]);
    return (
      <div {...props} style={{ ...placeholderStyle, ...style }} ref={this.savePlaceholderNode}>
        <div className={className} ref={this.saveFixedNode} style={affixStyle}>
          {children}
        </div>
      </div>
    );
  }
}