/**
 * View container.
 * @module components/theme/View/View
 */

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { Redirect } from 'react-router-dom';
import { createPortal } from 'react-dom';
import { injectIntl } from 'react-intl';
import qs from 'query-string';

import ContentMetadataTags from '@plone/volto/components/theme/ContentMetadataTags/ContentMetadataTags';
import Comments from '@plone/volto/components/theme/Comments/Comments';
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
import { listActions } from '@plone/volto/actions/actions/actions';
import { getContent } from '@plone/volto/actions/content/content';
import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url';
import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content';
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs';

import config from '@plone/volto/registry';
import SlotRenderer from '../SlotRenderer/SlotRenderer';

/**
 * View container class.
 * @class View
 * @extends Component
 */
class View extends Component {
  /**
   * Property types.
   * @property {Object} propTypes Property types.
   * @static
   */
  static propTypes = {
    actions: PropTypes.shape({
      object: PropTypes.arrayOf(PropTypes.object),
      object_buttons: PropTypes.arrayOf(PropTypes.object),
      user: PropTypes.arrayOf(PropTypes.object),
    }),
    listActions: PropTypes.func.isRequired,
    /**
     * Action to get the content
     */
    getContent: PropTypes.func.isRequired,
    /**
     * Pathname of the object
     */
    pathname: PropTypes.string.isRequired,
    location: PropTypes.shape({
      search: PropTypes.string,
      pathname: PropTypes.string,
    }).isRequired,
    /**
     * Version id of the object
     */
    versionId: PropTypes.string,
    /**
     * Content of the object
     */
    content: PropTypes.shape({
      /**
       * Layout of the object
       */
      layout: PropTypes.string,
      /**
       * Allow discussion of the object
       */
      allow_discussion: PropTypes.bool,
      /**
       * Title of the object
       */
      title: PropTypes.string,
      /**
       * Description of the object
       */
      description: PropTypes.string,
      /**
       * Type of the object
       */
      '@type': PropTypes.string,
      /**
       * Subjects of the object
       */
      subjects: PropTypes.arrayOf(PropTypes.string),
      is_folderish: PropTypes.bool,
    }),
    error: PropTypes.shape({
      /**
       * Error type
       */
      status: PropTypes.number,
    }),
  };

  /**
   * Default properties.
   * @property {Object} defaultProps Default properties.
   * @static
   */
  static defaultProps = {
    actions: null,
    content: null,
    versionId: null,
    error: null,
  };

  state = {
    hasObjectButtons: null,
    isClient: false,
  };

  componentDidMount() {
    // Do not trigger the actions action if the expander is present
    if (!hasApiExpander('actions', getBaseUrl(this.props.pathname))) {
      this.props.listActions(getBaseUrl(this.props.pathname));
    }

    this.props.getContent(
      getBaseUrl(this.props.pathname),
      this.props.versionId,
    );
    this.setState({ isClient: true });
  }

  /**
   * Component will receive props
   * @method componentWillReceiveProps
   * @param {Object} nextProps Next properties
   * @returns {undefined}
   */
  UNSAFE_componentWillReceiveProps(nextProps) {
    if (nextProps.pathname !== this.props.pathname) {
      // Do not trigger the actions action if the expander is present
      if (!hasApiExpander('actions', getBaseUrl(nextProps.pathname))) {
        this.props.listActions(getBaseUrl(nextProps.pathname));
      }

      this.props.getContent(
        getBaseUrl(nextProps.pathname),
        this.props.versionId,
      );
    }

    if (nextProps.actions.object_buttons) {
      const objectButtons = nextProps.actions.object_buttons;
      this.setState({
        hasObjectButtons: !!objectButtons.length,
      });
    }
  }

  /**
   * Default fallback view
   * @method getViewDefault
   * @returns {string} Markup for component.
   */
  getViewDefault = () => config.views.defaultView;

  /**
   * Get view by content type
   * @method getViewByType
   * @returns {string} Markup for component.
   */
  getViewByType = () =>
    config.views.contentTypesViews[this.props.content['@type']] || null;

  /**
   * Get view by content layout property
   * @method getViewByLayout
   * @returns {string} Markup for component.
   */
  getViewByLayout = () =>
    config.views.layoutViews[
      this.props.content[getLayoutFieldname(this.props.content)]
    ] || null;

  /**
   * Cleans the component displayName (specially for connected components)
   * which have the Connect(componentDisplayName)
   * @method cleanViewName
   * @param  {string} dirtyDisplayName The displayName
   * @returns {string} Clean displayName (no Connect(...)).
   */
  cleanViewName = (dirtyDisplayName) =>
    dirtyDisplayName
      .replace('Connect(', '')
      .replace('injectIntl(', '')
      .replace(')', '')
      .replace('connect(', '')
      .toLowerCase();

  /**
   * Render method.
   * @method render
   * @returns {string} Markup for the component.
   */
  render() {
    const { views } = config;
    if ([301, 302].includes(this.props.error?.code)) {
      const redirect = flattenToAppURL(this.props.error.url)
        .split('?')[0]
        .replace('/++api++', '');
      return <Redirect to={`${redirect}${this.props.location.search}`} />;
    } else if (this.props.error && !this.props.connectionRefused) {
      let FoundView;
      if (this.props.error.status === undefined) {
        // For some reason, while development and if CORS is in place and the
        // requested resource is 404, it returns undefined as status, then the
        // next statement will fail
        FoundView = views.errorViews.corsError;
      } else {
        FoundView = views.errorViews[this.props.error.status.toString()];
      }
      if (!FoundView) {
        FoundView = views.errorViews['404']; // default to 404
      }
      return (
        <div id="view">
          <BodyClass
            className={
              FoundView.displayName
                ? `view-${this.cleanViewName(FoundView.displayName)}`
                : null
            }
          />
          <FoundView {...this.props} />
        </div>
      );
    }
    if (!this.props.content) {
      return <span />;
    }
    const RenderedView =
      this.getViewByLayout() || this.getViewByType() || this.getViewDefault();

    return (
      <div id="view" tabIndex="-1">
        <ContentMetadataTags content={this.props.content} />
        <AlternateHrefLangs content={this.props.content} />
        {/* Body class if displayName in component is set */}
        <BodyClass
          className={
            RenderedView.displayName
              ? `view-${this.cleanViewName(RenderedView.displayName)}`
              : null
          }
        />
        <SlotRenderer name="aboveContent" content={this.props.content} />
        <RenderedView
          key={flattenToAppURL(this.props.content['@id'])}
          content={this.props.content}
          location={this.props.location}
          token={this.props.token}
          history={this.props.history}
        />
        <SlotRenderer name="belowContent" content={this.props.content} />
        {this.props.content.allow_discussion && (
          <Comments pathname={this.props.pathname} />
        )}
        {this.state.isClient &&
          createPortal(
            <Toolbar pathname={this.props.pathname} inner={<span />} />,
            document.getElementById('toolbar'),
          )}
      </div>
    );
  }
}

export default compose(
  injectIntl,
  connect(
    (state, props) => ({
      actions: state.actions.actions,
      token: state.userSession.token,
      content: state.content.data,
      error: state.content.get.error,
      apiError: state.apierror.error,
      connectionRefused: state.apierror.connectionRefused,
      pathname: props.location.pathname,
      versionId:
        qs.parse(props.location.search) &&
        qs.parse(props.location.search).version,
    }),
    {
      listActions,
      getContent,
    },
  ),
)(View);
