import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';
import injectService from 'ember-service/inject';
import computed from 'ember-computed';
import observer from 'ember-metal/observer';
import { A as emberArray } from 'ember-array/utils';
import { assert } from 'ember-metal/utils';
import MessageModel from 'web-client/models/message';
import NoticeModel from 'web-client/models/notice';
import RSVP from 'rsvp';

const LOAD_EARLIER_MESSAGES_PROMISE_KEY = '_loadEarlierMessagesPromise';

export default Model.extend({
  isArchived: attr('boolean'),
  lastReadId: attr('number'),
  lastUpdatedAt: attr('date'),
  markRead: attr('number'),
  isMuted: attr('boolean'),
  participants: hasMany('participant', {async: false}),
  isLocal: attr('boolean', {defaultValue: false}),
  lastMarkedNotSpam: attr('number', {defaultValue: 0}),
  spamLikelihood: attr('string', {defaultValue: 'low'}),

  session: injectService(),
  tmi: injectService(),
  imApi: injectService(),
  userEmotes: injectService('user-emotes'),

  unreadCount: computed('mostRecentMessage.messageId', 'lastReadId', 'lastLocalReadId', 'isMuted', function () {
    if (this.get('isMuted')) {
      return 0;
    }
    let lastId = this.get('mostRecentMessage.messageId'),
        lastReadId = this.get('lastLocalReadId') || this.get('lastReadId');
    return lastId - lastReadId;
  }),

  hasUnread: computed.gt('unreadCount', 0),

  earliestMessage: computed.readOnly('messages.firstObject'),
  mostRecentMessage: computed.readOnly('messages.lastObject'),

  init() {
    this._super(...arguments);
    this.set('notices', emberArray());
    this.set('messages', emberArray());
    this.set('isIgnored', false);
    this.set('lastLocalReadId', 0);

    // FIXME: The 'notices' and 'messages' properties could be deprecated (large refactor).
    // This exists because the conversation-chat-display component would concat `notices`
    // and `messages` and then sort them on every change to `notices` or `messages`, which
    // is expensive and causes browser freezing. The thread model could simply keep track
    // of the earliest and most recent MessageModel
    this.set('messagesAndNotices', emberArray()); // contains both `messages` and `notices`
  },

  addRemoteLastMessage(rawMessage) {
    if (this.get('_hasAddedRemoteLastMessage')) {
      return;
    }

    this.addRawMessage(rawMessage);
    this.set('_hasAddedRemoteLastMessage', true);
  },

  // Precondition: rawMessage "should" go after the
  // existing messages
  addRawMessage(rawMessage, isLocal=false) {
    let message = this._constructMessageModel(rawMessage, isLocal);
    this.get("messages").pushObject(message);
    this.get("messagesAndNotices").pushObject(message);
  },

  addLocalMessage(messageBody, nonce) {
    assert('Missing messageBody', !!messageBody);
    assert('Missing nonce', !!nonce);

    let nextMessageId = (this.get('mostRecentMessage.messageId') || 0) + 1;

    let rawMessage = {
      id: nextMessageId,
      body: messageBody,
      from_id: this.get('session.userData.id'),
      sent_ts: Date.now() / 1000,
      tags: {
        emotes: this.get("userEmotes").tryParseEmotes(messageBody)
      },
      nonce: nonce
    };

    this.addRawMessage(rawMessage, true);
  },

  addNotice(message) {
    let notice = NoticeModel.create({ message });
    this.get('notices').pushObject(notice);
    this.get('messagesAndNotices').pushObject(notice);
  },

  hasEarlierMessages() {
    let earliestMessage = this.get('earliestMessage');
    if (!earliestMessage) {
      return false;
    }

    return earliestMessage.messageId > 1;
  },

  // Dedups multiple calls to loading earlier messages
  loadEarlierMessages() {
    if (!this.hasEarlierMessages()) {
      return RSVP.resolve();
    }

    if (this.get(LOAD_EARLIER_MESSAGES_PROMISE_KEY)) {
      return this.get(LOAD_EARLIER_MESSAGES_PROMISE_KEY);
    }

    let earliestMessage = this.get('earliestMessage');

    let queryParams = {
      limit: 30
    };

    if (!earliestMessage.get('isLocal')) {
      queryParams.before = earliestMessage.get('messageId');
    }

    let query = {
      threadId: this.get('id'),
      queryParams
    };

    let loadEarlierMessagesPromise = new RSVP.Promise((resolve, reject) => {
      this.get('imApi').loadMessages(query).then((rawMessages) => {
        if (this.isDestroyed) { return; }
        this._prependRawMessages(rawMessages);
        resolve();
      }, reject).finally(() => {
        if (this.isDestroyed) { return; }
        this.set(LOAD_EARLIER_MESSAGES_PROMISE_KEY, null);
      });
    });

    this.set(LOAD_EARLIER_MESSAGES_PROMISE_KEY, loadEarlierMessagesPromise);

    return loadEarlierMessagesPromise;
  },

  // Precondition: rawMessages "should" all go before the
  // existing messages
  _prependRawMessages(rawMessages) {
    let newMessages = rawMessages.map(rawMessage => {
      return this._constructMessageModel(rawMessage);
    });

    this.get('messages').unshiftObjects(newMessages);
    this.get('messagesAndNotices').unshiftObjects(newMessages);
  },

  loadUnreadMessages(mostRecentId, lastMessageId) {
    assert('Missing mostRecentId', !!mostRecentId);
    assert('Missing lastMessageId', !!lastMessageId);

    let queryParams = {
      threadId: this.get('id'),
      queryParams: {
        before: lastMessageId + 1,
        limit: lastMessageId - mostRecentId
      }
    };

    this.get('imApi').loadMessages(queryParams).then((rawMessages) => {
      if (this.isDestroyed) { return; }

      this._appendRawMessages(rawMessages);
    });
  },

  // Precondition: rawMessages "should" all go after the
  // existing messages
  _appendRawMessages(rawMessages) {
    let newMessages = rawMessages.map(rawMessage => {
      return this._constructMessageModel(rawMessage);
    });

    this.get('messages').pushObjects(newMessages);
    this.get('messagesAndNotices').pushObjects(newMessages);
  },

  _constructMessageModel(rawMessage, isLocal=false) {
    assert(`From ID is missing`, !!rawMessage.from_id);
    assert(`Message ID is missing`, !!rawMessage.id);
    assert(`Body is missing`, !!rawMessage.body);

    let from = this.get('participants').find(participant => {
      return participant.get('id') === rawMessage.from_id.toString();
    });

    assert(`From participant model for id ${rawMessage.from_id} is missing`, !!from);

    let message = MessageModel.create({
      body: rawMessage.body,
      // FIXME: Do we need fromId?
      from: from,
      fromId: rawMessage.from_id,
      messageId: rawMessage.id,
      sentAt: new Date(rawMessage.sent_ts * 1000),
      tags: rawMessage.tags,
      thread: this,
      isLocal: isLocal,
      nonce: rawMessage.nonce
    });

    return message;
  },

  otherUser: computed('participants.@each.id', function () {
    let userId = this.get('session.userData.id').toString();
    return this.get('participants').find((participant) => {
      return participant.get('id') !== userId;
    });
  }),

  otherUsername: computed('otherUser.username', function () {
    return this.get('otherUser.username');
  }),

  otherDisplayname: computed('otherUser.username', 'otherUser.displayName', function () {
    return this.get('otherUser.displayName') || this.get('otherUser.username');
  }),

  _setIsIgnored: observer('participants.@each.username', function () {
    this.get('tmi').isIgnored(this.get('otherUsername')).then(isIgnored => {
      if (this.isDestroyed) {
        return;
      }
      this.set('isIgnored', isIgnored);
    });
  }),

  _syncLocal: observer('lastReadId', function () {
    this.set('lastLocalReadId', this.get('lastReadId'));
  }),

  _checkLocalReadId: observer('lastLocalReadId', function () {
    if (this.get('lastLocalReadId') > this.get('mostRecentMessage.messageId')) {
      this.set('lastLocalReadId', this.get('mostRecentMessage.messageId'));
    }
  })
});
