// @flow

import * as React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import {
  registerScrollListener,
  unregisterScrollListener,
} from './utils/onScroll';
import {
  getDimensions,
  getPositionOffset,
  getScrollOffset,
} from './utils/dimensions';
import createDetectElementResize from '../vendor/detectElementResize';

type Props = {
  /**
   * Function responsible for rendering children.
   * This function should implement the following signature:
   * ({ height, isScrolling, scrollLeft, scrollTop, width }) => PropTypes.element
   */
  children: (params: {
    onChildScroll: (params: {scrollTop: number}) => void,
    height: number,
    isScrolling: boolean,
    scrollLeft: number,
    scrollTop: number,
    width: number,
  }) => React.Node,

  /** Callback to be invoked on-resize: ({ height, width }) */
  onResize: (params: {height: number, width: number}) => void,

  /** Callback to be invoked on-scroll: ({ scrollLeft, scrollTop }) */
  onScroll: (params: {scrollLeft: number, scrollTop: number}) => void,

  /** Element to attach scroll event listeners. Defaults to window. */
  scrollElement: ?Element,

  /**
   * Wait this amount of time after the last scroll event before resetting child `pointer-events`.
   */
  scrollingResetTimeInterval: number,

  /** Height used for server-side rendering */
  serverHeight: number,

  /** Width used for server-side rendering */
  serverWidth: number,
};

type State = {
  height: number,
  width: number,
  isScrolling: boolean,
  scrollLeft: number,
  scrollTop: number,
};

type ResizeHandler = (element: Element, onResize: () => void) => void;

type DetectElementResize = {
  addResizeListener: ResizeHandler,
  removeResizeListener: ResizeHandler,
};

/**
 * Specifies the number of miliseconds during which to disable pointer events while a scroll is in progress.
 * This improves performance and makes scrolling smoother.
 */
export const IS_SCROLLING_TIMEOUT = 150;

const getWindow = () => (typeof window !== 'undefined' ? window : undefined);

export default class WindowScroller extends React.PureComponent<Props, State> {
  static defaultProps = {
    onResize: () => {},
    onScroll: () => {},
    scrollingResetTimeInterval: IS_SCROLLING_TIMEOUT,
    scrollElement: getWindow(),
    serverHeight: 0,
    serverWidth: 0,
  };

  _window = getWindow();
  _isMounted = false;
  _positionFromTop = 0;
  _positionFromLeft = 0;
  _detectElementResize: DetectElementResize = createDetectElementResize();

  state = {
    ...getDimensions(this.props.scrollElement, this.props),
    isScrolling: false,
    scrollLeft: 0,
    scrollTop: 0,
  };

  updatePosition(
    scrollElement: ?Element = this.props.scrollElement,
    props: Props = this.props,
  ) {
    const {onResize} = this.props;
    const {height, width} = this.state;

    const thisNode = ReactDOM.findDOMNode(this);
    if (thisNode instanceof Element && scrollElement) {
      const offset = getPositionOffset(thisNode, scrollElement);
      this._positionFromTop = offset.top;
      this._positionFromLeft = offset.left;
    }

    const dimensions = getDimensions(scrollElement, props);
    if (height !== dimensions.height || width !== dimensions.width) {
      this.setState({
        height: dimensions.height,
        width: dimensions.width,
      });
      onResize({
        height: dimensions.height,
        width: dimensions.width,
      });
    }
  }

  componentDidMount() {
    const scrollElement = this.props.scrollElement;

    this.updatePosition(scrollElement);

    if (scrollElement) {
      registerScrollListener(this, scrollElement);
      this._registerResizeListener(scrollElement);
    }

    this._isMounted = true;
  }

  componentWillReceiveProps(nextProps: Props) {
    const scrollElement = this.props.scrollElement;
    const nextScrollElement = nextProps.scrollElement;

    if (
      scrollElement !== nextScrollElement &&
      scrollElement &&
      nextScrollElement
    ) {
      this.updatePosition(nextScrollElement, nextProps);

      unregisterScrollListener(this, scrollElement);
      registerScrollListener(this, nextScrollElement);

      this._unregisterResizeListener(scrollElement);
      this._registerResizeListener(nextScrollElement);
    }
  }

  componentWillUnmount() {
    const scrollElement = this.props.scrollElement;
    if (scrollElement) {
      unregisterScrollListener(this, scrollElement);
      this._unregisterResizeListener(scrollElement);
    }

    this._isMounted = false;
  }

  render() {
    const {children} = this.props;
    const {isScrolling, scrollTop, scrollLeft, height, width} = this.state;

    return children({
      onChildScroll: this._onChildScroll,
      height,
      isScrolling,
      scrollLeft,
      scrollTop,
      width,
    });
  }

  _onChildScroll = ({scrollTop}) => {
    if (this.state.scrollTop === scrollTop) {
      return;
    }

    const scrollElement = this.props.scrollElement;
    if (scrollElement) {
      if (typeof scrollElement.scrollTo === 'function') {
        scrollElement.scrollTo(0, scrollTop + this._positionFromTop);
      } else {
        scrollElement.scrollTop = scrollTop + this._positionFromTop;
      }
    }
  };

  _registerResizeListener = element => {
    if (element === window) {
      window.addEventListener('resize', this._onResize, false);
    } else {
      this._detectElementResize.addResizeListener(element, this._onResize);
    }
  };

  _unregisterResizeListener = element => {
    if (element === window) {
      window.removeEventListener('resize', this._onResize, false);
    } else if (element) {
      this._detectElementResize.removeResizeListener(element, this._onResize);
    }
  };

  _onResize = () => {
    this.updatePosition();
  };

  // Referenced by utils/onScroll
  __handleWindowScrollEvent = () => {
    if (!this._isMounted) {
      return;
    }

    const {onScroll} = this.props;

    const scrollElement = this.props.scrollElement;
    if (scrollElement) {
      const scrollOffset = getScrollOffset(scrollElement);
      const scrollLeft = Math.max(
        0,
        scrollOffset.left - this._positionFromLeft,
      );
      const scrollTop = Math.max(0, scrollOffset.top - this._positionFromTop);

      this.setState({
        isScrolling: true,
        scrollLeft,
        scrollTop,
      });

      onScroll({
        scrollLeft,
        scrollTop,
      });
    }
  };

  // Referenced by utils/onScroll
  __resetIsScrolling = () => {
    this.setState({
      isScrolling: false,
    });
  };
}

@@ -1155,25 +1155,42 @@
 ement: ?
+(typeof window %7C 
 Element
+)
 ,
-%0A
 %0A  /**%0A 

Fix WindowScroller scrollElement prop types for window (#939)