import Page from './Page';
import ItemList from '../../common/utils/ItemList';
import DiscussionHero from './DiscussionHero';
import PostStream from './PostStream';
import PostStreamScrubber from './PostStreamScrubber';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import SplitDropdown from '../../common/components/SplitDropdown';
import listItems from '../../common/helpers/listItems';
import DiscussionControls from '../utils/DiscussionControls';

/**
 * The `DiscussionPage` component displays a whole discussion page, including
 * the discussion list pane, the hero, the posts, and the sidebar.
 */
export default class DiscussionPage extends Page {
  init() {
    super.init();

    /**
     * The discussion that is being viewed.
     *
     * @type {Discussion}
     */
    this.discussion = null;

    /**
     * The number of the first post that is currently visible in the viewport.
     *
     * @type {Integer}
     */
    this.near = null;

    this.refresh();

    // If the discussion list has been loaded, then we'll enable the pane (and
    // hide it by default). Also, if we've just come from another discussion
    // page, then we don't want Mithril to redraw the whole page – if it did,
    // then the pane would which would be slow and would cause problems with
    // event handlers.
    if (app.cache.discussionList) {
      app.pane.enable();
      app.pane.hide();

      if (app.previous instanceof DiscussionPage) {
        m.redraw.strategy('diff');
      }
    }

    app.history.push('discussion');

    this.bodyClass = 'App--discussion';
  }

  onunload(e) {
    // If we have routed to the same discussion as we were viewing previously,
    // cancel the unloading of this controller and instead prompt the post
    // stream to jump to the new 'near' param.
    if (this.discussion) {
      const idParam = m.route.param('id');

      if (idParam && idParam.split('-')[0] === this.discussion.id()) {
        e.preventDefault();

        const near = m.route.param('near') || '1';

        if (near !== String(this.near)) {
          this.stream.goToNumber(near);
        }

        this.near = null;
        return;
      }
    }

    // If we are indeed navigating away from this discussion, then disable the
    // discussion list pane. Also, if we're composing a reply to this
    // discussion, minimize the composer – unless it's empty, in which case
    // we'll just close it.
    app.pane.disable();

    if (app.composingReplyTo(this.discussion) && !app.composer.component.content()) {
      app.composer.hide();
    } else {
      app.composer.minimize();
    }
  }

  view() {
    const discussion = this.discussion;

    return (
      <div className="DiscussionPage">
        {app.cache.discussionList
          ? <div className="DiscussionPage-list" config={this.configPane.bind(this)}>
              {!$('.App-navigation').is(':visible') ? app.cache.discussionList.render() : ''}
            </div>
          : ''}

        <div className="DiscussionPage-discussion">
          {discussion
            ? [
              DiscussionHero.component({discussion}),
              <div className="container">
                <nav className="DiscussionPage-nav">
                  <ul>{listItems(this.sidebarItems().toArray())}</ul>
                </nav>
                <div className="DiscussionPage-stream">
                  {this.stream.render()}
                </div>
              </div>
            ]
            : LoadingIndicator.component({className: 'LoadingIndicator--block'})}
        </div>
      </div>
    );
  }

  config(...args) {
    super.config(...args);

    if (this.discussion) {
      app.setTitle(this.discussion.title());
    }
  }

  /**
   * Clear and reload the discussion.
   */
  refresh() {
    this.near = m.route.param('near') || 0;
    this.discussion = null;

    const preloadedDiscussion = app.preloadedApiDocument();
    if (preloadedDiscussion) {
      // We must wrap this in a setTimeout because if we are mounting this
      // component for the first time on page load, then any calls to m.redraw
      // will be ineffective and thus any configs (scroll code) will be run
      // before stuff is drawn to the page.
      setTimeout(this.show.bind(this, preloadedDiscussion), 0);
    } else {
      const params = this.requestParams();

      app.store.find('discussions', m.route.param('id').split('-')[0], params)
        .then(this.show.bind(this));
    }

    m.lazyRedraw();
  }

  /**
   * Get the parameters that should be passed in the API request to get the
   * discussion.
   *
   * @return {Object}
   */
  requestParams() {
    return {
      page: {near: this.near}
    };
  }

  /**
   * Initialize the component to display the given discussion.
   *
   * @param {Discussion} discussion
   */
  show(discussion) {
    this.discussion = discussion;

    app.history.push('discussion', discussion.title());
    app.setTitleCount(0);

    // When the API responds with a discussion, it will also include a number of
    // posts. Some of these posts are included because they are on the first
    // page of posts we want to display (determined by the `near` parameter) –
    // others may be included because due to other relationships introduced by
    // extensions. We need to distinguish the two so we don't end up displaying
    // the wrong posts. We do so by filtering out the posts that don't have
    // the 'discussion' relationship linked, then sorting and splicing.
    let includedPosts = [];
    if (discussion.payload && discussion.payload.included) {
      const discussionId = discussion.id();

      includedPosts = discussion.payload.included
        .filter(record => record.type === 'posts'
          && record.relationships
          && record.relationships.discussion
          && record.relationships.discussion.data.id === discussionId)
        .map(record => app.store.getById('posts', record.id))
        .sort((a, b) => a.id() - b.id())
        .slice(0, 20);
    }

    // Set up the post stream for this discussion, along with the first page of
    // posts we want to display. Tell the stream to scroll down and highlight
    // the specific post that was routed to.
    this.stream = new PostStream({discussion, includedPosts});
    this.stream.on('positionChanged', this.positionChanged.bind(this));
    this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
  }

  /**
   * Configure the discussion list pane.
   *
   * @param {DOMElement} element
   * @param {Boolean} isInitialized
   * @param {Object} context
   */
  configPane(element, isInitialized, context) {
    if (isInitialized) return;

    context.retain = true;

    const $list = $(element);

    // When the mouse enters and leaves the discussions pane, we want to show
    // and hide the pane respectively. We also create a 10px 'hot edge' on the
    // left of the screen to activate the pane.
    const pane = app.pane;
    $list.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));

    const hotEdge = e => {
      if (e.pageX < 10) pane.show();
    };
    $(document).on('mousemove', hotEdge);
    context.onunload = () => $(document).off('mousemove', hotEdge);

    // If the discussion we are viewing is listed in the discussion list, then
    // we will make sure it is visible in the viewport – if it is not we will
    // scroll the list down to it.
    const $discussion = $list.find('.DiscussionListItem.active');
    if ($discussion.length) {
      const listTop = $list.offset().top;
      const listBottom = listTop + $list.outerHeight();
      const discussionTop = $discussion.offset().top;
      const discussionBottom = discussionTop + $discussion.outerHeight();

      if (discussionTop < listTop || discussionBottom > listBottom) {
        $list.scrollTop($list.scrollTop() - listTop + discussionTop);
      }
    }
  }

  /**
   * Build an item list for the contents of the sidebar.
   *
   * @return {ItemList}
   */
  sidebarItems() {
    const items = new ItemList();

    items.add('controls',
      SplitDropdown.component({
        children: DiscussionControls.controls(this.discussion, this).toArray(),
        icon: 'fas fa-ellipsis-v',
        className: 'App-primaryControl',
        buttonClassName: 'Button--primary'
      })
    );

    items.add('scrubber',
      PostStreamScrubber.component({
        stream: this.stream,
        className: 'App-titleControl'
      }),
      -100
    );

    return items;
  }

  /**
   * When the posts that are visible in the post stream change (i.e. the user
   * scrolls up or down), then we update the URL and mark the posts as read.
   *
   * @param {Integer} startNumber
   * @param {Integer} endNumber
   */
  positionChanged(startNumber, endNumber) {
    const discussion = this.discussion;

    // Construct a URL to this discussion with the updated position, then
    // replace it into the window's history and our own history stack.
    const url = app.route.discussion(discussion, this.near = startNumber);

    m.route(url, true);
    window.history.replaceState(null, document.title, url);

    app.history.push('discussion', discussion.title());

    // If the user hasn't read past here before, then we'll update their read
    // state and redraw.
    if (app.session.user && endNumber > (discussion.lastReadPostNumber() || 0)) {
      discussion.save({lastReadPostNumber: endNumber});
      m.redraw();
    }
  }
}
