/**
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 */

/**
 * Jquery integration plugin.
 *
 * @class tinymce.core.JqueryIntegration
 * @private
 */

(function () {
  var undef, lazyLoading, patchApplied;
  var delayedInits = [], $, win;

  win = typeof global !== 'undefined' ? global : window;
  $ = win.jQuery;

  var getTinymce = function () {
    // Reference to tinymce needs to be lazily evaluated since tinymce
    // might be loaded through the compressor or other means
    return win.tinymce;
  };

  $.fn.tinymce = function (settings) {
    var self = this, url, base, lang, suffix = "";

    // No match then just ignore the call
    if (!self.length) {
      return self;
    }

    // Get editor instance
    if (!settings) {
      return getTinymce() ? getTinymce().get(self[0].id) : null;
    }

    self.css('visibility', 'hidden'); // Hide textarea to avoid flicker

    var init = function () {
      var editors = [], initCount = 0;

      // Apply patches to the jQuery object, only once
      if (!patchApplied) {
        applyPatch();
        patchApplied = true;
      }

      // Create an editor instance for each matched node
      self.each(function (i, node) {
        var ed, id = node.id, oninit = settings.oninit;

        // Generate unique id for target element if needed
        if (!id) {
          node.id = id = getTinymce().DOM.uniqueId();
        }

        // Only init the editor once
        if (getTinymce().get(id)) {
          return;
        }

        // Create editor instance and render it
        ed = getTinymce().createEditor(id, settings);
        editors.push(ed);

        ed.on('init', function () {
          var scope, func = oninit;

          self.css('visibility', '');

          // Run this if the oninit setting is defined
          // this logic will fire the oninit callback ones each
          // matched editor instance is initialized
          if (oninit) {
            // Fire the oninit event ones each editor instance is initialized
            if (++initCount == editors.length) {
              if (typeof func === "string") {
                scope = (func.indexOf(".") === -1) ? null : getTinymce().resolve(func.replace(/\.\w+$/, ""));
                func = getTinymce().resolve(func);
              }

              // Call the oninit function with the object
              func.apply(scope || getTinymce(), editors);
            }
          }
        });
      });

      // Render the editor instances in a separate loop since we
      // need to have the full editors array used in the onInit calls
      $.each(editors, function (i, ed) {
        ed.render();
      });
    };

    // Load TinyMCE on demand, if we need to
    if (!win.tinymce && !lazyLoading && (url = settings.script_url)) {
      lazyLoading = 1;
      base = url.substring(0, url.lastIndexOf("/"));

      // Check if it's a dev/src version they want to load then
      // make sure that all plugins, themes etc are loaded in source mode as well
      if (url.indexOf('.min') != -1) {
        suffix = ".min";
      }

      // Setup tinyMCEPreInit object this will later be used by the TinyMCE
      // core script to locate other resources like CSS files, dialogs etc
      // You can also predefined a tinyMCEPreInit object and then it will use that instead
      win.tinymce = win.tinyMCEPreInit || {
        base: base,
        suffix: suffix
      };

      // url contains gzip then we assume it's a compressor
      if (url.indexOf('gzip') != -1) {
        lang = settings.language || "en";
        url = url + (/\?/.test(url) ? '&' : '?') + "js=true&core=true&suffix=" + escape(suffix) +
          "&themes=" + escape(settings.theme || 'modern') + "&plugins=" +
          escape(settings.plugins || '') + "&languages=" + (lang || '');

        // Check if compressor script is already loaded otherwise setup a basic one
        if (!win.tinyMCE_GZ) {
          win.tinyMCE_GZ = {
            start: function () {
              var load = function (url) {
                getTinymce().ScriptLoader.markDone(getTinymce().baseURI.toAbsolute(url));
              };

              // Add core languages
              load("langs/" + lang + ".js");

              // Add themes with languages
              load("themes/" + settings.theme + "/theme" + suffix + ".js");
              load("themes/" + settings.theme + "/langs/" + lang + ".js");

              // Add plugins with languages
              $.each(settings.plugins.split(","), function (i, name) {
                if (name) {
                  load("plugins/" + name + "/plugin" + suffix + ".js");
                  load("plugins/" + name + "/langs/" + lang + ".js");
                }
              });
            },

            end: function () {
            }
          };
        }
      }

      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.onload = script.onreadystatechange = function (e) {
        e = e || window.event;

        if (lazyLoading !== 2 && (e.type == 'load' || /complete|loaded/.test(script.readyState))) {
          getTinymce().dom.Event.domLoaded = 1;
          lazyLoading = 2;

          // Execute callback after mainscript has been loaded and before the initialization occurs
          if (settings.script_loaded) {
            settings.script_loaded();
          }

          init();

          $.each(delayedInits, function (i, init) {
            init();
          });
        }
      };
      script.src = url;
      document.body.appendChild(script);
    } else {
      // Delay the init call until tinymce is loaded
      if (lazyLoading === 1) {
        delayedInits.push(init);
      } else {
        init();
      }
    }

    return self;
  };

  // Add :tinymce pseudo selector this will select elements that has been converted into editor instances
  // it's now possible to use things like $('*:tinymce') to get all TinyMCE bound elements.
  $.extend($.expr[":"], {
    tinymce: function (e) {
      var editor;

      if (e.id && "tinymce" in win) {
        editor = getTinymce().get(e.id);

        if (editor && editor.editorManager === getTinymce()) {
          return true;
        }
      }

      return false;
    }
  });

  // This function patches internal jQuery functions so that if
  // you for example remove an div element containing an editor it's
  // automatically destroyed by the TinyMCE API
  var applyPatch = function () {
    // Removes any child editor instances by looking for editor wrapper elements
    var removeEditors = function (name) {
      // If the function is remove
      if (name === "remove") {
        this.each(function (i, node) {
          var ed = tinyMCEInstance(node);

          if (ed) {
            ed.remove();
          }
        });
      }

      this.find("span.mceEditor,div.mceEditor").each(function (i, node) {
        var ed = getTinymce().get(node.id.replace(/_parent$/, ""));

        if (ed) {
          ed.remove();
        }
      });
    };

    // Loads or saves contents from/to textarea if the value
    // argument is defined it will set the TinyMCE internal contents
    var loadOrSave = function (value) {
      var self = this, ed;

      // Handle set value
      /*jshint eqnull:true */
      if (value != null) {
        removeEditors.call(self);

        // Saves the contents before get/set value of textarea/div
        self.each(function (i, node) {
          var ed;

          if ((ed = getTinymce().get(node.id))) {
            ed.setContent(value);
          }
        });
      } else if (self.length > 0) {
        // Handle get value
        if ((ed = getTinymce().get(self[0].id))) {
          return ed.getContent();
        }
      }
    };

    // Returns tinymce instance for the specified element or null if it wasn't found
    var tinyMCEInstance = function (element) {
      var ed = null;

      if (element && element.id && win.tinymce) {
        ed = getTinymce().get(element.id);
      }

      return ed;
    };

    // Checks if the specified set contains tinymce instances
    var containsTinyMCE = function (matchedSet) {
      return !!((matchedSet) && (matchedSet.length) && (win.tinymce) && (matchedSet.is(":tinymce")));
    };

    // Patch various jQuery functions
    var jQueryFn = {};

    // Patch some setter/getter functions these will
    // now be able to set/get the contents of editor instances for
    // example $('#editorid').html('Content'); will update the TinyMCE iframe instance
    $.each(["text", "html", "val"], function (i, name) {
      var origFn = jQueryFn[name] = $.fn[name],
        textProc = (name === "text");

      $.fn[name] = function (value) {
        var self = this;

        if (!containsTinyMCE(self)) {
          return origFn.apply(self, arguments);
        }

        if (value !== undef) {
          loadOrSave.call(self.filter(":tinymce"), value);
          origFn.apply(self.not(":tinymce"), arguments);

          return self; // return original set for chaining
        }

        var ret = "";
        var args = arguments;

        (textProc ? self : self.eq(0)).each(function (i, node) {
          var ed = tinyMCEInstance(node);

          if (ed) {
            ret += textProc ? ed.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, "") : ed.getContent({ save: true });
          } else {
            ret += origFn.apply($(node), args);
          }
        });

        return ret;
      };
    });

    // Makes it possible to use $('#id').append("content"); to append contents to the TinyMCE editor iframe
    $.each(["append", "prepend"], function (i, name) {
      var origFn = jQueryFn[name] = $.fn[name],
        prepend = (name === "prepend");

      $.fn[name] = function (value) {
        var self = this;

        if (!containsTinyMCE(self)) {
          return origFn.apply(self, arguments);
        }

        if (value !== undef) {
          if (typeof value === "string") {
            self.filter(":tinymce").each(function (i, node) {
              var ed = tinyMCEInstance(node);

              if (ed) {
                ed.setContent(prepend ? value + ed.getContent() : ed.getContent() + value);
              }
            });
          }

          origFn.apply(self.not(":tinymce"), arguments);

          return self; // return original set for chaining
        }
      };
    });

    // Makes sure that the editor instance gets properly destroyed when the parent element is removed
    $.each(["remove", "replaceWith", "replaceAll", "empty"], function (i, name) {
      var origFn = jQueryFn[name] = $.fn[name];

      $.fn[name] = function () {
        removeEditors.call(this, name);

        return origFn.apply(this, arguments);
      };
    });

    jQueryFn.attr = $.fn.attr;

    // Makes sure that $('#tinymce_id').attr('value') gets the editors current HTML contents
    $.fn.attr = function (name, value) {
      var self = this, args = arguments;

      if ((!name) || (name !== "value") || (!containsTinyMCE(self))) {
        if (value !== undef) {
          return jQueryFn.attr.apply(self, args);
        }

        return jQueryFn.attr.apply(self, args);
      }

      if (value !== undef) {
        loadOrSave.call(self.filter(":tinymce"), value);
        jQueryFn.attr.apply(self.not(":tinymce"), args);

        return self; // return original set for chaining
      }

      var node = self[0], ed = tinyMCEInstance(node);

      return ed ? ed.getContent({ save: true }) : jQueryFn.attr.apply($(node), args);
    };
  };
})();