
function AjaxEngine() {
  //for all comments except comments in overlays
  var globalCommentNodeCache = {};
  // necessary for overlays, cause they have their own cache
  var actualCommentNodeCache = globalCommentNodeCache;
  // which scripts are loaded
  var $loadedResources = $([]);
  // Array to store ajax callback functions
  var $responseFunctions = $([]);

  var lastOpenedWindow = null;
  var lastOpenedWindowCommand = null;

  var resultProcessingTimers = {};

  function init() {
      // initial collect of all comment nodes, CSS links and loaded scripts
      $( document ).ready( function () {
          var $head = $( "head" );
          $.each( $head.find( "script" ), function ( i, elem ) {
              if ( elem.src )
                  $loadedResources.push( elem.src );
          } );
          $.each( $head.find( "link" ), function ( i, elem ) {
              if ( elem.href )
                  $loadedResources.push( elem.href );
          } );
          addCommentsToCache( document, globalCommentNodeCache );
      } );

      $( document ).bind( "ajaxError", function ( event, jqXHR, settings ) {
          var jqXHRstatus = jqXHR.status;
          var url = settings.url.split( "?" )[0];
          if ( jqXHRstatus == 0 && window.navigator.onLine != false )
              return;
          if ( !debugMode && jqXHRstatus == 404 && url.match( /(\.css|\.js|\.jpg|\.png|\.gif)$/ ) )
              return;
          if ( jqXHRstatus == 500 && jqXHR.getResponseHeader("X-ErrorType") == "INTERNAL" ) {//our own error code
              sendServerCommand("ajaxError",{type:"INTERNAL"});
              return;
          }

          var errDialog = $( "<div class='wc-ErrorDialog'></div>" ).dialog(
              {
                  modal: true,
                  resizable: false,
                  draggable: true,
                  minHeight: 0,
                  minWidth: 300,
                  closeText: "",
                  title: window.navigator.onLine == false ? notReachableMessage.notReachableTitle : "Error " + jqXHRstatus,
                  close: function () {
                      $( errDialog ).remove();
                  }
              }
          );
          errDialog.parent().attr( "id", "XHRError" );

          if ( jqXHRstatus == 404 || window.navigator.onLine == false ) {
              // notReachableMessage variable is rendered in WebcoreResourcesServlet.java
              var $notReachableMessage = $( "<div class='Message'>" + notReachableMessage.notReachableMessage + "</div>" );
              var $notReachableRefreshButton = $( "<div class='Button'><input type='submit' class='wc-Button' value='" + notReachableMessage.notReachableRefreshButton + "'></div>" );
              var $notReachableContainer = $( "<div class='wc-OverlayBody'></div>" );

              $notReachableContainer.append( $notReachableMessage ).append( $( '<div class="wc-VerticalPadder"/>' ) ).append( $notReachableRefreshButton );
              errDialog.append( $notReachableContainer );
              $notReachableRefreshButton.bind( "click", function () {
                  errDialog.dialog( "close" );
              } );
          }
          else {
              $.get( "fatalError.htm", function ( data ) {
                  var $notReachableMessage = $( "<div class='Message'>" + data + "</div>" );
                  var $notReachableRefreshButton = $( "<div class='Button'><input type='submit' class='wc-Button' value='Ok'></div>" );
                  var $notReachableContainer = $( "<div class='wc-OverlayBody'></div>" );

                  $notReachableContainer.append( $notReachableMessage ).append( $( '<div class="wc-VerticalPadder"/>' ) ).append( $notReachableRefreshButton );
                  errDialog.append( $notReachableContainer );
                  $notReachableRefreshButton.bind( "click", function () {
                      errDialog.dialog( "close" );
                  } );
              } );
          }
          errDialog.dialog("option", {
              position: {
                  my: "center",
                  at: "center",
                  of: $(document)
              }
          })
      } );

      $( document ).bind( "ajaxStop.actionFinished", function () {
          webcore.setActionRunning( false );
      } );

      $.ajaxSetup( {
          // Disable caching of AJAX responses, for full script load the cache is enabled (loadJSResource function)
          cache: false,
          beforeSend: function ( xhr ) {
              webcore.setActionRunning( true );
              xhr.setRequestHeader( 'AJaX', 'true' );
              xhr.setRequestHeader( "windowId", windowId );
              xhr.setRequestHeader( "token", token );
          }
      } );
  }

  function getTime()
  {
      return new Date().getTime();
  }

  function loadJSResource( src ) {
    if ( jQuery.inArray( src, $loadedResources ) == -1) {
      $loadedResources.push( src );
      $.ajax({
        url: src,
        async: false,
        dataType: "script",
        cache: true
      });
    }
  }

  // remove redundant comment nodes in cache by a given (removed) node
  function removeCommentsFromCache(removeNode, commentNodeCache) {
      if (removeNode.nodeType == 8)
          delete commentNodeCache[removeNode.nodeValue];
      $.each( removeNode.childNodes, function( index, children ) {
          removeCommentsFromCache( children, commentNodeCache );
      });
  }

  // add new comment nodes to cache by a given (add) node
  function addCommentsToCache(addNode, commentNodeCache) {
      if (addNode.nodeType == 8)
          commentNodeCache[addNode.nodeValue] = addNode;
      $.each( addNode.childNodes, function( index, children ) {
          addCommentsToCache( children, commentNodeCache );
      });
  }

  function removeOldContent( nodesToRemove, commentNodeCache ) {
	$.each (nodesToRemove, function(index, node) {
		removeCommentsFromCache( node, commentNodeCache );
	    $(node).remove();	  
	});
  }

  function insertNewContent(destEndNode, actualResult, commentNodeCache) {
    // insert node content
    // put in span and get contents, cause we need comment and text nodes
    // too.
    actualResult = $('<span/>').html(actualResult);
    var $insertedContent = $(actualResult.contents()).insertBefore(destEndNode);
    $(destEndNode).parent().trigger("contentChanged");
    for ( var insertCount = 0; insertCount < $insertedContent.length; insertCount++) {
      addCommentsToCache($insertedContent[insertCount], commentNodeCache);
    }
    return $insertedContent;
  }

  function getMarkedContent( commentId, commentNodeCache )
  {
	if (!commentNodeCache)
		commentNodeCache = actualCommentNodeCache;

    var $returnNodes = $([]);
	var destStartName = " " + commentId + " start ";
    var destEndName = " " + commentId + " end ";
    var destStartNode = commentNodeCache[destStartName];

    if (destStartNode == null) {
      throw $.extend(new Error("start marker missing!"), {
        "type" : "START_MARKER_NOT_FOUND",
        "marker" : commentId
      });
    }

    var $parentOfTarget = $(destStartNode).parent();
    var siblingsAndSelf = $parentOfTarget.contents();
    var destEndNode = commentNodeCache[destEndName];

    if (destEndNode == null) {
      throw $.extend({
        "type" : "END_MARKER_NOT_FOUND",
        "marker" : commentId
      },new Error("end marker missing!"));
    }
    else if (siblingsAndSelf.index(destEndNode) == -1) {
      // it's possible that the end marker is misplaced, cause the browser
      // (or jQuery?) place automatically a tbody tag inside the table tag.
      // so we must move the comment node inside tbody
      var $tbodyElement = $parentOfTarget.children("tbody");
      if ($tbodyElement.length != 1)
        throw $.extend({
          "type" : "END_MARKER_NOT_FOUND",
          "marker" : commentId
        },new Error("end marker missing!"));
      else {
        $(destStartNode).detach();
        $(destStartNode).insertBefore($tbodyElement.contents()[0]);
        $parentOfTarget = $tbodyElement;
        siblingsAndSelf = $parentOfTarget.contents();
      }
    }

    var j = siblingsAndSelf.index(destStartNode) + 1;
    var endPosition = siblingsAndSelf.index(destEndNode);
    // if nothing is in marker, return end marker
    while (j < endPosition) {
      $returnNodes.push(siblingsAndSelf[j++]);
    }

    return $returnNodes;
  }
  
  function replaceMarkerContent( commentId, markerContent ) {
    var stepStartTime = getTime();
    var selectedContent = getMarkedContent( commentId );
    var removeTime = getTime();
    var destEndNode = actualCommentNodeCache[" " + commentId + " end "];
    // if selected content was empty, it had returned the end marker
    removeOldContent( selectedContent, actualCommentNodeCache );
    removeTime = getTime() - removeTime;
    var insertionTime = getTime();
    var insertedContent = insertNewContent(destEndNode, markerContent, actualCommentNodeCache);
    insertionTime = getTime() - insertionTime;
    
    $(window).trigger({
      type: 'ajaxReplacementStepComplete',
      name: commentId,
      completeTime: getTime() - stepStartTime,
      removeTime: removeTime,
      insertionTime: insertionTime,
      insertedContent: insertedContent
    });
  }

  function replaceChangedContent( result, commentNodeCache ) {
    $(window).trigger({type: 'replaceContentStart' });
    actualCommentNodeCache = commentNodeCache;
    var ajaxMarkerMap = result.content;
    if (result.entirePage === true)
        $(document).trigger({
            type: "replaceEntirePage",
            targetUrl: applicationServletPath + "ajaxCommandServlet?command=getLastPageContent&windowId=" + windowId
        });
    else
      $.each (ajaxMarkerMap, replaceMarkerContent);
    $(window).trigger({type: 'replaceContentStop' });
  }

  $(document).bind("replaceEntirePage.standard", function(e) {
    $( document ).unbind( "ajaxStop.actionFinished");
    window.location = e.targetUrl;
  });

  function ajaxPoll() {
      $.ajax({
          url: "ajaxPushServlet",
          success: ajaxEngine.evaluateResponse,
          global: false,
          async: true
      });
  }

  function loadResources(results) {
    actualCommentNodeCache = globalCommentNodeCache;
    var $resourcesToLoad = $(results.loadResources);
    var $scriptsToLoad = $([]);
    $resourcesToLoad.each(function (i, resource)
    {
      if (resource.href != null)
      { //CSS
        if (jQuery.inArray(resource.href, $loadedResources) == -1)
        {
          $.ajax({
            url: resource.href,
            async: false,
            cache: true,
            beforeSend: function (xhr)
            {
              xhr.setRequestHeader('AJaX', 'false');
            },
            success: function (cssData)
            {
              //add the path of css resource to the relative paths of resources in css source
              var cssPath = resource.href.substring(0, resource.href.lastIndexOf("/") + 1);
              cssData = cssData.replace(/url *\(/g,"url(" + cssPath);

              var $s = $("<style type='text/css'>" + cssData + "</style>");
              $s.appendTo("head");
            }
          });
          $loadedResources.push(resource.href);
        }
      }
      else if (resource.src != null && jQuery.inArray(resource.src, $loadedResources) == -1)
      {
        $scriptsToLoad.push(resource);
      }
    });
    $("head").append($scriptsToLoad);
  }

  function processResult(result, async) {
    try {
      switch (result.command) {
        case 'redirect':
          $(document).unbind("ajaxStop.progressDisplayHandler");
          $(document).trigger({type: "replaceEntirePage", targetUrl: result.location});
          break;
        case 'download':
          window.location = result.location;
          sendServerCommand("processCommandQueue");
          break;
        case 'ajaxReplace':
          var scrollPosition = $(window).scrollTop();
          $(document).bind("ajaxStop.resetScrollPosition", function () {
            $(window).scrollTop(scrollPosition);
            $(document).unbind("ajaxStop.resetScrollPosition");
          });
          replaceChangedContent(result, globalCommentNodeCache);
          break;
        case 'updateUrl':
            try {
                if (history.replaceState)
                    history.replaceState({}, window.document.title, result.url);
                webcore.pushHistoryState({}, window.document.title, result.url);
            }
            catch( e ){
                /*
                 ¯\_(ツ)_/¯
                https://github.com/ReactTraining/history/issues/291
                */
            }
          break;
        case 'openWindow':
          lastOpenedWindowCommand = result;
          lastOpenedWindow = WebcoreUtils.openPopup(result.location, result.name, result.width, result.height);
          if (async && lastOpenedWindow == null) {
            sendServerCommand("popupBlocked", {parameters: JSON.stringify(lastOpenedWindowCommand)});
          }
          break;
        case 'longPoll':
          ajaxPoll();
          break;
        case 'execute':
          eval(result.script);
          break;
        case 'executeServerCommand':
          sendServerCommand(result.serverCommand);
          break;
        case 'sendDeviceDetails':
          sendServerCommand('setDeviceDetails', {parameters: JSON.stringify(WebcoreUtils.getDeviceDetails())});
          break;
        default:
          var success = false;
          $responseFunctions.each(function (index, func) {
            if (func(result))
              success = true;
          });
          if (success)
            break;
          throw $.extend(new Error("unknown command received"),
            {"type": "UNKNOWN_COMMAND", "receivedCommand": result.command});
      }
    } catch (err) {
      sendServerCommand("ajaxError", $.extend({message: err.message, stack: err.stack}, err));
    }
  }

  function evaluateResponse(resultFromAjaxRequest) {
    if (resultFromAjaxRequest.length > 0) {
      var results = {};
      try {
        results = $.parseJSON(resultFromAjaxRequest);
      } catch (err) {
        // response not in json format, try to display it's content
        // as full Page
        // replacement and wait for other errors :)
        // may occur in case of server is not reachable (404) or
        // something like
        results.commands = { 0 : {
          command : 'ajaxReplace',
            entirePage : true,
            content : resultFromAjaxRequest }
        };
      }
      // first of all load resources
      try {
        var async = $.ajaxSetup().async;
        if (async)
          $.ajaxSetup({async: false});
        if (results.loadResources)
          loadResources(results);
        if (results.commands) {
          $.each(results.commands, function(index, result) {
            if (result.delay) {
              if (resultProcessingTimers[result.command])
                return;
              resultProcessingTimers[result.command] = window.setTimeout(function () {
                processResult(result, async);
                delete resultProcessingTimers[result.command];
              }, result.delay);
            }
            else
              processResult(result, async);
          });
        }
        if (async)
          $.ajaxSetup({async: true});
      } catch (err) {
        sendServerCommand("ajaxError", $.extend({ message:err.message, stack:err.stack},err ));
      }
    }
  } 

  /**
   * register function to consume ajax response, 
   */
  function registerAjaxCallbackFunction( commandId, func ) {
    if(func instanceof Function) {
      var callback = function( response ) {
        if(response.command === commandId ) {
          func( response );
          return true;
        }
        else
          return false;
      };
      $responseFunctions.push( callback );
    }
  }

  function ajaxAsynchron(elem) {
      var $form = $(elem).parents("form:first");
      $form.trigger({type: "formSubmit", submitElement: elem});

      if ( elem instanceof jQuery )
          elem = elem[0];

      var action = $form.attr("action");
      var formData = new Form( $form ).getData( elem );
      var $fileUpload = $form.find("input:file");
      $fileUpload = $fileUpload.length != 0 && $fileUpload[0].value != "" ? $fileUpload : null;

      $.ajax( action, {
          type: "POST",
          processData: false,
          contentType: false,
          data: formData,
          xhr: function() {
              var myXhr = $.ajaxSettings.xhr();
              if(myXhr.upload){
                  if( $fileUpload )
                  {
                      FileUploadField.uploadStartTime = new Date().getTime();
                      myXhr.upload.addEventListener('progress',function(evt) {FileUploadField.uploadProgressHandling(evt, $fileUpload.attr("id"))}, false);
                      myXhr.upload.addEventListener('load',function(evt) {FileUploadField.uploadFinishedHandling(evt, $fileUpload.attr("id"))}, false);
                  }
              }
              return myXhr;
          },
          success: evaluateResponse
      });

    /*
     * Solution for Safari's and Chrome's Popupblocker to ensure window
     * is opened directly in click handler scope.
     */
    if ( lastOpenedWindowCommand != null && !lastOpenedWindow ) {
        lastOpenedWindow = WebcoreUtils.openPopup(lastOpenedWindowCommand.location, lastOpenedWindowCommand.name, lastOpenedWindowCommand.width, lastOpenedWindowCommand.height);
        if( lastOpenedWindow == null && (!lastOpenedWindowCommand.name || lastOpenedWindowCommand.name != "_blank") )
        {
          sendServerCommand("popupBlocked", {parameters: JSON.stringify(lastOpenedWindowCommand)});
        }
    }
    lastOpenedWindowCommand = null;
      
    if ( lastOpenedWindow && lastOpenedWindow.focus )
      lastOpenedWindow.focus();
    lastOpenedWindow = null;
  }

   function sendServerCommand( commandName, data, sync ) {
       $.ajax({
           url: applicationServletPath + "ajaxCommandServlet",
           data: $.extend({command: commandName}, data),
           success: ajaxEngine.evaluateResponse,
           cache: false,
           async: !sync
       });
   }

  // public methods
  return {
    init : init, 
    ajaxAsynchron : ajaxAsynchron,
    replaceChangedContent : replaceChangedContent,
    addCommentsToCache : addCommentsToCache,
    evaluateResponse : evaluateResponse,
    getMarkedContent : getMarkedContent,
    loadJSResource : loadJSResource,
    registerAjaxCallbackFunction : registerAjaxCallbackFunction,
    sendServerCommand : sendServerCommand
  }
}

var ajaxEngine = new AjaxEngine();
ajaxEngine.init();


function submitAjax(elem, synch) {
  if (synch)
      $.ajaxSetup({async: false});

  //remove delayed widget actions
  $.each($(document).data(),
    function(index,elem) {
      if(index.indexOf("delayedControlTimer.")==0) {
        $(document).removeData(index);
        clearTimeout(elem);
      }
    }
  );
    
  ajaxEngine.ajaxAsynchron( elem );
    
  if (synch)
      $.ajaxSetup({async: true});
}

function postAjax(url) {
  $.post(url, ajaxEngine.evaluateResponse);
}





