import React from 'react';
import PropTypes from 'prop-types';

import Comment from '../containers/Comment';
import styles from './ModerationQueue.css';
import EmptyCard from '../../../components/EmptyCard';
import AutoLoadMore from './AutoLoadMore';
import ViewMore from './ViewMore';
import t from 'coral-framework/services/i18n';
import {
  WindowScroller,
  CellMeasurer,
  CellMeasurerCache,
  List,
} from 'react-virtualized';
import throttle from 'lodash/throttle';
import key from 'keymaster';

const hasComment = (nodes, id) => nodes.some(node => node.id === id);

// resetCursors will return the id cursors of the first and second comment of
// the current comment The spare cursor functions as a backup in case one
// of the comments gets deleted. Additionally the new view based on the new
// cursors are also returned.
function resetCursors(state, props) {
  let idCursors = [];
  if (props.comments && props.comments.length) {
    idCursors.push(props.comments[0].id);
    if (props.comments[1]) {
      idCursors.push(props.comments[1].id);
    }
  }
  const view = getVisibleComments(props.comments, idCursors[0]);
  return { idCursors, view };
}

// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
  const alt = invalidated === 1 ? 0 : 1;
  const idCursors = [];
  if (state.idCursors[alt]) {
    idCursors.push(state.idCursors[alt]);
    const index = props.comments.findIndex(node => node.id === idCursors[0]);
    const nextInLine = props.comments[index + 1];
    if (nextInLine) {
      idCursors.push(nextInLine.id);
    }
  }
  return idCursors;
}

// getVisibileComments returns a list containing comments
// which comes after the `idCursor`.
function getVisibleComments(comments, idCursor) {
  if (!comments) {
    return [];
  }

  const view = [];
  let pastCursor = false;
  comments.forEach(comment => {
    if (comment.id === idCursor) {
      pastCursor = true;
    }
    if (pastCursor) {
      view.push(comment);
    }
  });
  return view;
}

// Current keymapper to use for the CellMeasurer Cache.
let keyMapper = null;

// CellMeasurerCache is used to measure the size of the elements
// of the virtual list. We use a global one with a keyMapper that
// should resolve to a comment id, which is then used to cache the height.
const cache = new CellMeasurerCache({
  fixedWidth: true,
  defaultHeight: 250,
  keyMapper: index => keyMapper(index),
});

class ModerationQueue extends React.Component {
  listRef = null;
  callbackCaches = {
    clearHeightCache: {},
    selectCommentId: {},
  };

  constructor(props) {
    super(props);
    this.state = {
      ...resetCursors(this.state, props),
    };

    // Set keyMapper to map to comment ids.
    keyMapper = index => {
      const view = this.state.view;
      if (index < view.length) {
        return view[index].id;
      } else if (index === view.length) {
        return 'loadMore';
      }
      throw new Error(`unknown index ${index}`);
    };

    // Select first comment.
    if (this.state.view.length) {
      props.selectCommentId(this.state.view[0].id);
    }
  }

  componentDidMount() {
    key('j', () => this.selectDown());
    key('k', () => this.selectUp());

    // TODO: Workaround for issue https://github.com/bvaughn/react-virtualized/issues/866
    this.reflowList();
  }

  componentWillUnmount() {
    key.unbind('j');
    key.unbind('k');

    // When switching queues, clean it up first.
    // Removes dangling comments and reduce overly large
    // lists and restore chronological order.
    this.props.cleanUpQueue(this.props.activeTab);
  }

  componentWillReceiveProps(next) {
    const { comments: prevComments } = this.props;
    const { comments: nextComments } = next;

    // New comments where added and our cursor list is incomplete.
    if (
      this.state.idCursors.length < 2 &&
      nextComments.length > this.state.idCursors.length
    ) {
      this.setState(resetCursors(this.state, next));
      return;
    }

    let idCursors = this.state.idCursors;

    // Comments have been removed.
    if (
      prevComments &&
      nextComments &&
      nextComments.length < prevComments.length
    ) {
      // Invalidate first cursor if referenced comment was removed.
      if (
        this.state.idCursors[0] &&
        !hasComment(nextComments, this.state.idCursors[0])
      ) {
        idCursors = invalidateCursor(0, this.state, next);
      }

      // Invalidate second cursor if referenced comment was removed.
      if (
        this.state.idCursors[1] &&
        !hasComment(nextComments, this.state.idCursors[1])
      ) {
        idCursors = invalidateCursor(1, this.state, next);
      }

      // Selected comment was removed, determine and set next selected comment.
      if (
        this.props.selectedCommentId &&
        !hasComment(nextComments, this.props.selectedCommentId)
      ) {
        const view = this.state.view;
        let nextSelectedCommentId = null;

        // Determine a comment to select.
        const prevIndex = view.findIndex(
          comment => comment.id === this.props.selectedCommentId
        );
        if (prevIndex !== view.length - 1) {
          nextSelectedCommentId = view[prevIndex + 1].id;
        } else if (prevIndex > 0) {
          nextSelectedCommentId = view[prevIndex - 1].id;
        }
        this.props.selectCommentId(nextSelectedCommentId);
      }
    }

    // Comments changed.
    if (prevComments !== nextComments) {
      const nextView = getVisibleComments(nextComments, idCursors[0]);
      this.setState({ idCursors, view: nextView });

      // TODO: removing or adding a comment from the list seems to render incorrect, is this a bug?
      // Find first changed comment and perform a reflow.
      const index = this.state.view.findIndex(
        (comment, i) => !nextView[i] || nextView[i].id !== comment.id
      );
      this.reflowList(index);
    }
  }

  componentDidUpdate(prev) {
    const { commentCount, selectedCommentId } = this.props;

    const switchedToMultiMode = prev.singleView && !this.props.singleView;
    const switchedMode = prev.singleView !== this.props.singleView;
    const selectedDifferentComment =
      prev.selectedCommentId !== selectedCommentId && selectedCommentId;
    const moderatedLastComment =
      prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0;
    const hasMoreComment = commentCount > 0;

    if (switchedToMultiMode) {
      // Reflow virtual list.
      this.reflowList();
    }

    if (switchedMode || selectedDifferentComment) {
      this.scrollToSelectedComment();
    }

    if (moderatedLastComment && hasMoreComment) {
      this.props.loadMore();
    }
  }

  // Returns comment counts without dangling comments.
  getCommentCountWithoutDagling(props = this.props) {
    return props.comments.filter(comment =>
      props.commentBelongToQueue(props.activeTab, comment)
    ).length;
  }

  async selectDown() {
    const view = this.state.view;
    const index = view.findIndex(
      ({ id }) => id === this.props.selectedCommentId
    );
    if (
      index === view.length - 1 &&
      this.getCommentCountWithoutDagling() !== this.props.commentCount
    ) {
      await this.props.loadMore();
      this.selectDown();
      return;
    }
    if (index < view.length - 1) {
      this.props.selectCommentId(view[index + 1].id);
    }
  }

  selectUp() {
    const view = this.state.view;
    const index = view.findIndex(
      ({ id }) => id === this.props.selectedCommentId
    );

    if (index === 0 && view.length < this.props.comments.length) {
      this.viewNewComments(() => this.selectUp());
      return;
    }
    if (index > 0) {
      this.props.selectCommentId(view[index - 1].id);
    }
  }

  handleListRef = list => {
    this.listRef = list;
  };

  scrollToSelectedComment = (props = this.props, state = this.state) => {
    if (props.singleMode) {
      document
        .querySelector(`#comment_${props.selectedCommentId}`)
        .scrollIntoView();
    } else if (this.listRef) {
      const view = state.view;
      const index = view.findIndex(({ id }) => id === props.selectedCommentId);
      this.listRef.scrollToRow(index);
    }
  };

  viewNewComments = callback => {
    this.setState(resetCursors, () => {
      this.reflowList();
      callback && callback();
    });
  };

  reflowList = throttle(index => {
    if (index >= 0) {
      cache.clear(index);
      this.listRef && this.listRef.recomputeRowHeights(index);
    } else {
      cache.clearAll();
      this.listRef && this.listRef.recomputeRowHeights();
    }
  }, 500);

  rowRenderer = ({
    index, // Index of row within collection
    parent,
    style, // Style object to be applied to row (to position it)
  }) => {
    const view = this.state.view;
    const rowCount = view.length + 1;

    let child = null;
    let key = null;

    // Last element of list is our AutoLoadMore component and contains an
    // id indicating that this is the last element in list.
    if (index === rowCount - 1) {
      key = 'end-of-comment-list';
      child = (
        <div style={style} id={'end-of-comment-list'}>
          {this.props.hasNextPage && (
            <AutoLoadMore
              loadMore={this.props.loadMore}
              loading={this.props.isLoadingMore}
            />
          )}
        </div>
      );
    } else {
      const comment = view[index];

      // Use callback cache so not to change the identity of these arrow functions.
      // Otherwise shallow compare will fail to optimize.
      if (!this.callbackCaches.clearHeightCache[index]) {
        this.callbackCaches.clearHeightCache[index] = () =>
          this.reflowList(index);
      }
      if (!this.callbackCaches.selectCommentId[comment.id]) {
        this.callbackCaches.selectCommentId[comment.id] = () =>
          this.props.selectCommentId(comment.id);
      }

      key = comment.id;
      child = (
        <div style={style}>
          <Comment
            root={this.props.root}
            comment={comment}
            dangling={
              !this.props.commentBelongToQueue(this.props.activeTab, comment)
            }
            selected={comment.id === this.props.selectedCommentId}
            viewUserDetail={this.props.viewUserDetail}
            acceptComment={this.props.acceptComment}
            rejectComment={this.props.rejectComment}
            currentAsset={this.props.currentAsset}
            currentUserId={this.props.currentUserId}
            clearHeightCache={this.callbackCaches.clearHeightCache[index]}
            selectComment={this.callbackCaches.selectCommentId[comment.id]}
          />
        </div>
      );
    }

    return (
      <CellMeasurer
        cache={cache}
        columnIndex={0}
        key={key}
        parent={parent}
        rowIndex={index}
      >
        {child}
      </CellMeasurer>
    );
  };

  render() {
    const {
      comments,
      selectedCommentId,
      singleView,
      viewUserDetail,
      ...props
    } = this.props;

    if (comments.length === 0) {
      return (
        <div className={styles.root}>
          <EmptyCard>{t('modqueue.empty_queue')}</EmptyCard>
        </div>
      );
    }

    if (singleView) {
      const index = comments.findIndex(
        comment => comment.id === selectedCommentId
      );

      // This can happen temporarily when we call redux to change the selected comment
      // but it didn't fully take effect yet.
      if (index === -1) {
        return null;
      }

      const comment = comments[index];
      return (
        <div className={styles.root}>
          <Comment
            root={this.props.root}
            key={comment.id}
            comment={comment}
            selected={true}
            viewUserDetail={viewUserDetail}
            acceptComment={props.acceptComment}
            rejectComment={props.rejectComment}
            currentAsset={props.currentAsset}
            currentUserId={this.props.currentUserId}
            dangling={
              !this.props.commentBelongToQueue(this.props.activeTab, comment)
            }
          />
        </div>
      );
    }

    const view = this.state.view;

    return (
      <div className={styles.root}>
        <ViewMore
          viewMore={() => this.viewNewComments()}
          count={comments.length - view.length}
        />
        <WindowScroller onResize={this.reflowList}>
          {({ height, isScrolling, onChildScroll, scrollTop }) => (
            <List
              ref={this.handleListRef}
              autoHeight
              className={styles.list}
              style={{
                width: '100%',
              }}
              height={height}
              width={1280}
              scrollTop={scrollTop}
              isScrolling={isScrolling}
              onScroll={onChildScroll}
              rowCount={view.length + 1}
              deferredMeasurementCache={cache}
              rowRenderer={this.rowRenderer}
              rowHeight={cache.rowHeight}
            />
          )}
        </WindowScroller>
      </div>
    );
  }
}

ModerationQueue.propTypes = {
  selectCommentId: PropTypes.func.isRequired,
  selectedCommentId: PropTypes.string,
  viewUserDetail: PropTypes.func.isRequired,
  currentAsset: PropTypes.object,
  rejectComment: PropTypes.func.isRequired,
  acceptComment: PropTypes.func.isRequired,
  commentBelongToQueue: PropTypes.func.isRequired,
  cleanUpQueue: PropTypes.func.isRequired,
  commentCount: PropTypes.number.isRequired,
  loadMore: PropTypes.func.isRequired,
  singleView: PropTypes.bool,
  isLoadingMore: PropTypes.bool,
  hasNextPage: PropTypes.bool,
  comments: PropTypes.array,
  activeTab: PropTypes.string.isRequired,
  root: PropTypes.object.isRequired,
  currentUserId: PropTypes.string.isRequired,
};

export default ModerationQueue;
