import algoliasearch from 'algoliasearch/lite';
import loFind from 'lodash/find';
import pick from 'lodash/pick';
import PropTypes from 'prop-types';
import React from 'react';
import { Configure, InstantSearch } from 'react-instantsearch-dom';
import RefinementList from './components/RefinementList';
import SearchBox from './components/SearchBox';
import SearchResults from './components/SearchResults';
import SearchTabs from './components/SearchTabs';

// Style
let classes = {};
/* istanbul ignore next */
if (typeof window !== 'undefined' || typeof _COMPILED_ === 'undefined') classes = require('./style.module.scss');

class AlgoliaSearch extends React.Component {
  constructor(props) {
    super(props);

    const enabledModules = this.enableModuleMap(props.projectsMeta);
    this.state = {
      activeTab: 'All',
      filters: '',
      refinementListOpen: true,
      tabs: {
        All: { filters: '' },
        Guides: { filters: 'indexName:Page AND isReference:false' },
        Tutorials: { filters: 'indexName:Tutorial' },
        Reference: { filters: 'indexName:Page AND isReference:true' },
        Changelog: { filters: 'indexName:Blog' },
        Discussions: { filters: 'indexName:Discuss' },
        Pages: { filters: 'indexName:CustomPage' },
      },
      tabFilters: this.setTabFilters([], enabledModules, true),
      enabledModules,
      initializeAlgolia: 'PROXY',
    };
    this.handleTabChange = this.handleTabChange.bind(this);
    this.handleFocusIn = this.handleFocusIn.bind(this);
    this.rootNode = React.createRef();

    if (typeof window !== 'undefined' && 'matchMedia' in window) {
      this.MQ = window.matchMedia('(max-width: 700px)');
      this.MQ.addListener(() => this.setState({ refinementListOpen: !this.MQ.matches }));
    }
  }

  /* istanbul ignore next */
  // eslint-disable-next-line class-methods-use-this
  handleKeyboard(e) {
    // keyboard event handling should fail silently
    try {
      const { key } = e;

      const search = document.querySelector(`.${classes.AlgoliaSearch}`);
      const searchBox = search.querySelector('input[type=search]');
      const searchHits = search.querySelector('.SearchResults-list');
      const searchPage = search.querySelector('footer');
      const focused = document.activeElement;

      if (searchBox === focused) {
        if (key === 'Tab' && e.shiftKey) {
          searchBox.select();
          e.preventDefault();
        } else if (key === 'Tab' || key === 'ArrowDown' || key === 'Enter' || key === 'ArrowUp') {
          const firstlastChild = key === 'ArrowUp' ? 'lastElementChild' : 'firstElementChild';
          searchHits[firstlastChild].focus();
          e.preventDefault();
        }
      } else if (!(searchPage && searchPage.matches(':focus-within')) && key === 'Tab' && e.shiftKey) {
        searchBox.focus();
        e.preventDefault();
      } else if (searchHits && !searchHits.matches(':focus-within') && (key === 'ArrowDown' || key === 'ArrowUp')) {
        searchHits[key === 'ArrowUp' ? 'lastElementChild' : 'firstElementChild'].focus();
        e.stopPropagation();
        e.preventDefault();
      }
    } catch (error) {
      // eslint-disable-next-line no-console
      console.warn(error);
    }
  }

  handleTabChange(tab) {
    /* handle keyboard events
     */
    if (typeof window !== 'undefined' && typeof tab === 'object') {
      this.handleKeyboard(tab);
      return;
    }

    /* handle click events
     */
    if (tab in this.state.tabs) {
      this.setState({
        activeTab: tab,
        ...this.state.tabs[tab],
      });
    }
  }

  handleFocusIn() {
    if (this.state.initializeAlgolia === 'PROXY') this.setState({ initializeAlgolia: 'LIVE' });
  }

  handleRefinementBlur = e => {
    const searchBox = document.querySelector(`.${classes.AlgoliaSearch} input[type=search]`);
    searchBox.focus();
    searchBox.select();
    e.preventDefault();
    e.stopPropagation();
  };

  enableModuleMap = metadata => {
    return (metadata || []).reduce((obj, { name, subdomain, modules }) => {
      obj[name || subdomain] = modules;
      return obj;
    }, {});
  };

  setTabFilters = (filters = [], enabledModules, initiaLoad) => {
    const meta = enabledModules || this.state.enabledModules;
    const selectedModules = filters.length ? pick(meta, filters) : meta;

    const values = Object.values(selectedModules);
    const modules = Object.values(values).reduce((obj, child) => {
      Object.keys(child).forEach(key => {
        if (obj[key]) obj[key] = !!(obj[key] + child[key]);
        else obj[key] = !!child[key];
      });

      return obj;
    }, {});

    const tabFilters = Object.entries(modules)
      .filter(([, v]) => v)
      .map(([k]) => k);

    if (!initiaLoad) this.setState({ tabFilters });
    return tabFilters;
  };

  handleResultsReload = () => {
    const focused = document.activeElement;
    const search = document.querySelector(`.${classes.AlgoliaSearch}`);
    const searchHits = search.querySelector('.SearchResults-list');
    const searchBox = search.querySelector('input[type=search]');
    setTimeout(() => {
      if (searchHits) {
        let elem = searchHits.firstElementChild;
        if (focused.matches('[focus-hit]')) elem = searchHits[focused.attributes['focus-hit'].value];
        if (elem && searchBox !== focused) elem.focus();
      }
    }, 100);
  };

  render() {
    const { activeTab, filters } = this.state;
    const {
      appId,
      currentProject,
      defaultRefinement,
      indexName,
      searchApiKey,
      showRefinementList,
      style,
      projectsMeta = false,
      UrlManager,
    } = this.props;

    /**
     * Wrap the Algolia client in a conditional, so we don't hit their
     * API on empty searches to limit unnecessary requests. More here:
     * https://algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/react
     */
    const algoliaClient = {
      LIVE: algoliasearch(appId, searchApiKey),
      PROXY: {
        search: requests =>
          Promise.resolve({
            results: requests.map(() => ({
              hits: [],
              nbHits: 0,
              nbPages: 0,
              page: 0,
              processingTimeMS: 0,
            })),
          }),
      },
    };

    /* istanbul ignore next */
    const subdomains = !projectsMeta
      ? undefined
      : projectsMeta.reduce((all, { id, subdomain }) => {
          all[id] = subdomain;
          return all;
        }, {});
    const tabRename = !projectsMeta ? {} : loFind(projectsMeta, { id: currentProject });

    return (
      <InstantSearch indexName={indexName} searchClient={algoliaClient[this.state.initializeAlgolia]}>
        <Configure filters={filters} />
        <div
          ref={this.rootNode}
          className={classes.AlgoliaSearch}
          onFocus={this.handleFocusIn}
          onKeyDown={this.handleTabChange}
          role="tabpanel"
          style={style}
          tabIndex={0}
        >
          <div className={classes['AlgoliaSearch-Col']}>
            <SearchBox />
            <SearchTabs
              activeTab={activeTab}
              onTabChange={this.handleTabChange}
              tabFilters={this.state.tabFilters}
              tabNames={tabRename ? tabRename.nav_names : {}}
            />
            <SearchResults
              onReload={() => {
                if (typeof window !== 'undefined') this.handleResultsReload();
              }}
              subdomains={subdomains}
              tab="Guides"
              UrlManager={UrlManager}
            />
          </div>
          {showRefinementList && (
            <RefinementList
              attribute="subdomain"
              defaultRefinement={defaultRefinement}
              isOpen={this.state.refinementListOpen}
              onBlur={e => {
                if (typeof window !== 'undefined') this.handleRefinementBlur(e);
              }}
              onRefine={this.setTabFilters}
              showMore
            />
          )}
        </div>
      </InstantSearch>
    );
  }
}

AlgoliaSearch.propTypes = {
  appId: PropTypes.string.isRequired,
  currentProject: PropTypes.string,
  defaultRefinement: PropTypes.array,
  indexName: PropTypes.string.isRequired,
  projectsMeta: PropTypes.array,
  searchApiKey: PropTypes.string.isRequired,
  showRefinementList: PropTypes.any,
  style: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
  UrlManager: PropTypes.shape({
    options: PropTypes.object,
    remove: PropTypes.func.isRequired,
    upsert: PropTypes.func.isRequired,
  }).isRequired,
};

AlgoliaSearch.defaultProps = {
  showRefinementList: true,
};

export default AlgoliaSearch;
