/**
 *  @file
 *  File with utilities to handle media in html editing.
 */
(function ($) {

  Drupal.media = Drupal.media || {};
  /**
   * Utility to deal with media tokens / placeholders.
   */
  Drupal.media.filter = {
    /**
     * Replaces media tokens with the placeholders for html editing.
     * @param content
     */
    replaceTokenWithPlaceholder: function(content) {
      Drupal.media.filter.ensure_tagmap();
      var matches = content.match(/\[\[.*?\]\]/g);

      if (matches) {
        for (var i = 0; i < matches.length; i++) {
          var match = matches[i];
          if (match.indexOf('"type":"media"') == -1) {
            continue;
          }

          // Check if the macro exists in the tagmap. This ensures backwards
          // compatibility with existing media and is moderately more efficient
          // than re-building the element.
          var media = Drupal.settings.tagmap[match];
          var media_json = match.replace('[[', '').replace(']]', '');

          // Ensure that the media JSON is valid.
          try {
            var media_definition = JSON.parse(media_json);
          }
          catch (err) {
            // @todo: error logging.
            // Content should be returned to prevent an empty editor.
            return content;
          }

          // Re-build the media if the macro has changed from the tagmap.
          if (!media && media_definition.fid) {
            Drupal.media.filter.ensureSourceMap();
            var source;
            if (source = Drupal.settings.mediaSourceMap[media_definition.fid]) {
              media = document.createElement(source.tagName);
              media.src = source.src;
              media.innerHTML = source.innerHTML;
            }
            else {
              // If the media element can't be found, leave it in to be resolved
              // by the user later.
              continue;
            }
          }

          // Apply attributes.
          var element = Drupal.media.filter.create_element(media, media_definition);
          var markup  = Drupal.media.filter.outerHTML(element);

          // Use split and join to replace all instances of macro with markup.
          content = content.split(match).join(markup);
        }
      }

      return content;
    },

    /**
     * Returns alt and title field attribute data from the corresponding fields.
     *
     * Specifically looks for file_entity module's file_image_alt_text and
     * file_image_title_text fields as those are by default used to store
     * override values for image alt and title attributes.
     *
     * @param options (array)
     *   Options passed through a popup form submission.
     * @param includeFieldID (bool)
     *   If set, the returned object will have extra keys with the IDs of the
     *   found fields.
     *
     * If the alt or title fields were not found, their keys will be excluded
     * from the returned array.
     *
     * @return
     *   An object with the following keys:
     *   - alt: The value of the alt field.
     *   - altField: The id of the alt field.
     *   - title: The value of the title field.
     *   - titleField: The id of the title field.
     */
    parseAttributeFields: function(options, includeFieldID) {
      var attributes = {};

      for (var field in options) {
        // If the field is set to false, use an empty string for output.
        options[field] = options[field] === false ? '' : options[field];
        //if (field.match(/^field_file_image_alt_text/)) {
        if (field.match(new RegExp('^' + Drupal.settings.media.img_alt_field))) {
          attributes.alt = options[field];
          if (includeFieldID) {
            attributes.altField = field;
          }
        }

        //if (field.match(/^field_file_image_title_text/)) {
        if (field.match(new RegExp('^' + Drupal.settings.media.img_title_field))) {
          attributes.title = options[field];
          if (includeFieldID) {
            attributes.titleField = field;
          }
        }
      }

      return attributes;
    },

    /**
     * Ensures changes made to fielded attributes are done on the fields too.
     *
     * This should be called when creating a macro tag from a placeholder.
     *
     * Changed made to attributes represented by fields are synced back to the
     * corresponding fields, if they exist. The alt/title attribute
     * values encoded in the macro will override the alt/title field values (set
     * in the Media dialog) during rendering of both WYSIWYG placeholders and
     * the final file entity on the server. Syncing makes changes applied to a
     * placeholder's alt/title attribute using native WYSIWYG tools visible in
     * the fields shown in the Media dialog.
     *
     * The reverse should be done when creating a placeholder from a macro tag
     * so changes made in the Media dialog are reflected in the placeholder's
     * alt and title attributes or the values there become stale and the change
     * appears uneffective.
     *
     * @param file_info (object)
     *   A JSON decoded object of the file being inserted/updated.
     */
    syncAttributesToFields: function(file_info) {
      if (!file_info) {
        file_info = {};
      }
      if (!file_info.attributes) {
        file_info.attributes = {};
      }
      if (!file_info.fields) {
        file_info.fields = {};
      }
      var fields = Drupal.media.filter.parseAttributeFields(file_info.fields, true);

      // If the title attribute has changed, ensure the title field is updated.
      var titleAttr = file_info.attributes.title || false;
      if (fields.titleField && (titleAttr !== fields.title)) {
        file_info.fields[fields.titleField] = titleAttr;
      }

      // If the alt attribute has changed, ensure the alt field is updated.
      var altAttr = file_info.attributes.alt || false;
      if (fields.altField && (altAttr !== fields.alt)) {
        file_info.fields[fields.altField] = altAttr;
      }

      return file_info;
    },

    /**
     * Replaces media elements with tokens.
     *
     * @param content (string)
     *   The markup within the wysiwyg instance.
     */
    replacePlaceholderWithToken: function(content) {
      Drupal.media.filter.ensure_tagmap();

      // Locate and process all the media placeholders in the WYSIWYG content.
      var contentElements = $('<div/>');  // TODO: once baseline jQuery is 1.8+, switch to using $.parseHTML(content)
      contentElements.get(0).innerHTML = content;
      var mediaElements = contentElements.find('.media-element');
      if (mediaElements) {
        $(mediaElements).each(function (i) {
          // Attempt to derive a JSON macro representation of the media placeholder.
          // Note: Drupal 7 ships with JQuery 1.4.4, which allows $(this).attr('outerHTML') to retrieve the eement's HTML,
          // but many sites use JQuery update to increate this to 1.6+, which insists on $(this).prop('outerHTML).
          // Until the minimum jQuery is >= 1.6, we need to do this the old-school way.
          // See http://stackoverflow.com/questions/2419749/get-selected-elements-outer-html
          var markup = $(this).get(0).outerHTML;
          if (markup === undefined) {
            // Browser does not support outerHTML DOM property.  Use the more expensive clone method instead.
            markup = $(this).clone().wrap('<div>').parent().html();
          }
          var macro = Drupal.media.filter.create_macro($(markup));
          if (macro) {
            // Replace the placeholder with the macro in the parsed content.
            // (Can't just replace the string section, because the outerHTML may be subtly different,
            // depending on the browser. Parsing tends to convert <img/> to <img>, for instance.)
            Drupal.settings.tagmap[macro] = markup;
            $(this).replaceWith(macro);
          }
        });
        content = $(contentElements).html();
      }

      return content;
    },

    /**
     * Serializes file information as a url-encoded JSON object and stores it
     * as a data attribute on the html element.
     *
     * @param html (string)
     *    A html element to be used to represent the inserted media element.
     * @param info (object)
     *    A object containing the media file information (fid, view_mode, etc).
     */
    create_element: function (html, info) {
      if ($('<div>').append(html).text().length === html.length) {
        // Element is not an html tag. Surround it in a span element so we can
        // pass the file attributes.
        html = '<span>' + html + '</span>';
      }
      var element = $(html);

      // Parse out link wrappers. They will be re-applied when the image is
      // rendered on the front-end.
      if (element.is('a') && element.find('img').length) {
        element = element.children();
      }

      // Extract attributes represented by fields and use those values to keep
      // them in sync, usually alt and title.
      var attributes = Drupal.media.filter.parseAttributeFields(info.fields);
      info.attributes = $.extend(info.attributes, attributes);

      // Move attributes from the file info array to the placeholder element.
      if (info.attributes) {
        $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) {
          if (info.attributes[a]) {
            element.attr(a, info.attributes[a]);
          }
          else if (element.attr(a)) {
            // If the element has the attribute, but the value is empty, be
            // sure to clear it.
            element.removeAttr(a);
          }
        });
        delete(info.attributes);

        // Store information to rebuild the element later, if necessary.
        Drupal.media.filter.ensureSourceMap();
        Drupal.settings.mediaSourceMap[info.fid] = {
          tagName: element[0].tagName,
          src: element[0].src,
          innerHTML: element[0].innerHTML
        }
      }

      info.type = info.type || "media";

      // Store the data in the data map.
      Drupal.media.filter.ensureDataMap();

      // Generate a "delta" to allow for multiple embeddings of the same file.
      var delta = Drupal.media.filter.fileEmbedDelta(info.fid, element);
      if (Drupal.settings.mediaDataMap[info.fid]) {
        info.field_deltas = Drupal.settings.mediaDataMap[info.fid].field_deltas || {};
      }
      else {
        info.field_deltas = {};
      }
      info.field_deltas[delta] = info.fields;
      element.attr('data-delta', delta);

      Drupal.settings.mediaDataMap[info.fid] = info;

      // Store the fid in the DOM to retrieve the data from the info map.
      element.attr('data-fid', info.fid);

      // Add data-media-element attribute so we can find the markup element later.
      element.attr('data-media-element', '1')

      var classes = ['media-element'];
      if (info.view_mode) {
        // Remove any existing view mode classes.
        element.removeClass (function (index, css) {
          return (css.match (/\bfile-\S+/g) || []).join(' ');
        });
        classes.push('file-' + info.view_mode.replace(/_/g, '-'));
      }
      // Check for alignment info, after removing any existing alignment class.
      element.removeClass (function (index, css) {
        return (css.match (/\bmedia-wysiwyg-align-\S+/g) || []).join(' ');
      });
      if (info.fields && info.fields.alignment) {
        classes.push('media-wysiwyg-align-' + info.fields.alignment);
      }
      element.addClass(classes.join(' '));

      // Attempt to override the link_title if the user has chosen to do this.
      info.link_text = this.overrideLinkTitle(info);
      // Apply link_text if present.
      if ((info.link_text) && (!info.fields || !info.fields.external_url || info.fields.external_url.length === 0)) {
        $('a', element).html(info.link_text);
      }

      return element;
    },

    /**
     * Create a macro representation of the inserted media element.
     *
     * @param element (jQuery object)
     *    A media element with attached serialized file info.
     */
    create_macro: function (element) {
      var file_info = Drupal.media.filter.extract_file_info(element);
      if (file_info) {
        if (typeof file_info.link_text == 'string') {
          file_info.link_text = this.overrideLinkTitle(file_info);
          // Make sure the link_text-html-tags are properly escaped.
          file_info.link_text = file_info.link_text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
        }
        return '[[' + JSON.stringify(file_info) + ']]';
      }
      return false;
    },

    /**
     * Extract the file info from a WYSIWYG placeholder element as JSON.
     *
     * @param element (jQuery object)
     *    A media element with associated file info via a file id (fid).
     */
    extract_file_info: function (element) {
      var fid, file_info, value, delta;

      if (fid = element.data('fid')) {
        Drupal.media.filter.ensureDataMap();

        if (file_info = Drupal.settings.mediaDataMap[fid]) {
          file_info.attributes = {};

          $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) {
            if (value = element.attr(a)) {
              // Replace &quot; by \" to avoid error with JSON format.
              if (typeof value == 'string') {
                value = value.replace('&quot;', '\\"');
              }
              file_info.attributes[a] = value;
            }
          });

          // Extract the link text, if there is any.
          file_info.link_text = (Drupal.settings.mediaDoLinkText) ? element.find('a:not(:has(img))').html() : false;
          // When a file is embedded, its fields can be overridden. To allow for
          // the edge case where the same file is embedded multiple times with
          // different field overrides, we look for a data-delta attribute on
          // the element, and use that to decide which set of data in the
          // "field_deltas" property to use.
          if (delta = element.data('delta')) {
            if (file_info.field_deltas && file_info.field_deltas[delta]) {
              file_info.fields = file_info.field_deltas[delta];

              // Also look for an overridden view mode, aka "format".
              // Check for existance of fields to make it backward compatible.
              if (file_info.fields && file_info.fields.format && file_info.view_mode) {
                file_info.view_mode = file_info.fields.format;
              }
            }
          }
        }
        else {
          return false;
        }
      }
      else {
        return false;
      }

      return Drupal.media.filter.syncAttributesToFields(file_info);
    },

    /**
     * Gets the HTML content of an element.
     *
     * @param element (jQuery object)
     */
    outerHTML: function (element) {
      return element[0].outerHTML || $('<div>').append(element.eq(0).clone()).html();
    },

    /**
     * Gets the wrapped HTML content of an element to insert into the wysiwyg.
     *
     * It also registers the element in the tag map so that the token
     * replacement works.
     *
     * @param element (jQuery object) The element to insert.
     *
     * @see Drupal.media.filter.replacePlaceholderWithToken()
     */
    getWysiwygHTML: function (element) {
      // Create the markup and the macro.
      var markup = Drupal.media.filter.outerHTML(element),
        macro = Drupal.media.filter.create_macro(element);

      // Store macro/markup in the tagmap.
      Drupal.media.filter.ensure_tagmap();
      Drupal.settings.tagmap[macro] = markup;

      // Return the html code to insert in an editor and use it with
      // replacePlaceholderWithToken()
      return markup;
    },

    /**
     * Ensures the src tracking has been initialized and returns it.
     */
    ensureSourceMap: function() {
      Drupal.settings.mediaSourceMap = Drupal.settings.mediaSourceMap || {};
      return Drupal.settings.mediaSourceMap;
    },

    /**
     * Ensures the data tracking has been initialized and returns it.
     */
    ensureDataMap: function() {
      Drupal.settings.mediaDataMap = Drupal.settings.mediaDataMap || {};
      return Drupal.settings.mediaDataMap;
    },

    /**
     * Ensures the tag map has been initialized and returns it.
     */
    ensure_tagmap: function () {
      Drupal.settings.tagmap = Drupal.settings.tagmap || {};
      return Drupal.settings.tagmap;
    },

    /**
     * Return the overridden link title based on the file_entity title field
     * set.
     * @param file the file object.
     * @returns the overridden link_title or the existing link text if no
     * overridden.
     */
    overrideLinkTitle: function(file) {
      var file_title_field = Drupal.settings.media.img_title_field.replace('field_', '');
      var file_title_field_machine_name = '';
      if (typeof(file.fields) != 'undefined') {
        jQuery.each(file.fields, function(field, fieldValue) {
          if (field.indexOf(file_title_field) != -1) {
            file_title_field_machine_name = field;
          }
        });

        if (typeof(file.fields[file_title_field_machine_name]) != 'undefined' && file.fields[file_title_field_machine_name] != '') {
          return file.fields[file_title_field_machine_name];
        }
        else {
          return file.link_text;
        }
      }
      else {
        return file.link_text;
      }
    },

    /**
     * Generates a unique "delta" for each embedding of a particular file.
     */
    fileEmbedDelta: function(fid, element) {
      // Ensure we have an object to track our deltas.
      Drupal.settings.mediaDeltas = Drupal.settings.mediaDeltas || {};
      Drupal.settings.maxMediaDelta = Drupal.settings.maxMediaDelta || 0;

      // Check to see if the element already has one.
      if (element && element.data('delta')) {
        var existingDelta = element.data('delta');
        // If so, make sure that it is being tracked in mediaDeltas. If we're
        // going to create new deltas later on, make sure they do not overwrite
        // other mediaDeltas.
        if (!Drupal.settings.mediaDeltas[existingDelta]) {
          Drupal.settings.mediaDeltas[existingDelta] = fid;
          Drupal.settings.maxMediaDelta = Math.max(Drupal.settings.maxMediaDelta, existingDelta);
        }
        return existingDelta;
      }
      // Otherwise, generate a new one.
      var newDelta = Drupal.settings.maxMediaDelta + 1;
      Drupal.settings.mediaDeltas[newDelta] = fid;
      Drupal.settings.maxMediaDelta = newDelta;
      return newDelta;
    }
  }

})(jQuery);
