/**
 * This code is mostly from the old Etherpad. Please help us to comment this code. 
 * This helps other people to understand this code better and helps them to improve it.
 * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
 */

/**
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var makeCSSManager = require('./cssmanager').makeCSSManager;
var domline = require('./domline').domline;
var AttribPool = require('./AttributePool');
var Changeset = require('./Changeset');
var linestylefilter = require('./linestylefilter').linestylefilter;
var colorutils = require('./colorutils').colorutils;
var _ = require('./underscore');

// These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate.
function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider)
{
  var changesetLoader = undefined;

  // Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm
  if (!Array.prototype.indexOf)
  {
    Array.prototype.indexOf = function(elt /*, from*/ )
    {
      var len = this.length >>> 0;

      var from = Number(arguments[1]) || 0;
      from = (from < 0) ? Math.ceil(from) : Math.floor(from);
      if (from < 0) from += len;

      for (; from < len; from++)
      {
        if (from in this && this[from] === elt) return from;
      }
      return -1;
    };
  }

  function debugLog()
  {
    try
    {
      if (window.console) console.log.apply(console, arguments);
    }
    catch (e)
    {
      if (window.console) console.log("error printing: ", e);
    }
  }

  // for IE
  if ($.browser.msie)
  {
    try
    {
      document.execCommand("BackgroundImageCache", false, true);
    }
    catch (e)
    {}
  }


  //var socket;
  var channelState = "DISCONNECTED";

  var appLevelDisconnectReason = null;

  var padContents = {
    currentRevision: clientVars.collab_client_vars.rev,
    currentTime: clientVars.collab_client_vars.time,
    currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
    currentDivs: null,
    // to be filled in once the dom loads
    apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
    alines: Changeset.splitAttributionLines(
    clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text),

    // generates a jquery element containing HTML for a line
    lineToElement: function(line, aline)
    {
      var element = document.createElement("div");
      var emptyLine = (line == '\n');
      var domInfo = domline.createDomLine(!emptyLine, true);
      linestylefilter.populateDomLine(line, aline, this.apool, domInfo);
      domInfo.prepareForAdd();
      element.className = domInfo.node.className;
      element.innerHTML = domInfo.node.innerHTML;
      element.id = Math.random();
      return $(element);
    },

    applySpliceToDivs: function(start, numRemoved, newLines)
    {
      // remove spliced-out lines from DOM
      for (var i = start; i < start + numRemoved && i < this.currentDivs.length; i++)
      {
        debugLog("removing", this.currentDivs[i].attr('id'));
        this.currentDivs[i].remove();
      }

      // remove spliced-out line divs from currentDivs array
      this.currentDivs.splice(start, numRemoved);

      var newDivs = [];
      for (var i = 0; i < newLines.length; i++)
      {
        newDivs.push(this.lineToElement(newLines[i], this.alines[start + i]));
      }

      // grab the div just before the first one
      var startDiv = this.currentDivs[start - 1] || null;

      // insert the div elements into the correct place, in the correct order
      for (var i = 0; i < newDivs.length; i++)
      {
        if (startDiv)
        {
          startDiv.after(newDivs[i]);
        }
        else
        {
          $("#padcontent").prepend(newDivs[i]);
        }
        startDiv = newDivs[i];
      }

      // insert new divs into currentDivs array
      newDivs.unshift(0); // remove 0 elements
      newDivs.unshift(start);
      this.currentDivs.splice.apply(this.currentDivs, newDivs);
      return this;
    },

    // splice the lines
    splice: function(start, numRemoved, newLinesVA)
    {
      var newLines = _.map(Array.prototype.slice.call(arguments, 2), function(s) {
        return s;
      });

      // apply this splice to the divs
      this.applySpliceToDivs(start, numRemoved, newLines);

      // call currentLines.splice, to keep the currentLines array up to date
      newLines.unshift(numRemoved);
      newLines.unshift(start);
      this.currentLines.splice.apply(this.currentLines, arguments);
    },
    // returns the contents of the specified line I
    get: function(i)
    {
      return this.currentLines[i];
    },
    // returns the number of lines in the document
    length: function()
    {
      return this.currentLines.length;
    },

    getActiveAuthors: function()
    {
      var self = this;
      var authors = [];
      var seenNums = {};
      var alines = self.alines;
      for (var i = 0; i < alines.length; i++)
      {
        Changeset.eachAttribNumber(alines[i], function(n)
        {
          if (!seenNums[n])
          {
            seenNums[n] = true;
            if (self.apool.getAttribKey(n) == 'author')
            {
              var a = self.apool.getAttribValue(n);
              if (a)
              {
                authors.push(a);
              }
            }
          }
        });
      }
      authors.sort();
      return authors;
    }
  };

  function callCatchingErrors(catcher, func)
  {
    try
    {
      wrapRecordingErrors(catcher, func)();
    }
    catch (e)
    { /*absorb*/
    }
  }

  function wrapRecordingErrors(catcher, func)
  {
    return function()
    {
      try
      {
        return func.apply(this, Array.prototype.slice.call(arguments));
      }
      catch (e)
      {
        // caughtErrors.push(e);
        // caughtErrorCatchers.push(catcher);
        // caughtErrorTimes.push(+new Date());
        // console.dir({catcher: catcher, e: e});
        debugLog(e); // TODO(kroo): added temporary, to catch errors
        throw e;
      }
    };
  }

  function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta)
  {
    var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest);
    debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision);
    revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta);
    BroadcastSlider.setSliderLength(revisionInfo.latest);
    if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta);
  }

   /*
   At this point, we must be certain that the changeset really does map from
   the current revision to the specified revision.  Any mistakes here will
   cause the whole slider to get out of sync.
   */

  function applyChangeset(changeset, revision, preventSliderMovement, timeDelta)
  { 
    // disable the next 'gotorevision' call handled by a timeslider update
    if (!preventSliderMovement)
    {
      goToRevisionIfEnabledCount++;
      BroadcastSlider.setSliderPosition(revision);
    }

    try
    {
      // must mutate attribution lines before text lines
      Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
    }
    catch (e)
    {
      debugLog(e);
    }

    Changeset.mutateTextLines(changeset, padContents);
    padContents.currentRevision = revision;
    padContents.currentTime += timeDelta * 1000;

    debugLog('Time Delta: ', timeDelta)
    updateTimer();
    
    var authors = _.map(padContents.getActiveAuthors(), function(name)
    {
      return authorData[name];
    });
    
    BroadcastSlider.setAuthors(authors);
  }

  function updateTimer()
  {
    var zpad = function(str, length)
      {
        str = str + "";
        while (str.length < length)
        str = '0' + str;
        return str;
        }
        
    var date = new Date(padContents.currentTime);
    var dateFormat = function()
      {
        var month = zpad(date.getMonth() + 1, 2);
        var day = zpad(date.getDate(), 2);
        var year = (date.getFullYear());
        var hours = zpad(date.getHours(), 2);
        var minutes = zpad(date.getMinutes(), 2);
        var seconds = zpad(date.getSeconds(), 2);
        return (html10n.get("timeslider.dateformat", {
          "day": day,
          "month": month,
          "year": year,
          "hours": hours,
          "minutes": minutes, 
          "seconds": seconds
        }));
        }
        
        
        
        
        
    $('#timer').html(dateFormat());
    var revisionDate = html10n.get("timeslider.saved", {
      "day": date.getDate(),
      "month": [
                html10n.get("timeslider.month.january"),
                html10n.get("timeslider.month.february"),
                html10n.get("timeslider.month.march"),
                html10n.get("timeslider.month.april"),
                html10n.get("timeslider.month.may"),
                html10n.get("timeslider.month.june"),
                html10n.get("timeslider.month.july"),
                html10n.get("timeslider.month.august"),
                html10n.get("timeslider.month.september"),
                html10n.get("timeslider.month.october"),
                html10n.get("timeslider.month.november"),
                html10n.get("timeslider.month.december")
               ][date.getMonth()],
      "year": date.getFullYear()
    });
    $('#revision_date').html(revisionDate)

  }
  
  updateTimer();

  function goToRevision(newRevision)
  {
    padContents.targetRevision = newRevision;
    var self = this;
    var path = revisionInfo.getPath(padContents.currentRevision, newRevision);
    debugLog('newRev: ', padContents.currentRevision, path);
    if (path.status == 'complete')
    {
      var cs = path.changesets;
      debugLog("status: complete, changesets: ", cs, "path:", path);
      var changeset = cs[0];
      var timeDelta = path.times[0];
      for (var i = 1; i < cs.length; i++)
      {
        changeset = Changeset.compose(changeset, cs[i], padContents.apool);
        timeDelta += path.times[i];
      }
      if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
    }
    else if (path.status == "partial")
    {
      debugLog('partial');
      var sliderLocation = padContents.currentRevision;
      // callback is called after changeset information is pulled from server
      // this may never get called, if the changeset has already been loaded
      var update = function(start, end)
        {
          // if we've called goToRevision in the time since, don't goToRevision
          goToRevision(padContents.targetRevision);
          };

      // do our best with what we have...
      var cs = path.changesets;

      var changeset = cs[0];
      var timeDelta = path.times[0];
      for (var i = 1; i < cs.length; i++)
      {
        changeset = Changeset.compose(changeset, cs[i], padContents.apool);
        timeDelta += path.times[i];
      }
      if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);


      if (BroadcastSlider.getSliderLength() > 10000)
      {
        var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10
        changesetLoader.queueUp(start, 100);
      }

      if (BroadcastSlider.getSliderLength() > 1000)
      {
        var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1
        changesetLoader.queueUp(start, 10);
      }

      start = (Math.floor((newRevision) / 100) * 100);

      changesetLoader.queueUp(start, 1, update);
    }
    
    var authors = _.map(padContents.getActiveAuthors(), function(name){
      return authorData[name];
    });
    BroadcastSlider.setAuthors(authors);
  }

  changesetLoader = {
    running: false,
    resolved: [],
    requestQueue1: [],
    requestQueue2: [],
    requestQueue3: [],
    reqCallbacks: [],
    queueUp: function(revision, width, callback)
    {
      if (revision < 0) revision = 0;
      // if(changesetLoader.requestQueue.indexOf(revision) != -1)
      //   return; // already in the queue.
      if (changesetLoader.resolved.indexOf(revision + "_" + width) != -1) return; // already loaded from the server
      changesetLoader.resolved.push(revision + "_" + width);

      var requestQueue = width == 1 ? changesetLoader.requestQueue3 : width == 10 ? changesetLoader.requestQueue2 : changesetLoader.requestQueue1;
      requestQueue.push(
      {
        'rev': revision,
        'res': width,
        'callback': callback
      });
      if (!changesetLoader.running)
      {
        changesetLoader.running = true;
        setTimeout(changesetLoader.loadFromQueue, 10);
      }
    },
    loadFromQueue: function()
    {
      var self = changesetLoader;
      var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : self.requestQueue2.length > 0 ? self.requestQueue2 : self.requestQueue3.length > 0 ? self.requestQueue3 : null;

      if (!requestQueue)
      {
        self.running = false;
        return;
      }

      var request = requestQueue.pop();
      var granularity = request.res;
      var callback = request.callback;
      var start = request.rev;
      var requestID = Math.floor(Math.random() * 100000);

      sendSocketMsg("CHANGESET_REQ", {
        "start": start,
        "granularity": granularity,
        "requestID": requestID
      });

      self.reqCallbacks[requestID] = callback;
    },
    handleSocketResponse: function(message)
    {
      var self = changesetLoader;

      var start = message.data.start;
      var granularity = message.data.granularity;
      var callback = self.reqCallbacks[message.data.requestID];
      delete self.reqCallbacks[message.data.requestID];

      self.handleResponse(message.data, start, granularity, callback);
      setTimeout(self.loadFromQueue, 10);
    },
    handleResponse: function(data, start, granularity, callback)
    {
      debugLog("response: ", data);
      var pool = (new AttribPool()).fromJsonable(data.apool);
      for (var i = 0; i < data.forwardsChangesets.length; i++)
      {
        var astart = start + i * granularity - 1; // rev -1 is a blank single line
        var aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
        if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
        debugLog("adding changeset:", astart, aend);
        var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
        var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
        revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
      }
      if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
    },
    handleMessageFromServer: function (obj)
    {
      debugLog("handleMessage:", arguments);

      if (obj.type == "COLLABROOM")
      {
        obj = obj.data;

        if (obj.type == "NEW_CHANGES")
        {
          debugLog(obj);
          var changeset = Changeset.moveOpsToNewPool(
            obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);

          var changesetBack = Changeset.inverse(
            obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);

          var changesetBack = Changeset.moveOpsToNewPool(
            changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);

          loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
        }
        else if (obj.type == "NEW_AUTHORDATA")
        {
          var authorMap = {};
          authorMap[obj.author] = obj.data;
          receiveAuthorData(authorMap);

          var authors = _.map(padContents.getActiveAuthors(), function(name) {
            return authorData[name];
          });

          BroadcastSlider.setAuthors(authors);
        }
        else if (obj.type == "NEW_SAVEDREV")
        {
          var savedRev = obj.savedRev;
          BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
        }
      }
      else if(obj.type == "CHANGESET_REQ")
      {
        changesetLoader.handleSocketResponse(obj);
      }
      else
      {
        debugLog("Unknown message type: " + obj.type);
      }
    }
  };

  // to start upon window load, just push a function onto this array
  //window['onloadFuncts'].push(setUpSocket);
  //window['onloadFuncts'].push(function ()
  fireWhenAllScriptsAreLoaded.push(function()
  {
    // set up the currentDivs and DOM
    padContents.currentDivs = [];
    $("#padcontent").html("");
    for (var i = 0; i < padContents.currentLines.length; i++)
    {
      var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]);
      padContents.currentDivs.push(div);
      $("#padcontent").append(div);
    }
    debugLog(padContents.currentDivs);
  });

  // this is necessary to keep infinite loops of events firing,
  // since goToRevision changes the slider position
  var goToRevisionIfEnabledCount = 0;
  var goToRevisionIfEnabled = function() {
    if (goToRevisionIfEnabledCount > 0)
    {
      goToRevisionIfEnabledCount--;
    }
    else
    {
      goToRevision.apply(goToRevision, arguments);
    }
  }
      
  BroadcastSlider.onSlider(goToRevisionIfEnabled);

  var dynamicCSS = makeCSSManager('dynamicsyntax');
  var authorData = {};

  function receiveAuthorData(newAuthorData)
  {
    for (var author in newAuthorData)
    {
      var data = newAuthorData[author];
      var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId;
      if (bgcolor && dynamicCSS)
      {
        var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author));
        selector.backgroundColor = bgcolor
        selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000'; //see ace2_inner.js for the other part
      }
      authorData[author] = data;
    }
  }

  receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);

  return changesetLoader;
}

exports.loadBroadcastJS = loadBroadcastJS;
