text: /* eslint no-undefined: 0 */
  
  'use strict';
  
  const MimeNode = require('../mime-node');
  const mimeFuncs = require('../mime-funcs');
  
  /**
   * Creates the object for composing a MimeNode instance out from the mail options
   *
   * @constructor
   * @param {Object} mail Mail options
   */
  class MailComposer {
      constructor(mail) {
          this.mail = mail || {};
          this.message = false;
      }
  
      /**
       * Builds MimeNode instance
       */
      compile() {
          this._alternatives = this.getAlternatives();
          this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
          this._attachments = this.getAttachments(!!this._htmlNode);
  
          this._useRelated = !!(this._htmlNode && this._attachments.related.length);
          this._useAlternative = this._alternatives.length > 1;
          this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);
  
          // Compose MIME tree
          if (this.mail.raw) {
              this.message = new MimeNode().setRaw(this.mail.raw);
          } else if (this._useMixed) {
              this.message = this._createMixed();
          } else if (this._useAlternative) {
              this.message = this._createAlternative();
          } else if (this._useRelated) {
              this.message = this._createRelated();
          } else {
              this.message = this._createContentNode(
                  false,
                  []
                      .concat(this._alternatives || [])
                      .concat(this._attachments.attached || [])
                      .shift() || {
                      contentType: 'text/plain',
                      content: ''
                  }
              );
          }
  
          // Add custom headers
          if (this.mail.headers) {
              this.message.addHeader(this.mail.headers);
          }
  
          // Add headers to the root node, always overrides custom headers
          ['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
              let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
              if (this.mail[key]) {
                  this.message.setHeader(header, this.mail[key]);
              }
          });
  
          // Sets custom envelope
          if (this.mail.envelope) {
              this.message.setEnvelope(this.mail.envelope);
          }
  
          // ensure Message-Id value
          this.message.messageId();
  
          return this.message;
      }
  
      /**
       * List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
       *
       * @param {Boolean} findRelated If true separate related attachments from attached ones
       * @returns {Object} An object of arrays (`related` and `attached`)
       */
      getAttachments(findRelated) {
          let icalEvent, eventObject;
          let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
              let data;
              let isMessageNode = /^message\//i.test(attachment.contentType);
  
              if (/^data:/i.test(attachment.path || attachment.href)) {
                  attachment = this._processDataUrl(attachment);
              }
  
              data = {
                  contentType: attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'),
                  contentDisposition: attachment.contentDisposition || (isMessageNode ? 'inline' : 'attachment'),
                  contentTransferEncoding: 'contentTransferEncoding' in attachment ? attachment.contentTransferEncoding : 'base64'
              };
  
              if (attachment.filename) {
                  data.filename = attachment.filename;
              } else if (!isMessageNode && attachment.filename !== false) {
                  data.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
                  if (data.filename.indexOf('.') < 0) {
                      data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
                  }
              }
  
              if (/^https?:\/\//i.test(attachment.path)) {
                  attachment.href = attachment.path;
                  attachment.path = undefined;
              }
  
              if (attachment.cid) {
                  data.cid = attachment.cid;
              }
  
              if (attachment.raw) {
                  data.raw = attachment.raw;
              } else if (attachment.path) {
                  data.content = {
                      path: attachment.path
                  };
              } else if (attachment.href) {
                  data.content = {
                      href: attachment.href,
                      httpHeaders: attachment.httpHeaders
                  };
              } else {
                  data.content = attachment.content || '';
              }
  
              if (attachment.encoding) {
                  data.encoding = attachment.encoding;
              }
  
              if (attachment.headers) {
                  data.headers = attachment.headers;
              }
  
              return data;
          });
  
          if (this.mail.icalEvent) {
              if (
                  typeof this.mail.icalEvent === 'object' &&
                  (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
              ) {
                  icalEvent = this.mail.icalEvent;
              } else {
                  icalEvent = {
                      content: this.mail.icalEvent
                  };
              }
  
              eventObject = {};
              Object.keys(icalEvent).forEach(key => {
                  eventObject[key] = icalEvent[key];
              });
  
              eventObject.contentType = 'application/ics';
              if (!eventObject.headers) {
                  eventObject.headers = {};
              }
              eventObject.filename = eventObject.filename || 'invite.ics';
              eventObject.headers['Content-Disposition'] = 'attachment';
              eventObject.headers['Content-Transfer-Encoding'] = 'base64';
          }
  
          if (!findRelated) {
              return {
                  attached: attachments.concat(eventObject || []),
                  related: []
              };
          } else {
              return {
                  attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
                  related: attachments.filter(attachment => !!attachment.cid)
              };
          }
      }
  
      /**
       * List alternatives. Resulting objects can be used as input for MimeNode nodes
       *
       * @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
       */
      getAlternatives() {
          let alternatives = [],
              text,
              html,
              watchHtml,
              amp,
              icalEvent,
              eventObject;
  
          if (this.mail.text) {
              if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) {
                  text = this.mail.text;
              } else {
                  text = {
                      content: this.mail.text
                  };
              }
              text.contentType = 'text/plain; charset=utf-8';
          }
  
          if (this.mail.watchHtml) {
              if (
                  typeof this.mail.watchHtml === 'object' &&
                  (this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw)
              ) {
                  watchHtml = this.mail.watchHtml;
              } else {
                  watchHtml = {
                      content: this.mail.watchHtml
                  };
              }
              watchHtml.contentType = 'text/watch-html; charset=utf-8';
          }
  
          if (this.mail.amp) {
              if (typeof this.mail.amp === 'object' && (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)) {
                  amp = this.mail.amp;
              } else {
                  amp = {
                      content: this.mail.amp
                  };
              }
              amp.contentType = 'text/x-amp-html; charset=utf-8';
          }
  
          // NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
          if (this.mail.icalEvent) {
              if (
                  typeof this.mail.icalEvent === 'object' &&
                  (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
              ) {
                  icalEvent = this.mail.icalEvent;
              } else {
                  icalEvent = {
                      content: this.mail.icalEvent
                  };
              }
  
              eventObject = {};
              Object.keys(icalEvent).forEach(key => {
                  eventObject[key] = icalEvent[key];
              });
  
              if (eventObject.content && typeof eventObject.content === 'object') {
                  // we are going to have the same attachment twice, so mark this to be
                  // resolved just once
                  eventObject.content._resolve = true;
              }
  
              eventObject.filename = false;
              eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
              if (!eventObject.headers) {
                  eventObject.headers = {};
              }
          }
  
          if (this.mail.html) {
              if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) {
                  html = this.mail.html;
              } else {
                  html = {
                      content: this.mail.html
                  };
              }
              html.contentType = 'text/html; charset=utf-8';
          }
  
          []
              .concat(text || [])
              .concat(watchHtml || [])
              .concat(amp || [])
              .concat(html || [])
              .concat(eventObject || [])
              .concat(this.mail.alternatives || [])
              .forEach(alternative => {
                  let data;
  
                  if (/^data:/i.test(alternative.path || alternative.href)) {
                      alternative = this._processDataUrl(alternative);
                  }
  
                  data = {
                      contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
                      contentTransferEncoding: alternative.contentTransferEncoding
                  };
  
                  if (alternative.filename) {
                      data.filename = alternative.filename;
                  }
  
                  if (/^https?:\/\//i.test(alternative.path)) {
                      alternative.href = alternative.path;
                      alternative.path = undefined;
                  }
  
                  if (alternative.raw) {
                      data.raw = alternative.raw;
                  } else if (alternative.path) {
                      data.content = {
                          path: alternative.path
                      };
                  } else if (alternative.href) {
                      data.content = {
                          href: alternative.href
                      };
                  } else {
                      data.content = alternative.content || '';
                  }
  
                  if (alternative.encoding) {
                      data.encoding = alternative.encoding;
                  }
  
                  if (alternative.headers) {
                      data.headers = alternative.headers;
                  }
  
                  alternatives.push(data);
              });
  
          return alternatives;
      }
  
      /**
       * Builds multipart/mixed node. It should always contain different type of elements on the same level
       * eg. text + attachments
       *
       * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
       * @returns {Object} MimeNode node element
       */
      _createMixed(parentNode) {
          let node;
  
          if (!parentNode) {
              node = new MimeNode('multipart/mixed', {
                  baseBoundary: this.mail.baseBoundary,
                  textEncoding: this.mail.textEncoding,
                  boundaryPrefix: this.mail.boundaryPrefix,
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          } else {
              node = parentNode.createChild('multipart/mixed', {
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          }
  
          if (this._useAlternative) {
              this._createAlternative(node);
          } else if (this._useRelated) {
              this._createRelated(node);
          }
  
          []
              .concat((!this._useAlternative && this._alternatives) || [])
              .concat(this._attachments.attached || [])
              .forEach(element => {
                  // if the element is a html node from related subpart then ignore it
                  if (!this._useRelated || element !== this._htmlNode) {
                      this._createContentNode(node, element);
                  }
              });
  
          return node;
      }
  
      /**
       * Builds multipart/alternative node. It should always contain same type of elements on the same level
       * eg. text + html view of the same data
       *
       * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
       * @returns {Object} MimeNode node element
       */
      _createAlternative(parentNode) {
          let node;
  
          if (!parentNode) {
              node = new MimeNode('multipart/alternative', {
                  baseBoundary: this.mail.baseBoundary,
                  textEncoding: this.mail.textEncoding,
                  boundaryPrefix: this.mail.boundaryPrefix,
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          } else {
              node = parentNode.createChild('multipart/alternative', {
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          }
  
          this._alternatives.forEach(alternative => {
              if (this._useRelated && this._htmlNode === alternative) {
                  this._createRelated(node);
              } else {
                  this._createContentNode(node, alternative);
              }
          });
  
          return node;
      }
  
      /**
       * Builds multipart/related node. It should always contain html node with related attachments
       *
       * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
       * @returns {Object} MimeNode node element
       */
      _createRelated(parentNode) {
          let node;
  
          if (!parentNode) {
              node = new MimeNode('multipart/related; type="text/html"', {
                  baseBoundary: this.mail.baseBoundary,
                  textEncoding: this.mail.textEncoding,
                  boundaryPrefix: this.mail.boundaryPrefix,
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          } else {
              node = parentNode.createChild('multipart/related; type="text/html"', {
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          }
  
          this._createContentNode(node, this._htmlNode);
  
          this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));
  
          return node;
      }
  
      /**
       * Creates a regular node with contents
       *
       * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
       * @param {Object} element Node data
       * @returns {Object} MimeNode node element
       */
      _createContentNode(parentNode, element) {
          element = element || {};
          element.content = element.content || '';
  
          let node;
          let encoding = (element.encoding || 'utf8')
              .toString()
              .toLowerCase()
              .replace(/[-_\s]/g, '');
  
          if (!parentNode) {
              node = new MimeNode(element.contentType, {
                  filename: element.filename,
                  baseBoundary: this.mail.baseBoundary,
                  textEncoding: this.mail.textEncoding,
                  boundaryPrefix: this.mail.boundaryPrefix,
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          } else {
              node = parentNode.createChild(element.contentType, {
                  filename: element.filename,
                  disableUrlAccess: this.mail.disableUrlAccess,
                  disableFileAccess: this.mail.disableFileAccess,
                  normalizeHeaderKey: this.mail.normalizeHeaderKey
              });
          }
  
          // add custom headers
          if (element.headers) {
              node.addHeader(element.headers);
          }
  
          if (element.cid) {
              node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
          }
  
          if (element.contentTransferEncoding) {
              node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
          } else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
              node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
          }
  
          if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
              node.setHeader('Content-Disposition', element.contentDisposition || (element.cid ? 'inline' : 'attachment'));
          }
  
          if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
              element.content = Buffer.from(element.content, encoding);
          }
  
          // prefer pregenerated raw content
          if (element.raw) {
              node.setRaw(element.raw);
          } else {
              node.setContent(element.content);
          }
  
          return node;
      }
  
      /**
       * Parses data uri and converts it to a Buffer
       *
       * @param {Object} element Content element
       * @return {Object} Parsed element
       */
      _processDataUrl(element) {
          let parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
          if (!parts) {
              return element;
          }
  
          element.content = /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2]));
  
          if ('path' in element) {
              element.path = false;
          }
  
          if ('href' in element) {
              element.href = false;
          }
  
          parts[1].split(';').forEach(item => {
              if (/^\w+\/[^/]+$/i.test(item)) {
                  element.contentType = element.contentType || item.toLowerCase();
              }
          });
  
          return element;
      }
  }
  
  module.exports = MailComposer;
  