import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import isEqual from 'react-fast-compare';
import { toast } from 'react-toastify';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toast from '@plone/volto/components/manage/Toast/Toast';
import { Button } from 'semantic-ui-react';
import checkSVG from '@plone/volto/icons/check.svg';
import clearSVG from '@plone/volto/icons/clear.svg';
import { useIntl, defineMessages } from 'react-intl';
import { useLocation } from 'react-router-dom';
import config from '@plone/volto/registry';

const messages = defineMessages({
  autoSaveFound: {
    id: 'Autosaved content found',
    defaultMessage: 'Autosaved content found',
  },
  loadData: {
    id: 'Do you want to restore your autosaved content?',
    defaultMessage: 'Do you want to restore your autosaved content?',
  },
  loadExpiredData: {
    id: "Another person edited this content, and it's currently displayed. Do you want to replace it with your autosaved content?",
    defaultMessage:
      "Another person edited this content, and it's currently displayed. Do you want to replace it with your autosaved content?",
  },
});

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

const mapSchemaToData = (schema, data) => {
  if (!data) return {};
  const dataKeys = Object.keys(data);
  return Object.assign(
    {},
    ...Object.keys(schema.properties)
      .filter((k) => dataKeys.includes(k))
      .map((k) => ({ [k]: data[k] })),
  );
};

// will be used to avoid using the first mount call if there is a second call
let mountTime;

const getFormId = (props, location) => {
  const { type, pathname = location.pathname, isEditForm, schema } = props;
  const id = isEditForm
    ? ['form', type, pathname].join('-')
    : type
      ? ['form', pathname, type].join('-')
      : schema?.properties?.comment
        ? ['form', pathname, 'comment'].join('-')
        : ['form', pathname].join('-');

  return id;
};

/**
 * Toast content that has OK and Cancel buttons
 * @param {function} onUpdate
 * @param {function} onClose
 * @param {string} userMessage
 * @returns
 */
const ConfirmAutoSave = ({ onUpdate, onClose, userMessage }) => {
  const handleClickOK = () => onUpdate();
  const handleClickCancel = () => onClose();

  return (
    <div className="toast-box-center">
      <div>{userMessage}</div>
      <Button
        icon
        aria-label="Unchecked"
        className="save toast-box"
        onClick={handleClickOK}
      >
        <Icon
          name={checkSVG}
          size="24px"
          className="circled toast-box-blue-icon"
        />
      </Button>
      <Button
        icon
        aria-label="Unchecked"
        className="save toast-box"
        onClick={handleClickCancel}
      >
        <Icon
          name={clearSVG}
          size="24px"
          className="circled toast-box-blue-icon"
        />
      </Button>
    </div>
  );
};

/**
 * Will remove localStorage item using debounce
 * @param {string} id
 * @param {number} timerForDeletion
 */
const clearStorage = (id, timerForDeletion) => {
  timerForDeletion.current && clearTimeout(timerForDeletion.current);
  timerForDeletion.current = setTimeout(() => {
    localStorage.removeItem(id);
  }, 500);
};

/**
 * Stale if server date is more recent
 * @param {string} serverModifiedDate
 * @param {string} autoSaveDate
 * @returns {Boolean}
 */
const autoSaveFoundIsStale = (serverModifiedDate, autoSaveDate) => {
  const result = !serverModifiedDate
    ? false
    : new Date(serverModifiedDate) > new Date(autoSaveDate);
  return result;
};

const draftApi = (id, schema, timer, timerForDeletion, intl) => ({
  // - since Add Content Type will call componentDidMount twice, we will
  // use the second call (using debounce)- the first will ignore any setState comands;
  // - Delete local data only if user confirms Cancel
  // - Will tell user that it has local stored data, even if its less recent than the server data
  checkSavedDraft(state, updateCallback) {
    if (!schema) return;
    const saved = localStorage.getItem(id);

    if (saved && Object.keys(JSON.parse(saved)).length > 1) {
      const formData = mapSchemaToData(schema, state);
      // includes autoSaveDate
      const foundSavedData = JSON.parse(saved);
      // includes only form data found in schema (no autoSaveDate)
      const foundSavedSchemaData = mapSchemaToData(schema, foundSavedData);

      if (!isEqual(formData, foundSavedSchemaData)) {
        // eslint-disable-next-line no-alert
        // cancel existing setTimeout to avoid using first call if
        // successive calls are made
        mountTime && clearTimeout(mountTime);
        mountTime = setTimeout(() => {
          toast.info(
            <Toast
              position="top-right"
              info
              autoClose={false}
              title={intl.formatMessage(messages.autoSaveFound)}
              content={
                <ConfirmAutoSave
                  onUpdate={() => updateCallback(foundSavedSchemaData)}
                  onClose={() => clearStorage(id, timerForDeletion)}
                  userMessage={
                    autoSaveFoundIsStale(
                      state.modified,
                      foundSavedData.autoSaveDate,
                    )
                      ? intl.formatMessage(messages.loadExpiredData)
                      : intl.formatMessage(messages.loadData)
                  }
                />
              }
            />,
          );
        }, 300);
      }
    }
  },
  // use debounce mode
  onSaveDraft(state) {
    if (!schema) return;
    timer.current && clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      const formData = mapSchemaToData(schema, state);
      const saved = localStorage.getItem(id);
      const newData = JSON.parse(saved);

      localStorage.setItem(
        id,
        JSON.stringify({
          ...newData,
          ...formData,
          autoSaveDate: new Date(),
        }),
      );
    }, 300);
  },

  onCancelDraft() {
    if (!schema) return;
    clearStorage(id, timerForDeletion);
  },
});

export default function withSaveAsDraft(options) {
  const { forwardRef } = options;

  return (WrappedComponent) => {
    function WithSaveAsDraft(props) {
      const { schema } = props;
      const intl = useIntl();
      const location = useLocation();
      const id = getFormId(props, location);
      const timmeRef = React.useRef();
      const timmerForDeletionRef = React.useRef();
      const api = React.useMemo(
        () => draftApi(id, schema, timmeRef, timmerForDeletionRef, intl),
        [id, schema, timmeRef, timmerForDeletionRef, intl],
      );

      return (
        <WrappedComponent
          {...props}
          {...api}
          ref={forwardRef ? props.forwardedRef : null}
        />
      );
    }

    WithSaveAsDraft.displayName = `WithSaveAsDraft(${getDisplayName(
      WrappedComponent,
    )})`;

    const DraftWrappedComponent = forwardRef
      ? hoistNonReactStatics(
          React.forwardRef((props, ref) => (
            <WithSaveAsDraft {...props} forwardedRef={ref} />
          )),
          WrappedComponent,
        )
      : hoistNonReactStatics(WithSaveAsDraft, WrappedComponent);

    const WithExperimentalSaveAsDraft = forwardRef
      ? hoistNonReactStatics(
          React.forwardRef((props, ref) => {
            const ComponentToRender = config.experimental?.saveAsDraft?.enabled
              ? DraftWrappedComponent
              : WrappedComponent;

            return <ComponentToRender {...props} ref={ref} />;
          }),
          WrappedComponent,
        )
      : hoistNonReactStatics((props) => {
          const ComponentToRender = config.experimental?.saveAsDraft?.enabled
            ? DraftWrappedComponent
            : WrappedComponent;

          return <ComponentToRender {...props} />;
        }, WrappedComponent);

    WithExperimentalSaveAsDraft.displayName = `WithExperimentalSaveAsDraft(${getDisplayName(
      WrappedComponent,
    )})`;

    return WithExperimentalSaveAsDraft;
  };
}
