// ☆☆☆ x┃ヮ┃x ＜ hoshi-mittsu.js
//        RSS headline generator ☆☆☆

function HoshiMittsu(options) {
	this.options = {
		loading_label: options.loading_label || "loading...",
		output: options.output,
		source: options.source,
		error_class: options.error_class,
		coming_class: options.coming_class,
		list_type: options.list_type || 'ul',
		limit: options.limit || 5,
		sort_on: options.sort_on
	};

	if (!this.options.source) throw "source must be specified!";
	if (!this.options.output) throw "output must be specified!";

	this.nowT = (new Date()).getTime();
	this.outArea = document.getElementById(this.options.output);
	this.outAreaOriginal = this.outArea.innerHTML;

	this.outArea.innerHTML = this.options.loading_label;
}

HoshiMittsu.for_all = function(list) {
	var len = list.length;
	var objs = [];
	for (var i = 0;i < len;i++) {
		var li = list[i];
		objs.push( (new HoshiMittsu(li)).start() );
	}

	return objs;
}

HoshiMittsu.for_all_onload = function(list) {
	new HoshiMittsu.OnloadHandler(function(){ HoshiMittsu.for_all(list) });
}

HoshiMittsu.prototype = {
	start: function () {
		new Ajax.Request(
			this.options.source, {
				method: 'get',
				onSuccess: this.onSourceLoadSuccess.bind(this),
				onFailure: this.outputError.bind(this),
				onException: function(r, e){throw e}
			}
		);

		return this;
	},

	onSourceLoadSuccess: function(req) {
		if (!req.responseXML) {
			this.outputError();
			return;
		}

		var items = req.responseXML.getElementsByTagName('item');
		var components = req.responseXML.getElementsByTagName('component');
		var calendarMap = this.readCalComponents(components);
		this.pool = new HoshiMittsu.ItemPool(items);

		if (this.options.sort_on == "start-date")
			this.pool.sortByCalendar(calendarMap);

		this.clearList();
		this.pool.enumerate( this.writeItem.bind(this), this.options.limit );
	},

	readCalComponents: function(clist) {
		if (!clist) return;

		evmap = {};
		var len = clist.length;
		for (var i = 0;i < len;i++) {
			var c = clist[i];
			var children = c.childNodes;
			var ev = this.readAEvent(children);
			if (ev) {
				evmap[ev.ref] = ev;
			}
		}

		return evmap;
	},

	readAEvent: function(elist) {
		if (!elist) return;
		var len = elist.length;
		for (var i = 0;i < len;i++) {
			var e = elist[i];
			if (e.tagName == "Vevent") {
				var rid = null;
				if (e.getAttributeNS)
					rid = e.getAttributeNS('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'nodeID');
				else
					rid = e.getAttribute('rdf:nodeID');

				if (rid) {
					var eobj = {ref: rid};
					this.readEventData(e.childNodes, eobj);
					return eobj;
				}
			}
		}

		return null;
	},

	readEventData: function(elist, eobj) {
		if (!elist) return;
		var len = elist.length;
		for (var i = 0;i < len;i++) {
			var e = elist[i];
			if (e.tagName == "dtstart") {
				if (e.firstChild) {
					eobj.dtstart = HoshiMittsu.parseTime(e.firstChild.nodeValue);
				}
			}
		}
	},

	clearList: function() {
		this.outArea.innerHTML = "";
		var ul = $Hcreate(this.options.list_type);
		this.outArea.appendChild(ul);

		this.outList = ul;
	},

	writeItem: function(item) {
		var make_link = !!item.link;
	
		var li = $Hcreate('li');
		var a = $Hcreate(make_link ? 'a' : 'span');
		var t = document.createTextNode(item.title);

		if (make_link) {
			a.href = item.link;
		}

		a.appendChild(t);
		li.appendChild(a);

		if (this.options.coming_class && item.sortKey && item.sortKey > this.nowT)
			li.setAttribute('class', this.options.coming_class);

		this.outList.appendChild(li);
	},

	outputError: function() {
		if (this.options.error_class)
			this.outArea.innerHTML = '<p class="'+this.options.error_class+'">読み込みに失敗しました</p>';
	}
}

// ---------------------------------------------

HoshiMittsu.ItemPool = function(src) {
	this.list = [];
	var len = src.length;
	for (var i = 0;i < len;i++) {
		this.list.push( new HoshiMittsu.FeedItem(src[i]) );
	}

	this.sorted = this.list;
}

HoshiMittsu.ItemPool.prototype = {
	enumerate: function(f, lim) {
		var list = this.sorted;
		var len = list.length;
		if (len > lim) len = lim;

		for (var i = 0;i < len;i++) {
			f(list[i]);
		}
	},

	sortByCalendar: function(cmap) {
		if (!cmap)
			return;

		var list = this.list;
		var len = list.length;
		for (var i = 0;i < len;i++) {
			var item = list[i];
			if (item.nodeID) {
				if (cmap[item.nodeID]) {
					var calEvent = cmap[item.nodeID];
					item.sortKey = calEvent.dtstart.getTime();
				}
			}
			
			if (!item.sortKey)
				item.sortKey = item.dateParsed ? item.dateParsed.getTime() : 0;
			
		}

		this.sorted.sort(function(a,b){ return b.sortKey - a.sortKey; });
	}
}

// ---------------------------------------------

HoshiMittsu.FeedItem = function(src) {
	var list = src.childNodes;
	var len = list.length;
	for (var i = 0;i < len;i++) {
		var ch = list[i];
		if (ch.tagName) {
			var t = (ch.localName || ch.tagName).toLowerCase();
			if (t == 'title') {
				this.title = ch.firstChild.nodeValue;
			} else if (t.indexOf('date') >= 0) {
				this.date = ch.firstChild.nodeValue;
				this.dateParsed = HoshiMittsu.parseTime(this.date);
			} else if (t == 'link') {
				this.link = ch.firstChild.nodeValue;
			} else if (t.indexOf('topic') >= 0) {
				var rid = null;
				if (ch.getAttributeNS)
					rid = ch.getAttributeNS('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'nodeID');
				else
					rid = ch.getAttribute('rdf:nodeID');

				this.nodeID = rid;
			}
		}
	}
}

HoshiMittsu.FeedItem.prototype = {
}

// ---------------------------------------------

HoshiMittsu.OnloadHandler = function(func) {
	this.func = func;
	this.checkFunc = this.check.bind(this);

	this.check();
}

HoshiMittsu.OnloadHandler.prototype.check = function() {
	if (document && document.body) {
		this.func();
		return;
	}

	setTimeout(this.checkFunc, 250);
}

function $Hcreate(t) {
	if (document.createElementNS)
		return document.createElementNS('http://www.w3.org/1999/xhtml', t);

	return document.createElement(t);
}

HoshiMittsu.RX_TIME = /([0-9]+)-([0-9]+)-([0-9]+)T([0-9]+):([0-9]+):([0-9]+)([-+0-9]+:[0-9]+)?/
HoshiMittsu.parseTime = function(s) {
	if (s.match(HoshiMittsu.RX_TIME)) {
		var btime = RegExp['$1'] +'/'+ RegExp['$2'] +'/'+ RegExp['$3'] +' '+ RegExp['$4'] +':'+ RegExp['$5'] +':'+ RegExp['$6'];
		var tz = '';
		if (RegExp['$7']) {
			tz = ' GMT'+ RegExp['$7'].replace(':','');
		}

		return new Date(btime + tz);
	}

	return null;
}

// minimized prototype.js

/*  (c) 2005-2009 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
	emptyFunction: function() { },
	ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
	K: function(x) {return x}
}

var $break = {};

var Hash = function(object) {
  if (object instanceof Hash) this.merge(object);
  else Object.extend(this, object || {});
};

var Class = {
  create: function() {
    return function() {
      this.initialize.apply(this, arguments);
    }
  }
}

Object.extend = function(destination, source) {
  for (var property in source) {
    destination[property] = source[property];
  }
  return destination;
}

Object.extend(Object, {
  clone: function(object) {
    return Object.extend({}, object);
  }
});

Object.extend(Hash, {
  toQueryString: function(obj) {
    var parts = [];
    parts.add = arguments.callee.addPair;

    this.prototype._each.call(obj, function(pair) {
      if (!pair.key) return;
      var value = pair.value;

      if (value && typeof value == 'object') {
        if (value.constructor == Array) value.each(function(value) {
          parts.add(pair.key, value);
        });
        return;
      }
      parts.add(pair.key, value);
    });

    return parts.join('&');
  }
});

Hash.toQueryString.addPair = function(key, value, prefix) {
  key = encodeURIComponent(key);
  if (value === undefined) this.push(key);
  else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
}

Object.extend(String.prototype, {
  stripScripts: function() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  },

  toQueryParams: function(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return {};

    return match[1].split(separator || '&').inject({}, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (hash[key].constructor != Array) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  },

  include: function(pattern) {
    return this.indexOf(pattern) > -1;
  },

  strip: function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  }
});

var Enumerable = {
  each: function(iterator) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator(value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },

  toArray: function() {
    return this.collect(Prototype.K);
  },

  collect: function(iterator) {
    var results = [];
    this.each(function(value, index) {
      results.push(iterator(value, index));
    });
    return results;
  },
	
  select: function(iterator) {
    var results = [];
    this.each(function(value, index) {
      if (iterator(value, index))
        results.push(value);
    });
    return results;
  },
	
  include: function(object) {
    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  },

  inject: function(memo, iterator) {
    this.each(function(value, index) {
      memo = iterator(memo, value, index);
    });
    return memo;
  }
}


Object.extend(Hash.prototype, Enumerable);
Object.extend(Hash.prototype, {
  _each: function(iterator) {
    for (var key in this) {
      var value = this[key];
      if (value && value == Hash.prototype[key]) continue;

      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  }
});

Object.extend(Array.prototype, Enumerable);
Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  }	

});

var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) {}
    }

    return returnValue;
  }
}

var $A = Array.from = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0, length = iterable.length; i < length; i++)
      results.push(iterable[i]);
    return results;
  }
}


// bind

Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}

// AJAX

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
}

Ajax.Request = Class.create();
Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];


Ajax.Base = function() {};
Ajax.Base.prototype = {
  setOptions: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   ''
    }
    Object.extend(this.options, options || {});

    this.options.method = this.options.method.toLowerCase();
    if (typeof this.options.parameters == 'string')
      this.options.parameters = this.options.parameters.toQueryParams();
  }
}



Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
  _complete: false,

  initialize: function(url, options) {
    this.transport = Ajax.getTransport();
    this.setOptions(options);
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      // simulate other verbs over post
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Hash.toQueryString(params)) {
      // when GET, append parameters to URL
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      if (this.options.onCreate) this.options.onCreate(this.transport);
      Ajax.Responders.dispatch('onCreate', this, this.transport);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous)
        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (typeof extras.push == 'function')
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    return !this.transport.status
        || (this.transport.status >= 200 && this.transport.status < 300);
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState];
    var transport = this.transport, json = this.evalJSON();

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + this.transport.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(transport, json);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = this.getHeader('Content-type');
      if (contentType && contentType.strip().
        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
          this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
      Ajax.Responders.dispatch('on' + state, this, transport, json);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name);
    } catch (e) { return null }
  },

  evalJSON: function() {
    try {
      var json = this.getHeader('X-JSON');
      return json ? json.evalJSON() : null;
    } catch (e) { return null }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (typeof responder[callback] == 'function') {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) {}
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);
Ajax.Responders.register({
  onCreate: function() {
    Ajax.activeRequestCount++;
  },
  onComplete: function() {
    Ajax.activeRequestCount--;
  }
});


if (!window.Event) {
  var Event = new Object();
}

Object.extend(Event, {
  element: function(event) {
    return event.target || event.srcElement;
  },

  stop: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
      event.stopPropagation();
    } else {
      event.returnValue = false;
      event.cancelBubble = true;
    }
  },
	
  _observeAndCache: function(element, name, observer, useCapture) {
    if (!this.observers) this.observers = [];
    if (element.addEventListener) {
      this.observers.push([element, name, observer, useCapture]);
      element.addEventListener(name, observer, useCapture);
    } else if (element.attachEvent) {
      this.observers.push([element, name, observer, useCapture]);
      element.attachEvent('on' + name, observer);
    }
  },

  unloadCache: function() {
    if (!Event.observers) return;
    for (var i = 0; i < Event.observers.length; i++) {
      Event.stopObserving.apply(this, Event.observers[i]);
      Event.observers[i][0] = null;
    }
    Event.observers = false;
  },

  observe: function(element, name, observer, useCapture) {
    // var element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.attachEvent))
      name = 'keydown';

    this._observeAndCache(element, name, observer, useCapture);
  },
	
  stopObserving: function(element, name, observer, useCapture) {
    // var element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    if (element.removeEventListener) {
      element.removeEventListener(name, observer, useCapture);
    } else if (element.detachEvent) {
      element.detachEvent('on' + name, observer);
    }
  }	
});

/* prevent memory leaks in IE */
if (navigator.appVersion.match(/\bMSIE\b/))
  Event.observe(window, 'unload', Event.unloadCache, false);

if (!window.Element)
  var Element = new Object();
Element.ClassNames = Class.create();

Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = element; /*$(element)*/
  },
	
  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },
	
  set: function(className) {
    this.element.className = className;
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set(this.select(function(className) {
      return className != classNameToRemove;
    }).join(' '));
  },
	
  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set(this.toArray().concat(classNameToAdd).join(' '));
  }	
}

Object.extend(Element.ClassNames.prototype, Enumerable);

var Position = {
  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return [valueL, valueT];
  }
}

