/**
 * Edit html block.
 * @module components/manage/Blocks/HTML/Edit
 */

import { compose } from 'redux';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Popup } from 'semantic-ui-react';
import { defineMessages, injectIntl } from 'react-intl';
import loadable from '@loadable/component';
import isEqual from 'lodash/isEqual';

import Icon from '@plone/volto/components/theme/Icon/Icon';
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import showSVG from '@plone/volto/icons/show.svg';
import clearSVG from '@plone/volto/icons/clear.svg';
import codeSVG from '@plone/volto/icons/code.svg';
import indentSVG from '@plone/volto/icons/indent.svg';

const Editor = loadable(() => import('react-simple-code-editor'));

const messages = defineMessages({
  source: {
    id: 'Source',
    defaultMessage: 'Source',
  },
  preview: {
    id: 'Preview',
    defaultMessage: 'Preview',
  },
  placeholder: {
    id: '<p>Add some HTML here</p>',
    defaultMessage: '<p>Add some HTML here</p>',
  },
  prettier: {
    id: 'Prettify your code',
    defaultMessage: 'Prettify your code',
  },
  clear: {
    id: 'Clear',
    defaultMessage: 'Clear',
  },
  code: {
    id: 'Code',
    defaultMessage: 'Code',
  },
});

/**
 * Edit html block class.
 * @class Edit
 * @extends Component
 */
class Edit extends Component {
  /**
   * Property types.
   * @property {Object} propTypes Property types.
   * @static
   */
  static propTypes = {
    selected: PropTypes.bool.isRequired,
    block: PropTypes.string.isRequired,
    index: PropTypes.number.isRequired,
    data: PropTypes.objectOf(PropTypes.any).isRequired,
    onChangeBlock: PropTypes.func.isRequired,
    onSelectBlock: PropTypes.func.isRequired,
    onDeleteBlock: PropTypes.func.isRequired,
    handleKeyDown: PropTypes.func.isRequired,
    editable: PropTypes.bool,
  };

  /**
   * Default properties
   * @property {Object} defaultProps Default properties.
   * @static
   */
  static defaultProps = {
    editable: true,
  };
  /**
   * Constructor
   * @method constructor
   * @param {Object} props Component properties
   */
  constructor(props) {
    super(props);
    this.state = {
      isPreview: false,
    };
    this.onChangeCode = this.onChangeCode.bind(this);
    this.onPreview = this.onPreview.bind(this);
    this.onCodeEditor = this.onCodeEditor.bind(this);
  }

  codeEditorRef = React.createRef();
  savedSelection = {};

  componentDidUpdate(prevProps, prevState, snapshot) {
    // The selection is saved in the snapshot.
    this.savedSelection = snapshot;

    this.restoreSelectionAndFocus(this.codeEditorRef.current);
  }

  /**
   * @param {*} nextProps
   * @param {*} nextState
   * @returns {boolean}
   * @memberof Edit
   */
  shouldComponentUpdate(nextProps) {
    // Always rerender when the DOM node is not created for the Editor (the
    // first call to shouldComponentUpdate).
    if (!this._input) {
      return true;
    }

    // Rerender the entire component when the Editor in it changes its selection
    // because this way we get a call to getSnapshotBeforeUpdate where we can
    // save the selection.
    return (
      this.props.selected ||
      !isEqual(this.props.data, nextProps.data) ||
      this._input.selectionStart !== this.savedSelection.selectionStart ||
      this._input.selectionEnd !== this.savedSelection.selectionEnd
    );
  }

  /**
   * Change html handler
   * @method onChangeCode
   * @param {string} code New value html
   * @returns {undefined}
   */
  onChangeCode(code) {
    this.props.onChangeBlock(this.props.block, {
      ...this.props.data,
      html: code,
    });
  }

  getValue() {
    return this.props.data.html || '';
  }

  /**
   * Preview mode handler
   * @method onPreview
   * @returns {undefined}
   */
  async onPreview() {
    try {
      const code = (
        await this.props.prettierStandalone.format(this.getValue(), {
          parser: 'html',
          plugins: [this.props.prettierParserHtml],
        })
      ).trim();
      this.setState(
        {
          isPreview: !this.state.isPreview,
        },
        () => this.onChangeCode(code),
      );
    } catch (ex) {
      // error while parsing the user-typed HTML
      // TODO: show a toast notification or something similar to the user
    }
  }

  /**
   * Prettify handler
   * @method onPrettify
   * @returns {undefined}
   */
  onPrettify = async () => {
    try {
      const code = (
        await this.props.prettierStandalone.format(this.getValue(), {
          parser: 'html',
          plugins: [this.props.prettierParserHtml],
        })
      ).trim();
      this.onChangeCode(code);
    } catch (ex) {
      // error while parsing the user-typed HTML
      // TODO: show a toast notification or something similar to the user
    }
  };

  /**
   * Code Editor mode handler
   * @method onPreview
   * @returns {undefined}
   */
  onCodeEditor() {
    this.setState({ isPreview: !this.state.isPreview });
  }

  getSelection = (editor) => {
    if (!editor || !editor._input) {
      return {};
    }

    const o = {};
    if (editor._input.selectionStart) {
      o.selectionStart = editor._input.selectionStart;
    }
    if (editor._input.selectionEnd) {
      o.selectionEnd = editor._input.selectionEnd;
    }
    return o;
  };

  getSnapshotBeforeUpdate(prevProps, prevState) {
    return this.getSelection(this.codeEditorRef.current);
  }

  restoreSelectionAndFocus = (editor) => {
    // Don't restore selection when the block is not selected.
    if (
      this.props.selected &&
      editor._input &&
      typeof this.savedSelection?.selectionStart === 'number' &&
      typeof this.savedSelection?.selectionEnd === 'number'
    ) {
      editor._input.selectionStart = this.savedSelection?.selectionStart;
      editor._input.selectionEnd = this.savedSelection?.selectionEnd;
      editor._input.focus();
    }
  };

  /**
   * Render method.
   * @method render
   * @returns {string} Markup for the component.
   */
  render() {
    const placeholder =
      this.props.data.placeholder ||
      this.props.intl.formatMessage(messages.placeholder);
    const value = this.getValue();
    return (
      <>
        {this.props.selected && value && (
          <div className="toolbar">
            <Popup
              trigger={
                <Button
                  type="button"
                  icon
                  basic
                  aria-label={this.props.intl.formatMessage(messages.source)}
                  active={!this.state.isPreview}
                  onClick={this.onCodeEditor}
                >
                  <Icon name={codeSVG} size="24px" />
                </Button>
              }
              position="top center"
              content={this.props.intl.formatMessage(messages.code)}
              size="mini"
            />
            <Popup
              trigger={
                <Button
                  type="button"
                  icon
                  basic
                  aria-label={this.props.intl.formatMessage(messages.preview)}
                  active={this.state.isPreview}
                  onClick={this.onPreview}
                >
                  <Icon name={showSVG} size="24px" />
                </Button>
              }
              position="top center"
              content={this.props.intl.formatMessage(messages.preview)}
              size="mini"
            />
            <Popup
              trigger={
                <Button
                  type="button"
                  icon
                  basic
                  aria-label={this.props.intl.formatMessage(messages.prettier)}
                  onClick={this.onPrettify}
                >
                  <Icon name={indentSVG} size="24px" />
                </Button>
              }
              position="top center"
              content={this.props.intl.formatMessage(messages.prettier)}
              size="mini"
            />
            <div className="separator" />
            <Popup
              trigger={
                <Button.Group>
                  <Button
                    type="button"
                    icon
                    basic
                    onClick={() => this.onChangeCode('')}
                  >
                    <Icon name={clearSVG} size="24px" color="#e40166" />
                  </Button>
                </Button.Group>
              }
              position="top center"
              content={this.props.intl.formatMessage(messages.clear)}
              size="mini"
            />
          </div>
        )}
        {this.state.isPreview ? (
          <div dangerouslySetInnerHTML={{ __html: value }} />
        ) : (
          <Editor
            value={this.getValue()}
            readOnly={!this.props.editable}
            placeholder={placeholder}
            onValueChange={(code) => this.onChangeCode(code)}
            highlight={
              this.props.prismCore?.highlight &&
              this.props.prismCore?.languages?.html
                ? (code) =>
                    this.props.prismCore.highlight(
                      code,
                      this.props.prismCore.languages.html,
                      'html',
                    )
                : () => {}
            }
            padding={8}
            className="html-editor"
            ref={(node) => {
              if (node) {
                this.codeEditorRef.current = node;
              }
            }}
            ignoreTabKey={true}
          />
        )}
      </>
    );
  }
}

const withPrismMarkup = (WrappedComponent) => (props) => {
  const [loaded, setLoaded] = React.useState();
  const promise = React.useRef(null);
  const cancelled = React.useRef(false);

  React.useEffect(() => {
    promise.current = import('prismjs/components/prism-markup');
    promise.current.then(() => {
      if (!cancelled.current) {
        setLoaded(true);
      }
    });
    return () => {
      cancelled.current = true;
    };
  }, []);

  return loaded ? <WrappedComponent {...props} /> : null;
};

export default compose(
  injectLazyLibs(['prettierStandalone', 'prettierParserHtml', 'prismCore']),
  withPrismMarkup,
  injectIntl,
)(Edit);
