import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Map } from 'immutable';
import { get } from 'lodash';
import { connect } from 'react-redux';
import history from 'Routing/history';
import { logoutUser } from 'Actions/auth';
import {
  loadEntry,
  loadEntries,
  createDraftFromEntry,
  createEmptyDraft,
  discardDraft,
  changeDraftField,
  changeDraftFieldValidation,
  persistEntry,
  deleteEntry,
} from 'Actions/entries';
import {
  updateUnpublishedEntryStatus,
  publishUnpublishedEntry,
  deleteUnpublishedEntry
} from 'Actions/editorialWorkflow';
import { deserializeValues } from 'Lib/serializeEntryValues';
import { addAsset } from 'Actions/media';
import { openMediaLibrary, removeInsertedMedia } from 'Actions/mediaLibrary';
import { selectEntry, selectUnpublishedEntry, getAsset } from 'Reducers';
import { selectFields } from 'Reducers/collections';
import { Loader } from 'UI';
import { status } from 'Constants/publishModes';
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
import EditorInterface from './EditorInterface';
import withWorkflow from './withWorkflow';

const navigateCollection = (collectionPath) => history.push(`/collections/${collectionPath}`);
const navigateToCollection = collectionName => navigateCollection(collectionName);
const navigateToNewEntry = collectionName => navigateCollection(`${collectionName}/new`);
const navigateToEntry = (collectionName, slug) => navigateCollection(`${collectionName}/entries/${slug}`);

class Editor extends React.Component {
  static propTypes = {
    addAsset: PropTypes.func.isRequired,
    boundGetAsset: PropTypes.func.isRequired,
    changeDraftField: PropTypes.func.isRequired,
    changeDraftFieldValidation: PropTypes.func.isRequired,
    collection: ImmutablePropTypes.map.isRequired,
    createDraftFromEntry: PropTypes.func.isRequired,
    createEmptyDraft: PropTypes.func.isRequired,
    discardDraft: PropTypes.func.isRequired,
    entry: ImmutablePropTypes.map,
    mediaPaths: ImmutablePropTypes.map.isRequired,
    entryDraft: ImmutablePropTypes.map.isRequired,
    loadEntry: PropTypes.func.isRequired,
    persistEntry: PropTypes.func.isRequired,
    deleteEntry: PropTypes.func.isRequired,
    showDelete: PropTypes.bool.isRequired,
    openMediaLibrary: PropTypes.func.isRequired,
    removeInsertedMedia: PropTypes.func.isRequired,
    fields: ImmutablePropTypes.list.isRequired,
    slug: PropTypes.string,
    newEntry: PropTypes.bool.isRequired,
    displayUrl: PropTypes.string,
    hasWorkflow: PropTypes.bool,
    unpublishedEntry: PropTypes.bool,
    isModification: PropTypes.bool,
    collectionEntriesLoaded: PropTypes.bool,
    updateUnpublishedEntryStatus: PropTypes.func.isRequired,
    publishUnpublishedEntry: PropTypes.func.isRequired,
    deleteUnpublishedEntry: PropTypes.func.isRequired,
    currentStatus: PropTypes.string,
    logoutUser: PropTypes.func.isRequired,
  };

  componentDidMount() {
    const {
      entry,
      newEntry,
      entryDraft,
      collection,
      slug,
      loadEntry,
      createEmptyDraft,
      loadEntries,
      collectionEntriesLoaded,
    } = this.props;

    if (newEntry) {
      createEmptyDraft(collection);
    } else {
      loadEntry(collection, slug);
    }

    const leaveMessage = 'Are you sure you want to leave this page?';

    this.exitBlocker = (event) => {
      if (this.props.entryDraft.get('hasChanged')) {
        // This message is ignored in most browsers, but its presence
        // triggers the confirmation dialog
        event.returnValue = leaveMessage;
        return leaveMessage;
      }
    };
    window.addEventListener('beforeunload', this.exitBlocker);

    const navigationBlocker = (location, action) => {
      /**
       * New entry being saved and redirected to it's new slug based url.
       */
      const isPersisting = this.props.entryDraft.getIn(['entry', 'isPersisting']);
      const newRecord = this.props.entryDraft.getIn(['entry', 'newRecord']);
      const newEntryPath = `/collections/${collection.get('name')}/new`;
      if (isPersisting && newRecord && this.props.location.pathname === newEntryPath && action === 'PUSH') {
        return;
      }

      if (this.props.hasChanged) {
        return leaveMessage;
      }

    };
    const unblock = history.block(navigationBlocker);

    /**
     * This will run as soon as the location actually changes, unless creating
     * a new post. The confirmation above will run first.
     */
    this.unlisten = history.listen((location, action) => {
      const newEntryPath = `/collections/${collection.get('name')}/new`;
      const entriesPath = `/collections/${collection.get('name')}/entries/`;
      const { pathname } = location;
      if (pathname.startsWith(newEntryPath) || pathname.startsWith(entriesPath) && action === 'PUSH') {
        return;
      }
      unblock();
      this.unlisten();
    });

    if (!collectionEntriesLoaded) {
      loadEntries(collection);
    }
  }

  componentWillReceiveProps(nextProps) {
    /**
     * If the old slug is empty and the new slug is not, a new entry was just
     * saved, and we need to update navigation to the correct url using the
     * slug.
     */
    const newSlug = nextProps.entryDraft && nextProps.entryDraft.getIn(['entry', 'slug']);
    if (!this.props.slug && newSlug && nextProps.newEntry) {
      navigateToEntry(this.props.collection.get('name'), newSlug);
      nextProps.loadEntry(nextProps.collection, newSlug);
    }

    if (this.props.entry === nextProps.entry) return;
    const { entry, newEntry, fields, collection } = nextProps;

    if (entry && !entry.get('isFetching') && !entry.get('error')) {

      /**
       * Deserialize entry values for widgets with registered serializers before
       * creating the entry draft.
       */
      const values = deserializeValues(entry.get('data'), fields);
      const deserializedEntry = entry.set('data', values);
      const fieldsMetaData = nextProps.entryDraft && nextProps.entryDraft.get('fieldsMetaData');
      this.createDraft(deserializedEntry, fieldsMetaData);
    } else if (newEntry) {
      this.props.createEmptyDraft(collection);
    }
  }

  componentWillUnmount() {
    this.props.discardDraft();
    window.removeEventListener('beforeunload', this.exitBlocker);
  }

  createDraft = (entry, metadata) => {
    if (entry) this.props.createDraftFromEntry(entry, metadata);
  };

  handleChangeStatus = (newStatusName) => {
    const { updateUnpublishedEntryStatus, collection, slug, currentStatus } = this.props;
    const newStatus = status.get(newStatusName);
    this.props.updateUnpublishedEntryStatus(collection.get('name'), slug, currentStatus, newStatus);
  }

  handlePersistEntry = async (opts = {}) => {
    const { createNew = false } = opts;
    const { persistEntry, collection, entryDraft, newEntry, currentStatus, hasWorkflow, loadEntry, slug, createEmptyDraft } = this.props;

    await persistEntry(collection)

    if (createNew) {
      navigateToNewEntry(collection.get('name'));
      createEmptyDraft(collection);
    }
    else if (slug && hasWorkflow && !currentStatus) {
      loadEntry(collection, slug);
    }
  };

  handlePublishEntry = async (opts = {}) => {
    const { createNew = false } = opts;
    const { publishUnpublishedEntry, entryDraft, collection, slug, currentStatus, loadEntry } = this.props;
    if (currentStatus !== status.last()) {
      window.alert('Please update status to "Ready" before publishing.');
      return;
    } else if (!window.confirm('Are you sure you want to publish this entry?')) {
      return;
    } else if (entryDraft.get('hasChanged')) {
      if (window.confirm('Your unsaved changes will be saved before publishing. Are you sure you want to publish?')) {
        await persistEntry(collection);
      } else {
        return;
      }
    }

    await publishUnpublishedEntry(collection.get('name'), slug);

    if (createNew) {
      navigateToNewEntry(collection.get('name'));
    }
    else {
      loadEntry(collection, slug);
    }
  };

  handleDeleteEntry = () => {
    const { entryDraft, newEntry, collection, deleteEntry, slug } = this.props;
    if (entryDraft.get('hasChanged')) {
      if (!window.confirm('Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?')) {
        return;
      }
    } else if (!window.confirm('Are you sure you want to delete this published entry?')) {
      return;
    }
    if (newEntry) {
      return navigateToCollection(collection.get('name'));
    }

    setTimeout(async () => {
      await deleteEntry(collection, slug);
      return navigateToCollection(collection.get('name'));
    }, 0);
  };

  handleDeleteUnpublishedChanges = async () => {
    const { entryDraft, collection, slug, deleteUnpublishedEntry, loadEntry, isModification } = this.props;
    if (entryDraft.get('hasChanged') && !window.confirm('This will delete all unpublished changes to this entry, as well as your unsaved changes from the current session. Do you still want to delete?')) {
      return;
    } else if (!window.confirm('All unpublished changes to this entry will be deleted. Do you still want to delete?')) {
      return;
    }
    await deleteUnpublishedEntry(collection.get('name'), slug);

    if (isModification) {
      loadEntry(collection, slug);
    } else {
      navigateToCollection(collection.get('name'));
    }
  };

  render() {
    const {
      entry,
      entryDraft,
      fields,
      mediaPaths,
      boundGetAsset,
      collection,
      changeDraftField,
      changeDraftFieldValidation,
      openMediaLibrary,
      addAsset,
      removeInsertedMedia,
      user,
      hasChanged,
      displayUrl,
      hasWorkflow,
      unpublishedEntry,
      newEntry,
      isModification,
      currentStatus,
      logoutUser,
    } = this.props;

    if (entry && entry.get('error')) {
      return <div><h3>{ entry.get('error') }</h3></div>;
    } else if (entryDraft == null
      || entryDraft.get('entry') === undefined
      || (entry && entry.get('isFetching'))) {
      return <Loader active>Loading entry...</Loader>;
    }

    return (
      <EditorInterface
        entry={entryDraft.get('entry')}
        getAsset={boundGetAsset}
        collection={collection}
        fields={fields}
        fieldsMetaData={entryDraft.get('fieldsMetaData')}
        fieldsErrors={entryDraft.get('fieldsErrors')}
        mediaPaths={mediaPaths}
        onChange={changeDraftField}
        onValidate={changeDraftFieldValidation}
        onOpenMediaLibrary={openMediaLibrary}
        onAddAsset={addAsset}
        onRemoveInsertedMedia={removeInsertedMedia}
        onPersist={this.handlePersistEntry}
        onDelete={this.handleDeleteEntry}
        onDeleteUnpublishedChanges={this.handleDeleteUnpublishedChanges}
        onChangeStatus={this.handleChangeStatus}
        onPublish={this.handlePublishEntry}
        showDelete={this.props.showDelete}
        enableSave={entryDraft.get('hasChanged')}
        user={user}
        hasChanged={hasChanged}
        displayUrl={displayUrl}
        hasWorkflow={hasWorkflow}
        hasUnpublishedChanges={unpublishedEntry}
        isNewEntry={newEntry}
        isModification={isModification}
        currentStatus={currentStatus}
        onLogoutClick={logoutUser}
      />
    );
  }
}

function mapStateToProps(state, ownProps) {
  const { collections, entryDraft, mediaLibrary, auth, config, entries } = state;
  const slug = ownProps.match.params.slug;
  const collection = collections.get(ownProps.match.params.name);
  const collectionName = collection.get('name');
  const newEntry = ownProps.newRecord === true;
  const fields = selectFields(collection, slug);
  const entry = newEntry ? null : selectEntry(state, collectionName, slug);
  const boundGetAsset = getAsset.bind(null, state);
  const mediaPaths = mediaLibrary.get('controlMedia');
  const user = auth && auth.get('user');
  const hasChanged = entryDraft.get('hasChanged');
  const displayUrl = config.get('display_url');
  const hasWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
  const isModification = entryDraft.getIn(['entry', 'isModification']);
  const collectionEntriesLoaded = !!entries.getIn(['entities', collectionName])
  const unpublishedEntry = selectUnpublishedEntry(state, collectionName, slug);
  const currentStatus = unpublishedEntry && unpublishedEntry.getIn(['metaData', 'status']);
  return {
    collection,
    collections,
    newEntry,
    entryDraft,
    mediaPaths,
    boundGetAsset,
    fields,
    slug,
    entry,
    user,
    hasChanged,
    displayUrl,
    hasWorkflow,
    isModification,
    collectionEntriesLoaded,
    currentStatus,
  };
}

export default connect(
  mapStateToProps,
  {
    changeDraftField,
    changeDraftFieldValidation,
    openMediaLibrary,
    removeInsertedMedia,
    addAsset,
    loadEntry,
    loadEntries,
    createDraftFromEntry,
    createEmptyDraft,
    discardDraft,
    persistEntry,
    deleteEntry,
    updateUnpublishedEntryStatus,
    publishUnpublishedEntry,
    deleteUnpublishedEntry,
    logoutUser,
  }
)(withWorkflow(Editor));
