import Component from 'ember-component';
import observer from 'ember-metal/observer';
import $ from 'jquery';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';

const KEYCODES = {
  BACKSPACE: 8,
  TAB: 9,
  ENTER: 13,
  ESC: 27,
  SPACE: 32,
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40,
  TWO: 50,
  THREE: 51
};

let getCaretIndex = function (textarea) {
  return textarea.selectionStart;
};

let setCaretIndex = function (textarea, index) {

  textarea.setSelectionRange(index, index);
};

/*
  Renders a text area with support for suggestions (initiated with tab and @).

  Required properties:
    textareaValue - User input property.
    suggestions - generic suggestions to display when @ or Tab is entered.
    sendMessage - action to handle message sending when Enter is pressed.

  optional:
    placeholder - placeholder text to show in textarea when empty.
    track - tracking action.
    disableSuggestions - action to disable suggestions.
    isTextareaDisabled - textarea will disable itself while this property is true (typically a model's isLoading).
    numberOfSuggestionsToDisplay - Number of suggestions to display at any given time (default: 5).
    numberOfHashtagSuggestionsToDisplay - Number of hashtag suggestions to display at any given time (default: 15).

*/
export default Component.extend({
  bitsRoom: injectService(),
  conversations: injectService('twitch-conversations/conversations'),
  classNames: ['js-chat-input', 'chat-input'],

  // Properties passed in
  currentChannelRoomName: '',
  numberOfSuggestionsToDisplay: 5,
  numberOfHashtagSuggestionsToDisplay: 15,
  textareaValue: '',
  placeholder: '',
  isTextareaDisabled: false,
  suggestions() { return []; },
  hashtagSuggestions() { return []; },
  // Default to a tab index of 1, which seems appropriate for the main chat input.
  tabIndex: 1,

  isSuggestionsTriggeredWithTab: false,
  partialName: '',
  partialNameStartIndex: 0,
  isShowingSuggestions: false,
  isShowingHashtagSuggestions: false,
  caretIndex: 0,

  // TODO: Refactor out {{textarea}} -> <textarea> and use change/input events instead of this observer.
  updateCaretIndex: observer('textareaValue', function () {
    // The sendCaretIndex action can be undefined for instances of twitch-chat-input for Whispers
    if (!this.get('sendCaretIndex')) {
      return;
    }
    let caretIndex = getCaretIndex(this.get('chatTextArea'));
    this.get('sendCaretIndex')(caretIndex);
  }),

  isAnySuggestionsShowing: computed.or('isShowingSuggestions', 'isShowingHashtagSuggestions'),

  /*
    partialName refers to names that are partially typed before tab is pressed,
    e.g. soda<tab> would have a partial name of soda.
  */
  _setPartialName: observer('textareaValue', function () {
    /* textareaValue is set before didInsertElement,
      potentially calling this without an actual textarea. */
    if (!this.get('chatTextArea') || !this.get('isAnySuggestionsShowing')) {
      return;
    }

    let message = this.get('textareaValue'),
        caretIndex = getCaretIndex(this.get('chatTextArea')),
        startIndex, matches, startChar;

    if (caretIndex === -1) {
      this.closeSuggestions();
      this.sendAction('disableSuggestions');
      return;
    }
    startIndex = message.substring(0, caretIndex).search(/(\b|#|@)([^\u0000-\u007F]+|\w)*$/);
    startIndex = startIndex === -1 ? caretIndex : startIndex;
    let charAtStartIndex = message.charAt(startIndex); // Incase it matched \b.

    if (charAtStartIndex === '@' || charAtStartIndex === '#') {
      startIndex += 1;
    }

    if (!this.get('isSuggestionsTriggeredWithTab')) {
      startChar = (startIndex > 0) ? message.charAt(startIndex - 1) : message.charAt(startIndex);
      if (startChar !== '@' && startChar !== '#') {
        this.closeSuggestions();
        return;
      }
    }

    this.set('partialNameStartIndex', startIndex);
    matches = message.substring(startIndex).match(/^([^\u0000-\u007F]+|\w)*(?=($|^([^\u0000-\u007F]+|\w)))/);
    if (matches) {
      this.set('partialName', matches[0]);
    } else {
      this.set('isShowingSuggestions', false);
    }
  }),

  getStartChar() {
    let message = this.get('textareaValue'),
                  caretIndex = getCaretIndex(this.get('chatTextArea')),
                  startIndex, startChar;

    if (caretIndex === -1) {
      this.closeSuggestions();
      this.sendAction('disableSuggestions');
      return;
    }

    startIndex = message.substring(0, caretIndex).search(/\b(\w)*$/);
    startIndex = startIndex === -1 ? caretIndex : startIndex;
    startChar = (startIndex > 0) ? message.charAt(startIndex - 1) : message.charAt(startIndex);
    return startChar;
  },

  completeSuggestion(suggestion) {
    let before, after,
        message = this.get('textareaValue'),
        startIndex = this.get('partialNameStartIndex');

    before = `${message.substring(0, startIndex)}${suggestion}`;
    after = message.substring(startIndex + this.get('partialName').length);
    if (!after) {
      before += ' ';
    }

    this.set('textareaValue', before + after);
    this.set('isShowingSuggestions', false);
    this.set('isShowingHashtagSuggestions', false);
    this.set('partialName', '');

    this.trackSuggestionsCompleted(suggestion);

    /* setCaretIndex runs before textareaValue property propagates to the textarea value field,
       so we wait to avoid running setCaretIndex on a string shorter than we expect */
    this.runTask(() => {
      setCaretIndex(this.get('chatTextArea'), before.length);
    });
  },

  closeSuggestions() {
    if (this.isDestroyed){
      return;
    }
    this.set('isShowingSuggestions', false);
    this.set('isShowingHashtagSuggestions', false);
  },

  /* Tracking Methods */
  trackSuggestions(startCharacter) {
    this.sendAction('track', 'chat-suggestions', {
      start_character: startCharacter
    });
  },

  trackSuggestionsCompleted(suggestedName) {
    this.sendAction('track', 'chat-completed-suggestion', {
      target_login: suggestedName,
      channel: this.get('currentChannelRoomName')
    });
  },

  /* Assign event listeners on the textarea,
     and sets the textarea to use for operations like getCaretIndex */
  didInsertElement() {
    let $textarea = this.$('textarea');

    if ($textarea[0]) { this.set('chatTextArea', $textarea[0]); }

    this.addEventListener($textarea, 'mouseup', () => {
      this.set('isSuggestionsTriggeredWithTab', false);
      this._setPartialName();
    });

    this.addEventListener($textarea, 'keydown', this._onKeyDown.bind(this), {passive: false});

    this.addEventListener($textarea, 'keyup', this._onKeyUp.bind(this));

    this.addEventListener(document, 'mouseup', this.hideSuggestions.bind(this));

    this.addEventListener($textarea, 'focus', () => {
      if (!this.get('hasFocusedChatInputArea') && this.get('hasFocusedChatInputAreaInitially')){
        this.set('hasFocusedChatInputArea', true);
        this.sendAction('focusChatInputWindow');
      }
      this.set('hasFocusedChatInputAreaInitially', true);
    });

    this.get('conversations').on('conversationWindowClosed', () => {
      $('.js-chat-container .chat_text_input').focus();
    });
  },

  willDestroyElement() {
    this.get('conversations').off('conversationWindowClosed');
  },

  _onKeyUp(e) {
    let evt = e || window.event,
        key = evt.charCode || evt.keyCode;

    switch (key) {
      case KEYCODES.LEFT:
      case KEYCODES.RIGHT:
        this._setPartialName();
        this.set('isSuggestionsTriggeredWithTab', false);
        break;
    }
  },

  _onKeyDown(e) {
    let evt = e || window.event;
    let key = evt.charCode || evt.keyCode;
    let partialName = this.get('partialName');

    let handleDefault = (inputKey) => {
      if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) {
        return;
      }

      if (!/^\w+$/.test(String.fromCharCode(inputKey).toLowerCase())) {
        this.set('isSuggestionsTriggeredWithTab', false);
      }
    };

    if (!this.get('hasFocusedChatInputArea')){
      this.set('hasFocusedChatInputArea', true);
      this.sendAction('focusChatInputWindow');
    }

    switch (key) {
      case KEYCODES.BACKSPACE: {
        /* Backspaces at beginnings of line do not trigger listeners for textareaValue,
           so we need to special case it here.  */
        if (partialName.length === 0 || getCaretIndex(this.get('chatTextArea')) === 0) {
          this.set('isSuggestionsTriggeredWithTab', false);
          this.set('isShowingSuggestions', false);
        }
        break;
      }
      case KEYCODES.TAB: {
        let textareaWords = this.get('textareaValue').split(" ");
        if (evt.ctrlKey || evt.altKey) { break; }
        if (textareaWords[textareaWords.length - 1].length === 0) { break; }
        evt.preventDefault();
        if (!this.get('isAnySuggestionsShowing')) {
          this.set('isSuggestionsTriggeredWithTab', true);

          let startChar = this.getStartChar();
          if (startChar === '#') {
            this.set('isShowingHashtagSuggestions', true);
          } else {
            this.set('isShowingSuggestions', true);
          }

          this._setPartialName();
          this.trackSuggestions('Tab');
        }
        break;
      }
      case KEYCODES.TWO: {
        if (evt.shiftKey || evt.shiftLeft) {
          this.set('isSuggestionsTriggeredWithTab', false);
          this.set('isShowingHashtagSuggestions', false);
          this.set('isShowingSuggestions', true);
          this.trackSuggestions('@');
        }
        break;
      }
      case KEYCODES.THREE: {
        if (this.get('hashtagSuggestions')().length > 0 && (evt.shiftKey || evt.shiftLeft)) {
          this.set('isSuggestionsTriggeredWithTab', false);
          this.set('isShowingSuggestions', false);
          this.set('isShowingHashtagSuggestions', true);
          this.trackSuggestions('#');
        }
        break;
      }
      case KEYCODES.ESC: {
        evt.preventDefault(); // IE8
        this.closeSuggestions();
        break;
      }
      case KEYCODES.ENTER: {
        if (!evt.shiftKey && !evt.shiftLeft) {
          if (evt.stopPropagation) { evt.stopPropagation(); }
          evt.preventDefault();
          if (!this.get('isAnySuggestionsShowing')) {
            this.sendAction('sendMessage');
            this._currentWhisperTarget = -1; // Reset the whisper cycle on each message
          }
        }
        break;
      }
      case KEYCODES.UP:
      case KEYCODES.DOWN: {
        if (this.get('isAnySuggestionsShowing')) {
          evt.preventDefault();
        } else {
          this.runTask(() => {
            this.cycleWhisperTargets(key);
          });
        }
        break;
      }
      case KEYCODES.SPACE: {
        if (getCaretIndex(this.get('chatTextArea')) === 2 &&
            this.get('textareaValue').substring(0, 2) === '/w') {
          this.runTask(() => {
            this.set('isSuggestionsTriggeredWithTab', true);
            this.set('isShowingSuggestions', true);
            this._setPartialName();
            this.trackSuggestions('/w');
          });
        } else {
          handleDefault(key);
        }
        break;
      }
      default: {
        handleDefault(key);
      }
    }
  },

  hideSuggestions(e) {
    let $target = $(e.target),
        $textarea = $('textarea');

    if (!$target.hasClass('suggestion') && !$target.hasClass('suggestions') && !$textarea.is(e.target)) {
      this.closeSuggestions();
    }
  },

  _currentWhisperTarget: -1,

  /*
   * Build a list of whisper suggstions, but memoize it based on what the input
   * suggestions list was. We do this with a function instead of a CP to avoid
   * installing an observer on the suggestions list.
   */
  _memoizedWhisperSuggestions: null,
  uniqueWhisperSuggestions() {
    let suggestions = this.get('suggestions')();

    if (this._memoizedWhisperSuggestions) {
      let {memoizedRawSuggestions, memoizedWhisperSuggestions} = this._memoizedWhisperSuggestions;
      if (memoizedRawSuggestions === suggestions) {
        return memoizedWhisperSuggestions;
      }
    }

    let whisperSuggestions = suggestions.
      filterBy('whispered').
      sortBy('timestamp').reverse().
      mapBy('id').
      uniq();

    this._memoizedWhisperSuggestions = {
      memoizedRawSuggestions: suggestions,
      memoizedWhisperSuggestions: whisperSuggestions
    };

    return whisperSuggestions;
  },

  prepopulateWhisper(newId, oldId) {
    let message = this.get('textareaValue'),
        oldWhisper = oldId ? `/w ${oldId} ` : '',
        newWhisper = newId ? `/w ${newId} ` : '';

    if (message.length === oldWhisper.length) {
      message = message.replace(oldWhisper, newWhisper);
      this.set('textareaValue', message);
      setCaretIndex(this.get('chatTextArea'), message.length);
    }
  },

  cycleWhisperTargets(key) {
    let listOfTargets = this.uniqueWhisperSuggestions(),
        index = this._currentWhisperTarget,
        oldTarget = listOfTargets[index],
        newTarget;

    if (key === KEYCODES.UP) {
      index = (index + 1) % (listOfTargets.length + 1);
    } else if (key === KEYCODES.DOWN) {
      index = (index + listOfTargets.length) % (listOfTargets.length + 1);
    }
    this._currentWhisperTarget = index;

    newTarget = listOfTargets[index];
    this.prepopulateWhisper(newTarget, oldTarget);
  },

  actions: {
    selectSuggestion(suggestion) {
      this.completeSuggestion(suggestion);
      this.get('chatTextArea').focus();
    },
    dismissSuggestions() {
      this.closeSuggestions();
    },
    dismissBitsTooltip() {
      this.get('bitsRoom').dismissTooltip();
    }
  }
});
