/* global textsecure: false */
/* global Whisper: false */
/* global i18n: false */
/* global loadImage: false */
/* global Backbone: false */
/* global _: false */
/* global Signal: false */

// eslint-disable-next-line func-names
(function() {
  'use strict';

  window.Whisper = window.Whisper || {};

  const { MIME, VisualAttachment } = window.Signal.Types;

  Whisper.FileSizeToast = Whisper.ToastView.extend({
    templateName: 'file-size-modal',
    render_attributes() {
      return {
        'file-size-warning': i18n('fileSizeWarning'),
        limit: this.model.limit,
        units: this.model.units,
      };
    },
  });
  Whisper.UnableToLoadToast = Whisper.ToastView.extend({
    render_attributes() {
      return { toastMessage: i18n('unableToLoadAttachment') };
    },
  });

  Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
    template: i18n('dangerousFileType'),
  });
  Whisper.OneNonImageAtATimeToast = Whisper.ToastView.extend({
    template: i18n('oneNonImageAtATimeToast'),
  });
  Whisper.CannotMixImageAndNonImageAttachmentsToast = Whisper.ToastView.extend({
    template: i18n('cannotMixImageAdnNonImageAttachments'),
  });
  Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
    template: i18n('maximumAttachments'),
  });

  Whisper.FileInputView = Backbone.View.extend({
    tagName: 'span',
    className: 'file-input',
    initialize() {
      this.attachments = [];

      this.attachmentListView = new Whisper.ReactWrapperView({
        el: this.el,
        Component: window.Signal.Components.AttachmentList,
        props: this.getPropsForAttachmentList(),
      });
    },

    remove() {
      if (this.attachmentListView) {
        this.attachmentListView.remove();
      }
      if (this.captionEditorView) {
        this.captionEditorView.remove();
      }

      Backbone.View.prototype.remove.call(this);
    },

    render() {
      this.attachmentListView.update(this.getPropsForAttachmentList());
      this.trigger('staged-attachments-changed');
    },

    getPropsForAttachmentList() {
      const { attachments } = this;

      // We never want to display voice notes in our attachment list
      if (_.any(attachments, attachment => Boolean(attachment.isVoiceNote))) {
        return {
          attachments: [],
        };
      }

      return {
        attachments,
        onAddAttachment: this.onAddAttachment.bind(this),
        onClickAttachment: this.onClickAttachment.bind(this),
        onCloseAttachment: this.onCloseAttachment.bind(this),
        onClose: this.onClose.bind(this),
      };
    },

    onClickAttachment(attachment) {
      const getProps = () => ({
        url: attachment.videoUrl || attachment.url,
        caption: attachment.caption,
        attachment,
        onSave,
      });

      const onSave = caption => {
        // eslint-disable-next-line no-param-reassign
        attachment.caption = caption;
        this.captionEditorView.remove();
        Signal.Backbone.Views.Lightbox.hide();
        this.render();
      };

      this.captionEditorView = new Whisper.ReactWrapperView({
        className: 'attachment-list-wrapper',
        Component: window.Signal.Components.CaptionEditor,
        props: getProps(),
        onClose: () => Signal.Backbone.Views.Lightbox.hide(),
      });
      Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
    },

    onCloseAttachment(attachment) {
      this.attachments = _.without(this.attachments, attachment);
      this.render();
    },

    onAddAttachment() {
      this.trigger('choose-attachment');
    },

    onClose() {
      this.attachments = [];
      this.render();
    },

    // These event handlers are called by ConversationView, which listens for these events

    onDragOver(e) {
      if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
        return;
      }

      e.stopPropagation();
      e.preventDefault();
      this.$el.addClass('dropoff');
    },

    onDragLeave(e) {
      if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
        return;
      }

      e.stopPropagation();
      e.preventDefault();
      this.$el.removeClass('dropoff');
    },

    async onDrop(e) {
      if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
        return;
      }

      e.stopPropagation();
      e.preventDefault();

      const { files } = e.originalEvent.dataTransfer;
      for (let i = 0, max = files.length; i < max; i += 1) {
        const file = files[i];
        // eslint-disable-next-line no-await-in-loop
        await this.maybeAddAttachment(file);
      }

      this.$el.removeClass('dropoff');
    },

    onPaste(e) {
      const { items } = e.originalEvent.clipboardData;
      let imgBlob = null;
      for (let i = 0; i < items.length; i += 1) {
        if (items[i].type.split('/')[0] === 'image') {
          imgBlob = items[i].getAsFile();
        }
      }
      if (imgBlob !== null) {
        const file = imgBlob;
        this.maybeAddAttachment(file);

        e.stopPropagation();
        e.preventDefault();
      }
    },

    // Public interface

    hasFiles() {
      return this.attachments.length > 0;
    },

    async getFiles() {
      const files = await Promise.all(
        this.attachments.map(attachment => this.getFile(attachment))
      );
      this.clearAttachments();
      return files;
    },

    clearAttachments() {
      this.attachments.forEach(attachment => {
        if (attachment.url) {
          URL.revokeObjectURL(attachment.url);
        }
        if (attachment.videoUrl) {
          URL.revokeObjectURL(attachment.videoUrl);
        }
      });

      this.attachments = [];
      this.render();
      this.$el.trigger('force-resize');
    },

    // Show errors

    showLoadFailure() {
      const toast = new Whisper.UnableToLoadToast();
      toast.$el.insertAfter(this.$el);
      toast.render();
    },

    showDangerousError() {
      const toast = new Whisper.DangerousFileTypeToast();
      toast.$el.insertAfter(this.$el);
      toast.render();
    },

    showFileSizeError({ limit, units, u }) {
      const toast = new Whisper.FileSizeToast({
        model: { limit, units: units[u] },
      });
      toast.$el.insertAfter(this.$el);
      toast.render();
    },

    showCannotMixError() {
      const toast = new Whisper.CannotMixImageAndNonImageAttachmentsToast();
      toast.$el.insertAfter(this.$el);
      toast.render();
    },

    showMultipleNonImageError() {
      const toast = new Whisper.OneNonImageAtATimeToast();
      toast.$el.insertAfter(this.$el);
      toast.render();
    },

    showMaximumAttachmentsError() {
      const toast = new Whisper.MaxAttachmentsToast();
      toast.$el.insertAfter(this.$el);
      toast.render();
    },

    // Housekeeping

    addAttachment(attachment) {
      if (attachment.isVoiceNote && this.attachments.length > 0) {
        throw new Error('A voice note cannot be sent with other attachments');
      }

      this.attachments.push(attachment);
      this.render();
    },

    async maybeAddAttachment(file) {
      if (!file) {
        return;
      }

      const fileName = file.name;
      const contentType = file.type;

      if (window.Signal.Util.isFileDangerous(fileName)) {
        this.showDangerousError();
        return;
      }

      if (this.attachments.length >= 32) {
        this.showMaximumAttachmentsError();
        return;
      }

      const haveNonImage = _.any(
        this.attachments,
        attachment => !MIME.isImage(attachment.contentType)
      );
      // You can't add another attachment if you already have a non-image staged
      if (haveNonImage) {
        this.showMultipleNonImageError();
        return;
      }

      // You can't add a non-image attachment if you already have attachments staged
      if (!MIME.isImage(contentType) && this.attachments.length > 0) {
        this.showCannotMixError();
        return;
      }

      const renderVideoPreview = async () => {
        const objectUrl = URL.createObjectURL(file);
        try {
          const type = 'image/png';
          const thumbnail = await VisualAttachment.makeVideoScreenshot({
            objectUrl,
            contentType: type,
            logger: window.log,
          });
          const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
          const url = Signal.Util.arrayBufferToObjectURL({
            data,
            type,
          });
          this.addAttachment({
            file,
            size: file.size,
            fileName,
            contentType,
            videoUrl: objectUrl,
            url,
          });
        } catch (error) {
          URL.revokeObjectURL(objectUrl);
        }
      };

      const renderImagePreview = async () => {
        if (!MIME.isJPEG(contentType)) {
          const url = URL.createObjectURL(file);
          if (!url) {
            throw new Error('Failed to create object url for image!');
          }
          this.addAttachment({
            file,
            size: file.size,
            fileName,
            contentType,
            url,
          });
          return;
        }

        const url = await window.autoOrientImage(file);
        this.addAttachment({
          file,
          size: file.size,
          fileName,
          contentType,
          url,
        });
      };

      try {
        const blob = await this.autoScale({
          contentType,
          file,
        });
        let limitKb = 1000000;
        const blobType =
          file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];

        switch (blobType) {
          case 'image':
            limitKb = 6000;
            break;
          case 'gif':
            limitKb = 25000;
            break;
          case 'audio':
            limitKb = 100000;
            break;
          case 'video':
            limitKb = 100000;
            break;
          default:
            limitKb = 100000;
            break;
        }
        if ((blob.size / 1024).toFixed(4) >= limitKb) {
          const units = ['kB', 'MB', 'GB'];
          let u = -1;
          let limit = limitKb * 1000;
          do {
            limit /= 1000;
            u += 1;
          } while (limit >= 1000 && u < units.length - 1);
          this.showFileSizeError({ limit, units, u });
          return;
        }
      } catch (error) {
        window.log.error(
          'Error ensuring that image is properly sized:',
          error && error.stack ? error.stack : error
        );

        this.showLoadFailure();
        return;
      }

      try {
        if (Signal.Util.GoogleChrome.isImageTypeSupported(contentType)) {
          await renderImagePreview();
        } else if (Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)) {
          await renderVideoPreview();
        } else {
          this.addAttachment({
            file,
            size: file.size,
            contentType,
            fileName,
          });
        }
      } catch (e) {
        window.log.error(
          `Was unable to generate thumbnail for file type ${contentType}`,
          e && e.stack ? e.stack : e
        );
        this.addAttachment({
          file,
          size: file.size,
          contentType,
          fileName,
        });
      }
    },

    autoScale(attachment) {
      const { contentType, file } = attachment;
      if (
        contentType.split('/')[0] !== 'image' ||
        contentType === 'image/tiff'
      ) {
        // nothing to do
        return Promise.resolve(attachment);
      }

      return new Promise((resolve, reject) => {
        const url = URL.createObjectURL(file);
        const img = document.createElement('img');
        img.onerror = reject;
        img.onload = () => {
          URL.revokeObjectURL(url);

          const maxSize = 6000 * 1024;
          const maxHeight = 4096;
          const maxWidth = 4096;
          if (
            img.naturalWidth <= maxWidth &&
            img.naturalHeight <= maxHeight &&
            file.size <= maxSize
          ) {
            resolve(attachment);
            return;
          }

          const gifMaxSize = 25000 * 1024;
          if (file.type === 'image/gif' && file.size <= gifMaxSize) {
            resolve(attachment);
            return;
          }

          if (file.type === 'image/gif') {
            reject(new Error('GIF is too large'));
            return;
          }

          const canvas = loadImage.scale(img, {
            canvas: true,
            maxWidth,
            maxHeight,
          });

          let quality = 0.95;
          let i = 4;
          let blob;
          do {
            i -= 1;
            blob = window.dataURLToBlobSync(
              canvas.toDataURL('image/jpeg', quality)
            );
            quality = quality * maxSize / blob.size;
            // NOTE: During testing with a large image, we observed the
            // `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
            // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
            if (quality < 0.5) {
              quality = 0.5;
            }
          } while (i > 0 && blob.size > maxSize);

          resolve({
            ...attachment,
            file: blob,
          });
        };
        img.src = url;
      });
    },

    async getFile(attachment) {
      if (!attachment) {
        return Promise.resolve();
      }

      const attachmentFlags = attachment.isVoiceNote
        ? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
        : null;

      const scaled = await this.autoScale(attachment);
      const fileRead = await this.readFile(scaled);
      return {
        ...fileRead,
        url: undefined,
        videoUrl: undefined,
        flags: attachmentFlags || null,
      };
    },

    readFile(attachment) {
      return new Promise((resolve, reject) => {
        const FR = new FileReader();
        FR.onload = e => {
          resolve({
            ...attachment,
            data: e.target.result,
          });
        };
        FR.onerror = reject;
        FR.onabort = reject;
        FR.readAsArrayBuffer(attachment.file);
      });
    },
  });
})();