/**
 * Onlyoffice Editor client.
 */
(function($, cCometD, redux) {
  // ******** polyfills ********
  if (!String.prototype.endsWith) {
    String.prototype.endsWith = function(search, this_len) {
      if (this_len === undefined || this_len > this.length) {
        this_len = this.length;
      }
      return this.substring(this_len - search.length, this_len) === search;
    };
  }
  if (!String.prototype.startsWith) {
    String.prototype.startsWith = function(searchString, position) {
      position = position || 0;
      return this.substr(position, searchString.length) === searchString;
    };
  }

  // ******** Utils ********
  /**
   * Stuff grabbed from CW's commons.js
   */
  var pageBaseUrl = function(theLocation) {
    if (!theLocation) {
      theLocation = window.location;
    }

    var theHostName = theLocation.hostname;
    if (theLocation.port) {
      theHostName += ":" + theLocation.port;
    }

    return theLocation.protocol + "//" + theHostName;
  };

  /**
   * Add style to current document (to the end of head).
   */
  var loadStyle = function(cssUrl) {
    if (document.createStyleSheet) {
      document.createStyleSheet(cssUrl);
      // IE way
    } else {
      if ($("head").find("link[href='" + cssUrl + "']").length == 0) {
        var headElems = document.getElementsByTagName("head");
        var style = document.createElement("link");
        style.type = "text/css";
        style.rel = "stylesheet";
        style.href = cssUrl;
        headElems[headElems.length - 1].appendChild(style);
      } // else, already added
    }
  };

  /** For debug logging. */
  var log = function(msg, err) {
    var logPrefix = "[onlyoffice] ";
    if (typeof console != "undefined" && typeof console.log != "undefined") {
      var isoTime = " -- " + new Date().toISOString();
      var msgLine = msg;
      if (err) {
        msgLine += ". Error: ";
        if (err.name || err.message) {
          if (err.name) {
            msgLine += "[" + err.name + "] ";
          }
          if (err.message) {
            msgLine += err.message;
          }
        } else {
          msgLine += (typeof err === "string" ? err : JSON.stringify(err)
              + (err.toString && typeof err.toString === "function" ? "; " + err.toString() : ""));
        }

        console.log(logPrefix + msgLine + isoTime);
        if (typeof err.stack != "undefined") {
          console.log(err.stack);
        }
      } else {
        if (err !== null && typeof err !== "undefined") {
          msgLine += ". Error: '" + err + "'";
        }
        console.log(logPrefix + msgLine + isoTime);
      }
    }
  };

  var tryParseJson = function(message) {
    var src = message.data ? message.data : (message.error ? message.error : message.failure);
    if (src) {
      try {
        if (typeof src === "string" && (src.startsWith("{") || src.startsWith("["))) {
          return JSON.parse(src);
        }
      } catch (e) {
        log("Error parsing '" + src + "' as JSON: " + e, e);
      }
    }
    return src;
  };

  var messages = {}; // should be initialized by Editor.initMessages()

  var message = function(key) {
    var m = messages[key];
		return m ? m : key;
	};

  // ******** REST services ********
  var prefixUrl = pageBaseUrl(location);

  var initRequest = function(request) {
    var process = $.Deferred();

    // stuff in textStatus is less interesting: it can be "timeout",
    // "error", "abort", and "parsererror",
    // "success" or smth like that
    request.fail(function(jqXHR, textStatus, err) {
      if (jqXHR.status != 309) {
        // check if response isn't JSON
        var data;
        try {
          data = $.parseJSON(jqXHR.responseText);
          if (typeof data == "string") {
            // not JSON
            data = jqXHR.responseText;
          }
        } catch (e) {
          // not JSON
          data = jqXHR.responseText;
        }
        // in err - textual portion of the HTTP status, such as "Not
        // Found" or "Internal Server Error."
        process.reject(data, jqXHR.status, err, jqXHR);
      }
    });
    // hacking jQuery for statusCode handling
    var jQueryStatusCode = request.statusCode;
    request.statusCode = function(map) {
      var user502 = map[502];
      if (!user502) {
        map[502] = function() {
          // treat 502 as request error also
          process.fail("Bad gateway", 502, "error");
        };
      }
      return jQueryStatusCode(map);
    };

    request.done(function(data, textStatus, jqXHR) {
      process.resolve(data, jqXHR.status, textStatus, jqXHR);
    });

    request.always(function(data, textStatus, errorThrown) {
      var status;
      if (data && data.status) {
        status = data.status;
      } else if (errorThrown && errorThrown.status) {
        status = errorThrown.status;
      } else {
        status = 200;
        // what else we could to do
      }
      process.always(status, textStatus);
    });

    // custom Promise target to provide an access to jqXHR object
    var processTarget = {
      request : request
    };
    return process.promise(processTarget);
  };

  /* TODO cleanup
  var configGet = function(workspace, path) {
    var request = $.ajax({
      type : "GET",
      url : prefixUrl + "/portal/rest/onlyoffice/editor/config/" + workspace + path,
      dataType : "json"
    });
    return initRequest(request);
  };

  var configPost = function(workspace, path) {
    var request = $.ajax({
      type : "POST",
      url : prefixUrl + "/portal/rest/onlyoffice/editor/config/" + workspace + path,
      dataType : "json"
    });

    return initRequest(request);
  };
  
  var configGetByKey = function(key) {
    var request = $.ajax({
      type : "GET",
      url : prefixUrl + "/portal/rest/onlyoffice/editor/config/" + key,
      dataType : "json"
    });
    return initRequest(request);
  };
  
  var documentPost = function(workspace, path) {
    var request = $.ajax({
      type : "POST",
      url : prefixUrl + "/portal/rest/onlyoffice/editor/document/" + workspace + path,
      dataType : "json"
    });

    return initRequest(request);
  };

  var stateGet = function(userId, fileKey) {
    var request = $.ajax({
      type : "GET",
      url : prefixUrl + "/portal/rest/onlyoffice/editor/state/" + userId + "/" + fileKey,
      dataType : "json"
    });

    return initRequest(request);
  };
  */

  /**
   * Editor core class.
   */
  function Editor() {

    // Constants:
    var DOCUMENT_SAVED = "DOCUMENT_SAVED";

    var self = this;
    var store;
    var subscribedDocuments = [];

    /**
     * Initializes the redux store or returns existing one.
     */
    var eventStore = function() {
      if (!store) {
        store = redux.createStore(docActionsReducer);
      }
      return store;
    };

    /**
     * Changes the redux store depending on the action.
     */
    var docActionsReducer = function(state, action) {
      if (action.type === DOCUMENT_SAVED) {
        return action;
      }
      log("Unknown action type:" + action.type);
      return state ? state : {}; // TODO is it OK?
    };

    /**
     * Subscribes on a document updates using cometd. Dispatches events to the redux store.
     */
    var subscribeDocumentUpdates = function(cometdInfo) {
      // Use only one channel for one document
      if (subscribedDocuments.includes(cometdInfo.docId)) {
        return;
      }

      cCometD.configure({
        "url" : prefixUrl + cometdInfo.cometdPath,
        "exoId" : cometdInfo.user,
        "exoToken" : cometdInfo.userToken,
        "maxNetworkDelay" : 30000,
        "connectTimeout" : 60000
      });

      cometd = cCometD;
      cometdContext = {
        "exoContainerName" : cometdInfo.container
      };
      cometd.subscribe("/eXo/Application/Onlyoffice/editor/" + cometdInfo.docId, function(message) {
        // Channel message handler
        var result = tryParseJson(message);
        store.dispatch(result);
      }, cometdContext, function(subscribeReply) {
        // Subscription status callback
        if (subscribeReply.successful) {
          // The server successfully subscribed this client to the channel.
          log("Document updates subscribed successfully: " + JSON.stringify(subscribeReply));
          subscribedDocuments.push(cometdInfo.docId);
        } else {
          var err = subscribeReply.error ? subscribeReply.error : (subscribeReply.failure ? subscribeReply.failure.reason
              : "Undefined");
          log("Document updates subscription failed for " + cometdInfo.user, err);
        }
      });
    };

    var onError = function(event) {
      log("ONLYOFFICE Document Editor reports an error: " + JSON.stringify(event));
    };

    var onReady = function() {
      log("ONLYOFFICE Document Editor is ready");
    };

    var onBack = function() {
      log("ONLYOFFICE Document Editor on Back");
    };

    var onDocumentStateChange = function(event) {
      log("documentChange: " + JSON.stringify(event));
      if (event.data) {
        log("ONLYOFFICE The document changed");
        UI.documentChanged(event);
      } else {
        log("ONLYOFFICE Changes are collected on document editing service");
      }
    };

    /**
     * Create an editor configuration (for use to create the editor client UI).
     */
    var create = function(config) {
      var process = $.Deferred();
      if (config) {
        // prepare config
        log("ONLYOFFICE editor config: " + JSON.stringify(config));

        // customize Onlyoffice JS config
        config.type = "desktop";
        config.height = "100%";
        config.width = "100%";
        config.embedded = {
          fullscreenUrl : config.document.url,
          saveUrl : config.document.url,
          embedUrl : config.document.url,
          shareUrl : config.document.url,
          toolbarDocked : "top"
        };
        config.events = {
          "onDocumentStateChange" : onDocumentStateChange,
          "onError" : onError,
          "onReady" : onReady,
          "onBack" : onBack
        };
        config.editorConfig.customization = {
          "chat" : false,
          "compactToolbar" : true,
          "goback" : {
            "blank" : true,
            "text" : "Go to Document",
            "url" : config.explorerUrl
          },
          "help" : true,
          "logo" : {
            "image" : prefixUrl + "/onlyoffice/images/exo-icone.png",
            "imageEmbedded" : prefixUrl + "/eXoSkin/skin/images/themes/default/platform/skin/ToolbarContainer/HomeIcon.png",
            "url" : prefixUrl + "/portal"
          },
          "customer" : {
            "info" : "eXo Platform",
            "logo" : prefixUrl + "/eXoSkin/skin/images/themes/default/platform/skin/ToolbarContainer/HomeIcon.png",
            "mail" : "support@exoplatform.com",
            "name" : "Support",
            "www" : "exoplatform.com"
          }
        };
        config.editorConfig.plugins = {
          "autostart" : [],
          "pluginsData" : []
        };

        // XXX need let Onlyoffice to know about a host of API end-points,
        // as we will add their script dynamically to the DOM, the script will not be able to detect an URL
        // thus we use "extensionParams" observed in the script code
        if ("undefined" == typeof (extensionParams) || null == extensionParams) {
          extensionParams = {};
        }
        if ("undefined" == typeof (extensionParams.url) || null == extensionParams.url) {
          extensionParams.url = config.documentserverUrl;
        }

        // load Onlyoffice API script
        // XXX need load API script to DOM head, Onlyoffice needs a real element in <script> to detect the DS server URL
        $("<script>").attr("type", "text/javascript").attr("src", config.documentserverJsUrl).appendTo("head");

        // and wait until it will be loaded
        function jsReady() {
          return (typeof DocsAPI !== "undefined") && (typeof DocsAPI.DocEditor !== "undefined");
        }

        var attempts = 40;
        function waitReady() {
          attempts--;
          if (attempts >= 0) {
            setTimeout(function() {
              if (jsReady()) {
                process.resolve(config);
              } else {
                waitReady();
              }
            }, 750);
          } else {
            log("ERROR: ONLYOFFICE script load timeout: " + config.documentserverJsUrl);
            process.reject("ONLYOFFICE script load timeout. Ensure Document Server is running and accessible.");
          }
        }

        if (jsReady()) {
          process.resolve(config);
        } else {
          waitReady();
        }
      } else {
        process.reject("Editor config not found");
      }
      ;
      return process.promise();
    };
    this.create = create;

    this.initMessages = function(userMessages) {
      messages = userMessages;
    };

    /**
     * Initialize an editor page in current browser window.
     */
    this.initEditor = function(config) {
      // TODO Establish a Comet/WebSocket channel from this point.
      // A new editor page will join the channel and notify when the doc will be saved
      // so we'll refresh this explorer view to reflect the edited content.
      UI.initEditor();
      create(config).done(function(localConfig) {
        $(function() {
          try {
            UI.create(localConfig);
          } catch (e) {
            log("Error initializing Onlyoffice client UI " + e, e);
          }
        });
      }).fail(function(error) {
        log("ERROR: editor config failed : " + error);
        UI.showError("Error", "Failed to setup editor configuration");
      });
    };

    this.showInfo = function(title, text) {
      UI.showInfo(title, text);
    };

    this.showError = function(title, text) {
      UI.showError(title, text);
    };

    /**
     * Initializes a file activity in the activity stream.
     */
    this.initActivity = function(cometdInfo, activityId, editorLink) {
      // Listen to document updates
      subscribeDocumentUpdates(cometdInfo);
      // Init redux store
      eventStore();

      store.subscribe(function() {
        var state = store.getState();
        if (state.type === DOCUMENT_SAVED && state.docId === cometdInfo.docId) {
          UI.addRefreshBannerActivity(activityId);
        }
      });
      if (editorLink !== "null") {
        UI.addEditorButtonToActivity(activityId, editorLink);
      }
    };

    /**
     * Initializes a document preview (from the activity stream).
     */
    this.initPreview = function(cometdInfo, activityId, editorLink, previewIndex) {
      $("#Preview" + activityId + "-" + previewIndex).click(function() {
        // We set timeout here to avoid the case when the element is rendered but is going to be updated soon
        setTimeout(function() {
          subscribeDocumentUpdates(cometdInfo);
          // Init redux store
          eventStore();
          store.subscribe(function() {
            var state = store.getState();
            if (state.type === DOCUMENT_SAVED && state.docId === cometdInfo.docId) {
              UI.addRefreshBannerPDF();
            }
          });
        }, 100);
      });
      if (editorLink !== "null") {
        UI.addEditorButtonToPreview(activityId, editorLink, previewIndex);
      }
    };

    /**
     * Initializes JCRExplorer when a document is displayed.
     */
    this.initExplorer = function(cometdInfo) {
      var $JCRFileContent = $("#UIJCRExplorer .fileContent");
      if ($JCRFileContent.length > 0) {
        // Listen document updated
        subscribeDocumentUpdates(cometdInfo);
        // Init redux store
        eventStore();
        store.subscribe(function() {
          var state = store.getState();
          if (state.type === DOCUMENT_SAVED) {
            if (state.userId === cometdInfo.user) {
              UI.refreshPDFPreview();
            } else {
              UI.addRefreshBannerPDF();
            }
          }
        });
        UI.addEditorButtonToExplorer();
      }
    };
  }

  /**
   * Online Editor WebUI integration.
   */
  function UI() {
    var NOTICE_WIDTH = "380px";

    var docEditor;

    var hasDocumentChanged = false;

    var self = this;

    /**
     * Returns the html markup of the 'Edit Online' button.
     */
    var getEditorButton = function(editorLink) {
      return "<li><a href='" + editorLink
          + "' target='_blank'><i class='uiIconEcmsOnlyofficeOpen uiIconEcmsLightGray uiIconEdit'></i> " + message("EditButtonTitle")
          + "</a></li>";
    };

    /**
     * Returns the html markup of the refresh banner;
     */
    var getRefreshBanner = function() {
      return "<div class='documentRefreshBanner'><div class='refreshBannerContent'>" + message("UpdateBannerTitle") +
        " <span class='refreshBannerLink'>" + message("ReloadButtonTitle") + "</span></div></div>";
    };

    /**
     * Adds the 'Edit Online' button to a preview (from the activity stream) when it's loaded.
     */
    var tryAddEditorButtonToPreview = function(editorLink, attempts, delay) {
      var $elem = $(".previewBtn");
      if ($elem.length == 0 || !$elem.is(":visible")) {
        if (attempts > 0) {
          setTimeout(function() {
            tryAddEditorButtonToPreview(editorLink, attempts - 1, delay);
          }, delay);
        } else {
          log("Cannot find element " + $elem);
        }
      } else {
        $(".previewBtn").append("<div class='onlyOfficeEditBtn'>" + getEditorButton(editorLink) + "</div>");
      }
    };

    // TODO Use this in on-close window handler
    var saveAndDestroy = function() {
      if (docEditor) {
        try {
          docEditor.processSaveResult(true);
          docEditor.destroyEditor();
        } catch (e) {
          log("Error saving and destroying ONLYOFFICE editor", e);
        } finally {
          docEditor = null;
        }
      }
    };

    /**
     * Refreshes an activity preview by updating preview picture.
     */
    var refreshActivityPreview = function(activityId) {
      var $img = $("#Preview" + activityId + "-0 #MediaContent" + activityId + "-0 img");
      if ($img.length !== 0) {
        var src = $img.attr("src");
        if (src.includes("version=")) {
          src = src.substring(0, src.indexOf("version="));
        }
        var timestamp = new Date().getTime();

        src += "version=oview_" + timestamp;
        src += "&lastModified=" + timestamp;
        $img.attr("src", src);
      }
    };

    /**
     * Refreshes a document preview (PDF) by reloading viewer.js script.
     */
    var refreshPDFPreview = function() {
      var $banner = $(".document-preview-content-file #toolbarContainer .documentRefreshBanner");
      if ($banner.length !== 0) {
        $banner.remove();
      }
      setTimeout(function() {
        var $vieverScript = $(".document-preview-content-file script[src$='/viewer.js']")
        var viewerSrc = $vieverScript.attr("src");
        $vieverScript.remove();
        $(".document-preview-content-file").append("<script src='" + viewerSrc + "'></script>");
      }, 250); // XXX we need wait for office preview server generate a new preview
    };

    this.refreshPDFPreview = refreshPDFPreview;

    /**
     * Init editor page UI.
     */
    this.initEditor = function() {
      $("#LeftNavigation").parent(".LeftNavigationTDContainer").remove();
      $("#NavigationPortlet").remove();
      // $("body").addClass("maskLayer");
      $("#SharedLayoutRightBody").addClass("onlyofficeEditorBody");
    };

    this.isEditorLoaded = function() {
      return $("#UIPage .onlyofficeContainer").length > 0;
    };

    /**
     * TODO Should we use it when user close the page, to notify the channel doc is closed?
     */
    this.closeEditor = function() {
      saveAndDestroy();
      // Use Close menu to reload the view with right representation
      $("#UIDocumentWorkspace .fileContent .onlyofficeContainer .loading .onClose").click();
    };

    /**
     * TODO not used
     */
    this.disableMenu = function() {
      $("#uiActionsBarContainer i.uiIconEcmsOnlyofficeOpen").parent().addClass("editorDisabled").parent().removeAttr("onclick");
      $("#uiActionsBarContainer i.uiIconEcmsOnlyofficeClose").parent().addClass("editorDisabled").parent().removeAttr("onclick");
    };

    this.documentChanged = function(event) {
      // Used for UI messages.
      // TODO use this to broadcast this user changes to the doc channel and save the user version in JCR
      hasDocumentChanged = true;
    };

    /**
     * Create an editor client UI on current page.
     */
    this.create = function(localConfig) {
      var $editorPage = $("#OnlyofficeEditorPage");
      if ($editorPage.length > 0) {
        if (!docEditor) {
          // show loading while upload to editor - it is already added by WebUI side
          var $container = $editorPage.find(".onlyofficeContainer");

          // create and start editor (this also will re-use an existing editor config from the server)
          docEditor = new DocsAPI.DocEditor("onlyoffice", localConfig);
          hasDocumentChanged = false;
          // show editor
          $container.find(".editor").show("blind");
          $container.find(".loading").hide("blind");
        } else {
          log("WARN: Editor client already initialized");
        }
      } else {
        log("WARN: Editor element not found");
      }
    };

    /**
     * Ads the 'Edit Online' button to the JCRExplorer when a document is displayed.
     */
    this.addEditorButtonToExplorer = function() {
      $("#UIJCRExplorer .fileContent").closest("#UIJCRExplorer").find("#uiActionsBarContainer i.uiIconEcmsOnlyofficeOpen")
          .addClass("uiIconEdit");
    };

    /**
     * Ads the 'Edit Online' button to an activity in the activity stream.
     */
    this.addEditorButtonToActivity = function(activityId, editorLink) {
      $("#activityContainer" + activityId).find("div[id^='ActivityContextBox'] > .actionBar .statusAction.pull-left").append(
          getEditorButton(editorLink));
    };

    /**
     * Ads the 'Edit Online' button to a preview (opened from the activity stream).
     */
    this.addEditorButtonToPreview = function(activityId, editorLink, previewIndex) {
      $("#Preview" + activityId + "-" + previewIndex).click(function() {
        // We set timeout here to avoid the case when the element is rendered but is going to be updated soon
        setTimeout(function() {
          tryAddEditorButtonToPreview(editorLink, 100, 100);
        }, 100);
      });
    };

    /**
     * Ads the refresh banner to an activity in the activity stream.
     */
    this.addRefreshBannerActivity = function(activityId) {
      var $previewParent = $("#Preview" + activityId + "-0").parent();
      // If the activity contains only one preview
      if ($previewParent.find("#Preview" + activityId + "-1").length === 0) {
        if ($previewParent.find(".documentRefreshBanner").length === 0) {
          $previewParent.prepend(getRefreshBanner());
          $banner = $previewParent.find(".documentRefreshBanner");
          $(".documentRefreshBanner .refreshBannerLink").click(function() {
            refreshActivityPreview(activityId);
            $banner.remove();
          });
        }
        log("Activity document: " + activityId + " has been updated");
      }
    };

    /**
     * Ads the refresh banner to the PDF document preview.
     */
    this.addRefreshBannerPDF = function() {
      var $toolbarContainer = $(".document-preview-content-file #toolbarContainer");
      if ($toolbarContainer.find(".documentRefreshBanner").length === 0) {
        $toolbarContainer.append(getRefreshBanner());
        $(".documentRefreshBanner .refreshBannerLink").click(function() {
          refreshPDFPreview();
        });
      }
    };

    /**
     * Show notice to user. Options support "icon" class, "hide", "closer" and "nonblock" features.
     */
    this.showNotice = function(type, title, text, options) {
      var noticeOptions = {
        title : title,
        text : text,
        type : type,
        icon : "picon " + (options ? options.icon : ""),
        hide : options && typeof options.hide != "undefined" ? options.hide : false,
        closer : options && typeof options.closer != "undefined" ? options.closer : true,
        sticker : false,
        opacity : .75,
        shadow : true,
        width : options && options.width ? options.width : NOTICE_WIDTH,
        nonblock : options && typeof options.nonblock != "undefined" ? options.nonblock : false,
        nonblock_opacity : .25,
        after_init : function(pnotify) {
          if (options && typeof options.onInit == "function") {
            options.onInit(pnotify);
          }
        }
      };

      return $.pnotify(noticeOptions);
    };

    /**
     * Show error notice to user. Error will stick until an user close it.
     */
    this.showError = function(title, text, onInit) {
      return UI.showNotice("error", title, text, {
        icon : "picon-dialog-error",
        hide : false,
        delay : 0,
        onInit : onInit
      });
    };

    /**
     * Show info notice to user. Info will be shown for 8sec and hidden then.
     */
    this.showInfo = function(title, text, onInit) {
      return UI.showNotice("info", title, text, {
        hide : true,
        delay : 8000,
        icon : "picon-dialog-information",
        onInit : onInit
      });
    };

    /**
     * Show warning notice to user. Info will be shown for 8sec and hidden then.
     */
    this.showWarn = function(title, text, onInit) {
      return UI.showNotice("exclamation", title, text, {
        hide : false,
        delay : 30000,
        icon : "picon-dialog-warning",
        onInit : onInit
      });
    };
  }

  var editor = new Editor();
  var UI = new UI();

  $(function() {
    try {
      // load required styles
      loadStyle("/onlyoffice/skin/jquery-ui.css");
      loadStyle("/onlyoffice/skin/jquery.pnotify.default.css");
      loadStyle("/onlyoffice/skin/jquery.pnotify.default.icons.css");
      loadStyle("/onlyoffice/skin/onlyoffice.css");

      // configure Pnotify
      $.pnotify.defaults.styling = "jqueryui";
      // use jQuery UI css
      $.pnotify.defaults.history = false;
      // no history roller in the right corner
    } catch (e) {
      log("Error configuring Onlyoffice Editor style.", e);
    }
  });
  return editor;
})($, cCometD, Redux);