import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { RichUtils, Editor, Modifier, EditorState } from 'draft-js'
import clamp from 'lodash/clamp';
import upperFirst from 'lodash/upperFirst';
import CustomStyleManager from './custom-style-manager/custom-style-manager'
import EditorStateUtil from './util/editor-state-util'
import StyleButton from './toolbar/style-button'
import StyleOptionsMenu from './toolbar/style-options-menu'
import ColorSelectionButton from './toolbar/color-selection-button'
import LinkButton from './toolbar/link-button'
import ImageButton from './toolbar/image-button'
import Counter from './toolbar/counter'
import createDecorators from './custom-components/decorators'
import { fetchEntity, updateEntity } from '../../../actions/index'

interface TextEditorProps {
  blogPost: any;
  blogPostId: number
  contentState: string
  fetchEntity: any;
  editorIsSaved: boolean
  onMouseDown?: any
  setEditorSavedStatus: any
  updateBlogPost: any
  updateEntity: any
}

interface TextEditorState extends ComponentState {
  editorState: any
  pseudoStyles: any[]
  readOnly: boolean
}

class TextEditor extends React.Component<TextEditorProps, TextEditorState> {
  decorators: any
  editor: any
  setDomEditorRef: any
  styles: any

  constructor(props) {
    super(props);
    this.styles = new CustomStyleManager();
    this.bindFns();
    this.decorators = createDecorators(this.getEditorState, this.updateEditorState, this.getEntityDataByKey, this.mergeEntityData, this.setReadOnly);

    this.state = {
      readOnly: false,
      pseudoStyles: [],
      editorState: EditorStateUtil.loadEditorState("", this.decorators)
    };

    this.setDomEditorRef = (ref) => this.editor = ref; // Used to manage focus
    setTimeout(this._handleFocusEditor.bind(this), 0);
  };

  getEditorState() {
    return this.state.editorState;
  }

  setReadOnly(value) {
    this.setState({ readOnly: value });
  }

  bindFns() {
    this.setReadOnly = this.setReadOnly.bind(this);
    this.getEntityDataByKey = this.getEntityDataByKey.bind(this);
    this.getEditorState = this.getEditorState.bind(this);
    this.updateEditorState = this.updateEditorState.bind(this);
    this.mergeEntityData = this.mergeEntityData.bind(this);
  }

  updateEditorState(newEditorState, bypassReadOnly = false) {
    if (this.state.readOnly && bypassReadOnly) {
      this.setState({ readOnly: false }, () => {
        this.setState({
          editorState: newEditorState,
          readOnly: true,
        });
      });
      return;
    }

    if (!newEditorState.getCurrentInlineStyle().size) {
      const defaultBlockInline = this.styles.defaultBlockInline;
      const defaultStyles = defaultBlockInline[this.currentBlock().getType()] || defaultBlockInline.unstyled;
      defaultStyles.forEach(style => {
        newEditorState = EditorStateUtil.toggleInlineStyle(newEditorState, style)
      });
    }

    this.setState({
      editorState: newEditorState
    }, () => {
      const { editorState } = this.state;
      let { blogPost } = this.props;
      blogPost.editorContentState = EditorStateUtil.rawContentString(editorState);
      blogPost.htmlContent = this.styles.exportHTML(editorState);
      this.props.updateBlogPost(blogPost);
    });
  }

  loadEditorState(contentState) {
    return EditorStateUtil.loadEditorState(contentState, this.decorators);
  }

  // LIFECYCLE METHODS -----

  componentDidMount() {
    let contentState = this.props.blogPost.editorContentState;
    this.setState({
      editorState: this.loadEditorState(contentState)
    });
  }

  // EDITOR STATE RETRIEVAL -----

  currentInlineStylesInclude(styleCode) {
    return this.state.editorState.getCurrentInlineStyle().has(styleCode);
  }

  currentColor() {
    return EditorStateUtil.currentColor(this.state.editorState);
  }

  currentBlock() {
    return EditorStateUtil.currentBlock(this.state.editorState);
  }

  previousBlock() {
    return EditorStateUtil.previousBlock(this.state.editorState);
  }

  currentBlockType(blockType) {
    return this.currentBlock().getType();
  }

  currentBlockData(key) {
    return this.currentBlock().getData().get(key);
  }

  currentEntityType() {
    return EditorStateUtil.currentEntityType(this.state.editorState);
  }

  getEntityDataByKey(editorState, entityKey) {
    return EditorStateUtil.getEntityDataByKey(editorState, entityKey);
  }

  // EDITOR STATE STATE MANIPULATION -----

  toggleInlineStyle(styleCode) {
    const newEditorState = EditorStateUtil.toggleInlineStyle(this.state.editorState, styleCode);
    this.updateEditorState(newEditorState);
  }

  toggleColorStyle(styleCode) {
    const newEditorState = EditorStateUtil.toggleColorStyle(this.state.editorState, styleCode);
    this.updateEditorState(newEditorState);
  }

  toggleBlockType(styleCode) {
    const newEditorState = EditorStateUtil.toggleBlockType(this.state.editorState, styleCode);
    this.updateEditorState(newEditorState);
  }

  toggleBlockMetaStyle(styleCode) {
    this.setState({ readOnly: false }, () => {
      const newEditorState = EditorStateUtil.toggleBlockMetaStyle(this.state.editorState, styleCode);
      this.updateEditorState(newEditorState, true);
    });
  }

  mergeBlockData(data) {
    const newEditorState = EditorStateUtil.mergeBlockData(this.state.editorState, data);
    this.updateEditorState(newEditorState);
  }

  mergeBlockDataByKey(blockKey, data) {
    const newEditorState = EditorStateUtil.mergeBlockDataByKey(this.state.editorState, blockKey, data);
    this.updateEditorState(newEditorState);
  }

  applyEntity(entityType, mutability, data = {}) {
    const { editorState } = this.state;
    let newEditorState;
    switch(entityType) {
      case 'LINK':
        newEditorState = EditorStateUtil.applyEntity(editorState, 'LINK', 'MUTABLE', data);
        break;
      case 'IMAGE':
      case 'CTA':
      case 'HTML':
      case 'TESTIMONIAL':
      case 'TIP':
        newEditorState = EditorStateUtil.applyAtomicBlock(editorState, entityType, 'IMMUTABLE', data);
        break;
    };

    this.updateEditorState(newEditorState);
  }

  removeEntity() {
    const newEditorState = EditorStateUtil.removeEntity(this.state.editorState);
    this.updateEditorState(newEditorState);
  }

  mergeEntityData(entityKey, data) {
    const newEditorState = EditorStateUtil.mergeEntityData(this.state.editorState, entityKey, data);
    this.updateEditorState(newEditorState);
  }

  selectAtomicBlock(blockKey) {
    this.setState({ readOnly: true }, () => {
      const newEditorState = EditorStateUtil.selectAtomicBlock(this.state.editorState, blockKey);
      this.updateEditorState(newEditorState);
    });
  }

  removeAtomicBlock(blockKey) {
    this.setState({ readOnly: false }, () => {
      const newEditorState = EditorStateUtil.removeAtomicBlock(this.state.editorState, blockKey);
      this.updateEditorState(newEditorState, true);
    });
  }

  applyPseudoStyle(styleType) {
    const { editorState, pseudoStyles } = this.state;
    const newEditorState = EditorStateUtil.applyPseudoStyle(editorState, styleType);
    const newPseudoStyle = {
      selectionState: editorState.getSelection(),
      style: `PSEUDO_${upperFirst(styleType)}`,
    };
    pseudoStyles.push(newPseudoStyle);
    this.setState({ pseudoStyles });
    this.updateEditorState(newEditorState);
  }

  // RENDERING -----

  render() {
    return(
      <div id="text-editor-admin-container">
        <div id="editor-and-toolbar-container">
          { this.renderToolbar() }
          { this.renderEditor() }
        </div>
      </div>
    );
  }

  renderEditor() {
    const { editorState, readOnly } = this.state;
    const contentState = editorState.getCurrentContent();
    return (
      <div id="text-editor-admin" className="admin-editor" onClick={ this._handleFocusEditor.bind(this) }>
        <Editor
          editorState={ editorState }
          readOnly={ readOnly }
          ref={ this.setDomEditorRef }
          onChange={ this.updateEditorState }
          handleKeyCommand={ this._handleKeyCommand.bind(this) }
          onTab={ this._handleTab.bind(this) }
          handleBeforeInput={ this._handleBeforeInput.bind(this) }
          blockRenderMap={ this.styles.blockRenderMap }
          blockStyleFn={ (block) => this.styles.blockStyleFn(block, contentState) }
          blockRendererFn={ (block) => this.styles.blockRendererFn(block, contentState, this._customBlockProps()) }
          customStyleMap={ this.styles.customStylesMap }
          customStyleFn={ this.styles.customStyleFn }
        />
      </div>
    );
  }

  renderToolbar() {
    return (
      <div id="text-editor-admin-toolbar">
        <div className="document-info">
          <Counter editorState={ this.state.editorState } />
        </div>
        { this.renderBlockHeaderButtons() }
        { this.renderInlineStyleButtons() }
        { this.renderBlockListButtons() }
        { this.renderMetaStyleButtons() }
        { this.renderEntityButtons() }
        { this.renderCustomBlockButtons() }
      </div>
    );
  }

  renderInlineStyleButtons() {
    return (
      <div className="inline-buttons-container">
        { this.styles.inline.map(styleCode => this.renderStyleButton(styleCode, 'inline')) }
        <ColorSelectionButton
          currentColor={ this.currentColor() }
          toggleColorStyle={ this.toggleColorStyle.bind(this) }
        />
      </div>
    );
  }

  renderBlockListButtons() {
    const listStyles = this.styles.block.list;
    return (
      <div className="block-list-buttons-container">
        { listStyles.map(styleCode => this.renderStyleButton(styleCode, 'block')) }
      </div>
    );
  }

  renderBlockHeaderButtons() {
    const headerStyles = this.styles.block.header;
    return (
      <div className="block-header-buttons-container">
        <StyleOptionsMenu
          styleType="block"
          styleCategory="header"
          options={ headerStyles }
          currentOption={ headerStyles.filter(type => this.currentBlockType(type) === type)[0] }
          defaultOption="unstyled"
          menuDirection="vertical"
          renderStyleButton={ this.renderStyleButton.bind(this) }
        />
      </div>
    );
  }

  renderMetaStyleButtons() {
    const currentAlignDirection = this.currentBlockData('align');
    return (
      <div className="meta-buttons-container">
        <StyleOptionsMenu
          styleType="meta"
          styleCategory="align"
          options={ this.styles.meta.align }
          currentOption={ currentAlignDirection ? 'align-' + currentAlignDirection : null }
          defaultOption='align-left'
          alwaysPressed
          menuDirection="horizontal"
          renderStyleButton={ this.renderStyleButton.bind(this) }
        />
      </div>
    );
  }

  renderEntityButtons() {
    return (
      <div className="entity-buttons-container">
        <LinkButton
          applyEntity={ this.applyEntity.bind(this) }
          removeEntity={ this.removeEntity.bind(this) }
          isApplied={ this.currentEntityType() === 'LINK' }
          applyPseudoSelection={ () => this.applyPseudoStyle('SELECT') }
        />
        <ImageButton
          applyEntity={ this.applyEntity.bind(this) }
          isApplied={ this.currentEntityType() === 'IMAGE' }
        />
      </div>
    );
  }

  renderCustomBlockButtons() {
    const entityTypes = this.styles.customEntity;
    return (
      <div className="custom-block-buttons-container">
        { entityTypes.map(entityType => this.renderStyleButton(entityType, 'custom')) }
      </div>
    );
  }

  renderStyleButton(styleCode, type, options = {}) {
    const props = {
      key: styleCode,
      styleCode,
      options,
    };
    switch(type) {
      case 'inline':
        props["onMouseDown"] = this.toggleInlineStyle.bind(this);
        props["isPressed"] = this.currentInlineStylesInclude(styleCode);
        break;
      case 'block':
        props["onMouseDown"] = this.toggleBlockType.bind(this);
        props["isPressed"] = this.currentBlockType(styleCode) === styleCode;
        break;
      case 'custom':
        props["onMouseDown"] = (styleCode) => this.applyEntity(styleCode, 'IMMUTABLE');
        props["isPressed"] = this.currentEntityType() === styleCode;
      case 'meta':
        const [key, value] = styleCode.split('-');
        switch(key) {
          case 'align':
            const alignDirection = this.currentBlockData('align') || 'left';
            props["onMouseDown"] = this.toggleBlockMetaStyle.bind(this);
            props["isPressed"] = alignDirection === value;
            break;
          default:
            break;
        }
        break;
    }
    return <StyleButton { ...props } />
  }

  pasteHTML() {
    const rawHTML = this.refs.testHTML["value"];
    const newEditorState = EditorStateUtil.stateFromHTML(rawHTML);
    this.updateEditorState(newEditorState);
  }

  renderDebugButtons() {
    const editorState = this.state.editorState;
    const selectionState = editorState.getSelection().toJS();
    const contentState = editorState.getCurrentContent();
    const currentBlock = this.currentBlock();

    const currentEntityKey = currentBlock.getEntityAt(selectionState.anchorOffset);
    const currentEntity = currentEntityKey && contentState.getEntity(currentEntityKey).toJS();

    return (
      <div className="dev-debug-buttons">
        <a className="dev-debug-button" onClick={ () => console.log(this.state) }>Parent State</a>
        <a className="dev-debug-button" onClick={ () => console.log(editorState.toJS()) }>Editor State</a>
        <a className="dev-debug-button" onClick={ () => console.log(contentState.toJS()) }>Content State</a>
        <a className="dev-debug-button" onClick={ () => console.log(this.currentBlock().toJS()) }>Curent Block</a>
        <a className="dev-debug-button" onClick={ () => console.log(currentEntity) }>Current Entity</a>
        <a className="dev-debug-button" onClick={ () => console.log(this.styles.exportHTML(editorState)) }>HTML Output</a>
        <textarea style={ { marginTop: 20, width: 400, height: 200 } } ref="testHTML" placeholder="This is just temporary. I'm using it to paste in HTML and see how it renders">
        </textarea>
        <a className="dev-debug-button" onClick={ this.pasteHTML.bind(this) }>Paste HTML</a>
      </div>
    );
  }

  // PRIVATE -----

  _removePseudoStyles() {
    const { editorState, pseudoStyles } = this.state;
    const currentSelection = editorState.getSelection();
    let newEditorState = editorState;
    let newContentState = editorState.getCurrentContent();

    if (!pseudoStyles.length) { return; }

    while (pseudoStyles.length) {
      const { style, selectionState } = pseudoStyles.pop();
      newContentState = Modifier.removeInlineStyle(
        newEditorState.getCurrentContent(),
        selectionState,
        style
      );
    }

    newEditorState = EditorState.push(
      newEditorState,
      newContentState,
      'change-inline-style'
    );
    newEditorState = EditorState.forceSelection(newEditorState, currentSelection);
    this.updateEditorState(newEditorState);
  }

  _customBlockProps() {
    return {
      selectAtomicBlock: this.selectAtomicBlock.bind(this),
      removeAtomicBlock: this.removeAtomicBlock.bind(this),
      currentAlignDirection: () => this.currentBlockData('align') || 'left',
      toggleBlockMetaStyle: this.toggleBlockMetaStyle.bind(this),
      mergeBlockDataByKey: this.mergeBlockDataByKey.bind(this),
      mergeEntityData: this.mergeEntityData.bind(this),
      renderStyleButton: this.renderStyleButton.bind(this),
    };
  }

  // PRIVATE HANDLERS -----

  _handleFocusEditor(e) {
    if (e) {
      let parentEl = e.target;
      while (parentEl && parentEl.id !== 'text-editor-admin') {
        if (parentEl.dataset.remainReadOnly === 'true') { return; }
        parentEl = parentEl.parentElement;
      }
    }
    this.editor.focus();
    this._removePseudoStyles();
    if (this.state.readOnly) {
      this.setState({ readOnly: false });
    }
  }

  _handleKeyCommand(command) {
    const { editorState } = this.state;
    const handledEditorState = RichUtils.handleKeyCommand(editorState, command);

    if (handledEditorState) {
      this.updateEditorState(handledEditorState);
      return 'handled';
    }

    let newEditorState = editorState;
    switch(command) {
      case 'split-block':
        const data = this.currentBlock().toJS().data; // Cache metadata to re-apply
        newEditorState = EditorStateUtil.splitBlock(newEditorState);
        newEditorState = EditorStateUtil.mergeBlockData(newEditorState, data);
        const blockType = EditorStateUtil.currentBlock(newEditorState).getType();
        if (blockType !== 'unordered-list-item' && blockType !== 'ordered-list-item') {
          newEditorState = EditorStateUtil.mergeBlockData(newEditorState, { depth: 0 });
        }
        this.updateEditorState(newEditorState);
        return 'handled';
    }

    return 'not-handled';
  }

  _handleTab(e) {
    e.preventDefault();
    const { editorState } = this.state;
    const currentBlock = this.currentBlock();
    const blockType = currentBlock.getType();

    let newEditorState, depth;
    switch (blockType) {
      case 'ordered-list-item':
      case 'unordered-list-item':
        const [minDepth, maxDepth] = this._calculateDepthBounds(currentBlock, blockType);
        depth = this.currentBlockData('depth') || 0;
        depth += e.shiftKey ? -1 : 1;
        depth = clamp(depth, minDepth, maxDepth);
        newEditorState = EditorStateUtil.mergeBlockData(editorState, { depth });
        this.updateEditorState(newEditorState);
        break;
    }
  }

  _handleBeforeInput(chars) {
    const { editorState } = this.state;
    switch(chars) {
      case ' ':
        const currentEntityType = EditorStateUtil.currentEntityType(editorState);
        const nextEntityType = EditorStateUtil.nextEntityType(editorState);
        if (currentEntityType === 'LINK' && nextEntityType !== 'LINK') {
          const newEditorState = EditorStateUtil.insertText(editorState, ' ');
          this.updateEditorState(newEditorState);
          return 'handled';
        }
      default:
        return 'not-handled';
    }
  }

  // PRIVATE HELPERS -----

  _calculateDepthBounds(block, blockType) {
    let [minDepth, maxDepth] = [0, 4];
    if (blockType === 'ordered-list-item') {
      const prevBlock = this.previousBlock();
      if (prevBlock) {
        const prevBlockDepth = prevBlock.getData().get('depth') || 0;
        maxDepth = (prevBlockDepth + 1 > maxDepth ?  maxDepth : prevBlockDepth + 1);
      } else {
        maxDepth = 0;
      }
    }
    return [minDepth, maxDepth];
  }
}

const mapStateToProps = (reducers, props) => {
  return reducers.standardReducer;
};

function mapDispatchToProps(dispatch) {
  return bindActionCreators({ fetchEntity, updateEntity }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(TextEditor);
