/**
 * Author: Mattias Andersson
 * Contact: mattias.andersson@etraveli.com or mattias800@gmail.com
 * Last change: 2009-05-13
 */

var IBESorter = function () {

  // TODO: Sortera på avstånd från landmark!

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

    jsSort : function (list, sortBy, desc) {
      if (!list) return list;
      if (sortBy == 'hotelName' && !desc) list.sort(this.comparatorHotelName);
      else if (sortBy == 'hotelName' && desc) list.sort(this.comparatorHotelNameDesc);
      else if (sortBy == 'name' && !desc) list.sort(this.comparatorName);
      else if (sortBy == 'name' && desc) list.sort(this.comparatorNameDesc);
      else if (sortBy == 'bookingPrice' && !desc) list.sort(this.comparatorPrice);
      else if (sortBy == 'bookingPrice' && desc) list.sort(this.comparatorPriceDesc);
      else if (sortBy == 'recommended' && !desc) list.sort(this.comparatorRecommended);
      else if (sortBy == 'recommended' && desc) list.sort(this.comparatorRecommendedDesc);
      else if (sortBy == 'starRating' && !desc) list.sort(this.comparatorStars);
      else if (sortBy == 'starRating' && desc) list.sort(this.comparatorStarsDesc);
      else if (sortBy == 'priority' && !desc) list.sort(this.comparatorPriority);
      else if (sortBy == 'destination' && !desc) list.sort(this.comparatorDestination);
      else if (sortBy == 'destination' && desc) list.sort(this.comparatorDestinationDesc);
      else if (sortBy == 'checkInDateJs' && !desc) list.sort(this.comparatorCheckInDate);
      else if (sortBy == 'checkInDateJs' && desc) list.sort(this.comparatorCheckInDateDesc);
      else if (sortBy == 'checkOutDateJs' && !desc) list.sort(this.comparatorCheckOutDate);
      else if (sortBy == 'checkOutDateJs' && desc) list.sort(this.comparatorCheckOutDateDesc);
      else if (sortBy == 'numNights' && !desc) list.sort(this.comparatorNumNights);
      else if (sortBy == 'numNights' && desc) list.sort(this.comparatorNumNightsDesc);
      else if (sortBy == 'pricePerPerson' && !desc) list.sort(this.comparatorPricePerPerson);
      else if (sortBy == 'pricePerPerson' && desc) list.sort(this.comparatorPricePerPersonDesc);
      else if (sortBy == 'outDateJs' && desc) list.sort(this.comparatorDepartureDate);
      else if (sortBy == 'returnDateJs' && desc) list.sort(this.comparatorReturnDate);
      return list;
    },

    bubbleSort : function(list, sortBy, desc) {
      var swapped = undefined;
      do {
        swapped = false;
        for (var i = 0; i < list.length - 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);
      return list;
    },

    insertionSort : function(list, sortBy, desc) {
      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;
    },

    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;
    },

    // Comparators for use with array.sort.

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

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

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

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

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

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

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

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

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

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


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

    comparatorDestinationDesc : function (a, b) {
      return IBESorter.compareStringTo(a.destination, b.destination) * (-1);
    },

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

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

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

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

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

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

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

    comparatorHotelNameDesc : function (a, b) {
      return IBESorter.compareStringTo(a.hotelName, b.hotelName) * (-1);
    },

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

    comparatorNameDesc : function (a, b) {
      return IBESorter.compareStringTo(a.hotelName, b.name) * (-1);
    },

    comparatorPriority : function (a, b) {
      if (a.isPrioritizedInSearchResult && b.isPrioritizedInSearchResult) {
        return (+a.priorityNumber) - (+b.priorityNumber);
      }
      if (a.isPrioritizedInSearchResult && !b.isPrioritizedInSearchResult) {
        return -1;
      }
      if (!a.isPrioritizedInSearchResult && b.isPrioritizedInSearchResult) {
        return 1;
      }
      return 0;
    }

  };

}();

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

  this.html = html;
};

var IBEParser = function () {

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

  this.list = new Array();
  this.view = undefined;
  this.resultList = new Array();
  this.pc = 0;
  this.debug = false;

  this.modelStack = new Array();

  this.componentList = undefined;

  this.test = function () {
    var parser = new IBEParser();
    var model = new Array();
    var context = new Array();
    model['blabla'] = 3;
    model['prutt'] = 'liten';
    model['contentDescription'] = 'mattiasandersson';
    model['shortDescription'] = 'shortmattias';
    context['first'] = true;

    var view2 = "      <br/>" +
                "{% if this.model.contentDescription && this.model.contentDescription.length > 10 %}" +
                "A A A content: {% contentDescription %}" +
                "{% else %}" +
                "B B B short: {% shortDescription %}" +
                "{% endif %}";

    var view = '<b>ID:</b>' +
               '{% #first %}' +
               '{% blabla %}' +
               '{% if this.model.blabla == 3 %}' +
               '  bla bla är 3' +
               '  {% if this.model.prutt == \'stor\' %}' +
               '    <b>STOR PRUTT</b>' +
               '  {% endif %}' +
               '{% endif %}';
    var r = parser.parse(view2, model, context);
    ibelog('result:' + r);
  };

  this.parse = function (view, model, context, componentList) {
    this.view = view;
    this.model = model;
    this.context = context;
    this.componentList = componentList; // a list of components that can be parsed into the result
    var status = this.buildList();
    if (status) this.interpret();
    return this.getResult();
  };

  this.parseList = function (list, model, context) {
    this.list = list;
    this.model = model;
    this.context = context;
    this.interpret();
    return this.getResult();
  };

  this.interpret = function () {
    this.pc = 0;
    this.result = '';
    while (this.pc < this.list.length) {
      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++;
    }
  };

  this.buildList = function () {
    var resi = 0;
    this.list = new Array();
    var view = this.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);

    }

    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.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.getResult = function () {
    return this.resultList.join('');
  };

  this.getModel = function () {
    return this.modelStack[this.modelStack.length - 1];
  };

  this.pushModel = function (aModel) {
    // Push active model to stack, set active model to aModel
    this.modelStack.push(aModel);
    if (this.modelStack.length > 1) {
      this.context.parentModel = this.modelStack[this.modelStack.length - 2];
    } else {
      this.context.parentModel = undefined;
    }
    this.model = this.getModel();
  };

  this.popModel = function () {
    var removedModel = this.modelStack.pop();
    if (this.modelStack.length > 1) {
      this.context.parentModel = this.modelStack[this.modelStack.length - 2];
    } else {
      this.context.parentModel = undefined;
    }
    this.model = this.getModel();
    return removedModel;
  };

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

    if (tag.name == 'if') {
      // Find endif
      var ifIndex = this.pc;
      var endIfIndex = -1;
      var elseIndex = -1;
      var ifCounter = 0;
      for (var i = ifIndex + 1; i < this.list.length; 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".');
        this.pc = this.list.length; // End execution
      } 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) {
          var ifParser = new IBEParser();
          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);
          this.addResult(ifParser.getResult());
        }
        this.pc = endIfIndex;
      }
    } else if (tag.name == 'else') {
      ibeerror('Error: Found "else" with no "if".');
    } else if (tag.name == 'endif') {
      ibeerror('Error: Found "endif" with no "if".');

      // Context stuff
    } else if (tag.name.startsWith(contextPrefix)) {
      v = tag.name.substring(1);
      r = IBEUtil.lookup(v, this.context);
      if (!nullOrUndefined(r)) {
        this.addResult(r);
      } else {
        ibewarning('Trying to use context variable that does not exist: ' + v);
      }

      // Push/pop
    } else if (tag.name == 'push') {
      this.modelStack.push(this.model);
      this.pushModel(IBEUtil.lookup(tag.param, this.model));
      // TODO: Do we update context or parentContext?

    } else if (tag.name == 'endpush') {
      this.popModel();


      // Iterator
    } else if (tag.name == 'iter') {
      var iterIndex = this.pc;
      var endIterIndex = -1;
      var iterCounter = 0;

      for (var i = iterIndex + 1; i < this.list.length; i++) {
        if (this.list[i].name == 'iter') {
          iterCounter++;
        }
        if (this.list[i].name == 'enditer') {
          if (iterCounter == 0) {
            endIterIndex = i;
            break;
          } else {
            iterCounter--;
          }
        }
      }
      if (endIterIndex < 0) {
        ibeerror('"Iter" statement with missing "enditer".');
        this.killExecution();
      } else {
        // Found "enditer", parsing iter body and evaluating and everything.
        var iteratorList = IBEUtil.lookup(tag.param, this.model);
        var iteratorContext = IBEUtil.getCloneOfObject(this.context);
        if (iteratorList) {
          if (typeof iteratorList.length == "number") {
            for (var i = 0; i < iteratorList.length; i++) {
              var iteratorParser = new IBEParser();
              var iterViewTagList = subList(this.list, iterIndex + 1, endIterIndex - iterIndex - 1);
              var iteratorListItem = iteratorList[i];
              iteratorContext = IBEUtil.populateIteratorContext(iteratorContext, iteratorList, i, this.model, this.context);
              this.addResult(iteratorParser.parseList(iterViewTagList, iteratorListItem, iteratorContext));
            }
          } else {
            ibeerror("Iterator parameter is not a list. typeof=" + typeof iteratorList);
          }
        }
      }
      this.pc = endIterIndex;

      // Component insertion
    } else if (tag.name.startsWith(componentPrefix)) {
      v = tag.name.substring(1);
      c = this.findComponent(v);
      if (c && this.findComponent(v).resultNode) {
        var node = this.findComponent(v).divContent; //resultNode;
        this.addResult(node);
      } else {
        // Component did not exist, allowing this for now. Renders nothing.
      }

      // JS execution
    } else if (tag.name.startsWith('@')) {
      var vs = tag.name.substring(1) + (tag.param ? ' ' + tag.param : '');
      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') {
        console.log(this.model);
      } else if (vs == 'logContext') {
        console.log(this.context);
      } else {
        try {
          r = eval(vs);
          if (r) this.addResult(r);
        } catch (e) {
          ibeerror('Unable to evaluate script tag: ' + vs + '\nException:' + e);
        }
      }
      // Lookup
    } else {
      if (this.model == null) {
        // Allow null models
        r = "";
      } else {
        r = IBEUtil.lookup(tag.name, this.model);
      }
      if (!nullOrUndefined(r)) {
        this.addResult(r);
      } else {
        ibewarning('Trying to use model variable that does not exist: ' + tag.name);
        ibelog(this.model);
      }
    }
  };

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

  /** Script tag functions, for use in @ tags **/

  function insertArgument(text, arg0, arg1, arg2, arg3) {
    if (arg0) text = text.split('{0}').join(arg0);
    if (arg1) text = text.split('{1}').join(arg1);
    if (arg2) text = text.split('{2}').join(arg2);
    if (arg3) text = text.split('{3}').join(arg3);
    return text;
  }

  function useDefault(value, defaultValue) {
    if (value) return value;
    else return defaultValue;
  }


};

var IBEUtil = function () {

  var varSeparator = '.';

  return {
    lookup : function (name, model) {
      if (!model) {
        ibewarning('Error: Unable to lookup variable "' + name + '" in model, model is undefined.');
        return null;
      }

      var nnames;
      if (name.indexOf('[') < 0) {
        nnames = name.split(varSeparator);
      } else {
        nnames = splitList(splitList(name.split('['), ']'), varSeparator);
      }
      var names = new Array();
      // Remove "" and '' from keys, and remove empty elements and store new list in names.
      for (var i = 0; i < nnames.length; i++) if (nnames[i]) names[names.length] = removeFnutts(nnames[i]);

      if (names.length <= 1) {
        return model[name];
      } else {
        var newname = implode(removeListHead(names), varSeparator);
        var newmodel = model[names[0]];
        return this.lookup(newname, newmodel);
      }
    },

    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) {
      var tempClone = {};

      if (typeof(oldObject) == "object") {
        for (prop in oldObject) {
          // for array use private method getCloneOfArray
          if ((typeof(oldObject[prop]) == "object") && (oldObject[prop]).__isArray) {
            tempClone[prop] = this.getCloneOfArray(oldObject[prop]);
            // for object make recursive call to getCloneOfObject
          } else if (typeof(oldObject[prop]) == "object") {
            tempClone[prop] = this.getCloneOfObject(oldObject[prop]);
            // normal (non-object type) members
          } else {
            tempClone[prop] = oldObject[prop];
          }
        }
      }
      return tempClone;
    },

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

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

    populateIteratorContext : function (context, list, index, parentModel, parentContext) {
      // Populate context
      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.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; i < componentList.length; 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; i < componentList.length; 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; i < componentList.length; i++) {
        if (componentList[i].id == id) {
          return componentList[i];
        }
      }
      return null;
    },

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

  };

}();
/**
 *
 * @param sourceUrl
 * @param viewParameters format is object literal: {"paramName1":value1, "paramName2": value2 ...}
 */
function IBEWebComponentView(sourceUrl, viewParameters) {
  this.sourceUrl = sourceUrl;
  this.viewParameters = viewParameters;

  this.view = undefined;

  this.isLoading = false;

  this.componentCallbacks = new Array();

  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 u = this.sourceUrl;
      var time = new Date().getTime();
      var key = time / 13;
      u += (((u.indexOf('?') == -1) ? '?' : '&') + ('a' + key + '=' + time));
      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]);
          }
        }
        u += queryString.join("");
      }
      YAHOO.util.Connect.asyncRequest('GET', u, {
        success:this.viewFetchedFromUrl,
        failure:this.viewFetchedFromUrl,
        scope: this
      }, '');
    }
  };

  this.fetchViewFromDOM = function (elementId) {
    this.isLoading = false;
    var e = getObj(elementId);
    if (e) {
      this.view = e.innerHTML;
      this.view = this.view.replace(/%7B%%20/g, "{% ");
      this.view = this.view.replace(/%20%%7D/g, " %}");
      this.view = this.view.replace(/%7B/g, "{");
      this.view = this.view.replace(/%7D/g, "}");
      this.view = this.view.replace(/&amp;/g, "&");
    } else {
      ibeerror("IBEWebComponentView trying to fetch view from DOM object that is undefined.");
    }
    this.triggerAllCallbacks();
  };

  this.fetchAndRemoveViewFromDOM = function (elementId) {
    this.fetchViewFromDOM(elementId);
    var e = getObj(elementId);
    if (e) {
      e.innerHTML = '';
    }
  };

  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; i < this.componentCallbacks.length; i++) {
        var f = this.componentCallbacks[i];
        if (f) {
          f.render();
        }
      }
    }
    this.componentCallbacks = new Array();
  };

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

}

function IBEWebComponent(id, view, model, placeHolderId, renderCallback, callbackScope, mode) {

  this.id = id;
  this.view = view;
  this.model = model;
  this.placeHolderId = placeHolderId;
  this.mode = mode ? mode : 'insert'; // 'insert', 'append'

  // Callbacks
  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(renderCallback, 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.applyModel = function () {
    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) {
          var parser = new IBEParser();
          this.divContent = parser.parse(this.view.view, this.model, this.context, 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) {
          this.shouldRender = false;
          this.render();
        }
      }
    }
  };

  this.renderChildren = function () {
    this.childrenComponentsRendering = this.childrenComponents.length;
    for (var i = 0; i < this.childrenComponents.length; 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 () {
    var m = this.callbackList.length;
    for (var i = 0; i < m; 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!
    }

    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.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) {
      //setInnerHTML(this.placeHolderId, ''); // Clear loading animation? DO NOT USER setInnerHTML! DOM SUCKS! :)
      var placeHolder = getObj(this.placeHolderId);
      if (placeHolder) {
        if (mode == 'append') {
          placeHolder.appendChild(this.resultNode);
        } else {
          // Remove previous childs
          while (placeHolder.firstChild) placeHolder.removeChild(placeHolder.firstChild);
          placeHolder.appendChild(this.resultNode);
        }
      } else if (this.placeHolderId) {
        ibeerror('Error: Rendered component (id=' + this.id + ') but could not find placeholder (id=' + this.placeHolderId + ').');
      }

      this.hasBeenRendered = true;

      this.triggerAllCallbacks();

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

  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.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; i < this.childrenComponents.length; i++) {
      var c = this.childrenComponents[i];
      if (c.id == id) return c;
    }
    return null;
  };
}

function IBEWebComponentList(id, view, list, placeHolderId, separatorHtml, renderCallback, callbackScope, mode) {

  this.id = id;
  this.view = view;
  this.list = list;
  this.placeHolderId = placeHolderId;
  this.renderCallback = renderCallback;
  this.renderMoreCallback = undefined;
  this.callbackScope = callbackScope;
  this.separatorHtml = separatorHtml;
  this.mode = mode ? mode : 'insert';
  this.showLoadingWhileRendering = false;

  this.componentList = new Array();

  this.resultNode = undefined;

  this.isLoaded = false;
  this.divId = undefined;
  this.divNode = undefined;
  this.showMoreButtonNode = undefined;
  this.showMoreText = undefined;
  this.listNode = undefined;
  this.divCssClass = undefined;
  this.childrenDivCssClass = undefined;
  this.divContent = undefined;

  // Progressive rendering in passes
  this.rendersPerPass = 10;
  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.getComponentById = function (id) {
    return (this.componentList[this.getComponentIndexById(id)]);
  };

  this.getComponentIndexById = function (id) {
    for (var i = 0; i < this.componentList.length; 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; i < this.componentList.length; i++) {
      this.componentList[i] = this.componentList[i + 1];
    }
  };

  this.loadComponents = function () {
    if (this.isLoaded === false) {
      this.isLoaded = true;
      for (var i = 0; i < this.list.length; i++) {
        var e = this.list[i];
        var c = new IBEWebComponent(this.id + '_item_' + i, this.view, e, null, this.componentHasBeenRendered, this);
        c.storeContentInDivNode = false; // Store only strings, we dont 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 == undefined) {
      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>';
    this.divNode.appendChild(this.showMoreButtonNode);

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

    // 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(); }, 2);
    } else {
      // Just render them immediately.
      this.renderMoreComponents();
    }
  };

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

    var max = this.alreadyRendered + this.rendersPerPass;
    if (max > this.componentList.length) max = this.componentList.length;
    this.componentsRendering = max - this.alreadyRendered;

    for (var i = this.alreadyRendered; i < max; i++) {
      var c = this.componentList[i];
      c.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 isFirstRender = this.alreadyRendered == 0; // Only first if first pass
    var htmlArray = new Array();

    var isFirstElement = true;
    var elementsRendered = 0;
    for (var i = this.alreadyRendered; i < this.alreadyRendered + this.rendersPerPass && i < this.componentList.length; i++) {
      var c = this.componentList[i];
      if (isFirstElement) isFirstElement = false;
      else if (this.separatorHtml) htmlArray[htmlArray.length] = this.separatorHtml;
      htmlArray[htmlArray.length] = c.divContent;
      elementsRendered++;
    }

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

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

    this.listNode.appendChild(node);

    var previousAlreadyRendered = this.alreadyRendered;
    this.alreadyRendered = i;

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

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

  };

  this.remove = function () {
    for (var i = 0; i < this.componentList.length; 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 () {
    setVisible(this.showMoreElementId, true);
  };

  this.hideShowMoreButton = function () {
    setHidden(this.showMoreElementId, true);
  };

}

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