/* global Whisper: false */
/* global i18n: false */
/* global textsecure: false */
/* global _: false */
/* global emoji_util: false */
/* global Mustache: false */
/* global $: false */
// eslint-disable-next-line func-names
(function() {
  'use strict';
  const { Signal } = window;
  const { loadAttachmentData } = window.Signal.Migrations;
  window.Whisper = window.Whisper || {};
  const URL_REGEX = /(^|[\s\n]|
)((?:https?|ftp):\/\/[-A-Z0-9\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFD+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|])/gi;
  const ErrorIconView = Whisper.View.extend({
    templateName: 'error-icon',
    className: 'error-icon-container',
    initialize() {
      if (this.model.name === 'UnregisteredUserError') {
        this.$el.addClass('unregistered-user-error');
      }
    },
  });
  const NetworkErrorView = Whisper.View.extend({
    tagName: 'span',
    className: 'hasRetry',
    templateName: 'hasRetry',
    render_attributes() {
      let messageNotSent;
      if (!this.model.someRecipientsFailed()) {
        messageNotSent = i18n('messageNotSent');
      }
      return {
        messageNotSent,
        resend: i18n('resend'),
      };
    },
  });
  const SomeFailedView = Whisper.View.extend({
    tagName: 'span',
    className: 'some-failed',
    templateName: 'some-failed',
    render_attributes() {
      return {
        someFailed: i18n('someRecipientsFailed'),
      };
    },
  });
  const TimerView = Whisper.View.extend({
    templateName: 'hourglass',
    initialize() {
      this.listenTo(this.model, 'unload', this.remove);
    },
    update() {
      if (this.timeout) {
        clearTimeout(this.timeout);
        this.timeout = null;
      }
      if (this.model.isExpired()) {
        return this;
      }
      if (this.model.isExpiring()) {
        this.render();
        const totalTime = this.model.get('expireTimer') * 1000;
        const remainingTime = this.model.msTilExpire();
        const elapsed = (totalTime - remainingTime) / totalTime;
        this.$('.sand').css('transform', `translateY(${elapsed * 100}%)`);
        this.$el.css('display', 'inline-block');
        this.timeout = setTimeout(
          this.update.bind(this),
          Math.max(totalTime / 100, 500)
        );
      }
      return this;
    },
  });
  Whisper.ExpirationTimerUpdateView = Whisper.View.extend({
    tagName: 'li',
    className: 'expirationTimerUpdate advisory',
    templateName: 'expirationTimerUpdate',
    id() {
      return this.model.id;
    },
    initialize() {
      this.conversation = this.model.getExpirationTimerUpdateSource();
      this.listenTo(this.conversation, 'change', this.render);
      this.listenTo(this.model, 'unload', this.remove);
    },
    render_attributes() {
      const seconds = this.model.get('expirationTimerUpdate').expireTimer;
      let timerMessage;
      const timerUpdate = this.model.get('expirationTimerUpdate');
      const prettySeconds = Whisper.ExpirationTimerOptions.getName(seconds);
      if (timerUpdate && timerUpdate.fromSync) {
        timerMessage = i18n('timerSetOnSync', prettySeconds);
      } else if (this.conversation.id === textsecure.storage.user.getNumber()) {
        timerMessage = i18n('youChangedTheTimer', prettySeconds);
      } else {
        timerMessage = i18n('theyChangedTheTimer', [
          this.conversation.getTitle(),
          prettySeconds,
        ]);
      }
      return { content: timerMessage };
    },
  });
  Whisper.KeyChangeView = Whisper.View.extend({
    tagName: 'li',
    className: 'keychange advisory',
    templateName: 'keychange',
    id() {
      return this.model.id;
    },
    initialize() {
      this.conversation = this.model.getModelForKeyChange();
      this.listenTo(this.conversation, 'change', this.render);
      this.listenTo(this.model, 'unload', this.remove);
    },
    events: {
      'click .content': 'showIdentity',
    },
    render_attributes() {
      return {
        content: this.model.getNotificationText(),
      };
    },
    showIdentity() {
      this.$el.trigger('show-identity', this.conversation);
    },
  });
  Whisper.VerifiedChangeView = Whisper.View.extend({
    tagName: 'li',
    className: 'verified-change advisory',
    templateName: 'verified-change',
    id() {
      return this.model.id;
    },
    initialize() {
      this.conversation = this.model.getModelForVerifiedChange();
      this.listenTo(this.conversation, 'change', this.render);
      this.listenTo(this.model, 'unload', this.remove);
    },
    events: {
      'click .content': 'showIdentity',
    },
    render_attributes() {
      let key;
      if (this.model.get('verified')) {
        if (this.model.get('local')) {
          key = 'youMarkedAsVerified';
        } else {
          key = 'youMarkedAsVerifiedOtherDevice';
        }
        return {
          icon: 'verified',
          content: i18n(key, this.conversation.getTitle()),
        };
      }
      if (this.model.get('local')) {
        key = 'youMarkedAsNotVerified';
      } else {
        key = 'youMarkedAsNotVerifiedOtherDevice';
      }
      return {
        icon: 'shield',
        content: i18n(key, this.conversation.getTitle()),
      };
    },
    showIdentity() {
      this.$el.trigger('show-identity', this.conversation);
    },
  });
  Whisper.MessageView = Whisper.View.extend({
    tagName: 'li',
    templateName: 'message',
    id() {
      return this.model.id;
    },
    initialize() {
      //   loadedAttachmentViews :: Promise (Array AttachmentView) | null
      this.loadedAttachmentViews = null;
      this.listenTo(this.model, 'change:errors', this.onErrorsChanged);
      this.listenTo(this.model, 'change:body', this.render);
      this.listenTo(this.model, 'change:delivered', this.renderDelivered);
      this.listenTo(this.model, 'change:read_by', this.renderRead);
      this.listenTo(
        this.model,
        'change:expirationStartTimestamp',
        this.renderExpiring
      );
      this.listenTo(this.model, 'change', this.onChange);
      this.listenTo(
        this.model,
        'change:flags change:group_update',
        this.renderControl
      );
      this.listenTo(this.model, 'destroy', this.onDestroy);
      this.listenTo(this.model, 'unload', this.onUnload);
      this.listenTo(this.model, 'expired', this.onExpired);
      this.listenTo(this.model, 'pending', this.renderPending);
      this.listenTo(this.model, 'done', this.renderDone);
      this.timeStampView = new Whisper.ExtendedTimestampView();
      this.contact = this.model.isIncoming() ? this.model.getContact() : null;
      if (this.contact) {
        this.listenTo(this.contact, 'change:color', this.updateColor);
      }
    },
    events: {
      'click .retry': 'retryMessage',
      'click .error-icon': 'select',
      'click .timestamp': 'select',
      'click .status': 'select',
      'click .some-failed': 'select',
      'click .error-message': 'select',
      'click .menu-container': 'showMenu',
      'click .menu-list .reply': 'onReply',
    },
    retryMessage() {
      const retrys = _.filter(
        this.model.get('errors'),
        this.model.isReplayableError.bind(this.model)
      );
      _.map(retrys, 'number').forEach(number => {
        this.model.resend(number);
      });
    },
    showMenu(e) {
      if (this.menuVisible) {
        return;
      }
      this.menuVisible = true;
      e.stopPropagation();
      this.$('.menu-list').show();
      $(document).one('click', () => {
        this.hideMenu();
      });
    },
    hideMenu() {
      this.menuVisible = false;
      this.$('.menu-list').hide();
    },
    onReply() {
      this.model.trigger('reply', this.model);
    },
    onExpired() {
      this.$el.addClass('expired');
      this.$el.find('.bubble').one('webkitAnimationEnd animationend', e => {
        if (e.target === this.$('.bubble')[0]) {
          this.remove();
        }
      });
      // Failsafe: if in the background, animation events don't fire
      setTimeout(this.remove.bind(this), 1000);
    },
    onUnload() {
      if (this.avatarView) {
        this.avatarView.remove();
      }
      if (this.errorIconView) {
        this.errorIconView.remove();
      }
      if (this.networkErrorView) {
        this.networkErrorView.remove();
      }
      if (this.someFailedView) {
        this.someFailedView.remove();
      }
      if (this.timeStampView) {
        this.timeStampView.remove();
      }
      if (this.quoteView) {
        this.quoteView.remove();
      }
      // NOTE: We have to do this in the background (`then` instead of `await`)
      // as our tests rely on `onUnload` synchronously removing the view from
      // the DOM.
      // eslint-disable-next-line more/no-then
      this.loadAttachmentViews().then(views =>
        views.forEach(view => view.unload())
      );
      // No need to handle this one, since it listens to 'unload' itself:
      //   this.timerView
      this.remove();
    },
    onDestroy() {
      if (this.$el.hasClass('expired')) {
        return;
      }
      this.onUnload();
    },
    onChange() {
      this.renderSent();
      this.renderQuote();
    },
    select(e) {
      this.$el.trigger('select', { message: this.model });
      e.stopPropagation();
    },
    className() {
      return ['entry', this.model.get('type')].join(' ');
    },
    renderPending() {
      this.$el.addClass('pending');
    },
    renderDone() {
      this.$el.removeClass('pending');
    },
    renderSent() {
      if (this.model.isOutgoing()) {
        this.$el.toggleClass('sent', !!this.model.get('sent'));
      }
    },
    renderDelivered() {
      if (this.model.get('delivered')) {
        this.$el.addClass('delivered');
      }
    },
    renderRead() {
      if (!_.isEmpty(this.model.get('read_by'))) {
        this.$el.addClass('read');
      }
    },
    onErrorsChanged() {
      if (this.model.isIncoming()) {
        this.render();
      } else {
        this.renderErrors();
      }
    },
    renderErrors() {
      const errors = this.model.get('errors');
      this.$('.error-icon-container').remove();
      if (this.errorIconView) {
        this.errorIconView.remove();
        this.errorIconView = null;
      }
      if (_.size(errors) > 0) {
        if (this.model.isIncoming()) {
          this.$('.content')
            .text(this.model.getDescription())
            .addClass('error-message');
        }
        this.errorIconView = new ErrorIconView({ model: errors[0] });
        this.errorIconView.render().$el.appendTo(this.$('.bubble'));
      } else if (!this.hasContents()) {
        const el = this.$('.content');
        if (!el || el.length === 0) {
          this.$('.inner-bubble').append("