function CalendarDate (existingLi) {
  if (existingLi) {
    this.li = existingLi;
    this.fromHTML();
  } else this.li = document.createElement('li');
}
CalendarDate.prototype = {
  lang : {
    de : { monthNames:['Januar', 'Februar', 'M&auml;rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'] },
    en : { monthNames:['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] },
    get : function (key) {
      if (!this.l) {
        this.l = navigator.language;
        if (!this[this.l]) this.l = 'en';
      }
      return (this[this.l][key])?this[this.l][key]:key;
    }
  },

  // public readonly
  li : null,
  start : null,
  end : null,
  sourceId : -1,
  completeDay : false,
  title : '',
  location : '',
  description : '',
  url : '',
  status : '',
  repeatMode : 0,
  repeatNr : 0,
  repeatEnd : null,

  parseRrule : /* local */ function (rrule) {
    if (rrule == '') return;
    rrule = rrule.split(';');
    for (var k = 0; k < rrule.length; k++) {
      rrule[k] = rrule[k].toUpperCase().split('=');
      if (rrule[k].length == 1) continue;
      switch (rrule[k][0]) {
        case 'FREQ':
          if (rrule[k][1] == 'MONTHLY' && this.repeatMode == 0) this.repeatMode = 1;
          if (rrule[k][1] == 'MONTHLY' && this.repeatMode == -3) this.repeatMode = 3;
          if (rrule[k][1] == 'DAILY') this.repeatMode = 2;
          break;
        case 'UNTIL':
          if (rrule[k][1].length != 8) break;
          var y = parseInt(rrule[k][1].substring(0, rrule[k][1].length-4));
          var m = parseInt(rrule[k][1].substring(rrule[k][1].length-4, rrule[k][1].length-2));
          var d = parseInt(rrule[k][1].substring(rrule[k][1].length-2, rrule[k][1].length));
          this.repeatEnd = new Date(y, m-1, d, 0, 0, 0, 0);
          break;
        case 'INTERVAL':
          if (this.repeatNr == 0) this.repeatNr = parseInt(rrule[k][1]);
          else this.repeatNr = -parseInt(rrule[k][1]);
          break;
        case 'BYDAY':
          if (this.repeatMode == 0) this.repeatMode = -3; // will be set to +3 when FREQ is monthly.
          else if (this.repeatMode == 1) this.repeatMode = 3;
          if (parseInt(rrule[k][1]) < 0) {
            if (this.repeatNr == 0) this.repeatNr = -Number.MAX_VALUE; // just remember to make value negative for INTERVAL parsing
            else this.repeatNr = -this.repeatNr;
          }
          break;
      }
    }
    if (this.repeatMode < 0) this.repeatMode = 0; // invalid combination of rules
  },

  getMonthRepetitionI : /* private */ function (currentDateTime) {
    if (this.completeDay) var spl = new Array(this.start.getUTCFullYear(), this.start.getUTCMonth()+1, this.start.getUTCDate());
    else var spl = new Array(this.start.getFullYear(), this.start.getMonth()+1, this.start.getDate());
    var repI = spl[0]*32*12 + (spl[1]-1)*32 + spl[2];
    var currI = currentDateTime.getFullYear()*32*12 + (currentDateTime.getMonth()-1)*32 + currentDateTime.getDate();
    if (currI > repI)
      repI += Math.ceil((currI - repI + 0.0) / (this.repeatNr*32)) * this.repeatNr*32;
    return repI;
  },

  lastDayOfMonth : /* private */ function (Y, m) {
    if (m == 4 || m == 6 || m == 9 || m == 11) return 30;
    else if (m == 2) {
      if ((Y % 4) == 0 && ((Y % 100) != 0 || (Y % 400) == 0)) return 29;
      else return 28;
    } else return 31;
  },

  getNearestRepetition : /* local */ function (currentDateTime) {
    switch (this.repeatMode) {
      case 1: // every n month repeating event
        if (this.repeatNr <= 0) break;
        var repI = this.getMonthRepetitionI(currentDateTime);
        for (var i = 0; i < 100; i += this.repeatNr) { // if after adding 8 years, no valid date is found, it may never exist!
          var Y = Math.floor((repI >> 5) / 12);
          var m = (repI >> 5) % 12 + 1;
          var d = repI & 31;
          var checkdate = null;
          try {
            checkdate = new Date(Y, m-1, d, this.start.getHours(), this.start.getMinutes(), 0, 0);
          } catch (e) { checkdate = null; }
          if (checkdate && checkdate.getFullYear() == Y && checkdate.getMonth() == m-1 && checkdate.getDate() == d) {
            if (this.repeatEnd && checkdate > this.repeatEnd) break;
            if (checkdate >= currentDateTime) return checkdate;
          }
          repI += this.repeatNr*32;
        }
        break;
      case 2: // every n days repeating event
        if (this.repeatNr <= 0) break;
        var result = new Date(this.start);
        var th = result.getHours();
        var tm = result.getMinutes();
        result.setHours(12); // set to midday, to prevent day missmatch due to summer/winter time gap
        result.setMinutes(0);
        if (currentDateTime > this.start) {
          var dayDiff = Math.floor((currentDateTime - this.start) / (1000 * 3600 * 24));
          result.setDate(result.getDate() + (Math.ceil((dayDiff+0.1) / this.repeatNr)*this.repeatNr));
        }
        if (this.repeatEnd && result > this.repeatEnd) break;
        result.setHours(th);
        result.setMinutes(tm);
        return result;
      case 3:
        if (this.repeatNr == 0) break;
        var weekday = this.start.getDay();
        if (this.repeatNr > 0) {
          var ld = 1;
          var weekNr = Math.floor((this.start.getDate()-1) / 7);
        } else {
          var ld = this.lastDayOfMonth(this.start.getFullYear(), this.start.getMonth()+1);
          var weekNr = Math.floor((ld - this.start.getDate()) / 7);
        }
        var repI = this.getMonthRepetitionI(currentDateTime);
        for (var i = 0; i < 100; i++) {
          var Y = Math.floor((repI >> 5) / 12);
          var m = (repI >> 5) % 12 + 1;
          if (this.repeatNr < 0) ld = this.lastDayOfMonth(Y, m);
          var result = new Date(Y, m-1, ld, this.start.getHours(), this.start.getMinutes(), 0, 0);
          if (this.repeatNr > 0) result.setDate(result.getDate() + (((weekday + 7 - result.getDay()) % 7) + weekNr * 7));
          else result.setDate(result.getDate() - (weekNr * 7 + ((result.getDay() - weekday + 7) % 7)));
          if (this.repeatEnd && result > this.repeatEnd) break;
          if ((result >= currentDateTime) && (result.getMonth()+1 == m))
            return result;
          repI += 32 * Math.abs(this.repeatNr);
        }
        break;
    }
    result = new Date(this.start);
    return result;
  },

  fromHTML : /* private */ function () {
    if (this.start) throw 'Date already created!';
    this.completeDay = true;
    var span = this.li.getElementsByTagName('span');
    for (var j = 0; j < span.length; j++) {
      var className = ' '+span[j].className+' ';
      if (className.indexOf(' date ') != -1) { // parse date
        d = span[j].textContent.split('.');
        if (d.length != 3) d.unshift('-1');
        d[0] = Number.parseInt(d[0]);
        d[1] = Number.parseInt(d[1]);
        d[2] = Number.parseInt(d[2]);
        if (d[0] > 31) d.reverse();
        this.start = new Date(d[2], d[1]-1, d[0], 0, 0, 0, 0);
      } else if (className.indexOf(' title ') != -1) this.title = span[j].textContent;
      else if (className.indexOf(' location ') != -1) this.location = span[j].textContent;
      else if (className.indexOf(' description ') != -1) this.description = span[j].textContent;
    }
    if (this.li.getElementsByTagName('a').length > 0) this.url = this.li.getElementsByTagName('a')[0].href;
  },

  updateHTML : /* public */ function () {
    var m = this.start.getMinutes().toString();
    if (m.length == 1) m = '0'+m;
    var a_begin = '';
    var a_end = '';
    var hostAndProtocol = window.location.href.substring(0, window.location.href.indexOf(window.location.hostname))+window.location.hostname;
    if (this.url) {
      var target = '_blank';
      if (this.url.indexOf(hostAndProtocol) == 0) target = '_self';
      a_begin = '<A href="'+this.url+'" target="'+target+'">'+"\n";
      a_end = '<\/A>'+"\n";
    }
    this.li.innerHTML =
      a_begin
      +'<DIV class="split">'+"\n"
      +' <DIV class="left">'+"\n"
      +'  <SPAN class="date" title="'+this.start.toLocaleString()+((this.end)?'&nbsp;&nbsp;-&nbsp;&nbsp;'+this.end.toLocaleString():'')+'">'+this.start.getDate()+'. '+this.lang.get('monthNames')[this.start.getMonth()]+' '+this.start.getFullYear()+'<\/SPAN>'+"\n"
      +((this.completeDay)?'  <SPAN class="time">'+this.start.getHours()+':'+m+'<\/SPAN>'+"\n":'')
      +' <\/DIV>'+"\n"
      +' <DIV class="right">'+"\n"
      +'  <SPAN class="title">'+this.makeupText(this.title)+'<\/SPAN>'+"\n"
      +'  <SPAN class="location">'+this.makeupText(this.location)+'<\/SPAN>'+"\n"
      +'  <SPAN class="description">'+this.makeupText(this.description)+'<\/SPAN>'+"\n"
      +' <\/DIV>'+"\n"
      +'<\/DIV>'+"\n"
      +a_end;
  },

  makeupText : /* private */ function (text) {
    if (!text) text = '';
    text = text.replace(/&/g, '&amp;');
    text = text.replace(/</g, '&lt;');
    text = text.replace(/"/g, '&quot;');
    text = text.replace(/>/g, '&gt;');
    text = text.replace(/\n/g, ' <BR> ');
    text = text.split(' ');
    for (var i = 0; i < text.length; i++) {
      if (text[i].substring(0, 8) == 'https:\/\/' || text[i].substring(0, 7) == 'http:\/\/') {
        if (i > 0 && text[i-1].substring(text[i-1].length-1, text[i-1].length) == ':') {
          text[i-1] = '<A href="'+text[i]+'" class="protokoll" target="_blank">'+text[i-1].substring(0, text[i-1].length-1)+'<\/A>';
          text[i] = '';
        } else text[i] = '<A href="'+text[i]+'" target="_blank">'+text[i]+'<\/A>';
      }
    }
    return text.join('&#32;');
  }
};




function ICSCalendar (calendarBox) {
  var _this = this;
  this.calendarBox = calendarBox;
  this.dates = new Array();
  this.sources = new Array();
  // find or create ul
  if (!this.calendarBox) throw 'No calendar element given!';
  if (calendarBox.calendarObject) throw 'Calendar for given element already created!';
  calendarBox.calendarObject = this;
  if (this.calendarBox.getElementsByTagName('ul').length > 0) this.ul = this.calendarBox.getElementsByTagName('ul')[0];
  if (!this.ul) {
    this.ul = document.createElement('ul');
    this.calendarBox.appendChild(this.ul);
  } else {
    for (var i = 0; i < this.ul.getElementsByTagName('li').length; i++) {
      var ndate = new CalendarDate(this.ul.getElementsByTagName('li')[i]);
      ndate.updateHTML();
      this.dates.push(ndate);
    }
    this.ul.innerHTML = '';
  }
  this.ul.className = 'dates';
  this.filterBox = document.createElement('div');
  this.filterBox.style.display = 'none';
  this.filterLink = document.createElement('a');
  this.filterLink.className = 'button';
  this.filterLink.href = 'javascript: /* show filter */';
  this.filterLink.textContent = this.lang.get('filterlink');
  this.filterLink.cal = this;
  this.filterLink.addEventListener('click', function () {
    this.cal.filterBox.style.display = 'block';
    this.cal.filterLink.style.display = 'none';
  }, false);
  this.calendarBox.insertBefore(this.filterBox, this.ul);
  this.calendarBox.insertBefore(this.filterLink, this.ul);
  // interpret calendar settings
  var srcs = this.calendarBox.getElementsByTagName('source');
  function getAttributeOrEmpty(srcEl, attr) {
    var r = srcEl.getAttribute(attr);
    if (!r) r = '';
    return r;
  }
  for (var i = 0; i < srcs.length; i++) {
    var nsrc = {
      state : 0, // 0=never fetched  1=currently loading  2=successfully fetched,  3=error
      src : getAttributeOrEmpty(srcs[i], 'src'),
      generator : getAttributeOrEmpty(srcs[i], 'generator'),
      user : getAttributeOrEmpty(srcs[i], 'user'),
      password : getAttributeOrEmpty(srcs[i], 'password'),
      selected : { 
        promoters : getAttributeOrEmpty(srcs[i], 'filterPromoters').split(','),
        categories : getAttributeOrEmpty(srcs[i], 'filterCategories').split(',')
      },
      filterTypes : { // 0=no filter  1=whitelist  2=blacklist
        promoters : 1,
        categories : 1
      }
    };
    if (nsrc.src.indexOf('./') == 0) {
      var spl = window.location.href.split('?')[0].split('#')[0].split('/');
      if (spl[spl.length-1].lastIndexOf('.htm') >= spl[spl.length-1].length-5 || spl[spl.length-1] == '')
        spl.pop();
      nsrc.src = spl.join('/')+'/'+nsrc.src.substring(2);
    }
    for (var f in nsrc.selected) {
      if (nsrc.selected[f][0].indexOf('~') == 0) {
        nsrc.selected[f][0] = nsrc.selected[f][0].substring(1);
        nsrc.filterTypes[f] = 2;
      }
      while (nsrc.selected[f].length > 0 && nsrc.selected[f][0] == '') nsrc.selected[f].shift();
      for (var j = 0; j < nsrc.selected[f].length; j++) nsrc.selected[f][j] = parseInt(nsrc.selected[f][j]);
      if (nsrc.selected[f].length == 0) nsrc.filterTypes[f] = 0;
    }
    this.sources.push(nsrc);
    if (nsrc.generator == 'phpBB-calendar') { // autodetect nsrc.src.lastIndexOf('\/app.php\/calendar\/') == nsrc.src.length - 18
      if (!this.earlierDatesEl) {
        this.earlierDatesEl = document.createElement('a');
        this.earlierDatesEl.href = 'javascript:';
        this.earlierDatesEl.addEventListener('click', function () { _this.loadEarlierDates(); });
        this.earlierDatesEl.className = 'earlierDatesLink button';
        this.earlierDatesEl.textContent = this.lang.get('earlierDates');
        this.calendarBox.appendChild(this.earlierDatesEl);
      }
      nsrc.shortSrc = nsrc.src;
      this.getFilterInfo(this.sources.length-1);
      this.updateUrl(this.sources.length-1, false);
    } else { // create sync link for fixed url ics
      this.getSimpleInfo(this.sources.length-1);
    }
  }
  this.update();
}
ICSCalendar.prototype = {
  lang : {
    de : {
      filterError:'Fehler beim ermitteln der Filter-Möglichkeiten.', promoters:'Veranstalterinnen', categories:'Kategorien', update:'aktualisieren', all:'alle', whitelist:'nur die Folgenden...', blacklist:'alle außer...',
      synchint:'Synchronisieren Sie die folgende URL als <a href="https://de.wikipedia.org/wiki/ICalendar">iCalendar</a>, um die Termine in andere Anwendungen zu übernehmen.', filterlink:'Filtern und Synchronisieren',
      timeRange:'Zeitfenster', timeRangeSince:'Alle ab # Tagen vor heute', timeRangeDates:'Von # bis #', earlierDates:String.fromCharCode(8595)+' frühere Termine '+String.fromCharCode(8595)
    },
    en : {
      filterError:'Error getting filter information.', promoters:'Promoters', categories:'Categories', update:'update', all:'all', whitelist:'just the following...', blacklist:'all except of...',
      synchint:'Synchronize the following URL as a <a href="https://en.wikipedia.org/wiki/ICalendar">iCalendar</a>, to get the dates into other applications.', filterlink:'filter and synchronize',
      timeRange:'time range', timeRangeSince:'All since # days before now', timeRangeDates:'From # til #', earlierDates:String.fromCharCode(8595)+' earlier dates '+String.fromCharCode(8595)
    },
    get : function (key) {
      if (!this.l) {
        this.l = navigator.language;
        if (!this[this.l]) this.l = 'en';
      }
      return (this[this.l][key])?this[this.l][key]:key;
    }
  },

  // private
  calendarBox : null,
  ul : null,
  filterBox : null,
  filterLink : null,
  dates : null,
  sources : null,
  earlierDatesEl : null,

  loadEarlierDates : /* private */ function () {
    var _this = this;
    for (var sid = 0; sid < this.sources.length; sid++) {
      if (!this.sources[sid].earliestDate) continue;
      var to = this.sources[sid].earliestDate;
      var from = new Date(to);
      from.setDate(to.getDate() - parseInt(this.sources[sid].filterInfo.maxDays));
      this.sources[sid].earliestDate = from;
      var src = this.sources[sid].src.split('?');
      if (src.length == 1) src.push();
      src[1] = src[1].split('&');
      for (var i = src[1].length-1; i >= 0; i--)
        if (src[1][i].indexOf('from=') == 0 || src[1][i].indexOf('to=') == 0)
          src[1].splice(i, 1);
      src[1].push('from='+from.getFullYear()+'-'+(from.getMonth()+1)+'-'+from.getDate());
      src[1].push('to='+to.getFullYear()+'-'+(to.getMonth()+1)+'-'+to.getDate());
      src[1] = src[1].join('&');
      this.sources[sid].src = src.join('?');
      this.loadSource(sid, function (_sid) {
        _this.addDatesFromSrc(_sid);
        _this.sortAndPrintDates();
      });
    }
  },

  filterHtmlVisualization : /* private */ function (sid, key) {
    function radioChange () {
      if (this.elementsDeactivate)
        for (var i = 0; i < this.elementsDeactivate.length; i++)
          for (var j = 0; j < this.elementsDeactivate[i].getElementsByTagName('input').length; j++)
            this.elementsDeactivate[i].getElementsByTagName('input')[j].disabled = this.checked;
      if (this.elementsActivate)
        for (var i = 0; i < this.elementsActivate.length; i++)
          for (var j = 0; j < this.elementsActivate[i].getElementsByTagName('input').length; j++)
            this.elementsActivate[i].getElementsByTagName('input')[j].disabled = !this.checked;
    }
    var filter = this.sources[sid].filterInfo[key];
    var col = document.createElement('form');
    col.setAttribute('filterKey', key);
    var b = document.createElement('b');
    b.textContent = this.lang.get(key)+':';
    col.appendChild(b);
    var radioRow = document.createElement('div');
    radioRow.className = 'radioRow';
    col.appendChild(radioRow);
    if (key == 'timeRange') {
      var l = document.createElement('label');
      var radio = document.createElement('input');
      radio.checked = true;
      radio.setAttribute('name', 'timeRangeType');
      radio.setAttribute('type', 'radio');
      l.appendChild(radio);
      var spl = this.lang.get('timeRangeSince').split('#');
      spl.push('');
      l.appendChild(document.createTextNode(spl[0]));
      l.style.marginRight = '0px';
      radioRow.appendChild(l);
      var inp = document.createElement('input');
      inp.setAttribute('type', 'number');
      inp.setAttribute('name', 'days');
      var maxDays = parseInt(this.sources[sid].filterInfo.maxDays);
      inp.setAttribute('max', maxDays);
      inp.value = Math.ceil(maxDays/2);
      inp.defaultValue = inp.value;
      this.sources[sid].earliestDate = new Date();
      this.sources[sid].earliestDate.setDate(this.sources[sid].earliestDate.getDate() - parseInt(inp.value));
      inp.style.width = '30px';
      radioRow.appendChild(inp);
      l = document.createElement('label');
      l.textContent = spl[1];
      radioRow.appendChild(l);
      radioRow = document.createElement('div');
      radioRow.className = 'radioRow';
      l = document.createElement('label');
      radio = document.createElement('input');
      radio.checked = false;
      radio.setAttribute('name', 'timeRangeType');
      radio.setAttribute('type', 'radio');
      l.appendChild(radio);
      spl = this.lang.get('timeRangeDates').split('#');
      spl.push('');
      spl.push('');
      l.appendChild(document.createTextNode(spl[0]));
      l.style.marginRight = '0px';
      radioRow.appendChild(l);
      inp = document.createElement('input');
      inp.setAttribute('type', 'date');
      inp.setAttribute('name', 'dateFrom');
      radioRow.appendChild(inp);
      l = document.createElement('label');
      l.textContent = spl[1];
      l.style.marginRight = '0px';
      radioRow.appendChild(l);
      inp = document.createElement('input');
      inp.setAttribute('type', 'date');
      inp.setAttribute('name', 'dateTo');
      radioRow.appendChild(inp);
      l = document.createElement('label');
      l.textContent = spl[2];
      radioRow.appendChild(l);
      col.appendChild(radioRow);
    } else {
      var list = document.createElement('div');
      var l = document.createElement('label');
      var radio = document.createElement('input');
      radio.setAttribute('name', 'filtertype');
      radio.checked = this.sources[sid].filterTypes[key] == 0;
      radio.setAttribute('type', 'radio');
      radio.addEventListener('change', radioChange);
      radio.elementsDeactivate = [list];
      l.appendChild(radio);
      l.appendChild(document.createTextNode(this.lang.get('all')));
      radioRow.appendChild(l);
      l = document.createElement('label');
      radio = document.createElement('input');
      radio.checked = this.sources[sid].filterTypes[key] == 1;
      radio.setAttribute('name', 'filtertype');
      radio.setAttribute('type', 'radio');
      radio.addEventListener('change', radioChange);
      radio.elementsActivate = [list];
      l.appendChild(radio);
      l.appendChild(document.createTextNode(this.lang.get('whitelist')));
      radioRow.appendChild(l);
      l = document.createElement('label');
      radio = document.createElement('input');
      radio.checked = this.sources[sid].filterTypes[key] == 2;
      radio.setAttribute('name', 'filtertype');
      radio.setAttribute('type', 'radio');
      radio.addEventListener('change', radioChange);
      radio.elementsActivate = [list];
      l.appendChild(radio);
      l.appendChild(document.createTextNode(this.lang.get('blacklist')));
      radioRow.appendChild(l);
      col.appendChild(list);
      list.className = 'filterList';
      for (var i = 0; i < filter.length; i++) {
        var label = document.createElement('label');
        var cb = document.createElement('input');
        cb.checked = this.sources[sid].selected[key].indexOf(filter[i].id) >= 0;
        cb.disabled = true;
        cb.type = 'checkbox';
        cb.setAttribute('filterId', filter[i].id);
        label.appendChild(cb);
        label.appendChild(document.createTextNode(filter[i].title));
        list.appendChild(label);
      }
    }
    return col;
  },

  rmHTTP : /* private */ function (url) {
    if (url.toLowerCase().indexOf('http:\/\/') == 0) return url.substr(7);
    if (url.toLowerCase().indexOf('https:\/\/') == 0) return url.substr(8);
    return url;
  },

  updateUrl : /* private */ function (sid, updateCalendar) {
    var url = this.sources[sid].shortSrc+'?action=ics';
    if (this.sources[sid].user != '') url += '&user='+encodeURIComponent(this.sources[sid].user)+'&password='+encodeURIComponent(this.sources[sid].password);
    if (this.sources[sid].settingsEl) {
      var cols = this.sources[sid].settingsEl.getElementsByTagName('form');
      for (var i = 0; i < cols.length; i++) {
        var filterKey = cols[i].getAttribute('filterKey');
        if (filterKey == '') continue;
        if (filterKey == 'timeRange') {
          if (cols[i].getElementsByTagName('input')[0].checked) {
            this.sources[sid].earliestDate = new Date();
            this.sources[sid].earliestDate.setDate(this.sources[sid].earliestDate.getDate() - parseInt(cols[i].days.value));
            if (cols[i].days.defaultValue != cols[i].days.value)
              url += '&from=D'+(-parseInt(cols[i].days.value));
          } else {
            this.sources[sid].earliestDate = null;
            if (cols[i].dateFrom.value != '') {
              url += '&from='+cols[i].dateFrom.value;
              this.sources[sid].earliestDate = cols[i].dateFrom.valueAsDate;
            }
            if (!this.sources[sid].earliestDate) {
              this.sources[sid].earliestDate = new Date();
              this.sources[sid].earliestDate.setDate(this.sources[sid].earliestDate.getDate() - parseInt(cols[i].days.defaultValue));
            }
            var dTo = null;
            if (cols[i].dateTo.value != '') {
              dTo = cols[i].dateTo.valueAsDate;
              url += '&to='+cols[i].dateTo.value;
            }
            if (!dTo) dTo = new Date();
            var diff = (dTo - this.sources[sid].earliestDate) / (1000 * 60 * 60 * 24);
            if (diff > parseInt(cols[i].days.max)) {
              this.sources[sid].earliestDate = dTo;
              this.sources[sid].earliestDate.setDate(this.sources[sid].earliestDate.getDate() - parseInt(cols[i].days.max));
            }
          }
        } else {
          if (cols[i].getElementsByTagName('input')[0].checked) this.sources[sid].filterTypes[filterKey] = 0;
          if (cols[i].getElementsByTagName('input')[1].checked) this.sources[sid].filterTypes[filterKey] = 1;
          if (cols[i].getElementsByTagName('input')[2].checked) this.sources[sid].filterTypes[filterKey] = 2;
          var filterList = null;
          for (var j = 0; j < cols[i].getElementsByTagName('div').length; j++)
            if ((' '+cols[i].getElementsByTagName('div')[j].className+' ').indexOf(' filterList ') != -1) {
              filterList = cols[i].getElementsByTagName('div')[j].getElementsByTagName('input');
              break;
            }
          this.sources[sid].selected[filterKey] = new Array();
          if (filterList)
            for (var j = 0; j < filterList.length; j++)
              if (filterList[j].checked)
                this.sources[sid].selected[filterKey].push(filterList[j].getAttribute('filterId'));
        }
      }
    }
    for (var f in this.sources[sid].selected) {
      if (this.sources[sid].filterTypes[f] == 0 || this.sources[sid].selected[f].length == 0) continue;
      url += '&'+f+'=';
      if (this.sources[sid].filterTypes[f] == 2) url += '~,';
      url += this.sources[sid].selected[f].join(',');
    }
    this.sources[sid].syncUrl.textContent = url;
    this.sources[sid].syncUrl.href = 'webcal://'+this.rmHTTP(url);
    this.sources[sid].src = url;
    if (updateCalendar) this.update();
  },

  getSimpleInfo : /* private */ function (sid) { // creates a settings-block for simple ics-file sources
    this.sources[sid].settingsEl = document.createElement('div');
    this.filterBox.appendChild(this.sources[sid].settingsEl);
    var syncUrl = document.createElement('a');
    syncUrl.href = this.sources[sid].src;
    this.sources[sid].settingsEl.appendChild(syncUrl);
    var absUrl = syncUrl.href; // this way, an absoute path is guaranteed
    syncUrl.textContent = absUrl;
    absUrl = absUrl.split('\/\/');
    if (absUrl.length > 1) absUrl[0] = 'webcal:';
    syncUrl.href = absUrl.join('\/\/');
  },

  getFilterInfo : /* private */ function (sid) { // creates a settings-block for phpBB-calendar based sources
    var _this = this;
    this.sources[sid].settingsEl = document.createElement('div');
    this.filterBox.appendChild(this.sources[sid].settingsEl);
    var syncUrl = document.createElement('a');
    _this.sources[sid].syncUrl = syncUrl;
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open('GET', this.sources[sid].shortSrc+'?action=filterinfo', true);
    xmlHttp.onreadystatechange = function () {
      if (xmlHttp.readyState != 4) return;
      try {
        _this.sources[sid].filterInfo = JSON.parse(xmlHttp.responseText);
      } catch (e) {
        _this.sources[sid].settingsEl.innerHTML = _this.lang.get('filterError');
        return;
      }
      if (_this.sources[sid].filterInfo.promoters) _this.sources[sid].settingsEl.appendChild(_this.filterHtmlVisualization(sid, 'promoters'));
      if (_this.sources[sid].filterInfo.categories) _this.sources[sid].settingsEl.appendChild(_this.filterHtmlVisualization(sid, 'categories'));
      _this.sources[sid].settingsEl.appendChild(_this.filterHtmlVisualization(sid, 'timeRange'));
      var syncBox = document.createElement('div');
      var updateBut = document.createElement('a');
      updateBut.href = 'javascript: /* update */';
      updateBut.className = 'button';
      updateBut.textContent = _this.lang.get('update')+' -> ';
      updateBut.cal = _this;
      updateBut.sid = sid;
      updateBut.addEventListener('click', function () { this.cal.updateUrl(this.sid, true); }, false);
      var syncHint = document.createElement('p');
      syncHint.innerHTML = _this.lang.get('synchint');
      syncBox.appendChild(syncHint);
      syncBox.appendChild(updateBut);
      syncBox.appendChild(syncUrl);
      _this.sources[sid].settingsEl.appendChild(syncBox);
    };
    xmlHttp.onerror = function () {
      _this.sources[sid].settingsEl.innerHTML = _this.lang.get('filterError');
    };
    try {
      xmlHttp.send(null);
    } catch (e) {
      this.sources[sid].settingsEl.innerHTML = this.lang.get('filterError');
    }
  },

  sortAndPrintDates : /* private */ function () {
    this.ul.innerHTML = '';
    this.dates.sort(function (d1, d2) { return d2.start - d1.start; });
    var now = new Date();
    var reachedNow = false;
    for (var i = 0; i < this.dates.length; i++) {
      // gray out passed dates
      this.dates[i].li.className = '';
      if (this.dates[i].start < now) {
        if (this.dates[i].end && this.dates[i].end > now) this.dates[i].li.className += 'current';
        else {
          this.dates[i].li.className += 'passed';
          if (!reachedNow && i > 0) this.dates[i].li.className += ' nowLine';
          reachedNow = true;
        }
      }
      if (this.dates[i].status == 'cancelled') this.dates[i].li.className += ' cancelled';
      this.ul.appendChild(this.dates[i].li);
    }
  },

  icsToDate : /* private */ function (cal, eventID, endDate) {
    if (!endDate || !cal.VEVENT[eventID].DTEND) var d = cal.VEVENT[eventID].DTSTART.split('T');
    else var d = cal.VEVENT[eventID].DTEND.split('T');
    var a = new Array (0, 0, 0, 0, 0, 0, 0);
    a[0] = d[0].substring(0, d[0].length-4);
    a[1] = parseInt(d[0].substring(d[0].length-4, d[0].length-2))-1;
    a[2] = parseInt(d[0].substring(d[0].length-2, d[0].length));
    if (d.length > 1) {
      a[3] = parseInt(d[1].substring(0, 2));
      a[4] = parseInt(d[1].substring(2, 4));
      a[5] = parseInt(d[1].substring(4, 6));
      var date = new Date(Date.UTC(a[0], a[1], a[2], a[3], a[4], a[5], a[6]));
    } else var date = new Date(a[0], a[1], a[2], a[3], a[4], a[5], a[6]); // Events with complete day begin in the local timezone
    if (cal.VEVENT[eventID].DTSTART_TZID && cal.VTIMEZONE)
      for (var k = 0; k < cal.VTIMEZONE.length; k++)
        if (cal.VTIMEZONE[k].TZID == cal.VEVENT[eventID].DTSTART_TZID) {
          var sign = parseInt(cal.VTIMEZONE[k].STANDARD[0].TZOFFSETFROM.substring(0, 1)+'1');
          var dh = parseInt(cal.VTIMEZONE[k].STANDARD[0].TZOFFSETFROM.substring(1, 3));
          var dm = parseInt(cal.VTIMEZONE[k].STANDARD[0].TZOFFSETFROM.substring(3, 5));
          date.setUTCHours(date.getUTCHours() - dh * sign);
          date.setUTCMinutes(date.getUTCMinutes() - dm * sign);
          break;
        }
    return date;
  },

  interpretICS : /* private */ function (ics) {
    ics = ics.split("\r").join('').split("\n");
    var i = 0;
    function readBlock () {
      var o = new Object();
      var l = ''; // last variable name in o
      while (i < ics.length) {
        if (ics[i].charAt(0) == ' ' && l != '') {
          o[l] += ics[i].substring(1, ics[i].length).split("\\n").join("\n").split("\\,").join(",").split("\\;").join(";");
          i++;
          continue;
        }
        var s = ics[i].split(':');
        i++;
        if (s[0] == 'END') return o;
        else if (s[0] == 'BEGIN') {
          if (!o[s[1]]) o[s[1]] = new Array();
          o[s[1]].push(readBlock());
        } else {
          l = s[0].split(';');
          for (var j = 1; j < l.length; j++) {
            l[j] = l[j].split('=');
            if (l[j].length == 2) o[l[0]+'_'+l[j][0]] = l[j][1];
          }
          l = l[0];
          s.shift();
          o[l] = s.join(':').split("\\n").join("\n").split("\\,").join(",").split("\\;").join(";");
        }
      }
      return o;
    }
    return readBlock();
  },

  loadSource : /* private */ function (sid, callback) {
    var _this = this;
    this.sources[sid].ics = null;
    // compose src
    var currentSrc = this.sources[sid].src;
    currentSrc += ((this.sources[sid].src.indexOf('?') >= 0)?'&':'?')+'nocache='+Math.trunc(Math.random()*10000);
    // get
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open('GET', currentSrc, true);
    xmlHttp.onreadystatechange = function () {
      if (xmlHttp.readyState != 4) return;
      _this.sources[sid].ics = _this.interpretICS(xmlHttp.responseText);
      _this.sources[sid].state = 2;
      if (callback) callback(sid);
    };
    xmlHttp.onerror = function () {
      _this.sources[sid].state = 3;
      if (callback) callback(sid);
    };
    try {
      xmlHttp.send(null);
    } catch (e) {
      _this.sources[sid].state = 3;
      if (callback) callback(sid);
    }
  },

  addDatesFromSrc : /* private */ function (sid) {
    if (!this.sources[sid].ics || !this.sources[sid].ics.VCALENDAR) return;
    for (var i = 0; i < this.sources[sid].ics.VCALENDAR.length; i++)
      if (this.sources[sid].ics.VCALENDAR[i].VEVENT)
        for (var j = 0; j < this.sources[sid].ics.VCALENDAR[i].VEVENT.length; j++) {
          if (!this.sources[sid].ics.VCALENDAR[i].VEVENT[j].DTSTART) continue;
          var date = new CalendarDate();
          if (this.sources[sid].ics.VCALENDAR[i].VEVENT[j].RRULE) date.parseRrule(this.sources[sid].ics.VCALENDAR[i].VEVENT[j].RRULE);
          date.sourceId = sid;
          date.start = this.icsToDate(this.sources[sid].ics.VCALENDAR[i], j, false);
          date.end = this.icsToDate(this.sources[sid].ics.VCALENDAR[i], j, true);
          date.completeDay = this.sources[sid].ics.VCALENDAR[i].VEVENT[j].DTSTART.indexOf('T') >= 0;
          date.title = this.sources[sid].ics.VCALENDAR[i].VEVENT[j].SUMMARY;
          date.location = this.sources[sid].ics.VCALENDAR[i].VEVENT[j].LOCATION;
          date.description = this.sources[sid].ics.VCALENDAR[i].VEVENT[j].DESCRIPTION;
          date.url = this.sources[sid].ics.VCALENDAR[i].VEVENT[j].URL;
          date.status = this.sources[sid].ics.VCALENDAR[i].VEVENT[j].STATUS;
          if (!date.status) date.status = '';
          else date.status = date.status.toLowerCase();
          date.updateHTML();
          this.dates.push(date);
          if (date.repeatMode > 0) {
            var duration = date.end.getTime() - date.start.getTime();
            var prevRep = date.start;
            var nextRep = null;
            var nDate = null;
            do {
              nextRep = new Date(prevRep.getFullYear(), prevRep.getMonth(), prevRep.getDate(), 23, 59, 1, 0);
              nextRep = date.getNearestRepetition(nextRep);
              if (nextRep <= prevRep) break;
              prevRep = nextRep;
              nDate = new CalendarDate();
              nDate.sourceId = date.sourceId;
              nDate.start = nextRep;
              nDate.end = new Date(nextRep);
              nDate.end.setMilliseconds(nDate.end.getMilliseconds() + duration);
              nDate.completeDay = date.completeDay;
              nDate.title = date.title;
              nDate.location = date.location;
              nDate.description = date.description;
              nDate.url = date.url;
              nDate.status = date.status;
              nDate.updateHTML();
              this.dates.push(nDate);
            } while (nextRep <= new Date());
          }
        }
  },

  update : /* public */ function () {
    var _this = this;
    for (var i = 0; i < this.sources.length; i++)
      if (this.sources[i].state == 1) return; // a load request is already in work
      else this.sources[i].state = 1; // mark all sources as loading
    function sourcesLoad () {
      // wait until all sources are loaded
      for (var i = 0; i < _this.sources.length; i++)
        if (_this.sources[i].state == 1) return;
      // remove all existing dates, that belong to a successfully (re)loaded source
      for (var i = _this.dates.length-1; i >= 0; i--)
        if (_this.dates[i].sourceId >= 0 && _this.dates[i].sourceId < _this.sources.length && _this.sources[_this.dates[i].sourceId].state == 2) 
          _this.dates.splice(i, 1);
      // import dates from sources
      for (var i = 0; i < _this.sources.length; i++)
        if (_this.sources[i].state == 2) _this.addDatesFromSrc(i);
        else {
          // TODO: Generate error message for load error
        }
      // update HTML
      _this.sortAndPrintDates();
    }
    for (var i = 0; i < this.sources.length; i++) this.loadSource(i, sourcesLoad);
    sourcesLoad(); // if only dates from HTML exist
  }
};

// auto-detect calendar in HTML
(function () {
  var div = document.getElementsByTagName('div');
  if (div.length > 0 && (' '+div[div.length-1].className+' ').indexOf(' calendar ') != -1) new ICSCalendar(div[div.length-1]);
})();
