/*
Copyright (c) 2008, KIMI. All rights reserved.
Code licensed under the BSD License:
http://ocal.jp/javascripts/bsd-license.txt
*/

/**
 * @fileOverview iCalendarパーサーです。クロスドメイン用にJSONPでもOKです。
 * TODO: VALARM
 */

/**
 * @class iCalendar
 */
var iCalendar = function(){};

/**
 * JSONPの結果を window[iCalendar.GLOBAL]に設定
 * @type {String}
 */
iCalendar.GLOBAL='ocal_jp';

/**
 * <strong>prototype.js が必要</strong>
 * @param {String} url iCalendar URL
 * @param {Boolean} async 同期/非同期
 * @static
 */
iCalendar.load=function(url, async){
  var ajax = new Ajax.Request(
    url,
    {
      onSuccess: function(res){
        var vcalendar = iCalendar.parse(res.responseText);
        if(iCalendar.onSuccess)iCalendar.onSuccess(vcalendar);
      },
      onFailure: function(){
        iCalendar.onFailure();
      },
      asynchronous: (async==null || async)
    }
  );
  if(!async){
    var vcalendar = iCalendar.parse(ajax.transport.responseText);
    if(iCalendar.onSuccess)iCalendar.onSuccess(vcalendar);
  }
};

/**
 * JSONP用のメソッド。こんなデータを期待しています。
 * <pre>
iCalendar.callback({
  "vcalendar":{
    "x_wr_calname": "GEO",
    "calscale": "GREGORIAN",
    "version": 2.0,
    "vevent": [{
      "sequence": 1,
      "uid": "X@uid.ocal.jp",
      "description": "",
      "last_modified": "2008-02-25T16:28:51Z",
      "class": "PUBLIC",
      "summary": "summary",
      "dtend": "20080128",
      "dtstamp": "2008-02-27T16:00:12Z",
      "location": "",
      "dtstart": "20080119",
      "created": "2008-01-23T10:00:41Z"
    }, {
      ...
    }],
    "method": "PUBLISH",
    "x_wr_caldesc": "GEO",
    "vtimezone": {
      "tzid": "Asia/Tokyo",
      "standard": {
        "tzoffsetto": "+0900",
        "tzname": "JST",
        "dtstart": "19700101T000000",
        "x_lic_location": "Asia/Tokyo",
        "tzoffsetfrom": "+0900"
      }
    },
    "x_wr_timezone": "Asia/Tokyo"
  }
});
 * </pre>
 * 結果はwindow[iCalendar.GLOBAL]からVcalendarオブジェクトとして取得できる。
 * @param {Object} json JSON形式のiCalendar
 * @static
 */
iCalendar.callback=function(json){
  var vcalendar = new Vcalendar();
  var recurrences = {'count':0};
  for(var key in json.vcalendar){
    if(key == 'vevent'){
      for(var i=0, z=json.vcalendar.vevent.length; i<z; i++){
        var vevent = new Vevent();
        var evt=json.vcalendar.vevent[i];
        for(var key in evt)
          vevent.put(key, evt[key]);
        if(vevent.recurrence_id){
          recurrences.count++;
          if(!recurrences[vevent.uid])
            recurrences[vevent.uid] = new Array();
          recurrences[vevent.uid].push(vevent);
        }else{
          vcalendar.addVevent(vevent);
        }
      }
      continue;
    }
    vcalendar.put(key, json.vcalendar[key]);
  }
  if(recurrences.count!=0){
    for(var i=0, z=vcalendar.vevents.length; i<z; i++){
      var rec_vevents = recurrences[vcalendar.vevents[i].uid];
      if(rec_vevents){
        vcalendar.vevents[i].recurrences = new Object();
        for(var j=0, zz=rec_vevents.length; j<zz; j++){
          var ymd = rec_vevents[j].recurrence_id.toYYYYMMDD();
          vcalendar.vevents[i].recurrences[ymd] = rec_vevents[j];
        }
      }
    }
  }
  window[iCalendar.GLOBAL] = vcalendar;
};

/**
 * @static
 * @param {String} s text/icalendar
 * @return {Vcalendar} 解析した結果
 */
iCalendar.parse=function(s){
  var lines=s.split(/\r?\n/);
  var len = lines.length;
  if(lines[0] != 'BEGIN:VCALENDAR')return;

  for(var i=0; i<len; i++){
    if(lines[i].charAt(0) == ' '){
      lines[i-1] += lines[i].substring(1);
      lines[i] = '';
    }
  }

  var vcalendar = new Vcalendar();
  var recurrences = {'count':0};
  for(var i=1; i<len && lines[i] != 'END:VCALENDAR'; i++){
    if(lines[i] == '')continue;
    if(lines[i] == 'BEGIN:VEVENT'){
      var vevent = new Vevent();
      for(i++; i<len && lines[i] != 'END:VEVENT'; i++){
        if(lines[i].match(/^([^:]+):(.*)$/)){
          var key = RegExp.$1, value = RegExp.$2, params = null;
          if(key.indexOf(';') != -1){
            params = key.split(';');
            key = params.shift();
          }
          vevent.put(key, value, params);
        }
      }
      if(vevent.recurrence_id){
        recurrences.count++;
        if(!recurrences[vevent.uid])
          recurrences[vevent.uid] = new Array();
        recurrences[vevent.uid].push(vevent);
      }else{
        vcalendar.addVevent(vevent);
      }
      continue;
    }
    if(lines[i].match(/^([^:]+):(.*)$/)){
      var key = RegExp.$1, value = RegExp.$2;
      vcalendar.put(key, value);
    }
  }

  if(recurrences.count==0)return vcalendar;
  for(var i=0, z=vcalendar.vevents.length; i<z; i++){
    var rec_vevents = recurrences[vcalendar.vevents[i].uid];
    if(rec_vevents){
      vcalendar.vevents[i].recurrences = new Object();
      for(var j=0, zz=rec_vevents.length; j<zz; j++){
        var ymd = rec_vevents[j].recurrence_id.toYYYYMMDD();
        vcalendar.vevents[i].recurrences[ymd] = rec_vevents[j];
      }
    }
  }

  return vcalendar;
};

/**
 * @class Vcalendar
 */
var Vcalendar = function(){
  /**
   * VEVENT の配列
   * @type {Array}
   */
  this.vevents=new Array();
  /**
   * UID から VEVENT を取得するためのINDEX。直接触ることはない。
   * @type {Object}
   */
  this.uididx=new Object();
};

Vcalendar.prototype={
  /**
   * 予定を探す
   * @param {Date} from 検索開始日
   * @param {Date} to 検索終了日
   * @return {Object} {"sdate": 予定の開始日, "vevent": VEVENT}
   */
  find:function(from, to){
    var ret = new Array();
    for(var i=0, z=this.vevents.length; i<z; i++){
      var dates = this.vevents[i].find(from, to);
      if(!dates)continue;
      for(var j=0, zz=dates.length; j<zz; j++){
        if(this.vevents[i].hasRecurrence()){
          var vevent = this.vevents[i].recurrences[dates[j].toYYYYMMDD()];
          if(vevent){
            var recurrencedates = vevent.find(from, to);
            if(!recurrencedates)continue;
            for(var k=0, zzz=recurrencedates.length; k<zzz; k++){
              var o = new Object();
              o.sdate = recurrencedates[k];
              o.vevent = vevent;
              ret.push(o);
            }
            continue;
          }
        }
        var o = new Object();
        o.sdate = dates[j];
        o.vevent = this.vevents[i];
        ret.push(o);
      }
    }
    return ret;
  },
  /**
   * DTSTART, DTEND, UIDが存在しないと追加しない。
   * UIDがユニークじゃないとおかしなことになる。チェックしろって？
   * @param {Vevent} vevent VEVENT
   */
  addVevent:function(vevent){
    if(vevent.dtstart && vevent.dtend && vevent.uid){
      this.vevents.push(vevent);
      this.uididx[vevent.uid] = this.vevents.length-1;
    }
  },
  /**
   * UIDを指定してVEVENTを取得
   * @param {String} uid UID
   * @return {Vevent} VEVENT
   */
  getVevent:function(uid){
    return this.vevents[this.uididx[uid]];
  },
  /**
   * UIDを指定してVEVENTを削除
   * @param {String} uid UID
   */
  removeVevent:function(uid){
    var evt = new Array(), idx = new Object();
    if(this.uididx[uid]){
      for(var i=0, z=this.vevents.length; i<z; i++){
        if(this.vevents[i].uid != uid){
          evt.push(this.vevents[i]);
          idx[uid] = evt.length-1;
        }
      }
      this.vevents = evt;
      this.uididx = idx;
    }
  },
  /**
   * @param {String} key VEVENT以外の要素
   * @param {String} value VEVENT以外の要素の値
   */
  put:function(key, value){
    key=key.toLowerCase().replace(/-/g, '_');
    if(key == 'uididx' || key == 'vevents')return;
    this[key]=value;
  }
};

/**
 * @class Vevent
 */
var Vevent=function(){
  /**
   * 例えば DTSTART;VALUE=DATE:20080101 の場合
   * params.dtstart = 'VALUE=DATE'
   * となる
   * @type {Object}
   */
  this.params=new Object();
  /**
   * 例えば RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20100101T210000Z の場合
   * <pre>
rrule.freq = 'WEEKLY'
rrule.byday = ['MO', 'WE', 'FR']
rrule.until = {Date}
   * </pre>
   * となる
   * @type {Object}
   */
  this.rrule=new Object();
};

/**
 * @static
 * @param {String} s VEVENTの要素
 * @return {Boolean} Date型にすべきか？
 */
Vevent.isDateField=function(s){
  return(s=='dtstart'||s=='dtend'||s=='exdate'||s=='created'||s=='dtstamp'||s=='recurrence_id');
};

/**
 * RFC2445 4.3.4, 4.3.5, 4.3.12 の表現
 * TODO: 抜本的に見直しを..
 * 'Z'がなければVTIMEZONEをみないといけないはずだ。
 * @static
 * @param {String} s Dateの文字列表現
 * @param {Integer} timezoneOffset これはまずい
 * @return {Date} Date型に変換したもの
 */
Vevent.parseDate=function(s, timezoneOffset){
  if(s.match(/^(\d{4})-?(\d{2})-?(\d{2})$/)){
    return new Date(RegExp.$1, RegExp.$2-1, RegExp.$3, 0, 0, 0);
  }
  if(s.match(/^(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):?(\d{2}):?(\d{2})Z?$/)){
    var d = new Date(RegExp.$1, RegExp.$2-1, RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$6);
    if(s.charAt(s.length-1) == 'Z'){
      var ms=(timezoneOffset || d.getTimezoneOffset())*60000;
      d.setTime(d.getTime() - ms);
    }
    return d;
  }
};

/**
 * @static
 * @param {Number} year 年
 * @param {Number} month 月 0-11
 * @param {String} sDay BYDAYの文字列を想定
 * @return {Date} 作成したDate
 */
Vevent.createDate=function(year, month, sDay){
  if(sDay.match(/^(-?\d)(SU|MO|TU|WE|TH|FR|SA)$/)){
    var num = parseInt(RegExp.$1, 10);
    var day = Date.en2day(RegExp.$2);
    var ret;
    if(num>0){
      ret = new Date(year, month, 1);
      for(; day != ret.getDay(); ret.add(1));
      num--;
      if(num!=0) ret.add(num*7);
    }else if(num<0){
      month++;
      if(month==12){year++;month=0;}
      ret = new Date(year, month, 1);
      ret.add(-1);
      for(; day != ret.getDay(); ret.add(-1));
      num++;
      if(num!=0) ret.add(num*7);
    }else{
      return;
    }
    return ret;
  }
};

Vevent.prototype={
  /**
   * @return {Boolean} recurrencesを持っているか？
   */
  hasRecurrence:function(){
    return (this.recurrences != null);
  },
  /**
   * @return {Boolean} Date.isZeroHourZeroTime(this.dtstart, this.dtend) &&
   *                   this.dtstart.after(this.dtend);
   */
  isAllday:function(){
    return Date.isZeroHourZeroTime(this.dtstart, this.dtend) &&
           this.dtstart.before(this.dtend);
  },
  /**
   * @return {Number} 予定の日数
   */
  howlong:function(){
    var ret = Date.sub(this.dtstart, this.dtend, 1);
    return (this.isAllday())? ret-1: ret;
  },
  /**
   * @param {Date} _from 検索開始日
   * @param {Date} _to 検索終了日
   * @return {Array} 見つかった予定の開始日, 見つからない場合はNULL
   */
  find:function(_from, _to){
    var from = new Date(_from), to = new Date(_to);
    var protrude = this.howlong();
    if(protrude)from.add(-protrude);

    if(!this.rrule.freq){
      if(this.dtstart.between(from, to))
        return [this.dtstart];
      else
        return;
    }

    if(this.rrule.until){
      if(this.rrule.until.before(from)) return;
      if(to.after(this.rrule.until))    to=this.rrule.until;
    }
    if(to.before(this.dtstart))   return;
    if(from.before(this.dtstart)) from=this.dtstart;

    var count   =(this.rrule.count)   ?this.rrule.count   :0;
    var interval=(this.rrule.interval)?this.rrule.interval:0;
    if(interval) count *= interval;

    var ret = new Array();

    if(this.rrule.freq == 'DAILY'){
      for(var d=new Date(from); d.before(to); d.add(1)){
        if(interval>1 && Date.sub(this.dtstart, d, 1)%interval != 0)
          continue;
        if(count>1 && Date.sub(this.dtstart, d, 1) >= count)
          break;
        ret.push(new Date(d));
      }
    }else if(this.rrule.freq == 'WEEKLY'){
      var byday = (this.rrule.byday)?
        this.rrule.byday : [this.dtstart.getDay()];

      for(var d=new Date(from); d.before(to); d.add(1)){
        if(interval>1 && Date.sub(this.dtstart, d, 2)%interval != 0)
          continue;
        if(count>1 && Date.sub(this.dtstart, d, 2) >= count)
          break;

        var day = d.getEnDay();
        for(var i=0, z=byday.length; i<z; i++){
          if(day == byday[i]){
            ret.push(new Date(d));
            break;
          }
        }
      }
    }else if(this.rrule.freq == 'MONTHLY'){
      var buf=new Array();
      var end = to.getMonth() + (to.getFullYear()-from.getFullYear())*12 - from.getMonth();
      for(var m=from.getMonth(), y=from.getFullYear(), i=0; i<=end; i++){
        if(this.rrule.byday){
          for(var j=0, z=this.rrule.byday.length; j<z; j++)
            buf.push(Vevent.createDate(y, m, this.rrule.byday[j]))
        }else if(this.rrule.bymonthday){
          for(var j=0, z=this.rrule.bymonthday.length; j<z; j++){
            var d = new Date(y, m, 1);
            if(this.rrule.bymonthday[j]<0){
              d.nextMonth();
              d.add(this.rrule.bymonthday[j]);
            }else if(this.rrule.bymonthday[j] <= d.eom()){
              d.setDate(this.rrule.bymonthday[j]);
            }else{
              continue;
            }
            buf.push(d);
          }
        }else{
          buf.push(new Date(y, m, this.dtstart.getDate()));
        }
        if(++m==12){y++; m=0;}
      }

      for(var i=0, z=buf.length; i<z; i++)
        if(buf[i].between(from, to))
          ret.push(buf[i]);
    }else if(this.rrule.freq == 'YEARLY'){
      var buf=new Array();
      for(var y=from.getFullYear(); y<=to.getFullYear(); y++){
        var m = (this.rrule.bymonth)?this.rrule.bymonth:[this.dtstart.getMonth()+1];
        for(var i=0, z=m.length; i<z; i++){
          if(this.rrule.byday){
            for(var j=0, zz=this.rrule.byday.length; j<zz; j++)
              buf.push(Vevent.createDate(y, m[i]-1, this.rrule.byday[j]));
          }else{
            buf.push(new Date(y, m[i]-1, this.dtstart.getDate()));
          }
        }
      }

      for(var i=0, z=buf.length; i<z; i++)
        if(buf[i].between(from, to))
          ret.push(buf[i]);
    }

    if(ret.length && this.exdate){
      for(var i=0, z=ret.length; i<z; i++){
        if(ret[i].toYYYYMMDD() == this.exdate.toYYYYMMDD()){
          ret.splice(i,1);
          break;
        }
      }
    }

    return (ret.length)?ret:null;
  },
  /**
   * RRULEについては分解してプロパティーに設定し、オリジナルは'__rrule'になる
   * @param {String} key VEVENTの要素
   * @param {String} value VEVENTの要素の値
   * @param {String} params VEVENTの要素のパラメータ
   */
  put:function(key, value, params){
    key=key.toLowerCase().replace(/-/g, '_');
    if(key == 'params')return;
    if(Vevent.isDateField(key)){
      value = Vevent.parseDate(value);
    }else if(key == 'rrule'){
      key = '__rrule';
      for(var a=value.split(';'), z=a.length, i=0; i<z; i++){ 
        var b=a[i].split('=');
        if(b[0] == 'FREQ'){
          this.rrule.freq = b[1];
        }else if(b[0] == 'UNTIL'){
          this.rrule.until = Vevent.parseDate(b[1]);
        }else if(b[0] == 'COUNT'){
          this.rrule.count = parseInt(b[1],10);
        }else if(b[0] == 'INTERVAL'){
          this.rrule.interval = parseInt(b[1],10);
        }else if(b[0] == 'BYDAY'){
          this.rrule.byday = b[1].split(',');
        }else if(b[0] == 'BYMONTHDAY'){
          this.rrule.bymonthday = b[1].split(',').to_i();
        }else if(b[0] == 'BYMONTH'){
          this.rrule.bymonth = b[1].split(',').to_i();
        }else if(b[0] == 'WKST'){
          this.rrule.wkst = b[1];
        }
      }
    }
    this[key]=value
    if(params){
      this.params[key] = params;
    }
  },
  /**
   * -1SU ≫ '最終日曜日', 3MO ≫ '第3月曜日'
   * @return {String} BYDAY を日本語にする
   */
  byday_to_s:function(){
    if(!this.rrule.byday) return;
    var z=this.rrule.byday.length;
    var ret=new Array(z);
    for(var i=0; i<z; i++){
      if(this.rrule.byday[i].match(/^(-?\d)(SU|MO|TU|WE|TH|FR|SA)$/)){
        var num = RegExp.$1, day = RegExp.$2;
        ret[i]=((num == '-1')? '最終': '第'+num) + Date.en2localday(day);
      }else if(this.rrule.byday[i].match(/^(SU|MO|TU|WE|TH|FR|SA)$/)){
        ret[i]=Date.en2localday(this.rrule.byday[i]);
      }
    }
    return ret.join(',')+'曜日';
  }
};

/*
var Vtimezone = function(){
};

Vtimezone.prototype={
  put:function(key, value){
    key=key.toLowerCase().replace(/-/g, '_');
    this[key]=value;
  }
};
*/

