import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import Spin from 'react-bem-components/lib/Spin';
import Scrollable from 'ui/Scrollable';
import scrollTo from 'lib/scrollTo';

import './index.css';

const SCROLL_THROTTLING_RATE = 100;

// на каком удалении от края вьюпорта заменять элементы списка на заглушку
const SCROLL_OPTIMIZATION_MARGIN = 600;
// на каком расстоянии от края списка вызывать обработчик приближения к краю
const SCROLL_FEED_MARGIN = 200;

const ScrollableList = React.createClass({

    getInitialState() {
        return _.pick(this.props, ['items', 'busyTop', 'busyBottom']);
    },

    componentDidMount() {
        this._computeState();
        window.addEventListener('resize', this._resizeHandler = () => this._computeState());
    },

    componentWillReceiveProps(nextProps) {
        const isChunkReceived =
            (nextProps.items && nextProps.items.length !== this.state.items.length) ||
            (nextProps.busyBottom === false || nextProps.busyTop === false);

        this.setState(nextProps);

        if (isChunkReceived) {
            this.setState({ hitsTop: false, hitsBottom: false });
            setTimeout(() => {
                if (!this._unmounted) {
                    this._computeState();
                    this._setScrollPosition();
                }
            }, 0);
        } else {
            this._setScrollPosition();
        }
    },

    componentWillUnmount() {
        window.removeEventListener('resize', this._resizeHandler);
        this._unmounted = true;
    },

    _getListHeight() {
        const scrollPane = ReactDOM.findDOMNode(this.refs.scrollPane);

        try {
            return scrollPane.childNodes[0].offsetHeight;
        } catch (e) {
            // empty
        }
    },

    _getDefaultScrollEvent() {
        if (this._lastScrollEvent) {
            this._lastScrollEvent.realHeight = this._getListHeight();

            return this._lastScrollEvent;
        }

        const scrollPane = ReactDOM.findDOMNode(this.refs.scrollPane);

        if (!scrollPane) {
            return {};
        }

        return {
            topPosition: 0,
            containerHeight: scrollPane.offsetHeight,
        };
    },

    _computeState(event) {
        if (!event) {
            event = this._getDefaultScrollEvent();
        }

        if (event.topPosition === undefined) {
            return;
        }

        const nextState = _.assign(
            this._getRenderingOptimizationState(event),
            this._getContinuousFeedState(event)
        );

        this.setState(nextState);

        this._lastScrollEvent = event;
        this._handleBorderHits(event);
    },

    _getRenderingOptimizationState(event) {
        const { items } = this.state;

        const nextState = {
            topPadding: 0,
            bottomPadding: 0,
            visibleSpan: [0, items.length],
        };

        let y = 0;
        const topBorder = event.topPosition - SCROLL_OPTIMIZATION_MARGIN;
        const bottomBorder = event.topPosition + event.containerHeight + SCROLL_OPTIMIZATION_MARGIN;

        for (let i = 0; i < items.length; i++) {
            if (y < topBorder) {
                nextState.visibleSpan[0] = i;
                nextState.topPadding = y;
            }
            if (y > bottomBorder) {
                if (!nextState.bottomPadding) {
                    nextState.visibleSpan[1] = i;
                }
                nextState.bottomPadding = y - bottomBorder;
            }
            y += items[i].height;
        }

        return nextState;
    },

    _getContinuousFeedState(event) {
        const nextState = {};

        const topBorder = event.topPosition - SCROLL_FEED_MARGIN;
        const bottomBorder = event.topPosition + event.containerHeight + SCROLL_FEED_MARGIN;

        nextState.hitsTop = topBorder <= 0;
        nextState.hitsBottom = event.realHeight && bottomBorder >= event.realHeight;

        if (this.state.hitsTop !== nextState.hitsTop) {
            nextState.topHitHandled = false;
        }

        if (this.state.hitsBottom !== nextState.hitsBottom) {
            nextState.bottomHitHandled = false;
        }

        nextState.isScrollable = event.realHeight > event.containerHeight;

        return nextState;
    },

    _handleBorderHits(event) {
        const { onTopHit, onBottomHit } = this.props;
        const { hitsTop, hitsBottom, topHitHandled, bottomHitHandled } = this.state;

        if (!topHitHandled && hitsTop && onTopHit) {
            onTopHit(event);
            this.setState({ topHitHandled: true });
        }

        if (!bottomHitHandled && hitsBottom && onBottomHit) {
            onBottomHit(event);
            this.setState({ bottomHitHandled: true });
        }
    },

    _setScrollPosition() {
        const position = this.state.scrollTo;

        if (typeof position === 'number') {
            this.refs.scrollPane.scrollYTo(position);
        } else if (position !== undefined) {
            scrollTo(this.refs.scrollPane, position);
        }
    },

    _renderItems() {
        const { items, visibleSpan } = this.state;

        if (!visibleSpan) {
            return null;
        }

        const { isScrollable, busyTop, busyBottom } = this.state;

        const className = [
            'scrollable-list__content',
            isScrollable ? 'scrollable-list__content_scrollable' : null,
            busyTop ? 'scrollable-list_busy-top' : null,
            busyBottom ? 'scrollable-list_busy-bottom' : null,
        ].filter(Boolean).join(' ');

        return (
            <div className={className}>
                <ScrollableList.Progress type="top" />
                <div style={{ height: this.state.topPadding }} />
                {items.slice(visibleSpan[0], visibleSpan[1]).map(item => item.component)}
                <div style={{ height: this.state.bottomPadding }} />
                <ScrollableList.Progress type="bottom" />
            </div>
        );
    },

    render() {
        return (
            <Scrollable
                className={this.props.className}
                onScroll={_.throttle(this._computeState, SCROLL_THROTTLING_RATE)}
                ref="scrollPane"
            >
                {this._renderItems()}
            </Scrollable>
        );
    },

});

ScrollableList.Progress = props => (
    <div className={`scrollable-list__progress scrollable-list__${props.type}-progress`}>
        <Spin visible />
    </div>
);

ScrollableList.defaultProps = {
    items: [],
};

export default ScrollableList;
