/**
 * Author: Mattias Andersson
 * Contact: mattias.andersson@etraveli.com or mattias800@gmail.com
 * Last change: 2011-02-xx
 */

var IBESorter = (function () {

  return {
    sort : function(list, sortBy, desc) {
      return this.jsSort(list, sortBy, desc);
    },

    jsSort : function (list, sortBy, desc) {
      if (!list) return list;

      /**
       * NOTE!
       * Comparators must only use the name that is given by sortBy variable.
       * If sortBy = "translatedName", only the member translatedName may be used in the sort.
       * If you need a custom, special-case, supercomparator, define it in your code where you need it.
       * IBESorter is meant to simplify sorting on member variables and should never be used for special cases.
       */
      switch (sortBy) {
        case 'distanceToCenter':
          list.sort(this.comparatorDistanceToCenter);
          break;

        case 'translatedName':
          list.sort(this.comparatorTranslatedName);
          break;

        case 'origin.translatedName':
          list.sort(this.comparatorOriginTranslatedName);
          break;

        case 'price.destination.translatedName':
          list.sort(function (a, b) {
            return IBESorter.compareStringTo(a.price.destination.translatedName, b.price.destination.translatedName);
          });
          break;

        case 'destination.translatedName':
          list.sort(function (a, b) {
            return IBESorter.compareStringTo(a.destination.translatedName, b.destination.translatedName);
          });
          break;

        case 'price.price':
          list.sort(this.comparatorPricePrice);
          break;

        case 'hotelName':
          list.sort(this.comparatorHotelName);
          break;

        case 'name':
          list.sort(function (a, b) {
            return IBESorter.compareStringTo(a.name, b.name);
          });
          break;

        case 'timeInformation.inDate.timestamp':
          list.sort(this.comparatorTimeInformationInDateTimestamp);
          break;

        case 'timeInformation.outDate.timestamp':
          list.sort(this.comparatorTimeInformationOutDateTimestamp);
          break;

        case 'timeInformation.numDays':
          list.sort(this.comparatorTimeInformationNumDays);
          break;

        case 'bookingPrice':
          list.sort(this.comparatorBookingPrice);
          break;

        case 'recommended':
          list.sort(this.comparatorRecommended);
          break;

        case 'starRating':
          list.sort(this.comparatorStarRating);
          break;

        case 'priority':
          list.sort(function(a, b) {
            return a.priority - b.priority;
          });
          break;

        case 'destination':
          list.sort(this.comparatorDestination);
          break;

        case 'checkInDateJs':
          list.sort(this.comparatorCheckInDate);
          break;

        case 'checkOutDateJs':
          list.sort(this.comparatorCheckOutDate);
          break;

        case 'numNights':
          list.sort(this.comparatorNumNights);
          break;

        case 'pricePerPerson':
          list.sort(this.comparatorPricePerPerson);
          break;

        case 'outDateJs':
          list.sort(this.comparatorDepartureDate);
          break;

        case 'returnDateJs':
          list.sort(this.comparatorReturnDate);
          break;

        case 'departureDateTime':
          list.sort(this.comparatorDepartureTime);
          break;

        case 'arrivalDateTime':
          list.sort(this.comparatorArrivalTime);
          break;

        case 'firstSegment.carrier.translatedName':
          list.sort(this.comparatorFirstSegmentCarrierTranslatedName);
          break;

        case 'firstSegment.flight':
          list.sort(this.comparatorFirstSegmentFlight);
          break;

        default:
          list = IBESorter._insertionSort(list, sortBy);
      }

      if (desc) list.reverse();

      return list;
    },

    _bubbleSort : function(list, sortBy, desc) {
      var start = new Date().getTime();
      var swapped = undefined;
      do {
        swapped = false;
        for (var i = 0, l = list.length; i < l - 1; i++) {
          var a = IBEUtil.lookup(sortBy, list[i]);
          var b = IBEUtil.lookup(sortBy, list[i + 1]);

          if (stringIsNumeric(a) && stringIsNumeric(b)) {
            a = parseInt(a);
            b = parseInt(b);
          }

          if (a > b && !desc) {
            IBESorter.toggle(list, i, i + 1);
            swapped = true;
          } else {
            if (a < b && desc) {
              IBESorter.toggle(list, i, i + 1);
              swapped = true;
            }
          }
        }
      } while (swapped);
      var end = new Date().getTime();
      var time = end - start;
      if (time > 100) ibewarning("Bubble sort took " + time + " ms. Create a custom comparator in ibe-components.js that sorts using the property '" + sortBy + "' and it will go a lot faster.");
      return list;
    },

    /**
     * Sorts a list using insertion sort. Is stable!
     * @param list
     * @param sortBy
     * @param desc
     */
    _insertionSort : function(list, sortBy, desc) {
      var start = new Date().getTime();
      if (list !== null && list !== undefined && propertyExists(list.length)) {
        for (var i = 0, temp = 0; i < list.length - 1; i++) {
          for (var j = i; 0 <= j; j--) {
            var c = this.compareObjectsTo(list[j], list[j + 1], sortBy, desc);
            if (c > 0) {
              this.toggle(list, j, j + 1);
            } else {
              break;
            }
          }
        }
      }
      return list;
    },

    /**
     * Sorts a list using insertion sort using a comparator. Is stable!
     * @param list The list to sort
     * @param comparator Name of comparator, or a comparator function.
     */
    _insertionSortWithComparator : function(list, comparator) {
      if (typeof comparator === "string") comparator = this[comparator];
      if (typeof comparator !== "function") ibeerror("IBESorter is trying to run insertion sort with comparator, but comparator is neither a function nor a valid comparator name.")
      var start = new Date().getTime();
      if (list !== null && list !== undefined && propertyExists(list.length)) {
        for (var i = 0, temp = 0; i < list.length - 1; i++) {
          for (var j = i; 0 <= j; j--) {
            var c = comparator(list[j], list[j + 1]);
            if (c > 0) {
              this.toggle(list, j, j + 1);
            } else {
              break;
            }
          }
        }
      }
      var end = new Date().getTime();
      var time = end - start;
      if (time > 100) ibewarning("Insertion sort took " + time + " ms. Create a custom comparator in ibe-components.js that sorts using the property '" + sortBy + "' and it will go a lot faster.");
      return list;
    },

    compareObjectsTo : function(o1, o2, sortBy, desc) {
      var v1 = sortBy ? IBEUtil.lookup(sortBy, o1) : o1;
      var v2 = sortBy ? IBEUtil.lookup(sortBy, o2) : o2;
      return this.compareTo(v1, v2) * (desc ? -1 : 1);
    },

    compareTo : function(v1, v2) {
      var r = undefined;
      if (stringIsNumeric(v1) && stringIsNumeric(v2)) {
        r = v1 - v2;
      } else {
        r = this.compareStringTo(v1, v2);
      }
      return r;
    },

    compareStringTo : function (v1, v2) {
      if (!v1 && !v2) {
        return 0;
      }
      else {
        if (!v1) {
          return 1;
        }
        else {
          if (!v2) {
            return -1;
          }
          else {
            v1 = v1.toLowerCase();
            v2 = v2.toLowerCase();
            var c1 = v1.charCodeAt(0);
            var c2 = v2.charCodeAt(0);
            if (c1 == c2) {
              return this.compareStringTo(v1.substring(1), v2.substring(1));
            } else {
              return c1 - c2;
            }
          }
        }
      }
    },

    toggle : function(list, i1, i2) {
      var tmp = list[i1];
      list[i1] = list[i2];
      list[i2] = tmp;
    },

    isBrowserSortingStable : function() {

      var stableList = [], stableTestCount = 1000, i;
      for (i = 0; i < stableTestCount; i++) stableList.push({priority:0, index:i});
      ibelogs("pre list", stableList);
      stableList.sort(function(a, b) {
        return a.priority - b.priority;
      });
      ibelogs("post list", stableList);
      for (i = 0; i < stableTestCount; i++) if (stableList[i].index != i) return false;
      return true;
    },



    // Comparators for use with array.sort.

    comparatorPricePerPerson : function (a, b) {
      return a.pricePerPerson - b.pricePerPerson;
    },

    comparatorNumNights : function (a, b) {
      return a.numNights - b.numNights;
    },

    comparatorCheckInDate : function (a, b) {
      return a.checkInDateJs - b.checkInDateJs;
    },

    comparatorCheckOutDate : function (a, b) {
      return a.checkOutDateJs - b.checkOutDateJs;
    },

    comparatorDepartureDate : function (a, b) {
      return a.outDateJs - b.outDateJs;
    },

    comparatorReturnDate : function (a, b) {
      return a.returnDateJs - b.returnDateJs;
    },

    comparatorDestination : function (a, b) {
      if (a && b) {
        return IBESorter.compareStringTo(a.destination, b.destination);
      } else {
        return 0;
      }
    },

    comparatorTranslatedName : function (a, b) {
      if (a && b) {
        return IBESorter.compareStringTo(a.translatedName, b.translatedName);
      } else {
        return 0;
      }
    },

    comparatorDistanceToCenter : function (a, b) {
      return a.distanceToCenter - b.distanceToCenter;
    },

    comparatorOriginTranslatedName : function (a, b) {
      return IBESorter.compareStringTo(a.origin.translatedName, b.origin.translatedName);
    },

    comparatorTimeInformationInDateTimestamp: function (a, b) {
      return a.timeInformation.inDate.timestamp - b.timeInformation.inDate.timestamp;
    },

    comparatorTimeInformationOutDateTimestamp: function (a, b) {
      return a.timeInformation.outDate.timestamp - b.timeInformation.outDate.timestamp;
    },

    comparatorTimeInformationNumDays: function (a, b) {
      return a.timeInformation.numDays - b.timeInformation.numDays;
    },

    comparatorBookingPrice : function (a, b) {
      return a.bookingPrice - b.bookingPrice;
    },

    comparatorPricePrice : function (a, b) {
      return a.price.price - b.price.price;
    },

    comparatorStarRating : function (a, b) {
      return a.starRating - b.starRating;
    },

    comparatorRecommended : function (a, b) {
      return b.hasPromo - a.hasPromo;
    },

    comparatorHotelName : function (a, b) {
      return IBESorter.compareStringTo(a.hotelName, b.hotelName);
    },

    comparatorDepartureTime : function(a, b) {
      return a.departureDateTime - b.departureDateTime;
    },

    comparatorArrivalTime : function(a, b) {
      return a.arrivalDateTime - b.arrivalDateTime;
    },

    comparatorFirstSegmentCarrierTranslatedName : function(a, b) {
      return IBESorter.compareStringTo(a.firstSegment.carrier.translatedName, b.firstSegment.carrier.translatedName);
    },

    comparatorFirstSegmentFlight : function(a, b) {
      return IBESorter.compareStringTo(a.firstSegment.flight, b.firstSegment.flight);
    }

  };

})();

var IBETag = function (name, param, html) {
  this.name = name;
  this.param = param;

  this.html = html;
  this.type = "IBETag";
};

function ensureIsIbeTag(tag) {
  return tag.type == "IBETag";
}

/**
 * Empty or undefined list returns true, if not empty, all elements must be of type IBETag. It only detects lists with other stuff than IBETag items.
 * @param tagList
 */
function ensureIsIbeTagList(tagList) {
  if (tagList && tagList.length) {
    for (var i = 0; i < tagList.length; i++) {
      if (!ensureIsIbeTag(tagList[i])) return false;
    }
  }
  return true;
}

var IBEParserContainer = function(args) {

  this.args = args = $.extend({
                                niterConfig:{}
                              }, args);

  this.clipboard = {};
  this.globals = {};
  this.niterConfig = args.niterConfig;

  this.debugMode = false;

  this.contextStack = new Array();

  this.getNiterState = function(niterName) {
    return this.niterConfig[niterName];
  };

  this.addComponentViewToClipboard = function(name, view) {
    if (!name) {
      ibeerror("Trying to add IBEWebComponentView to clipboard of component, but the clipboard name is undefined.");
      return;
    }
    if (!view) {
      ibeerror("Trying to add IBEWebComponentView to clipboard of component, but the view is undefined.");
      return;
    }
    var p = new IBEParser();
    p.setViewWithHtml(view.getView());
    var status = p.buildList();
    if (!status) {
      ibeerror("Trying to set component clipboard using a IBEWebComponentView, but parsing the specified view failed.");
      return;
    }
    this.addTagListToClipboard(name, p.getByteCodeList());
  };

  this.addStringViewToClipboard = function(name, string) {
    if (!name) {
      ibeerror("Trying to add a string to clipboard of component, but the clipboard name is undefined.");
      return;
    }
    if (!string) {
      ibeerror("Trying to add a string to clipboard of component, but the string is undefined.");
      return;
    }
    var view = new IBEWebComponentView();
    view.setViewWithHtml(string);
    this.addComponentViewToClipboard(name, view);
  };

  this.addTagListToClipboard = function(name, tagList) {
    if (!name) {
      ibeerror("Trying to add a byte code list to clipboard of component, but the clipboard name is undefined.");
      return;
    }
    if (!ensureIsIbeTagList(tagList)) {
      ibeerror("Trying to add content to component clipboard, but the content is not a list of only tags.");
      return;
    }
    this.clipboard[name] = tagList;
  };

  /*
   {
   model:m,
   context:c,
   custom:custom
   }
   */
};

var IBEParser = function (component, parserContainer, args) {
  args = $.extend({
                    clipboard:undefined
                  }, args);

  this.type = "IBEParser";

  var startTag = '{%';
  var endTag = '%}';
  var contextPrefix = '#';
  var componentPrefix = '&';

  this.parserContainer = parserContainer;
  if (!this.parserContainer) this.parserContainer = new IBEParserContainer();

  this.component = component; // The component which content is being parsed. So that we can access it from the {% .. %} code.
  this.list = new Array();
  this.view = undefined;
  this.resultList = new Array();
  this.pc = 0;
  this.debug = false;
  this.custom = new Object(); // The user can put custom stuff on this object. Through {% this.custom.xxx = 'xxx'; %}
  this.globals = this.parserContainer.globals; // Share globals between all parsers

  this.scriptFailed = false; // If set to true, further execution of the script will be cancelled. 

  this.componentList = undefined;

  this.itemsPushedToStack = 0;

  this.init = function () {
    if (args.clipboard) this.applyArgumentClipboard(args.clipboard);
  };

  this.setViewWithHtml = function(htmlCode) {
    this.view = htmlCode;
  };

  this.applyArgumentClipboard = function(clipboard) {
    if (clipboard) {
      for (var id in clipboard) {
        var view = clipboard[id];
        if (typeof view === "string") {
          this.parserContainer.addStringViewToClipboard(id, view);
        } else if (typeof view.view === "string") {
          this.parserContainer.addComponentViewToClipboard(id, view);
        } else {
          ibewarning("Specified predefined clipboard for component, but the specified clipboard is neither a string or a IBEWebComponentView.");
        }
      }
    }

  };

  this.getNiterState = function(niterName) {
    return this.parserContainer.getNiterState(niterName);
  };

  this.getByteCodeList = function() {
    return this.list;
  };

  this.isDebugMode = function() {
    return this.parserContainer.debugMode;
  };

  this.debugPrint = function(s, s1, s2, s3, s4, s5) {
    // TODO: UGLY! Fix it?
    if (this.isDebugMode()) {
      if (s5) {
        ibelogs(s, s1, s2, s3, s4, s5);
      }
      else if (s4) {
        ibelogs(s, s1, s2, s3, s4);
      }
      else if (s3) {
        ibelogs(s, s1, s2, s3);
      }
      else if (s2) {
        ibelogs(s, s1, s2);
      }
      else if (s1) {
        ibelogs(s, s1);
      }
      else {
        ibelogs(s);
      }

    }
  };

  /**
   *
   * @param view A string containing the view. Not an IBEWebComponentView-object.
   * @param model
   * @param context
   * @param componentList
   * @param args if initStack == false, the model will not be pushed to stack. This is useful for scope push, for if case for example, when the model will remain the same. This prevents us from pushing same model twice.
   */
  this.parse = function (view, model, args) {
    args = $.extend({
                      pushModelToStack:true,
                      childComponents:undefined,
                      context:undefined
                    }, args);

    var context = args.context;
    if (args.pushModelToStack) {
      this.pushContext({
                         model: model,
                         context:context
                       });
    } else {
      this.updateLocalState(); // Must update state of parser
    }
    if (typeof view === "string") {
      this.view = view;
    } else {
      ibeerror("IBEParser.parse() must receive the view as a string.");
      ibelogs("view", view, "typeof view", typeof view);
      ibetrace();
    }
    this.componentList = args.childComponents; // a list of components that can be parsed into the result

    if (args.clipboard) this.applyArgumentClipboard(args.clipboard); // Apply clipboard before interpretation.

    var status = this.buildList();
    if (status) this.interpret();

    this.popAllItemsForParser();

    return this.getResult();
  };

  /**
   * Takes an already parsed token list, ready to be interpreted.
   * @param list
   * @param model
   * @param context
   * @param args
   */
  this.parseList = function (list, model, context, args) {
    args = $.extend({pushModelToStack:true}, args);
    if (args.pushModelToStack) {
      this.pushContext({
                         model: model,
                         context:context
                       });
    } else {
      this.updateLocalState(); // Must update state of parser
    }
    this.list = list;
    this.interpret();

    this.popAllItemsForParser();

    return this.getResult();
  };

  this.popAllItemsForParser = function() {
    while (this.itemsPushedToStack > 0) {
      this.popContext();
    }
  };

  this.dealloc = function() {
  };

  this.interpret = function () {
    this.scriptFailed = false;
    this.pc = 0;
    this.result = '';
    this.resultList = new Array();
    if (this.list !== undefined) {
      while (this.pc < this.list.length && !this.scriptFailed) {
        if (this.debug) ibelog('pc=' + this.pc);
        var e = this.list[this.pc];
        if (e.html) {
          this.addResult(e.html);
        } else {
          if (e.name) {
            this.parseTag();
          } else {
            if (e.html === undefined && e.name === undefined) ibewarning("Empty tag in script syntax list.");
          }
        }
        this.pc++;
      }
    } else {
      ibeerror("Component cannot interpret byte code list, since it is undefined.");
      ibetrace();
    }
  };

  this.parserEval = function(s) {
    return eval(s);
  };

  this.buildList = function () {
    var resi = 0;
    this.list = new Array();
    var view = this.view;
    if (view) {
      for (var i = 0; i < 1000; i++) {
        var i1 = view.indexOf(startTag); // TODO: Replace indexOf with custom find function which works with strings.
        if (i1 < 0) {
          // No more tags
          this.list[resi++] = new IBETag(null, null, view); // HTML code
          break;
        }

        // Sanity check
        var i2 = view.indexOf(endTag);
        if (i2 < i1) {
          ibeerror('Error: End tag found without matching start tag.');
          return false;
        }
        var i1n = view.indexOf(startTag, i1 + 1);
        if (i1n < i2 && i1n != -1) {
          ibeerror('Error: Start tag found inside of other tag.');
          return false;
        }

        // Create HTML tag and script tag.
        var html = view.substring(0, i1);
        if (html) this.list[resi++] = new IBETag(null, null, html); // HTML code
        var tagContent = trim(view.substring(i1 + startTag.length, i2));
        this.list[resi++] = IBEUtil.createTag(tagContent); // Tag

        // Remove the parsed part from the view
        view = view.substring(i2 + endTag.length);

      }
    } else {
      ibeerror("Trying to parse view into byte code list, but the view content is undefined.");
    }

    return true;
  };

  this.printList = function () {
    for (var i = 0; i < this.list.length; i++) {
      var listItem = this.list[i];
      if (listItem.html) ibelog('[' + i + '] html!');
      if (listItem.name) ibelog('[' + i + '] name=' + listItem.name + ' param=' + listItem.param);
    }
  };

  this.hasParserContainer = function() {
    return this.parserContainer ? true : false;
  };

  this.getParserContainer = function() {
    return this.parserContainer;
  };

  this.printStack = function(m) {
    ibelogs(m ? m : "model stack=", this.getContextStack());
  };

  this.addResult = function (r) {
    if (r === undefined || r === null) {
      ibeerror('Error: Rendering result (probably from parser) is undefined.');
    } else {
      this.resultList.push(r);
    }
  };

  this.killExecution = function() {
    this.pc = this.list.length;
  };

  this.getComponent = function() {
    return this.component;
  };

  this.getPane = function() {
    var c = this.getComponent();
    if (c) {
      return c.pane;
    }
    else {
      return undefined;
    }
  };

  this.getPaneId = function() {
    var p = this.getPane();
    if (p) {
      return p._id;
    }
    else {
      return undefined;
    }
  };

  this.getResult = function () {
    return this.resultList.join('');
  };

  this.getContextStack = function() {
    return this.parserContainer.contextStack;
  };

  this.getContextStackSize = function() {
    return this.getContextStack().length;
  };

  this.getModel = function () {
    if (this.getContextStackSize() == 0) return undefined;
    return this.getContextStack().peek().model;
  };

  this.getContext = function() {
    if (this.getContextStackSize() == 0) return {};
    return this.getContextStack().peek().context;
  };

  this.getCustom = function() {
    if (this.getContextStackSize() == 0) return undefined;
    return this.getContextStack().peek().custom;
  };

  this.getParentModel = function() {
    if (this.getContextStack().length > 1) {
      return this.getContextStack()[this.getContextStack().length - 2].model;
    } else {
      return undefined;
    }
  };

  this.getParentContext = function() {
    if (this.getContextStack().length > 1) {
      return this.getContextStack()[this.getContextStack().length - 2].context;
    } else {
      return undefined;
    }
  };

  this.getParentCustom = function() {
    if (this.getContextStack().length > 1) {
      return this.getContextStack()[this.getContextStack().length - 2].custom;
    } else {
      return undefined;
    }
  };

  this.getContextStackItem = function(i) {
    var v = this.getContextStack()[i];
    if (v) {
      return v;
    }
    else {
      return undefined;
    }
  };

  this.getContextStackModel = function(i) {
    var v = this.getContextStack()[i];
    if (v) {
      return v.model;
    }
    else {
      return undefined;
    }
  };

  this.getContextStackItemFromTop = function(i) {
    return this.getContextStack()[this.getContextStackSize() - 1 - i];
  };

  this.getContextStackModelFromTop = function(i) {
    var v = this.getContextStack()[this.getContextStackSize() - 1 - i];
    if (v) {
      return v.model;
    }
    else {
      return undefined;
    }
  };

  this.getContextStackContextFromTop = function(i) {
    var v = this.getContextStack()[this.getContextStackSize() - 1 - i];
    if (v) {
      return v.context;
    }
    else {
      return undefined;
    }
  };

  this.pushModel = function (model) {
    // Push active model to stack, set active model to aModel
    this.pushContext({
                       model: model
                     });
  };

  this.pushContext = function (context) {
    if (!context.custom) context.custom = {};
    if (!context.context) context.context = {};
    this.getContextStack().push(context);
    this.itemsPushedToStack++;
    this.updateLocalState();
  };

  this.peekContext = function () {
    return this.getContextStack().peek();
  };

  this.popContext = function () {
    if (this.getContextStackSize() == 0) {
      ibeerror("Trying to pop from context, but context stack is empty.");
      return undefined;
    }
    var removedModel = this.getContextStack().pop();
    this.itemsPushedToStack--;
    this.updateLocalState();
    return removedModel;
  };

  this.updateLocalState = function() {
    this.model = this.getModel();
    this.parentModel = this.getContextStackModelFromTop(1);
    this.parentParentModel = this.getContextStackModelFromTop(2);
    this.parentParentParentModel = this.getContextStackModelFromTop(3);
    this.parentContext = this.getContextStackContextFromTop(1);
    this.parentParentContext = this.getContextStackContextFromTop(2);
    this.parentParentParentContext = this.getContextStackContextFromTop(3);
    this.context = this.getContext();
    this.custom = this.getCustom();
    this.context.parentModel = this.getParentModel();
    this.context.parentContext = this.getParentContext();
    this.context.parentCustom = this.getParentCustom();
  };

  this.validateTag = function(tag) {
    if (nullOrUndefined(tag)) this.failedValidationResult("Tag is undefined.");
    if (!tag.name) this.failedValidationResult("Tag has no name, tag is empty.");
    if (typeof tag.name !== "string") this.failedValidationResult("Tag name is a number, must be a string.");
    if (typeof tag.name.startsWith !== "function") this.failedValidationResult("Tag name is a string but has no startsWith method, this must be fixed by dev.");
    return {
      result: true
    };
  };

  this.failedValidationResult = function(message) {
    return {
      result: false,
      message: message
    }
  };

  /**
   * Given start index and endTag, this method finds the content between these two tags and endIndex and returns them in an object.
   * @param startIndex
   * @param endTag
   */
  this.findTagContent = function(startIndex, endTag, args) {
    args = $.extend({
                      illegalTags:[],
                      allowNestedTags:true
                    }, args);
    var tag = this.list[startIndex];
    var startTag = tag.name;
    var endIndex = -1;
    var counter = 0;

    for (var i = startIndex + 1, l = this.list.length; i < l; i++) {
      if (this.list[i].name == startTag) {
        if (args.allowNestedTags) {
          counter++;
        } else {
          ibeerror("Found nested tags of type '" + startTag + "' which is illegal. Script parsing cancelled.");
          this.killExecution();
          return {
            error:true,
            endIndex:-1
          };
        }
      }
      if (this.list[i].name == endTag) {
        if (counter == 0) {
          endIndex = i;
          break;
        } else {
          counter--;
        }
      }
      if (args.illegalTags.contains(this.list[i].name)) {
        ibeerror("Found tag of type '" + this.list[i].name + "' in a '" + startTag + "' which is illegal. Script parsing cancelled.");
        this.killExecution();
        return {
          error:true,
          endIndex:-1
        };
      }
    }
    if (counter < 0) {
      ibeerror('"' + startTag + '" statement with missing "' + endTag + '".');
      this.killExecution();
    } else {
      var copiedList = subList(this.list, startIndex + 1, endIndex - startIndex - 1);
      return {
        list:copiedList,
        startIndex:startIndex,
        endIndex:endIndex
      };
    }
    return {
      error:true,
      endIndex:-1
    };
  };

  this.parseTag = function () {
    var v, c, r, tagLowerCase;
    var tag = this.list[this.pc];

    var validation = this.validateTag(tag);
    if (!validation.result) {
      ibewarning("Django tag failed validation: " + validation.message);
      ibewarning("Tag that failed:");
      ibewarning(tag);
      ibewarning("View that failed:");
      ibewarning(this.view.view);
      this.scriptFailed = true;
      return;
    }

    tagLowerCase = tag.name.toLowerCase();

    this.debugPrint("Tag=", tag.name, "Parameter=", tag.param, "Stack=", this.getContextStack());
    this.debugPrint("this.model", this.model);

    switch (tagLowerCase) {

      case 'if':
        // Find endif
        this.debugPrint("If-case, condition=", tag.param);
        var ifIndex = this.pc;
        var endIfIndex = -1;
        var elseIndex = -1;
        var ifCounter = 0;
        for (var i = ifIndex + 1, l = this.list.length; i < l; i++) {
          if (this.list[i].name == 'if') {
            ifCounter++;
          }
          if (this.list[i].name == 'else' && ifCounter == 0) {
            elseIndex = i;
          }
          if (this.list[i].name == 'endif') {
            if (ifCounter == 0) {
              endIfIndex = i;
              break;
            } else {
              ifCounter--;
            }
          }
        }
        if (endIfIndex < 0) {
          ibeerror('Error: "If" statement with missing "endif". ' + "tag name=" + tag.name + "tag param=" + tag.param);
          this.killExecution();
        } else {
          var ifConditionResult = undefined;
          try {
            ifConditionResult = eval(tag.param);
          } catch (e) {
            ibeerror('Error: If condition expression not valid: ' + tag.param + ' Forgot this.model?\nException: ' + e);
          }
          if (ifConditionResult || elseIndex > 0) {
            this.debugPrint("Creating parser for if-case, stack size before=" + this.getContextStackSize());
            var ifParser = new IBEParser(this.component, this.parserContainer);
            var ifList;
            if (ifConditionResult) {
              // Run "if" code
              if (elseIndex > 0) { // has else code
                ifList = subList(this.list, ifIndex + 1, elseIndex - ifIndex - 1);
              } else { // only if, no else
                ifList = subList(this.list, ifIndex + 1, endIfIndex - ifIndex - 1);
              }
            } else {
              if (elseIndex > 0) {
                // Run "else" code
                ifList = subList(this.list, elseIndex + 1, endIfIndex - elseIndex - 1);
              }
            }
            ifParser.parseList(ifList, this.model, this.context, {pushModelToStack:false});
            this.addResult(ifParser.getResult());
            ifParser.dealloc();
            this.debugPrint("Deallocating parser for if-case, stack size after=" + this.getContextStackSize());
          }
          this.pc = endIfIndex;
        }
        this.debugPrint("End of If-case, condition=" + tag.param);
        break;

      case 'else':
        ibeerror('Error: Found "else" with no "if".');
        break;

      case 'endif':
        ibeerror('Error: Found "endif" with no "if".');
        break;

      case 'debug':
        var b = tag.param === "on";
        // If was off, and turning off, display no message in log.
        if (b || this.parserContainer.debugMode) ibelog("Turning " + (b ? "on" : "off") + " debug mode.");
        this.parserContainer.debugMode = b;
        break;

      case 'sort':
        this.debugPrint("Sort, parameter=" + tag.param);
        var s = tag.param.split(" ");
        if (!s || !s.length || s.length !== 2) {
          ibewarning("Sort parameter not valid. First is list to sort, second is name of member to sort by.");
        } else {
          var listToSort = IBEUtil.lookup(s[0], this.model, this.context, this);
          IBESorter.sort(listToSort, s[1]);
        }
        break;

      case 'push':
        this.debugPrint("Push, parameter=" + tag.param);
        var objToPush = IBEUtil.lookup(tag.param, this.model, this.context, this);
        this.pushModel(objToPush);
        // TODO: Do we update context or parentContext?
        break;

      case 'endpush':
        this.debugPrint("End of Push");
        this.popContext();
        break;

      case 'void':
        break;

      case 'throw':
        ibethrow(tag.param);
        break;

      case 'copy':
        // Copy paste
        var clipName = tag.param ? tag.param : "default";
        var f = this.findTagContent(this.pc, "endcopy");
        this.parserContainer.clipboard[clipName] = f.list;
        // XXX: Could remove the tags here, to prevent the copy from happening several times in an iterator.
        // But the current parser is not reused, and the current list is not reused.
        break;

      case 'endcopy':
        break;

      case 'paste':
        clipName = tag.param ? tag.param : "default";
        var list = this.parserContainer.clipboard[clipName];
        var pasteParser = new IBEParser(this.component, this.parserContainer);
        this.addResult(pasteParser.parseList(list, this.model, this.context));
        pasteParser.dealloc();
        break;

      case 'ensurestacksize':
        var size = tag.param;
        var real = this.getContextStackSize();
        if (real != size) {
          ibelogs("Script checked stack size, but size is not matching. Expected=" + size + " Real=" + real);
          ibeerror("Script checked stack size, but size is not matching. Expected=" + size + " Real=" + real);
        }
        break;

      case 'printstack':
        this.printStack(tag.param);
        break;

      case 'global':
        var ps = tag.param.split(" ");
        if (ps.length != 2) ibeerror("global macro takes two parameters, 1st is name of global, 2nd is value to assign.");
        var globalName = ps[0];
        var valueName = removeListHead(ps).join();
        var value;
        if (stringIsNumeric(valueName)) {
          value = valueName;
        } else if (stringContainsAString(valueName)) {
          value = eval(valueName);
        } else {
          value = IBEUtil.lookup(valueName, this.model);
        }
        this.globals[globalName] = value;
        break;

      case "kill":
        this.killExecution();
        break;

      case "iter":
      case "niter":
        var isNiter = tagLowerCase == "niter";

        this.debugPrint("Niter, parameter=" + tag.param);

        if (isNiter && tag.param.indexOf(" ") < 0) {
          ibeerror("niter macro requires a name for the iterator. Syntax: {% niter aName thePropertyToIteratorOver");
          break;
        }

        var niterName = "";

        if (isNiter) {
          niterName = tag.param.substring(0, tag.param.indexOf(" "));
          tag.param = tag.param.substring(niterName.length + 1);
        }

        f = this.findTagContent(this.pc, isNiter ? "endniter" : "enditer");
        if (f.error) {
          this.killExecution();
          return;
        }

        // Found "endniter", parsing niter body and evaluating and everything.
        var iteratorModel;
        if (tag.param) {
          iteratorModel = IBEUtil.lookup(tag.param, this.model, this.context, this);
        } else {
          // If nothing specified, {% iter %} , just use current model
          iteratorModel = this.model;
        }
        var iteratorContext;
        try {
          iteratorContext = IBEUtil.getCloneOfObject(this.context);
        } catch (e) {
          ibeerror("Unable to make clone of iterator context. Object=");
          ibelog(this.context);
          ibelog(e);
        }
        if (iteratorModel) {
          if (typeof iteratorModel.length == "number") {
            var start = 0;
            var end = iteratorModel.length;
            if (isNiter) {
              var state = this.getNiterState(niterName);
              if (state.usePages) {
                // Example: Display 0-9, 10-19, 20-29, etc.
                state.totalPages = Math.ceil(iteratorModel.length / state.itemsPerPage);
                state.itemsShowing = undefined;
                if (!state.currentPage) state.currentPage = 0;
                start = state.currentPage * state.itemsPerPage;
                end = start + state.itemsPerPage;
              } else {
                // Display 0-9, 0-19, 0-29, etc. 0-n.
                state.currentPage = undefined;
                state.totalPages = undefined;
                if (state.showingAll) {
                  state.itemsShowing = iteratorModel.length;
                }
                end = state.itemsShowing;
              }
              if (end >= iteratorModel.length) {
                if (!state.usePages) state.allItemsAreShowingCallback(this);
                end = iteratorModel.length;
              }
            }
            for (i = start; i < end; i++) {
              var iteratorParser = new IBEParser(this.component, this.parserContainer);
              var iteratorListItem = iteratorModel[i];
              iteratorContext = IBEUtil.populateIteratorContext(iteratorContext, iteratorModel, i, this.model, this.context);
              if (!iteratorModel)              ibelogs("PARSING ITERATOR! model=", iteratorModel, "context=", iteratorContext, "parentModel=", this.model);
              var iteratorResult = iteratorParser.parseList(f.list, iteratorListItem, iteratorContext, {});
              this.addResult(iteratorResult);
              iteratorParser.dealloc();
            }
          } else {
            ibeerror("Iterator parameter is not a list. typeof=" + typeof iteratorModel);
          }
        } else {
          ibewarning("Specified iterable object is undefined: " + (tag.param ? tag.param : "current model"));
        }
        this.pc = f.endIndex;
        break;

      default:
        // Context stuff
        this.debugPrint("Statement=", tag.name, "parameter=", tag.param);
        if (tag.name.startsWith(contextPrefix)) {
          v = tag.name.substring(1);
          r = IBEUtil.lookupInContextsInContextStack(v, this.getContextStack(), this);
          if (!nullOrUndefined(r)) {
            this.addResult(r);
          } else {
            ibewarning('Trying to use context variable that does not exist: ' + v);
          }
        } else {
          if (tag.name.startsWith(componentPrefix)) {
            v = tag.name.substring(1);
            c = this.findComponent(v);
            if (c && c.resultNode) {
              var node = c.divContent; //resultNode;
              this.addResult(node);
            } else {
              // Component did not exist, allowing this for now. Renders nothing.
            }

            // JS execution
          } else {
            var vs;
            if (tag.name.startsWith('=')) {
              vs = tag.name.substring(1) + (tag.param ? ' ' + tag.param : '');
              try {
                r = this.parserEval(vs);
              } catch (e) {
                ibeerror('Unable to evaluate script tag: ' + vs + '\nException:' + e);
                ibelogs("Model", this.model);
              }
            } else {
              vs = tag.name.substring(1) + (tag.param ? ' ' + tag.param : '');
              if (tag.name.startsWith('@')) {
                if (vs == 'printModel') {
                  if (this.model) this.addResult(nlToBr(printAllToString(this.model)));
                } else {
                  if (vs == 'printContext') {
                    this.addResult(nlToBr(printAllToString(this.context)));
                  } else {
                    if (vs == 'logModel') {
                      ibelog(this.model);
                    } else {
                      if (vs == 'logContext') {
                        ibelog(this.context);
                      } else {
                        if (vs == 'paneId' && this.component && this.component.pane) {
                          this.addResult(this.component.pane._id);
                        } else {
                          if (vs == 'componentId' && this.component) {
                            this.addResult(this.component.id);
                          } else {
                            try {
                              r = this.parserEval(vs);
                              if (r !== undefined) this.addResult(r);
                            } catch (e) {
                              ibeerror('Unable to evaluate script tag: ' + vs + '\nException:' + e);
                              ibelogs("Model", this.model);
                            }
                          }
                        }
                      }
                    }
                  }
                }
                // Lookup
              } else {
                if (this.model == null) {
                  // Allow null models
                  r = "";
                } else {
                  r = IBEUtil.lookup(tag.name, this.model, this.context, this);
                }
                if (r !== undefined) {
                  this.addResult(r);
                } else {
                  ibewarning('Trying to use model variable that does not exist: ' + tag.name);
                  ibelogs("this.model=", this.model);
                }
              }
            }
          }
        }
    }
  };

  this.findComponent = function (id) {
    for (var i = 0, l = this.componentList.length; i < l; i++) {
      var c = this.componentList[i];
      if (c.id == id) return c;
    }
    return null;
  };

  this.init();

};

var IBEUtil = (function () {

  var VAR_SEPARATOR = '.';
  var debugOutput = false;

  return {
    lookup : function (name, model, context, parser) {
      if (debugOutput) ibelogs("lookup", "name", name, "model", model, "context", context, "parser", parser);

      if (typeof name === "object") {
        ibelogs("Trying to value in model, but name of value is an object.");
        ibelogs("name", name);
        ibetrace();
        //return undefined;
      }
      name = "" + name; // Ensure string

      if (this.shouldUseEval(name)) {
        if (name.startsWith("@")) name = name.substring(1);
        return this.lookupByEval(name, parser);
      }

      var result = undefined;
      if (model) result = this.lookupInObject(name, model, {parser:parser});
      if (result === undefined && parser && parser.getContextStackSize()) result = this.lookupInModelsInContextStack(name, parser.getContextStack());
      if (result === undefined) result = this.lookupByEval(name, parser);
      return result;
    },

    lookupInModelsInContextStack : function (name, contextStack, parser) {
      if (contextStack && contextStack.length) {
        for (i = contextStack.length - 1; i >= 0; i--) {
          var r = IBEUtil.lookupInObject(name, contextStack[i].model, {parser:parser});
          if (r !== undefined) return r;
        }
      }
    },

    lookupInContextsInContextStack : function (name, contextStack, parser) {
      if (contextStack && contextStack.length) {
        for (i = contextStack.length - 1; i >= 0; i--) {
          var r = IBEUtil.lookupInObject(name, contextStack[i].context, {parser:parser});
          if (r !== undefined) return r;
        }
      }
    },

    lookupInArray : function (name, modelStack, parser) {
      if (modelStack && modelStack.length) {
        for (i = modelStack.length - 1; i >= 0; i--) {
          var r = IBEUtil.lookupInObject(name, modelStack[i], {parser:parser});
          if (r !== undefined) return r;
        }
      }
    },

    lookupByEval : function (name, parser) {
      try {
        if (parser) {
          return parser.parserEval(name);
        } else {
          return eval(shortName);
        }
      } catch (e) {
        ibewarning('Unable to lookup variable "' + name + '" by eval.');
        ibelog(e);
      }
      return undefined;
    },

    lookupInObject : function (name, model, debugArgs) {
      debugArgs = $.extend({
                             level:0, // Keep track of how many levels down we are. Only for debug purposes.
                             firstName:name, // Keep track of first name specified. Only for debug purposes.
                             firstModel:model, // Keep track of first model specified. Only for debug purposes.
                             currentFullName:"",
                             parser:undefined
                           }, debugArgs);
      if (debugOutput) ibelogs("lookupInObject", "name", name, "model", model);
      if (model === undefined) {
        ibewarning("Trying to lookup variable named '" + name + "' in '" + debugArgs.firstName + "', but '" + debugArgs.currentFullName + "' is undefined. Model=", debugArgs.firstModel);
        if (debugArgs && debugArgs.parser) ibelogs("Stack=", debugArgs.parser.getContextStack());
        return undefined;
      }
      var nnames;
      if (name.indexOf('[') < 0) {
        nnames = name.split(VAR_SEPARATOR);
      } else {
        nnames = splitList(splitList(name.split('['), ']'), VAR_SEPARATOR);
      }
      var names = new Array();
      // Remove "" and '' from keys, and remove empty elements and store new list in names.
      for (var i = 0, l = nnames.length; i < l; i++) if (nnames[i]) names[names.length] = removeFnutts(nnames[i]);

      if (names.length <= 1) {
        // There is only one variable, ex "price"
        return model[name];
      } else {
        // There are recursive variables, ex "price.formattedPrice".
        var newname = implode(removeListHead(names), VAR_SEPARATOR);
        var newmodel = model[names[0]];
        debugArgs.currentFullName += (debugArgs.currentFullName ? "." : "") + names[0];
        debugArgs.level++;
        return this.lookupInObject(newname, newmodel, debugArgs);
      }
    },

    shouldUseEval : function(name) {
      if (!name) {
        ibeerror("Trying evaluate if lookup should use eval, but name is undefined.");
      }
      if (typeof name !== "string") return false;
      if (name.indexOf("(") >= 0 || name.indexOf(")") >= 0) return true;// Contains (), must be a function call! Use eval!
      if (name.startsWith("@")) return true; // Force use of eval
      return false;
    },


    createTag : function(code) {
      var i = code.indexOf(' ');
      var t = new IBETag();
      if (i < 0) {
        t.name = code;
      } else {
        t.name = code.substring(0, i);
        t.param = code.substring(i + 1);
      }
      return t;
    },

    ensureUniqueElementId : function (id) {
      while (getObj(id)) {
        id = id + Math.floor(Math.random() * 10);
      }
      return id;
    },

    getCloneOfObject : function (oldObject, level, name) {
      var tempClone = {};
      for (prop in oldObject) {
        var t = typeof(oldObject[prop]);
        if (t === "string" || t === "number" || t === "undefined" || t === "boolean" || t === "object") {
          // Allow undefined properties, they don't do any harm.
          tempClone[prop] = oldObject[prop];
        } else {
          // No warning message for functions.
          if (t !== "function") ibewarning("Cloning object, skipping property '" + prop + '", invalid type. Typeof=' + t);
        }
      }
      return tempClone;
    },

    getDeepCloneOfObject : function (oldObject, level, name) {
      if (!level) level = 1;
      var levelLimit = 10;
      var tempClone = {};

      if (level > levelLimit) {
        ibewarning("Cloning object is limited to " + levelLimit + " levels.");
        return undefined;
      }

      if (typeof(oldObject) == "object") {
        for (prop in oldObject) {
          if (prop === "__proto__") continue; // Skip prototype stuff
          // for array use private method getCloneOfArray
          if (!nullOrUndefined(oldObject[prop]) && (typeof(oldObject[prop]) == "object") && (oldObject[prop]).__isArray) {
            tempClone[prop] = this.getCloneOfArray(oldObject[prop], level + 1, prop);
            // for object make recursive call to getCloneOfObject
          } else {
            if (typeof(oldObject[prop]) == "object") {
              tempClone[prop] = this.getCloneOfObject(oldObject[prop], level + 1, prop);
              // normal (non-object type) members
            } else {
              var t = typeof(oldObject[prop]);
              if (t === "string" || t === "number" || t === "undefined" || t === "boolean") {
                // Allow undefined properties, they don't do any harm.
                tempClone[prop] = oldObject[prop];
              } else {
                // No warning message for functions.
                if (t !== "function") ibewarning("Cloning object, skipping property '" + prop + '", invalid type. Typeof=' + t);
              }
            }
          }
        }
      }
      return tempClone;
    },

    getCloneOfArray : function(oldArray, level) {
      //private method (to copy array of objects) - getCloneOfObject will use this internally
      var tempClone = [];

      for (var arrIndex = 0, l = oldArray.length; arrIndex <= l; arrIndex++) {
        if (typeof(oldArray[arrIndex]) == "object") {
          tempClone.push(this.getCloneOfObject(oldArray[arrIndex]), level + 1, arrIndex);
        } else {
          tempClone.push(oldArray[arrIndex]);
        }
      }
      return tempClone;
    },

    populateIteratorContext : function (context, list, index, parentModel, parentContext) {
      context.index = index;
      context.count = index + 1;
      context.first = (index == 0);
      context.last = (index == list.length - 1);
      context.even = (index % 2 == 0);
      context.odd = !context.even;
      context.evenEnd = (list.length % 2 == 0 ? context.odd : context.even);
      context.oddEnd = !context.evenEnd;
      context.size = list.length;
      context.parentModel = parentModel;
      context.parentContext = parentContext;
      return context;
    }

  };

})();

var IBEWebComponentManager = (function () {

  var componentList = new Array();

  return {
    addComponent : function (c) {
      var r = this.ensureUniqueId(c.id);
      componentList[componentList.length] = c;
      return r;
    },

    ensureUniqueId : function (id) {
      for (var i = 0, l = componentList.length; i < l; i++) {
        var c = componentList[i];
        if (c.id == id) {
          return this.ensureUniqueId(id + '_r' + Math.floor(Math.random() * 10));
        }
      }
      return id;
    },

    removeComponent : function (c) {
      var i;
      for (i = 0,l = componentList.length; i < l; i++) {
        if (componentList[i] == c) {
          c.remove();
          // Remove hole caused by component removal
          componentList.splice(i, 1);
          break;
        }
      }
    },

    print : function () {
      printAll(componentList);
    },

    getComponentWithId : function (id) {
      for (var i = 0, l = componentList.length; i < l; i++) {
        if (componentList[i].id == id) {
          return componentList[i];
        }
      }
      return null;
    },

    renderMoreOfListWithId : function (id, renderAll) {
      var c = this.getComponentWithId(id);
      c.renderMoreComponents(renderAll);
    }

  };

})();

/**
 * DEPRECATED! DO NOT USE!
 * @param view
 */
function createDynamicIBEWebComponentView(view) {
  ibewarning("You are using createDynamicIBEWebComponentView, it fetches view via AJAX. Please put view in DOM instead.");
  return new IBEWebComponentView("/ajax.component.view.dynamic.action", {view : view});
}
/**
 *
 * @param sourceUrl
 * @param viewParameters format is object literal: {"paramName1":value1, "paramName2": value2 ...}
 */
function IBEWebComponentView(sourceUrl, viewParameters) {
  this.sourceUrl = sourceUrl;
  this.viewParameters = viewParameters;

  if (sourceUrl) ibewarning("Do not use URL's in views. Add them to the DOM using script tags instead.");

  this.view = undefined;

  this.isLoading = false;

  this.componentCallbacks = new Array();

  /**
   * Returns the view as a string containing HTML and Django code.
   */
  this.getView = function() {
    return this.view;
  };

  this.fetchView = function (force) {
    if ((force || this.view === undefined) && this.isLoading === false && this.sourceUrl !== undefined) {
      this.fetchViewFromUrl();
    }
  };

  this.fetchViewFromUrl = function () {
    if (this.isLoading === false && this.sourceUrl !== undefined) {
      this.isLoading = true;
      var url = this.sourceUrl;
      url += url.indexOf('?') == -1 ? '?' : '&';
      if (this.viewParameters) {
        var queryString = [], pName, params = this.viewParameters;
        for (pName in params) {
          if (params.hasOwnProperty(pName)) {
            queryString.push("&"),queryString.push(pName),queryString.push("="),queryString.push(params[pName]);
          }
        }
        url += queryString.join("");
      }
      YAHOO.util.Connect.asyncRequest('GET', url, {
        success: this.viewFetchedFromUrl,
        failure: this.viewFetchedFromUrl,
        scope: this
      }, '');
    }
  };

  /**
   * Decodes important character in script tags that might have been encoded into HTML entities depending on how the view was created in DOM.
   * @param scriptCode
   */
  this.decodeHtmlEntitiesForScriptTags = function(scriptCode) {
    scriptCode = scriptCode.replace(/%7B%%20/g, "{% ");
    scriptCode = scriptCode.replace(/%20%%7D/g, " %}");
    scriptCode = scriptCode.replace(/%7B/g, "{");
    scriptCode = scriptCode.replace(/%7D/g, "}");
    scriptCode = scriptCode.replace(/&amp;/g, "&");
    return scriptCode;
  };

  this.fetchViewFromDOM = function (elementId) {
    this.isLoading = false;
    var e = getObj(elementId);
    if (e) {
      this.view = this.decodeHtmlEntitiesForScriptTags(e.innerHTML);
    } else {
      ibeerror("IBEWebComponentView trying to fetch view from DOM object that is undefined; id = " + elementId);
      ibetrace();
    }
    this.triggerAllCallbacks();
  };

  this.fetchAndRemoveViewFromDOM = function (elementId) {
    this.fetchViewFromDOM(elementId);
    var e = getObj(elementId);
    if (e) {
      var p = e.parentNode;
      if (p) p.removeChild(e);
    }
  };

  this.setViewWithHtml = function (htmlCode) {
    this.view = htmlCode;
    this.triggerAllCallbacks();
  };

  this.addComponentToCallbackList = function(c) {
    this.componentCallbacks[this.componentCallbacks.length] = c;
  };

  this.triggerAllCallbacks = function() {
    if (this.componentCallbacks !== undefined) {
      for (var i = 0, l = this.componentCallbacks.length; i < l; i++) {
        var f = this.componentCallbacks[i];
        if (f) {
          f.render();
        }
      }
    }
    this.componentCallbacks = new Array();
  };

  this.viewFetchedFromUrl = function (o) {
    this.view = o.responseText;
    this.triggerAllCallbacks();
  };

}

/**
 * An object that encapsulates iterator commands and state. The iterator state is updated when the component renders.
 * This means, if you update the model (say a list of hotels for example), the getTotalPages() will give you the number of pages for the old list, until you render the component.
 * So, you should updates texts which contains these numbers AFTER you render the component.
 * @param component
 * @param iterName
 * @param args
 */
function IBEIteratorContainer(component, iterName, args) {
  args = $.extend({autoRender:true}, args);
  this.component = component;
  this.iterName = iterName;
  this.state = this.component.getNiterState(iterName);
  this.autoRender = args.autoRender;

  if (!this.state) {
    ibeerror("Trying to get iterator, but there is no iterator with name: " + iterName);
  }

  this._autoRender = function() {
    if (this.autoRender) {
      component.render();
    }
  };

  this.showMoreItems = function() {
    this.state.itemsShowing += this.state.itemsPerPage;
    this._autoRender();
  };

  this.reset = function() {
    this.state.itemsShowing = this.state.itemsPerPage;
    this.state.showingAll = false;
    this.state.currentPage = 0;
    this._autoRender();
  };

  this.showAllItems = function() {
    this.state.showingAll = true;
    this._autoRender();
  };

  this.showNextPage = function() {
    this.state.currentPage++;
    this._autoRender();
  };

  this.showPrevPage = function() {
    this.state.currentPage--;
    this._autoRender();
  };

  this.setItemsPerPage = function(itemsPerPage) {
    if (!this.state.usePages) ibewarning("Trying to set items per page but iterator is not using pages: " + this.iterName);
    this.state.itemsPerPage = itemsPerPage;
    this._autoRender();
  };

  this.setItemsShowing = function(s) {
    if (this.state.usePages) ibewarning("Trying to set items showing, but iterator is using pages: " + this.iterName);
    this.state.itemsShowing = s;
    this._autoRender();
  };

  this.setCurrentPage = function(p) {
    this.state.currentPage = p;
    this._autoRender();
  };

  this.getCurrentPage = function() {
    return this.state.currentPage;
  };

  this.getTotalPages = function() {
    return this.state.totalPages;
  };

  this.setAllAreShowingCallback = function(f) {
    this.state.allItemsAreShowingCallback = f;
  };


}

function IBEWebComponent(args) {
  this.args = args = $.extend({
                                id : undefined, // Specify id if you want to
                                view : undefined, // ID of DOM element which innerHTML will become view.
                                viewHtml : undefined, // Specify HTML string to use as view. Overrides view argument.
                                model : undefined,
                                placeHolderId : undefined,
                                renderCallback : undefined,
                                callbackScope : undefined,
                                mode : "insert", // insert, append
                                clipboard : {},
                                beforeRenderAnimation : undefined,
                                beforeRenderAnimationDuration : 'slow',
                                afterRenderAnimation : undefined,
                                afterRenderAnimationDuration : 'slow',
                                niterConfig:undefined // Settings for the niters
                              }, args);

  var listValues = [args.beforeRenderAnimation, args.afterRenderAnimation], listNames = ['args.beforeRenderAnimation', 'args.afterRenderAnimation'];

  validateArgumentList(listValues, {rules:{allowUndefined:true, requireValueIsOneOf:['fadeIn', 'fadeOut', 'slideDown', 'slideUp', 'hide', 'show']}}, listNames);

  if (typeof args.beforeRenderAnimationDuration === 'string') {
    validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf : ['slow', 'fast', 'normal']}}, 'args.beforeRenderAnimationDuration');
  } else {
    validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.beforeRenderAnimationDuration');
  }

  if (typeof args.afterRenderAnimationDuration === 'string') {
    validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf : ['slow', 'fast', 'normal']}}, 'args.afterRenderAnimationDuration');
  } else {
    validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.afterRenderAnimationDuration');
  }

  validateArgument(args.renderCallback, {rules:{allowUndefined:true, requireFunction:true}}, "args.renderCallback");
  validateArgument(args.placeHolderId, {rules:{allowUndefined:true, requireString:true}}, "args.placeHolderId");
  validateArgument(args.mode, {rules:{allowUndefined:false, requireValueIsOneOf:["insert", "append"]}}, "args.mode");
  validateArgument(args.id, {rules:{allowUndefined:true, requireString:true}}, "args.id");

  this.id = args.id;
  this.view = args.view;
  this.viewHtml = args.viewHtml;
  this.model = args.model;
  this.pane = undefined;
  this.placeHolderId = args.placeHolderId;
  this.mode = args.mode;
  this.niterConfig = args.niterConfig;

  // Extend every niter config with default values.
  for (var niterName in this.niterConfig) {
    this.niterConfig[niterName] = $.extend({
                                             itemsShowing: 0,
                                             itemsPerPage:10,
                                             usePages:false,
                                             allItemsAreShowingCallback:function() {
                                             }
                                           }, this.niterConfig[niterName]);
    this.niterConfig[niterName].itemsShowing = this.niterConfig[niterName].itemsPerPage;
  }

  // Callbacks

  if (this.viewHtml) {
    this.setViewWithHtml(this.viewHtml);
  } else if (typeof this.view === "string") {
    // Lookup view if typeof == string
    var elementId = this.view;
    this.view = new IBEWebComponentView();
    this.view.fetchViewFromDOM(elementId);
  }
  this.callbackList = new Array();
  this.callbackScopeList = new Array();

  this.addRenderCallback = function (c, s) {
    var i = this.callbackList.length;
    this.callbackList[i] = c;
    this.callbackScopeList[i] = s;
  };

  this.addRenderCallback(args.renderCallback, args.callbackScope);

  this.shouldRender = false; // If true, applyModel() will run render when done.
  this.isBeingRemoved = false;
  this.isRemoved = false;
  this.hasBeenRendered = false;
  this.storeContentInDivNode = true; // Store in div.innerHTML directly.

  this.context = new Array();
  this.childrenComponents = new Array();
  this.childrenComponentsRendering = undefined;
  this.childrenHasBeenRendered = false;

  this.showLoadingWhileRendering = false;

  this.resultNode = undefined; // A DOM node.

  this.divId = undefined; // Set it when rendering!
  this.divNode = undefined;
  this.divCssClass = undefined;
  this.divContent = undefined;

  this.id = IBEWebComponentManager.addComponent(this);
  this.divCounter = 0;

  this.setModel = function(m) {
    this.model = m;
    this.hasBeenRendered = false;
    this.resultNode = undefined;
  };

  this.getNiterState = function(iterName) {
    return this.niterConfig[iterName];
  };

  this.getModel = function() {
    return this.model;
  };

  this.setViewWithHtml = function(html) {
    this.view = new IBEWebComponentView();
    this.view.setViewWithHtml(html);
  };

  /**
   * Returns an iterator object which allows you to control the iterator and gets its state.
   * @param iterName
   * @param args autoRender, if true, the component will automatically rerender when iterator changes state.
   */
  this.getIterator = function(iterName, args) {
    args = $.extend({autoRender:true}, args);
    return new IBEIteratorContainer(this, iterName, args)
  };

  this.applyModel = function (args) {
    args = $.extend({renderAfter:false}, args);
    if (this.view === undefined) {
      ibeerror('Error: Trying to apply model to null view.');
    } else {
      if (this.view.view === undefined) {
        this.view.addComponentToCallbackList(this);
        this.view.fetchView();
      } else {
        // Create containing div, ensure unique div element id.
        if (this.divId === undefined) {
          this.divId = this.id;
        }

        while (getObj(this.divId)) {
          this.divId = this.divId + Math.floor(Math.random() * 10);
        }

        this.divNode = document.createElement('div');
        this.divNode.id = this.divId;

        // Apply model
        if (this.model || this.model == null) {
          this.parseContainer = new IBEParserContainer({niterConfig:this.niterConfig});
          var parser = new IBEParser(this, this.parseContainer, {clipboard:this.args.clipboard});

          this.divContent = parser.parse(this.view.view, this.model, {
            context:this.context,
            childComponents:this.childrenComponents
          });

          if (this.storeContentInDivNode) {
            this.divNode.innerHTML = this.divContent;
          }
        } else {
          this.divNode.innerHTML = new String(this.view.view);
        }

        // Save result
        if (this.divCssClass) {
          this.divNode.className = this.divCssClass;
        }
        this.resultNode = this.divNode;

        if (this.shouldRender || args.renderAfter) {
          this.shouldRender = false;
          this.render();
        }
      }
    }
  };

  this.renderChildren = function () {
    this.childrenComponentsRendering = this.childrenComponents.length;
    for (var i = 0, l = this.childrenComponents.length; i < l; i++) {
      this.childrenComponents[i].addRenderCallback(this.childRendered, this);
      this.childrenComponents[i].render();
    }
    // Fetch view as well!
    this.view.fetchView();
  };

  this.childRendered = function () {
    this.childrenComponentsRendering--;
    if (this.childrenComponentsRendering == 0) {
      this.allChildrenRendered();
    }
  };

  this.allChildrenRendered = function () {
    this.childrenHasBeenRendered = true;
    // And render this, now that we have all children ready to be parsed into this component
    this.render();
  };

  this.triggerAllCallbacks = function () {
    for (var i = 0, l = this.callbackList.length; i < l; i++) {
      var c = this.callbackList[i];
      if (typeof c == 'function') {
        var s = this.callbackScopeList[i];
        if (s) {
          c.apply(s);
        } else {
          c();
        }
      }
    }
  };

  this.render = function () {
    if (this.isRemoved) return; // Do nothing!

    var placeHolder = $('#' + this.placeHolderId), that = this;

    if (args.beforeRenderAnimation !== undefined && args.afterRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponent();
        placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
      });
    } else if (args.beforeRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponent();
      });
    } else if (args.afterRenderAnimation !== undefined) {
      that.renderComponent();
      placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
    } else {
      that.renderComponent(); // default behavior is rendering without any animation.
    }
  };

  /**
   * Does the actual rendering of the component
   */
  this.renderComponent = function () {
    if (this.showLoadingWhileRendering === true) {
      setInnerHtmlToLoadingAnimation(this.placeHolderId);
      // Wait 2 ms so that loading screen has been rendered!
      var that = this;
      setTimeout(function () {
        that.actuallyRender();
      }, 2);
    } else {
      // Just render them immediately.
      this.actuallyRender();
    }
  };

  this.actuallyRender = function () {

    if (this.childrenHasBeenRendered === false && this.childrenComponents.length > 0) {
      // We have children, render them first! When rendering is done, this.render will be called again. Via callbacks, so must return after.
      this.renderChildren();
      return;
    }
    if (this.resultNode) {
      this.applyModel({renderAfter:false}); // Always update!
      //setInnerHTML(this.placeHolderId, ''); // Clear loading animation? DO NOT USER setInnerHTML! DOM SUCKS! :)
      if (this.placeHolderId) {
        var placeHolder = getObj(this.placeHolderId);
        if (placeHolder) {
          if (this.mode === 'append') {
            placeHolder.appendChild(this.resultNode);
          } else {
            // Remove previous children
            while (placeHolder.firstChild) placeHolder.removeChild(placeHolder.firstChild);
            placeHolder.appendChild(this.resultNode);
          }
        } else {
          ibeerror('Error: Rendered component (id=' + this.id + ') but could not find placeholder (id=' + this.placeHolderId + ').');
        }
      }

      this.hasBeenRendered = true;

      this.triggerAllCallbacks();

    } else {
      // Apply model is asyncronous, so must return the function, and not allow more execution!
      this.applyModel({renderAfter:true});
    }
  };

  this.remove = function () {
    var e = getObj(this.divId);
    if (e) {
      if (!this.isBeingRemoved) {
        this.isBeingRemoved = true; // To prevent stack overflow
        e.parentNode.removeChild(e);
        IBEWebComponentManager.removeComponent(this);
      }
    } else {
      // content div does not exist. might not have been rendered yet.
    }
    this.isRemoved = true;
  };

  this.getResultAsHtmlString = function() {
    return this.resultNode.innerHTML;
  };

  this.getResultAsHtmlNode = function() {
    return this.resultNode;
  };

  this.hide = function () {
    var e = getObj(this.divId);
    if (e) {
      setHidden(this.divId, true);
    } else {
      ibeerror('Error: Trying to hide component with missing or incorrect id=' + this.divId);
    }
  };

  this.show = function () {
    var e = getObj(this.divId);
    if (e) {
      setVisible(this.divId, true);
    } else {
      ibeerror('Error: Trying to show component with missing or incorrect id=' + this.divId);
    }
  };

  this.addChildComponent = function (component) {
    var i = this.childrenComponents.length;
    this.childrenComponents[i] = component;
    // Ensure no rendering to DOM
    component.placeHolderId = undefined;
  };

  this.getChildComponent = function (id) {
    for (var i = 0, l = this.childrenComponents.length; i < l; i++) {
      var c = this.childrenComponents[i];
      if (c.id == id) return c;
    }
    return null;
  };
}

/**
 * id, view, list, placeHolderId, separatorHtml, renderCallback, callbackScope, mode
 *
 * @param args
 */
function IBEWebComponentList(args) {
  args = $.extend({
                    id : undefined,
                    view : undefined,
                    list : undefined,
                    placeHolderId : undefined,
                    separatorHtml : undefined,
                    renderCallback : undefined,
                    callbackScope : undefined,
                    renderMoreCallback : undefined,
                    mode : 'insert',
                    showLoadingWhileRendering : false,
                    renderPerPass : 10,
                    showMoreText : undefined,
                    showAllTextEnabled : true,
                    clipboard:{},
                    beforeRenderAnimation : undefined,
                    beforeRenderAnimationDuration : 'slow',
                    afterRenderAnimation : undefined,
                    afterRenderAnimationDuration : 'slow'
                  }, args);

  this.validateArguments = function() {
    var listValues = [args.beforeRenderAnimation,args.afterRenderAnimation], listNames = ['args.beforeRenderAnimation','args.afterRenderAnimation'];

    validateArgumentList(listValues, {rules:{allowUndefined:true, requireValueIsOneOf:['fadeIn', 'fadeOut', 'slideDown', 'slideUp', 'hide', 'show']}}, listNames);

    if (typeof args.beforeRenderAnimationDuration === 'string') {
      validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf : ['slow', 'fast', 'normal']}}, 'args.beforeRenderAnimationDuration');
    } else {
      validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.beforeRenderAnimationDuration');
    }

    if (typeof args.afterRenderAnimationDuration === 'string') {
      validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf : ['slow', 'fast', 'normal']}}, 'args.afterRenderAnimationDuration');
    } else {
      validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.afterRenderAnimationDuration');
    }
  };

  this.validateArguments();

  this.id = args.id;
  this.view = args.view;
  this.list = args.list;
  this.placeHolderId = args.placeHolderId;
  this.renderCallback = args.renderCallback;
  this.renderMoreCallback = args.renderMoreCallback;
  this.callbackScope = args.callbackScope;
  this.separatorHtml = args.separatorHtml;
  this.mode = args.mode;
  this.showLoadingWhileRendering = args.showLoadingWhileRendering;

  this.renderAll = false;

  this.componentList = new Array();

  this.resultNode = undefined;

  this.isLoaded = false;
  this.divId = undefined;
  this.divNode = undefined;

  this.showMoreButtonNode = undefined;
  this.showMoreText = args.showMoreText;
  this.showAllText = undefined;
  this.showAllTextEnabled = args.showAllTextEnabled;

  this.listNode = undefined;
  this.divCssClass = undefined;
  this.childrenDivCssClass = undefined;
  this.divContent = undefined;

  // Progressive rendering in passes
  this.rendersPerPass = args.renderPerPass;
  this.alreadyRendered = 0;
  this.showMoreElementId = undefined;
  this.listDivElementId = undefined;
  this.showMoreDivCssClass = undefined;
  this.shouldClearList = false;

  this.isFirstRender = true;

  this.showMoreLinkClass = "";

  this.id = IBEWebComponentManager.addComponent(this);

  this.componentsRendering = 0;

  // Ensure unique id.
  while (getObj(this.id)) {
    this.id = this.id + Math.floor(Math.random() * 10);
  }

  this.setList = function(list) {
    this.isLoaded = false;
    this.list = list;
    this.componentList = [];
  };

  this.getResultAsHtmlNode = function() {
    return this.resultNode;
  };

  this.getListAsHtmlNode = function() {
    return this.listNode;
  };

  this.getComponentById = function (id) {
    return (this.componentList[this.getComponentIndexById(id)]);
  };

  this.getComponentIndexById = function (id) {
    for (var i = 0, l = this.componentList.length; i < l; i++) {
      if (this.componentList[i].id == id)  return i;
    }
    return null;
  };

  this.getComponentByIndex = function (index) {
    return this.componentList[index];
  };

  this.removeComponentById = function (id) {
    var index = this.getComponentIndexById(id);
    this.removeComponentByIndex(index);
  };

  this.removeComponentByIndex = function (index) {
    this.componentList[index].remove();
    for (var i = index, l = this.componentList.length; i < l; i++) {
      this.componentList[i] = this.componentList[i + 1];
    }
  };

  this.loadComponents = function () {
    if (this.isLoaded === false) {
      this.isLoaded = true;
      for (var i = 0, l = this.list.length; i < l; i++) {
        var e = this.list[i];
        var c = new IBEWebComponent({id:this.id + '_item_' + i,
                                      view:this.view,
                                      model:e,
                                      renderCallback:this.componentHasBeenRendered,
                                      callbackScope:this,
                                      clipboard : args.clipboard
                                    });
        c.storeContentInDivNode = false; // Store only strings, we do not want to parse every single component HTML.
        c.divCssClass = this.childrenDivCssClass;
        // Populate context
        c.context = IBEUtil.populateIteratorContext(c.context, this.list, i);
        /*
         c.context.index = i;
         c.context.count = i + 1;
         c.context.first = (i == 0);
         c.context.last = (i == this.list.length - 1);
         c.context.even = (i % 2 == 0);
         c.context.odd = !c.context.even;
         c.context.size = this.list.length;
         */
        this.componentList[i] = c;
      }
    }
  };

  this.render = function () {

    this.alreadyRendered = 0;

    // Create container
    if (this.divId === undefined) {
      this.divId = this.id;
    }
    this.divId = IBEUtil.ensureUniqueElementId(this.divId);
    this.showMoreElementId = IBEUtil.ensureUniqueElementId(this.divId + '_showmore');
    this.listDivElementId = IBEUtil.ensureUniqueElementId(this.divId + '_list');

    // Create container div
    this.divNode = document.createElement('div');
    this.divNode.id = this.divId;
    if (this.divCssClass) {
      this.divNode.className = this.divCssClass;
    }

    // Create list div
    this.listNode = document.createElement('div');
    this.listNode.id = this.listDivElementId;
    this.divNode.appendChild(this.listNode);

    // Create button div
    this.showMoreButtonNode = document.createElement('div');
    this.showMoreButtonNode.id = this.showMoreElementId;
    if (this.showMoreDivCssClass) {
      this.showMoreButtonNode.className = this.showMoreDivCssClass;
    }

    if (!this.showMoreText) {
      this.showMoreText = UiText.get('Hotel.Result.List.ShowMoreButton.Label') + '&nbsp;&raquo;';
    }
    this.showMoreButtonNode.innerHTML = '<a href="javascript:void(0);" class="' + this.showMoreLinkClass + '" onclick="IBEWebComponentManager.renderMoreOfListWithId(\'' + this.id + '\');">' + this.showMoreText + '</a>';

    if (!this.showAllText) {
      this.showAllText = UiText.get('Result.List.ShowAllButton.Label', 'Display All') + '&nbsp;&raquo;';
    }
    // Display "show all" text iff, it's enabled (default is true).
    if (this.showAllTextEnabled) {
      this.showMoreButtonNode.innerHTML += '&nbsp;&nbsp;|&nbsp; &nbsp;<a href="javascript:void(0);" class="' + this.showMoreLinkClass + '" onclick="IBEWebComponentManager.renderMoreOfListWithId(\'' + this.id + '\', true);">' + this.showAllText + '</a>';
    }

    this.divNode.appendChild(this.showMoreButtonNode);

    // Render list from scratch, so clear anything that was there before.
    this.shouldClearList = true;

    var placeHolder = $('#' + this.placeHolderId), that = this;

    if (args.beforeRenderAnimation !== undefined && args.afterRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponents();
        placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
      });
    } else if (args.beforeRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponents();
      });
    } else if (args.afterRenderAnimation !== undefined) {
      that.renderComponent();
      placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
    } else {
      that.renderComponents(); // default behavior is rendering without any animation.
    }
  };

  this.renderComponents = function() {
    // Show loading animation
    if (this.showLoadingWhileRendering === true) {
      setInnerHtmlToLoadingAnimation(this.placeHolderId);
      // Wait 2 ms so that loading screen has been rendered!
      var thisC = this; // Need to do this to keep the scope of the callback execution.
      setTimeout(function() {
        thisC.renderMoreComponents(this.renderAll);
      }, 2);
    } else {
      // Just render them immediately.
      this.renderMoreComponents(this.renderAll);
    }
  };

  this.renderMoreComponents = function (renderAll) {
    // Trigger rendering of components, as many as will be displayed
    if (this.isLoaded === false) {
      this.loadComponents();
    }

    var listSize = this.list.length;
    if (this.renderAll || renderAll) this.rendersPerPass = listSize;
    var max = this.alreadyRendered + this.rendersPerPass;
    if (max > listSize) max = listSize;
    this.componentsRendering = max - this.alreadyRendered;

    for (var i = this.alreadyRendered; i < max; i++) {
      this.componentList[i].render();
    }
  };

  this.componentHasBeenRendered = function () {
    this.componentsRendering--;
    if (this.componentsRendering == 0) {
      this.allComponentsFinishedRendering();
    }
  };

  this.allComponentsFinishedRendering = function () {
    var placeHolder = getObj(this.placeHolderId);

    if (!placeHolder) {
      ibeerror('Error: No placeholder in DOM for component with id=' + this.id);
      return;
    }
    // Clear loading screen
    if (this.showLoadingWhileRendering === true) {
      while (placeHolder.firstChild) placeHolder.removeChild(placeHolder.firstChild);
    }

    if (this.shouldClearList) {
      this.shouldClearList = false;
      clearElement(placeHolder);
    }
    placeHolder.appendChild(this.divNode);

    this.renderMore();
  };

  /**
   * Renders 10 more of the list.
   */
  this.renderMore = function () {
    var htmlArray = new Array();

    var isFirstRenderPass = this.alreadyRendered === 0; // True if this is the first rendering pass, that is for example, the first 20 in list.
    var isFirstElementInCurrentRenderPass = true; // True if currently rendered item is the first item to be rendered in this pass.
    var elementsRenderedInThisRenderPass = 0; // Amount of elements that have been rendered so far in this pass
    var previousAlreadyRendered = this.alreadyRendered;
    var maxElementsAfterThisRenderPass = this.alreadyRendered + this.rendersPerPass;

    for (var i = this.alreadyRendered, l = this.componentList.length; i < maxElementsAfterThisRenderPass && i < l; i++) {
      var c = this.componentList[i];
      var isFirstElementInTotal = this.alreadyRendered === 0; // Only first if first pass

      if (!isFirstElementInTotal) {
        if (this.separatorHtml) htmlArray.push(this.separatorHtml);
      }
      htmlArray.push(c.divContent);

      elementsRenderedInThisRenderPass++;
      this.alreadyRendered++;
      isFirstElementInCurrentRenderPass = false;
    }

    var htmlResult = htmlArray.join('');
    this.divContent += htmlResult;

    var node = document.createElement('div');
    node.innerHTML = htmlResult;

    this.listNode.appendChild(node);


    // Hide/show the "Show more"-button
    if (this.alreadyRendered == this.componentList.length) {
      this.hideShowMoreButton();
    } else {
      this.showShowMoreButton();
    }

    if (isFirstRenderPass === true) {
      if (this.renderCallback) {
        this.renderCallback.apply(this.callbackScope);
        this.isFirstRender = false;
      }
    } else {
      if (this.renderMoreCallback) {
        this.renderMoreCallback.apply(this.callbackScope, [previousAlreadyRendered, elementsRenderedInThisRenderPass]);
      }
    }

  };

  this.remove = function () {
    for (var i = 0, l = this.componentList.length; i < l; i++) {
      var c = this.componentList[i];
      c.remove();
    }
    var e = getObj(this.divId);
    if (e) {
      e.parentNode.removeChild(e);
    } else {
      ibeerror('Error: Cannot remove list component with no unique ID.');
    }
  };

  this.hide = function () {
    var e = getObj(this.divId);
    if (e) {
      setHidden(this.divId, true);
    } else {
      ibeerror('Error: Trying to hide component with missing or incorrect id.');
    }
  };

  this.show = function () {
    var e = getObj(this.divId);
    if (e) {
      setVisible(this.divId, true);
    } else {
      ibeerror('Error: Trying to show component with missing or incorrect id.');
    }
  };

  this.showShowMoreButton = function () {
    $('#' + this.showMoreElementId).show();
  };

  this.hideShowMoreButton = function () {
    $('#' + this.showMoreElementId).hide();
  };

}

function createWebComponentFromHtml(id, htmlCode, model, destinationElementId) {
  var v = new IBEWebComponentView();
  v.setViewWithHtml(htmlCode);
  return new IBEWebComponent({id:id, view:v, model:model, placeHolderId:destinationElementId});
}

/**
 * Theory is based on parts of http://phrogz.net/js/classes/OOPinJS2.html
 *
 * USED by WindowComponent
 *      by DynamicBarComponent
 */
function IPane(id, _modelFetchCallback) {
  this._id = id;
  this._type = "IPane";
  this._parent = undefined;
  this._children = new Array();

  /**
   * Model stuff
   */
  this._model = undefined;
  this._modelFetchCallback = _modelFetchCallback;
  this._hasUpdatedModel = true;

  this._placeHolder = undefined;

  this._validMessageTypes = [
  /**
   * Sorts the content of the pane.
   * Parameters:
   * order = the variable name which we order by.
   * desc = true or false. Optional.
   */
    "sort",

  /**
   * Tells the pane to focus on a specific date.
   * Parameters: {timeInformation=..., resultItem=...}
   * timeInformation = JS object
   * resultItem = our general reference object (xs-item) JS object
   */
    "focusOnDate",

  /**
   * Tells the pane to focus on a specific month.
   * Parameters:
   * month = the month, number, 0 - 11
   */
    "focusOnMonth",

  /**
   * Tells the pane to focus on a specific month.
   * Parameters:
   * resultItemId = the id value from what is returned from xtremeController.getResultItem(id). aka id=resultItemId
   */
    "showSearchForm"
    // end of enum
  ];

}

IPane.prototype.getId = function() {
  return this._id;
};

IPane.prototype.getModel = function() {
  return this._model;
};

IPane.prototype.setModel = function(m) {
  if (!equalsObject(m, this._model)) {
    this._hasUpdatedModel = true;
    this._model = m;
  }
};

IPane.prototype.clearModel = function() {
  this._hasUpdatedModel = true;
  this._model = undefined;
};

IPane.prototype.setModelFetchCallback = function(f) {
  this._modelFetchCallback = f;
};

IPane.prototype.addChildPane = function(child) {
  if (child) {
    child._parent = this;
    this.getAllChildPanes().push(child);
  }
};

IPane.prototype.getAllChildPanes = function() {
  if (!this._children) this._children = new Array();
  return this._children;
};

IPane.prototype.getChildPaneAtIndex = function(index) {
  return this.getAllChildPanes()[index];
};

IPane.prototype.getParentPane = function() {
  return this._parent;
};

IPane.prototype.hasParentPane = function() {
  return this._parent !== undefined;
};

IPane.prototype.removeChildPaneAtIndex = function(index) {
  this.getAllChildPanes().splice(index, 1);
};

IPane.prototype.removePaneWithId = function(id) {
  this.getTop()._removePaneWithId(id);
};

IPane.prototype._removePaneWithId = function(id) {
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    if (this.getAllChildPanes()[i]._id == id) {
      this.removeChildPaneAtIndex(i);
    } else {
      this.getAllChildPanes()[i]._removePaneWithId(args);
    }
  }
};

IPane.prototype.removeAllChildPanes = function() {
  this._children = new Array();
};

/**
 * Implementations of update should only update model (by running fetcher callback, setModel(controller.something) or whatever you are using).
 * It must not generate any HTML!
 * @param args
 */
IPane.prototype.update = function(args) {
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i].update(args);
  }
};

/**
 * Overload this method!
 *
 * Sends a message down the tree which can be received by the leaves.
 * Override this to listen to panes who want to send you messages.
 * When you want to listen to a specific message, check the "message" parameter
 * and string and act on it. Components who use a specific "message" must define what that
 * "message" string means for them.
 *
 * @param message - string, camelcase identifier
 * @param args - any js object which is needed to perform the task
 */
IPane.prototype.send = function(message, args) {
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i].send(message, args);
  }
};

/**
 * Sends a message to the current group and its children.
 * @param message
 * @param args
 */
IPane.prototype.sendGroup = function(message, args) {
  this._parent.send(message, args);
};

/**
 * Sends a message to a Pane with a specific id. It can be located anywhere in the tree.
 * @param id
 * @param message
 * @param args
 */
IPane.prototype.sendTo = function(id, message, args) {
  this.getTop()._sendTo(id, message, args);
};

IPane.prototype._sendTo = function(id, message, args) {
  if (this._id == id) {
    this.send(message, args);
  }
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i]._sendTo(id, message, args);
  }
};

IPane.prototype.sendToPanesGroup = function(id, message, args) {
  this.getTop()._sendToPanesGroup(id, message, args);
};

IPane.prototype._sendToPanesGroup = function(id, message, args) {
  if (this._id == id) this.sendGroup(message, args);
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i]._sendToPanesGroup(id, message, args);
  }
};

IPane.prototype.getTop = function() {
  return this._getTop();
};

IPane.prototype._getTop = function() {
  if (this.hasParentPane()) {
    return this.getParentPane()._getTop();
  }
  else {
    return this;
  }
};

/**
 * Overload this method!
 *
 * Must render the HTML to a node. No HTML should be rendered outside of this function, and this function should do
 * nothing else. It must not update the model in any way, but should use this._model.
 * @param node
 */
IPane.prototype.renderToNode = function(node) {
  if (this.getAllChildPanes() && this.getAllChildPanes().length) {
    for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
      var child = this.getAllChildPanes()[i];
      if (child && child.getHtmlNode && typeof child.getHtmlNode === "function") {
        var childNode = child.getHtmlNode();
        if (childNode) node.appendChild(childNode);
      } else {
        ibewarning("Added Pane that does not extend the IPane interface.");
      }
    }
  }
};

/**
 * Returns true if the Pane has new content (and should be rendered again).
 */
IPane.prototype.needsRender = function() {

  if (this.getAllChildPanes() && this.getAllChildPanes().length) {
    // If it has children, see if any of them has new HTML.
    for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
      if (this.getAllChildPanes()[i].needsRender()) return true;
    }
    return false;
  } else {
    // No children, this is a leaf, see if this has new HTML. If we dont have a placeholder, we must render it.
    var b = this._hasUpdatedModel || !this._placeHolder;
    return b;
  }
};

/**
 * Checks if this Pane has a placeholder, if not, it creates one.
 */
IPane.prototype._ensurePlaceHolderExists = function() {
  if (!this._placeHolder) {
    this._placeHolder = document.createElement("div");
    var css = this.getPlaceHolderClassAttribute();
    if (typeof(css) === "string" && this._placeHolder.setAttribute) {
      this._placeHolder["className"] = css;
    }

    this._placeHolder.id = IBEUtil.ensureUniqueElementId(this._id ? this._id : this._type);
  }
};

/**
 * You may override this method.
 */
IPane.prototype.getPlaceHolderClassAttribute = function() {
  return undefined;
};

/**
 * Actually renders the content. Private method, called by IPane when needed.
 */
IPane.prototype._renderHtmlNode = function() {
  $(this._placeHolder).children().remove();
  this._hasUpdatedModel = false;
  this.renderToNode(this._placeHolder);
};

IPane.prototype.redraw = function() {
  if (this._placeHolder) {
    // We have a placeholder in the DOM, just rerender the component to the DOM!
    this._renderHtmlNode();
  }
};


/**
 * This is a public method that can be used to get the HTML node that this Pane creates.
 * Do NOT overload this method!
 */
IPane.prototype.getHtmlNode = function() {
  this._ensurePlaceHolderExists();
  if (this.needsRender()) {
    this._renderHtmlNode();
  }
  return this._placeHolder;
};

IPane.prototype.toString = function() {
  return this._toString(0);
};

IPane.prototype._toString = function(level) {
  var s = "";
  for (var i = 0; i < level; i++) s += "--";
  s += "> ";
  s += this.getId() + " (" + ( this.getAllChildPanes() ? this.getAllChildPanes().length : 0) + " children) parent=" + ( this.getParentPane() ? this.getParentPane().getId() : "N/A");
  s += "\n";
  if (this.getAllChildPanes() && this.getAllChildPanes().length) {
    for (var j = 0; j < this.getAllChildPanes().length; j++) {
      s += this.getAllChildPanes()[j]._toString(level + 1);
    }
  }
  return s;
};


/* *********************
 List pane
 ********************* */

function ListPane(id, _modelFetchCallback, _contentPaneType) {
  IPane.prototype.constructor();
  this._id = id;
  this._type = "ListPane";

  this._contentPaneType = _contentPaneType;

  this._modelModifierCallback = function(obj, index) {
    // Default modifier, does nothing.
    return obj;
  };

  this._paneInitCallback = function(pane, index) {
    // Default pane init, does nothing.
  };
}

ListPane.prototype = new IPane();
ListPane.prototype.constructor = ListPane;

/**
 * Sets the type of pane to use. Pass the constructor as parameter. setContentPaneType(DynamicList)
 * @param f
 */
ListPane.prototype.setContentPaneType = function(f) {
  if (f && typeof f === "function") {
    this._contentPaneType = f;
  }
  else {
    ibeerror("Trying to set PaneList content pane type, but specified type is not a function. Please pass constructor as parameter.");
  }
};

ListPane.prototype.update = function(args) {
  this._fetchModel();
};

ListPane.prototype._fetchModel = function() {
  if (this._modelFetchCallback && typeof this._modelFetchCallback === "function") {
    var m = this._modelFetchCallback();
    if (nullOrUndefined(m)) {
      ibewarning("ListPane component model fetch callback returned null or undefined.");
      this.setModel(new Array());
    } else {
      if (nullOrUndefined(m.length)) {
        ibewarning("ListPane component model fetch callback returned an object, must return an array.");
        this.setModel(new Array());
      } else {
        this.setModel(m);
      }
    }
  } else {
    ibewarning("ListPane component is trying to fetch model, but no model fetch callback has been set.");
  }
  for (var i = 0, l = this.getModel().length; i < l; i++) {
    this.getModel()[i] = this._modelModifierCallback(this.getModel()[i], i);
  }
};

ListPane.prototype._buildChildList = function() {
  var list = this.getModel();
  if (this.list && this.list.length && this._contentPaneType) {
    for (var i = 0, l = this._model.length; i < l; i++) {
      var itemModel = this._model[i];
      var itemPane = new this._contentPaneType();
      this._paneInitCallback(itemPane, i);
      itemPane.setModel(itemModel);
      this.addChildPane(itemPane);
    }
  }
};

ListPane.prototype.renderToNode = function(node) {
  this.removeAllChildPanes();
  this._buildChildList();
  $.each(this.getAllChildPanes(), function(obj, index) {
    node.appendChild(obj.getHtmlNode());
  });
};


