/**
 * for i18n
 */
function _(japanese) { return japanese; }
var TMZ = [9, 0];
if (window.TZ) {
  TMZ = TZ.slice(1).split(':');
  if (TZ[0] == '+') {
    TMZ[0] = TMZ[0] - 0, TMZ[1] = TMZ[1] - 0;
  } else if (TZ[0] == '-') {
    TMZ[0] = 0 - TMZ[0], TMZ[1] = 0 - TMZ[1];
  } else {
    TMZ = [9, 0];
  }
}

/**
 * global params
 */
var LOCAL_API_BASE, BASE_URL, LOCAL_TAG_DIR;
(function() {
  var pathname = document.location.pathname;
  LOCAL_API_BASE = pathname[pathname.length - 1] == '/' ? pathname : pathname + '/';
  BASE_URL = pathname.slice(0, pathname.lastIndexOf('/') + 1);
  LOCAL_TAG_DIR = BASE_URL + 'tag/';

  var idx = BASE_URL.indexOf('/tag/');
  if(idx == -1)
    LOCAL_TAG_DIR = BASE_URL + 'tag/';
  else
    LOCAL_TAG_DIR = BASE_URL.slice(0, idx + 5);
})();

var GROUP_COLOR_MAP_LENGTH = 25;

/**
 * useful methods
 */
Element.addMethods({
  show_now_loading: function(element, message) {
    element = $(element);
    if (!element)
      return;

    element.update(
      new Element('div').setStyle({
        margin: '8px',
        paddingLeft: '25px',
        background: 'transparent url(/static/img/ajax-loader.gif) 0 0 no-repeat',
        whiteSpace: 'nowrap',
        color: '#666'
      }).update(message||'now loading...')
    );
  },

  getParentByTagName: function(element, tagName) {
    tagName = tagName.toLowerCase();
    do {
      element = element.parentNode;
    } while (element && element.tagName.toLowerCase() != tagName)

    return Element.extend(element);
  },

  /**
   * Hack for IE! Use when layout is broken in IE!
   * useful but not good for performance.
   * exapmle: $('bookmark-list').refresh();
   */
  refresh: function(element) {
    if (Prototype.Browser.IE) {
      element.addClassName('refresh');
      element.removeClassName('refresh');
    }
  }
});

Number.prototype.digit = function(len) {
  len = len || 0;
  var st = this.toString();

  var ret = [];
  for (var i = st.length; i < len; i++) ret.push('0');
  ret.push(st);

  return ret.join('');
};

function observe_if_exists() {
  var args = $A(arguments), element = args.shift();
  element = $(element);
  if(element) {
    element.observe.apply(element, args);
  }
}

// HTML renderer
function render_group(group, options) {
  options = Object.extend({ link: true, color: true }, (options||{}));
  var color = options.color ? (group.color || 0) : 0;
  var html = [];

  if (options.link)
    html.push('<a class="group group-', group.id, '" href="', group.link, '">');
  else
    html.push('<span class="group group-', group.id, '">');

  html.push(
    '<span class="group-icon"><img class="portrait_small" src="', group.icon_small, '" /></span>',
    '<span class="group-name group-color-', color, '">', group.name, '</span>'
  );

  if (options.link)
    html.push('</a>');
  else
    html.push('</span>');

  return html.join('');
}

function render_user(user, options) {
  return [
    '<a class="user" href="', user.link, '">',
    '<span class="user-icon"><img class="portrait_small" src="', user.portrait_s, '" /></span>',
    '<span class="user-name">', user.nickname, '</span>',
    '</a> '
  ].join('');
}

function is_equal_tags(lhs, rhs) {
  lhs = lhs || [], rhs = rhs || [];
  lhs = lhs.sort(), rhs = rhs.sort();
  if(lhs.length != rhs.length)
    return false;

  for(var idx=0;idx<lhs.length;++idx) {
    // 大文字小文字は区別
    if(lhs[idx].toLowerCase() != rhs[idx].toLowerCase())
      return false;
  }

  return true;
}

var BookmarkList = Class.create({
  initialize: function(container) {
    this.el = {};
    this.el.container = $(container);

    this.el.bookmarks = new Element('div', { className:'bookmarks' });
    this.el.container.appendChild(this.el.bookmarks);

    this.last_hash = null;
    setInterval(this.watch_url.bind(this), 200);
  },

  watch_url: function() {
    if (this.last_hash == location.hash)
      return;

    var mode = 'recent';
    var page = 1;
    var query = '';
    var tags = '';
    var tagged = [];
    // XXX: location.hash unescape URL forcedly, so we use raw URL strings.
    var raw_hash = (location.hash.empty())? location.hash : location.toString().match('#.+$')[0];
    var hash = raw_hash.split('/');
    hash[0] = hash[0] || '#recent';
    hash[1] = hash[1] || '';
    hash[2] = hash[2] || '';

    mode = ['recent','unread','comment','posts','starred','search'].find(function(m){ return m==hash[0].substr(1); });

    if (hash[1] && /^p[-+\d]+$/.test(hash[1]) && !hash[2]) {
      page = hash[1];
    } else {
      page = hash[2];
      if (mode == 'search')
        query = hash[1];
      else
        tags = hash[1];
    }

    if (page) {
      try {
        page = parseInt(page.substr(1), 10);
      } catch(e) {
        page = 1;
      }
    } else {
      page = 1;
    }

    query = decodeURIComponent(query);
    tagged = $A(tags.split('+'))
      .findAll(function(v){ return v != ''; }).collect(function(v) { return decodeURIComponent(v); });

    if (!is_equal_tags(this.tagged, tagged))
      page = 1;
    this.page = page;
    this.query = query;
    this.tagged = tagged;
    this.change_mode(mode);
    if (TagFilter.filter_tags)
      TagFilter.filter_tags(tagged);
    if (Search.sync)
      Search.sync(query);

    this.get_json();

    this.last_hash = location.hash;
  },

  change_mode: function(mode) {
    $A($$('.filter-mode')).each(function(e){ $(e).removeClassName('selected'); });
    var el_new = $('filter-mysearch-' + encodeURIComponent(this.query)) || $('filter-' + mode);
    if (el_new)
      el_new.addClassName('selected');

    this.mode = mode;
  },

  toggle_group: function() {
    if (this.mode != 'starred')
      this.get_json();
  },

  get_json: function() {
    var mode_page = this.mode + '_' + this.page;

    if (mode_page == this.mode_page) {
      this.page = 1;
      this.set_page_hash(this.mode, this.query, this.tagged, this.page);
    }
    this.mode_page = mode_page;

    var excepts = [];
    if (this.mode) {
      excepts = $$('#filter-group-container .filter-group').select(function(e){
        return !$(e).hasClassName('selected');
      }).map(function(e){
        return e.id.split('-')[2];
      });
    }

    new Ajax.Request(
      LOCAL_API_BASE + 'list_bookmarks',
      {
        method: 'get',
        parameters: {
          page: (this.page == -1 ? -1 : this.page - 1),
          mode: this.mode,
          excepts: excepts.join(','),
          tagged: this.tagged.join('+'),
          q: this.query.replace(/'/g,'\\\'') // XXX for emacs, js2-mode  (/'/g,'\\\'')
        },
        onSuccess: function(res) {
          window.json = res.responseText.evalJSON();
          try {
            // when this.page points last page. modify path
            if (this.page == -1) {
              this.page = json.page_info.page + 1;
              this.mode_page = this.mode + '_' + this.page;

              this.set_page_hash(this.mode, this.query, this.tagged, this.page);
              this.last_hash = location.hash;
            }

            if (this.page == json.page_info.page + 1) {
              this.build();
            }
          } catch(e) {
            alert('BookmarkList::get_json:'+e);
          }
        }.bind(this)
      }
    );
  },

  set_page_hash: function(mode, query, tags, page) {
    if(mode == undefined)
      mode = this.mode;
    if(query == undefined)
      query = this.query;
    if(tags == undefined)
      tags = TagFilter.tags;
    if(page == undefined)
      page = this.page;

    var hash = mode;
    if (query)
      hash += '/' + query;
    if (tags && tags.length)
      hash += '/' + tags.join('+');
    hash += '/p' + page;

    document.location.href = location.pathname + '#' + hash;
  },

  build: function() {
    var spec = json.spec;
    var html = [];
    var last_url = null;

    if (!spec['no-paging']) {
      var pager = this.render_pager(json.page_info);
      $$('.pager').each(function(e){ e.update(pager); });
    }

    var items = json.bookmarks;
    if (items.length == 0) {
      this.render_no_bookmarks();
      return;
    }

    for (var i = 0, len = items.length; i < len; i++) {
      var item = items[i];
      var classes = ['bookmark'];

      if (item.url == last_url)
        classes.push('same-url');
      if (!spec.rate)
        classes.push('no-rate');

      html.push(
        '<div id="bookmark_', i, '" class="', classes.join(' '), '">',
        this.render_title(spec, item, i)
      );

      if (spec.group || spec.user || spec.rate || spec.comment) {
        html.push(
          '<div class="bookmark-detail">',
          this.render_rate(spec, item, i),
          this.render_data(spec, item, i),
          '</div>'
        );
      } else {
        /** hack for IE6 **/
        html.push('<div style="position:relative;width:100%;font-size:1px;"></div>');
      }
      html.push('</div>');

      last_url = item.url;
    }

    this.el.bookmarks.update(html.join(''));
  },

  render_no_bookmarks: function() {
    this.el.bookmarks.update([
      '<div class="bookmark no-bookmark">',
//      (this.mode == 'search' ? '検索に該当する記事はありません。' : '条件に該当する記事はありません。'),
      _('絞り込み条件に該当する記事は見つかりませんでした。'),
      '</div>'
    ].join(''));
  },

  render_pager: function(page_info) {
    return [
      '<a class="pager-to-start', (!page_info.has_prev ? ' pager-to-start-nomore' : ''), '" href="',
        page_info.has_prev ? 'javascript:GoTo.start()' : 'javascript:void(0)',
      '"><span>', _('始めへ'), '</span></a>',
      '<a class="pager-to-previous ', (!page_info.has_prev ? ' pager-to-previous-nomore' : ''), '" href="',
        page_info.has_prev ? 'javascript:GoTo.prev()' : 'javascript:void(0)',
      '"><span>', _('前へ'), '</span></a>',

      '<span class="pages">', page_info.page + 1, '</span>',

      '<a class="pager-to-next', (!page_info.has_next ? ' pager-to-next-nomore' : ''), '" href="',
        page_info.has_next ? 'javascript:GoTo.next()' : 'javascript:void(0)',
      '"><span>', _('次へ'), '</span></a>',
      '<a class="pager-to-end', (!page_info.has_next ? ' pager-to-end-nomore' : ''), '" href="',
        page_info.has_next ? 'javascript:GoTo.end()' : 'javascript:void(0)',
      '"><span>', _('最後へ'), '</span></a>'
    ].join('');
  },

  render_title: function(spec, item, index) {
    /^(http|https):\/\/([^\/]+)/.exec(item.url);
    var domain = RegExp.$2;

    return [
      '<div class="bookmark-title">',
        '<h4><a target="_blank" href="/view?uri=', encodeURIComponent(item.url), '">', this.highlight_query(item.title), '</a></h4>',
        '<div class="bookmark-title-info">',
          this.render_title_buttons(spec, item, index),
          '<img src="', 'http://www.google.com/s2/favicons?domain=', encodeURIComponent(domain), '">', '&nbsp;',
          '<span style="font-size:small;color:gray;">', domain, '</span>',
        '</div>',
      '</div>'
    ].join('');
  },

  render_title_buttons: function(spec, item, index) {
    return [
      '<div class="bookmark-title-buttons buttons">',
        (!(spec.group||spec.user) ? this.render_date(item.date_added) : ''),
        this.render_rebookmark(index),
        this.render_star(spec, item, index),
      '</div>'
    ].join('');
  },

  render_rebookmark: function(index) {
    if (MY_USER_ID == null)
      return '';

    return [
      '<a class="btn-rebookmark" title="', _('ブックマークを他のグループに転載'), '" href="javascript:Bookmarks.rebookmark(', index, ')">',
      '<span>', _('転載'), '</span>',
      '</a>'
    ].join('');
  },

  render_star: function(spec, item, index) {
    if (spec['no-star'] || MY_USER_ID == null)
      return '';

    return [
      '<a class="btn-star', (item.is_starred?' checked':''), '" href="javascript:Star.toggle(', index, ')">',
      '<span>', _('お気に入り'), '</span>',
      '</a>'
    ].join('');
  },

  render_rate: function(spec, item, index) {
    if (!spec.rate)
      return '';

    return [
      '<div class="bookmark-rate">',
        '<div class="view-rate">',
          '<span class="number">', Math.round(item.viewing_rate), '<span class="percent">%</span></span>',
        '</div>',
      '</div>'
    ].join('');
  },

  render_data: function(spec, item, index) {
    var html = [];

    html.push(
      '<div class="bookmark-data">',
        (spec.group||spec.user?this.render_info(spec, item, index):''),
        this.render_tags(spec, item, index),
        this.render_comments(spec, item, index),
        this.render_form(spec, item, index),
      '</div>'
    );
    return html.join('');
  },

  render_info: function(spec, item, index) {
    if (!spec.group && !spec.user && !spec.date)
      return '';

    var groupsetting = window.group_setting || {};
    // TODO spec.groupのチェックいらなくね？
    var group = spec.group && item.group_id ?
      Object.extend(json.groups[item.group_id], groupsetting[item.group_id] || {}) : false;
    var user = spec.user && item.user_id ? json.users[item.user_id] : false;
    var date = spec.date ? item.date_added : false;

    var html = [
      '<div class="bookmark-info">',
        '<span class="bookmark-info-group">', render_group(group), '</span>',
        this.render_remove_bookmark(item, index)];

    if(item.from_group_id && json.groups[item.from_group_id]) {
      html.push(
        '<span class="bookmark-from-group">',
          '<span class="bookmark-from-group-from">from</span>',
          render_group(json.groups[item.from_group_id]),
        '</span>'
      );
    }
    if(spec.user || spec.date) {
      html.push('<span class="bookmark-info-user-and-date">');
      if(spec.user)
        html.push(render_user(user));
      if(spec.date)
        html.push(this.render_date(date));
      html.push('</span>');
    }
    html.push('</div>');

    return html.join('');
  },

  render_tags: function(spec, item, index) {
    if (!spec.tags)
      return '';

    var tags = item.tags;

    var html = ['<div class="bookmark-tags tags">',
      '<div class="first-child"></div><div class="tags-list">'];

    var group = json.groups && item.group_id ? json.groups[item.group_id] : null;
    var can_edit = group && group.can_post;

    for (var i = 0, len = tags.length; i < len; i++) {
      var tag = tags[i];

      html.push(
        '<a class="tag" rel="tag" href="javascript:Tags.click(\'', tag, '\')">',
        tag.escapeHTML(),
        '</a>'
      );

      if (can_edit) {
        // 削除ボタン
        var jtag = tag.gsub("'", "\\'").gsub('\\\\', '\\\\');
        var args = [index.toString(), encodeURIComponent("'" + jtag + "'"), true];
        html.push('<a class="btn-remove" title="', _('タグを削除'), '" href="javascript:Tags.remove(', args.join(','), ')"><span>[x]</span></a> ');
      }
      html.push(' ');
    }

    html.push('</div></div>');

    return html.join('');
  },

  render_date: function(date) {
    if (!date)
      return '';

    date = new Date(date.substring(0,4), date.substring(5,7)-1, date.substring(8,10),
                    date.substring(11,13)-0+TMZ[0], date.substring(14,16)-0+TMZ[1], date.substring(17,19));
    var html = ['<span class="date">'];

    var delta = new Date().getTime() - date.getTime();
    if (delta < 1000*60) {
      html.push(_('ついさっき'));
    } else if (delta < 1000*60*60) {
      html.push(new Template(_('#{minutes}分前')).evaluate({ minutes:Math.floor(delta/(1000*60)) }));
    } else if (delta < 1000*60*60*24) {
      html.push(new Template(_('#{hours}時間前')).evaluate({ hours:Math.floor(delta/(1000*60*60)) }));
    } else if (delta < 1000*60*60*24*365) {
      html.push(new Template(_('#{month}/#{date} #{hours}:#{minutes}')).evaluate({
        month: (date.getMonth()+1).digit(2),
        date: date.getDate().digit(2),
        hours: date.getHours().digit(2),
        minutes: date.getMinutes().digit(2)
      }));
    } else {
      html.push(new Template(_('#{year}/#{month}/#{date}')).evaluate({
        year: date.getFullYear(),
        month: (date.getMonth()+1).digit(2),
        date: date.getDate().digit(2)
      }));
    }

    html.push('</span>');
    return html.join('');
  },

  render_remove_bookmark: function(item, index) {
    if (!item.is_bookmarked)
      return '';

    return [
      '<a class="btn-remove" title="', _('ブックマークを削除'), '" href="javascript:Bookmarks.remove(', index, ')"><span>[x]</span></a>',
      '<span class="remove-confirm" style="display:none; color:#2f8926;">',
        _('削除しますか？'), '&nbsp;',
        '<a href="javascript:Bookmarks.remove_yes(', index, ')">', _('はい'), '</a>',
        ' / ',
        '<a href="javascript:Bookmarks.remove_no(', index, ')">', _('いいえ'), '</a>',
      '</span>'
    ].join('');
  },

  render_comments: function(spec, item, index) {
    if(!spec.comment)
      return '';
    if(!item.group_id)
      return '';

    var html = ['<div class="bookmark-comment">'];
    var group = json.groups && item.group_id ? json.groups[item.group_id] : null;
    var comments = item.recent_comments;

    if (comments.length) {
      html.push('<div class="bookmark-comments">');

      for (var i = 0, len = comments.length; i < len; i++) {
        var comment = comments[i];

        var comment_remove = '';
        if (comment.user_id == MY_USER_ID || (group && group.is_managed))
          comment_remove = this.render_comment_remove(index, comment.comment_id);

        html.push(
          '<div id="comment_', comment.comment_id, '" class="comment">',
          render_user(comment.user||json.users[comment.user_id]),
          '<span class="comment-body">', this.highlight_query(comment.comment), '</span>',
          '<span class="comment-meta">',
          this.render_date(comment.date),
          '</span>',
          comment_remove,
          '</div>'
        );
      }
      html.push('</div>'); //< end of "bookmark-comments"
    }

    html.push('</div>');  //< end of "bookmark-comment"
    return html.join('');
  },

  render_comment_remove: function(index, comment_id) {
    return [
      '<a class="btn-remove" title="', _('コメントを削除'), '" href="javascript:Comments.remove(', comment_id, ')"><span>[x]</span></a>',
      '<span',
      ' id="comment_confirm_', comment_id, '"',
      ' class="remove-confirm" style="display:none; color:#2f8926;"',
      '>',
      _('削除しますか？'), '&nbsp;',
      '<a href="javascript:Comments.remove_yes(', comment_id, ',', index, ')">', _('はい'), '</a>',
      ' / ',
      '<a href="javascript:Comments.remove_no(', comment_id, ')">', _('いいえ'), '</a>',
      '</span>'
    ].join('');
  },

  render_form: function(spec, item, index) {
    var html = [];
    var comments = item.recent_comments;
    var group = json.groups && item.group_id ? json.groups[item.group_id] : null;

    // ブックマークのボタン
    html.push('<div class="bookmark-buttons buttons">');

    if (group && group.footprint)
      html.push(
        '<a class="readericons-toggle"',
        ' href="javascript:ReaderIcons.toggle(', index, ')"',
        '><span>', _('回覧'), '</span></a>');

    if (spec.comment && comments.length < item.comments_count)
      html.push(
        '<a class="comment-more"',
        ' href="javascript:Comments.view_all(', index, ')"',
        '><span>', _('すべて表示'), '</span></a>');

    if (spec.comment && group && group.can_comment)
      html.push(
        '<a class="comment-add"',
        ' href="javascript:Comments.toggle(', index, ')"',
        '><span>', _('コメント追加'), '</span></a>');

    if (spec.tags && group && group.can_post)
      html.push(
        '<a class="tag-add"',
        ' href="javascript:Tags.toggle(', index, ');"',
        '><span>', _('タグ追加'), '</span></a>');

    html.push('</div>');
    html.push('<div class="bookmark-bottom" style="display:none;"></div>');
    return html.join('');
  },

  highlight_query: function(st) {
    if (this.mode == 'search' && Search.query.free.length) {
      return st.replace(new RegExp(Search.query.free.join('|'), 'gi'), function(m){ return '<span class="search-query-highlight">' + m + '</span>'; });
    } else {
      return st;
    }
  }
});

var GoTo = {
  page: function(page) {
    page = page >= 0 ? page : -1;
    if (page != -1)
      $$('.pages').each(function(e){ e.update(page); });

    BookmarkList.set_page_hash(undefined, undefined, undefined, page);
    document.body.parentNode.scrollTop = 0;
  },

  start: function() {
    GoTo.page(1);
  },

  end: function() {
    GoTo.page(-1);
  },

  next: function(n) {
    n = n || 1;
    GoTo.page(BookmarkList.page + n);
  },

  prev: function(n) {
    n = n || 1;
    GoTo.page(BookmarkList.page - n);
  }
};

var Bookmarks = Class.create({
  initialize: function() {},

  edit: function(index) {
    var item = json.bookmarks[index];
    document.location = [
      '/post',
      '?url=', encodeURIComponent(item.url),
      '&group=', json.groups[item.group_id].id
    ].join('');
  },

  // rebookmark系
  rebookmark: function(index) {
    var el = $$('#bookmark_' + index + ' .rebookmark-confirm');
    if (el.length == 0)
      this.create_rebookmark_confirm(index);
    else if (Element.visible(el[0]))
      this.hide_rebookmark_confirm(index);
    else
      this.show_rebookmark_confirm(index);
  },

  create_rebookmark_confirm: function(index) {
    var item = json.bookmarks[index];

    new Insertion.After($$('#bookmark_' + index + ' .bookmark-title')[0], [
      '<div class="rebookmark-confirm">',
      '<div class="rebookmark-groups"><form>',
      '<input type="hidden" name="bookmark_id" value="', item.bookmark_id,'" />',
      '<span class="rebookmark-now-loading"><img src="/static/imgs/ajax-loader.gif" />', _('この記事がまだ投稿されていないグループを検索中です...'), '</span>',
      '</form></div></div>'
    ].join(''));

    new Ajax.Request(
      '/api/bookmark/enum_rebookmarkable_groups',
      {
        method: 'get',
        parameters: {
          bookmark_id: item.bookmark_id
        },
        onSuccess: function(res) {
          var groups = res.responseText.evalJSON().groups;
          var html = [];

          if (groups.length) {
            groups.each(function(group) {
              var gid = group.group_id;
              html.push(
                '<span class="post-group">',
                '<input type="checkbox" id="group-', index, '-', gid, '" name="group" value="', gid, '" />',
                '<label for="group-', index, '-', gid, '">',
                render_group(group, { link: false, color: false }),
                '</label>',
                '</span>'
              );
            });
            html.push(
              '<div class="rebookmark-confirm-buttons buttons">',
              '<a href="javascript:Bookmarks.rebookmark_no(',index,')" class="rebookmark-cancel"><span>', _('キャンセル'), '</span></a>',
              '<a href="javascript:Bookmarks.rebookmark_yes(',index,')" class="rebookmark-ok"><span>', _('転載'), '</span></a>',
              '</div>'
            );

          } else {
            html.push('<span class="no-group">', _('転載できるグループがありません'), '</span>');
          }

          Element.replace($$('#bookmark_' + index + ' .rebookmark-now-loading')[0], html.join(''));
        },
        onFailure: function(res) {
          alert(res.responseText.evalJSON().reason);
        }
      }
    );
  },

  show_rebookmark_confirm: function(index) {
    $$('#bookmark_' + index + ' .rebookmark-confirm').map(Element.show);
  },

  hide_rebookmark_confirm: function(index) {
    $$('#bookmark_' + index + ' .rebookmark-confirm').map(Element.hide);
  },

  rebookmark_yes: function(index) {
    var container = $$('#bookmark_' + index + ' .rebookmark-confirm')[0];
    var form = container.select('form')[0];

    // remove error message
    container.select('.error').map(Element.remove);

    new Ajax.Request(
      '/api/bookmark/rebookmark',
      {
        method: 'post',
        parameters: Form.serialize(form),
        onSuccess: function(res) {
            // reload bookmarks
            BookmarkList.get_json();
        },
        onFailure: function(res) {
          // shake
          new Effect.Shake(container, { distance:5, duration:0.4 });

try{
          // show error message
          var msg = res.responseText.evalJSON().reason;
          //var msg = res.responseText;
          new Insertion.Bottom(
            container,
            ['<div class="error">', msg.escapeHTML(), '</div>'].join(''));
} catch(e) {alert(e);}
        }.bind(this)
      }
    );
  },

  rebookmark_no: function(index) {
    if($$('#bookmark_' + index + ' .rebookmark-confirm').length != 0)
      this.hide_rebookmark_confirm(index);
  },

  // remove系
  remove: function(index) {
    $$('#bookmark_' + index + ' .remove-confirm')[0].toggle();
  },

  remove_yes: function(index) {
    var item = json.bookmarks[index];
    new Ajax.Request(
      '/api/bookmark/remove_bookmark',
      {
        method: 'post',
        parameters: {
          url: item.url,
          bookmark_id: item.bookmark_id
        },
        onSuccess: this.on_remove_ok.bind(this, index),
        onFailure: this.on_remove_fail.bind(this, index)
      }
    );
  },

  remove_no: function(index) {
    $$('#bookmark_' + index + ' .remove-confirm')[0].hide();
  },

  on_remove_ok: function(index, res) {
    try {
      var response = res.responseText.evalJSON();
    } catch(e) {
      // error occured
      return on_remove_fail(res);
    }

    $$('#bookmark_' + index + ' .remove-confirm')[0].hide();

    if(response['bookmark_deleted']) {
      // bookmarkは削除された
      Effect.Fade('bookmark_' + index, {
        afterFinish: function() {
          var target = $('bookmark_' + index);
          var next = target.next();
          var need_title = !target.hasClassName('same-url') && next && next.hasClassName('same-url');

          target.remove();

          if (need_title)
            next.removeClassName('same-url');

        }.bind(this)
      });
    } else {
      // bookmarkは削除されてない (他の人が所有者かも)
      if(response['bookmark']) {
        // bookmark情報を更新
        try {
          Object.extend(json.bookmarks[index], response['bookmark']);
          Object.extend(json.users, response['users']);

          $$('#bookmark_' + index + ' .bookmark-data')[0].replace(
            BookmarkList.render_data(json.spec, json.bookmarks[index], index)
          );
        } catch(e) { alert(e); }
      }
    }
  },

  on_remove_fail: function(res) {
    new Effect.Shake(
      $$('#bookmark_' + index + ' .remove-confirm')[0],
      { distance:5, duration:0.4 }
    );
  }
});
Bookmarks = new Bookmarks();

var Star = Class.create({
  initialize: function() {},

  toggle: function(index) {
    var element = $$('#bookmark_' + index + ' .btn-star')[0];
    element.toggleClassName('checked');

    new Ajax.Request(
      '/api/bookmark/add_star',
      {
        method: 'post',
        parameters: 'url=' + encodeURIComponent(json.bookmarks[index].url) + (element.hasClassName('checked')?'':'&remove=1'),
        onSuccess: function(res) {},
        onFailure: function(res) {}
      }
    );
  }
});
Star = new Star();


var ReaderIcons = Class.create({
  initialize: function() {
  },

  toggle: function(index) {
    var item = json.bookmarks[index];

    var container = $$('#bookmark_' + index + ' .bookmark-bottom')[0];
    var el = container.down();
    if (el && el.hasClassName('readericons-user') && container.visible()) {
      container.hide();
    } else {
      container.show_now_loading();
      container.show();
      new Ajax.Request(
        '/api/bookmark/list_readers',
        {
          method: 'get',
          parameters: {
            url: item.url,
            group_id: item.group_id
          },
          onSuccess: function(res) {
            container.update(this.render_icons(res.responseText.evalJSON()));
          }.bind(this)
        }
      );
    }
  },

  render_icons: function(users, index) {
    var html = [];

    for (var i = 0, len = users.length; i < len; i++) {
      var user = users[i].user;
      var date_read = users[i].date_read;

      var cls = ['user-icon', 'readericons-user'];
      if (user.user_id == MY_USER_ID)
        cls.push('readericons-me');
      if (!date_read)
        cls.push('readericons-unread');

      html.push(
        '<span class="', cls.join(' '), '" title="', user.nickname, '">',
        '<img class="portrait_small" src="', user.portrait_s, '" />',
        '</span>');
    }

    return html.join('');
  }
});
ReaderIcons = new ReaderIcons();


var Comments = Class.create({
  initialize: function() {
    /// 投稿中の(url, group_id)のHash
    this.postings = new Hash();
  },

  view_all: function(index) {
    var item = json.bookmarks[index];

    var btn = $$('#bookmark_' + index + ' .comment-more')[0];
    btn.href = 'javascript:void(0)';

    new Ajax.Request(
      '/api/bookmark/list_comments',
      {
        method: 'get',
        parameters: {
          url: item.url,
          group_id: item.group_id
        },
        onSuccess: function(res) {
          item.recent_comments = res.responseText.evalJSON();
          this.update_comments(item, index);
          $(btn).remove();
        }.bind(this)
      }
    );
  },

  update_comments: function(item, index){
    var html = BookmarkList.render_comments(json.spec, item, index);
    $$('#bookmark_' + index + ' .bookmark-comment')[0].replace(html);
  },

  toggle: function(index) {
    var container = $$('#bookmark_' + index + ' .bookmark-bottom')[0];
    var el = container.down();
    if (el && el.hasClassName('comment-form') && container.visible()) {
      container.hide();
    } else {
      container.update([
        '<div class="comment-form">',
        '<textarea class="comment-textarea" rows="3"></textarea>',
        '</div>',
        '<div class="comment-buttons buttons">',
        '<span class="comment-length comment-length-invalid">', _('コメントは1000文字まで'), '</span>',
        '<a',
        ' href="javascript:Comments.cancel(', index, ')"',
        ' class="comment-cancel"',
        '><span>', _('キャンセル'), '</span></a>',
        '<a',
        ' href="javascript:Comments.post(', index, ')"',
        ' class="comment-post"',
        '><span>', _('投稿'), '</span></a>',
        '</div>'
      ].join('')).show();

      var textarea = $($$('#bookmark_' + index + ' .comment-textarea')[0]);
      var info = $($$('#bookmark_' + index + ' .comment-length')[0]);
      textarea.observe('keyup', function(){
        var text = textarea.value;
//        textarea.rows = Math.max(3, Math.min(10, text.split('\n').length));

        var len = text.length;
        if (len > 1000)
          info.update(new Template(_('#{limit}文字オーバー')).evaluate({ limit:(len-1000) }));
        else
          info.update(new Template(_('あと#{limit}文字')).evaluate({ limit:(1000-len) }));

        if (len == 0 || len > 1000)
          info.addClassName('comment-length-invalid');
        else
          info.removeClassName('comment-length-invalid');
      });
      textarea.focus();
    }
  },

  cancel: function(index) {
    var container = $$('#bookmark_' + index + ' .bookmark-bottom')[0];
    $(container).hide();
  },

  post: function(index) {
    var textarea = $$('#bookmark_' + index + ' .comment-textarea')[0];
    var post_button = $$('#bookmark_' + index + ' .comment-post')[0];
    var cancel_button = $$('#bookmark_' + index + ' .comment-cancel')[0];
    var value = textarea.value;

    if (!value || value.length > 1000) {
      var info = $$('#bookmark_' + index + ' .comment-length')[0];
      new Effect.Shake(info, { distance:5, duration:0.4 });
      return;
    }

    // 投稿中だったら投稿しない
    if (this.postings.get(index)) {
      return;

    } else {
      this.postings.set(index, true);
      post_button.disabled = true;
      textarea.disabled = true;
      cancel_button.disabled = true;
    }

    var item = json.bookmarks[index];
    new Ajax.Request(
      '/api/bookmark/post_comment',
      {
        method: 'post',
        parameters: {
          url: item.url,
          group_id: item.group_id,
          comment: value
        },

        onSuccess: function(res) {
          this.postings.unset(index);

          var json = res.responseText.evalJSON();
          if(json.reason) {
            // error
            new Effect.Shake(textarea, { distance:5, duration:0.4 });

            new Insertion.Before(
              $$('#bookmark_' + index + ' .comment-buttons')[0],
              ['<div class="comment-error error">', json.reason.escapeHTML(), '</div>'].join(''));
            return;
          } else {
            // ok
            item.recent_comments.push(json.comment);
            item.comments_count++;
            this.update_comments(item, index);
            this.cancel(index);
          }
        }.bind(this),

        onFailure: function(res) {
          this.postings.unset(index);
          var error_div = $$('#bookmark_' + index + ' .comment-error')[0];
          if(!error_div) {
            var button_container = $$('#bookmark_' + index + ' .comment-buttons')[0];
            new Insertion.Bottom(button_container, [
              '<div class="comment-error error"></div>'
            ].join(''));
            error_div = $$('#bookmark_' + index + ' .comment-error')[0];
          }
          error_div.innerHTML = res.responseText.evalJSON()['reason'];
          new Effect.Shake(textarea, {distance:5, duration:0.4});
          clearTimeout(error_div.timer_id);
          error_div.timer_id = setTimeout(function(){
            textarea.disabled = false;
            post_button.disabled = false;
            cancel_button.disabled = false;
          }, 1100);
        }.bind(this)
      }
    );
  },

  remove: function(comment_id) {
    $('comment_confirm_' + comment_id).toggle();
  },

  remove_yes: function(comment_id, index) {
    new Ajax.Request(
      '/api/bookmark/remove_comment',
      {
        method: 'post',
        parameters: {
          comment_id: comment_id
        },
        onSuccess: function(res) {
          Effect.Fade('comment_' + comment_id);
          var item = json.bookmarks[index];
          item.recent_comments = $A(item.recent_comments).select(function(e){ return e.comment_id != comment_id; });
          item.comments_count--;
        }
      }
    );
  },

  remove_no: function(comment_id) {
    $('comment_confirm_' + comment_id).hide();
  }
});
Comments = new Comments();

var Tags = Class.create({
  initialize: function() {
    /// 現在リクエスト中の...
    this.requests = new Hash();
  },

  update_item: function(item, index) {
    $$('#bookmark_' + index + ' .bookmark-tags')[0].replace(
      BookmarkList.render_tags(json.spec, item, index)
    );
  },

  click: function(tag) {
    if(TagFilter.reset) { ///< ちょっとひどいチェック方法
      TagFilter.update_hash([tag]);
    } else {
      location.href = '/tag/' + encodeURIComponent(tag);
    }
  },

  toggle: function(index) {
    var container = $$('#bookmark_' + index + ' .bookmark-bottom')[0];
    var el = container.down();
    if (el && el.hasClassName('tag-form') && container.visible()) {
      container.hide();
    } else {
      container.update([
        '<div class="tag-form">',
        '<input',
        ' class="tag-textbox"',
        ' type="text" class="tag-textbox" />',
        '</div>',
        '<div class="tag-buttons buttons">',
        '<span class="tag-length tag-length-invalid">0/20</span>',
        '<a',
        ' href="javascript:Tags.cancel(', index, ')"',
        ' class="tag-cancel"',
        '><span>', _('キャンセル'), '</span></a>',
        '<a',
        ' href="javascript:Tags.post(', index, ')"',
        ' class="tag-post"',
        '><span>', _('投稿'), '</span></a>',
        '</div>'
      ].join('')).show();

      var textbox = $($$('#bookmark_' + index + ' .tag-textbox')[0]);
      var info = $($$('#bookmark_' + index + ' .tag-length')[0]);
      new TagAutocomplete(textbox);
      textbox.observe('keyup', function(){
        var text = textbox.value;
        var len = text.length;
        info.update(len + '/20');
        if (len == 0 || len > 20)
          info.addClassName('tag-length-invalid');
        else
          info.removeClassName('tag-length-invalid');
      });
      textbox.focus();
    }
  },

  cancel: function(index) {
    var container = $$('#bookmark_' + index + ' .bookmark-bottom')[0];
    $(container).hide();
  },

  post: function(index) {
    var textbox = $$('#bookmark_' + index + ' .tag-textbox')[0];
    var post_button = $$('#bookmark_' + index + ' .tag-post')[0];
    var cancel_button = $$('#bookmark_' + index + ' .tag-cancel')[0];
    var value = textbox.value;

    if (!value || value.length > 20) {
      new Effect.Shake(textbox, { distance:5, duration:0.4 });
      return;
    }

    // 重複してリクエストしないように
    if(this.requests.get(index)) {
      return;
    } else {
        post_button.disabled = true;
        this.requests.set(index, true);
        textbox.disabled = true;
        cancel_button.disabled = true;
    }

    var item = json.bookmarks[index];

    new Ajax.Request(
      '/api/bookmark/add_tag',
      {
        method: 'post',
        parameters: {
          url: item.url,
          group_id: item.group_id,
          tag: value
        },

        onSuccess: function(res) {
try {
          this.requests.unset(index);

          item.tags.push(value);
          this.update_item(item, index);
          this.cancel(index);

          // tagcloudも更新
          if($('tag-filter'))
            TagFilter.update(true);
} catch(e) {alert(e);}
        }.bind(this),

        onFailure: function(res) {
          this.requests.unset(index);

          var error_div = $$('#bookmark_' + index + ' .tag-error')[0];
          if(!error_div) {
            var button_container = $$('#bookmark_' + index + ' .tag-buttons')[0];
            new Insertion.Bottom(button_container, [
              '<div class="tag-error error"></div>'
            ].join(''));
          }
          error_div.innerHTML = res.responseText.evalJSON()['reason'];

          new Effect.Shake(textbox, {distance:5, duration:0.4});
          clearTimeout(error_div.timer_id);
          error_div.timer_id = setTimeout(function(){
            textbox.disabled = false;
            post_button.disabled = false;
            cancel_button.disabled = false;
          }, 1100);
        }.bind(this)
      }
    );
  },

  remove: function(index, tag) {
    // FIXME: 後方互換性をのこす
    //        問題なければタグはデフォルトでエンコードするAPIに統一よろしく > hamabe
    var encoded = encoded || false;
    var item = json.bookmarks[index];

    // 重複してリクエストしないように
    if(this.requests.get(index))
      return;
    this.requests.set(index, true);
try{
    new Ajax.Request(
      '/api/bookmark/remove_tag',
      {
        method: 'GET',
        parameters: {
          url: item.url,
          group_id: item.group_id,
          tag: tag
        },

        onComplete: function(response) {
            this.requests.unset(index);

            var error = '';
            var success = false;
            if(200 == response.status) {
              var json_response = response.responseText.evalJSON();

              if(json_response && json_response.error)
                error = json_response.error;
              else
                success = true;
            } else if(400 <= response.status) {
              error = response.responseText;
            }

            if(success) {
              // ok
              item.tags = $A(item.tags).findAll(function(v) { return v != tag; });
              this.update_item(item, index);

              // tagcloudも更新
              if($('tag-filter'))
                TagFilter.update(true);
            } else {
              // error
              new Effect.Shake($('bookmark_' + index), { distance:5, duration:0.4 });
            }
          }.bind(this)
        }
    );
} catch(e) {
  console.log(e);
}
  }
});
Tags = new Tags();


/**
 * タグクラウド
 */
var TagCloud = Class.create({
  initialize: function(container, options) {
    this.options = Object.extend({
      // リクエストパラメータをクッキーに保存するための接頭辞
      cookie_key: 'TagCloud',
      // リクエストURL
      request_url: LOCAL_API_BASE + 'list_tags'
    }, (options||{}));

    this.build(container);
    this.sort = Cookie.get(this.options.cookie_key + '_sort', 'name');
    this.slider.setValue(parseInt(Cookie.get(this.options.cookie_key + '_value', '0')));
  },

  // TagCloudの組み立て
  build: function(container) {
//    Element.update(container, this.render_container());
    this.setup_slider();
    this.setup_sorter();
  },

  // containerのHTML生成
  render_container: function() {
    return [
      '<div id="tagcloud-sorter">',
      '  <a class="sort-by-count">by count</a> | <a class="sort-by-name">by name</a>',
      '</div>',

      '<div id="tagcloud-slider" class="slider">',
      '  <div class="slider-recent">', _('新'), '</div>',
      '  <div class="slider-old">', _('古'), '</div>',
      '  <div class="slider-track"><div class="slider-handle"></div></div>',
      '</div>',

      '<ul id="tagcloud-list"></ul>'
    ].join('');
  },

  // Sliderのセットアップ
  setup_slider: function() {
    this.slider = new Control.Slider(
      $$('#tagcloud-slider .slider-handle')[0],
      $$('#tagcloud-slider .slider-track')[0], {
        axis: 'horizontal',
        range: $R(0, 5),
        values: [0,1,2,3,4,5],
        onChange:  this.before_request.bind(this)
      }
    );
    this.slider.get_param = function() {
      var map = [1,2,3,4,5,0];
      return map[this.slider.value];
    }.bind(this);

    $$('#tagcloud-slider .slider-recent')[0].observe('click', function(){
      this.slider.setValue(this.slider.minimum);
    }.bind(this));

    $$('#tagcloud-slider .slider-old')[0].observe('click', function(){
      this.slider.setValue(this.slider.maximum);
    }.bind(this));
  },

  // Sorterのセットアップ
  setup_sorter: function() {
    $$('#tagcloud-sorter .sort-by-count')[0].observe('click', function(){
      this.sort = 'count';
      this.update();
    }.bind(this));

    $$('#tagcloud-sorter .sort-by-name')[0].observe('click', function(){
      this.sort = 'name';
      this.update();
    }.bind(this));
  },

  // this.sortの値を見てSorterの[選択|非選択]を切り替える
  update_sorter: function() {
    var el_selected = $$('#tagcloud-sorter .selected')[0];
    if (el_selected) el_selected.removeClassName('selected');

    $$('#tagcloud-sorter .sort-by-' + this.sort)[0].addClassName('selected');
  },

  update: function(force) {
    this.before_request(force);
  },

  // Ajax.Request事前処理
  before_request: function(force) {
    if(!force) {
      // 多重読み込みを防止する
      var       req = this.slider.value + '-' + this.sort;
      if (this.current == req)
        return;
    }

    this.current = req;
    this.update_sorter();

    // Cookieにリクエストパラメータを保存
    Cookie.set(this.options.cookie_key + '_sort', this.sort);
    Cookie.set(this.options.cookie_key + '_value', this.slider.value);

    this.request();
  },

  // Ajax.Request
  request: function() {
    new Ajax.Request(
      this.options.request_url,
      {
        method: 'GET',
        parameters: {
          period: this.slider.get_param(),
          sort: this.sort
        },
        onSuccess: this.request_onSuccess.bind(this),
        onFailure: this.request_onFailure.bind(this)
      }
    );
  },

  // リクエスト成功
  request_onSuccess: function(res) {
    var resobj = res.responseText.evalJSON();
    $('tagcloud-list').update(this.render_list_tags(resobj));
    this.request_callback(resobj);
  },

  // リクエスト成功時のコールバック関数
  request_callback: function(resobj) {
  },

  // リクエスト失敗
  request_onFailure: function(res) {
    alert('/*** failed ***/\n' + res.responseText);
  },

  // タグリストのHTML生成
  render_list_tags: function(tags) {
    var html = [];
    for (var i = 0, len = tags.length; i < len; i++) {
      var tag = tags[i][0], count = tags[i][1], ratio = tags[i][2];

      html.push(
        '<li>',
          '<a',
          ' href="', LOCAL_TAG_DIR, encodeURIComponent(tag), '"',
          ' style="font-size:', (75+ratio), '%;"',
          ' rel="tag"',
          ' class="tag level_', Math.floor(ratio/20), '"',
          '>',
          tag.escapeHTML(),
          '</a>',
          '<span class="count">', count, '</span>',
        '</li>',
        (Prototype.Browser.IE ? '　' : ' ')
      );
    }
    return html.join('');
  }
});


/**
 * 投稿画面のタグ入力補助
 */
var TagSuggest = Class.create(TagCloud, {
  initialize: function() {
    TagCloud.prototype.initialize.apply(this, arguments);

    this.box = $('post-form-tags');
    setInterval(this.check_tags.bind(this), 1000);
  },

  request_callback: function(resobj) {
    // タグ名のみからなるリストとしてキャッシュ
    this.cache = $A(resobj).collect(function(e){ return e[0].escapeHTML(); });
    // 生成されたタグリストのDOM要素をキャッシュ
    this.tag_elements = $('tagcloud-list').getElementsByTagName('li');
  },

  // タグリストのHTML生成
  render_list_tags: function(tags) {
    var html = [];
    for (var i = 0, len = tags.length; i < len; i++) {
      var tag = tags[i][0], count = tags[i][1], ratio = tags[i][2];

      html.push(
        '<li>',
        '<a',
        ' href="javascript:TagSuggest.toggle(', i, ')"',
        ' style="font-size:', (75+ratio), '%;"',
        ' class="tag level_', Math.floor(ratio/20), '"',
        '>',
        tag.escapeHTML(),
        '</a>',
        '<span class="count">', count, '</span>',
        '</li>',
        (Prototype.Browser.IE ? '　' : ' ')
      );
    }
    return html.join('');
  },

  // タグリストをクリックしたときの処理
  toggle: function(index) {
    var tag = this.cache[index].unescapeHTML();
    var tags = this.box.value.split(/[ |　]+/g)
      .select(function(e){ return e; });

    if (tags.indexOf(tag) != -1) {
      this.tag_elements[index].className = '';
      tags = tags.select(function(e){ return e != tag; });

    } else {
      this.tag_elements[index].className = 'selected';
      tags.push(tag);
    }

    this.box.value = (tags.length) ? (tags.join(' ') + ' ') : '';
    this.box.focus();
  },

  // タグ入力用テキストボックスを監視し、選択項目を同期させる
  check_tags: function() {
    var tags = this.box.value.split(/[ 　]+/g);
    var cloud_tags = this.cache;
    var checksum = {};

    $A(tags).each(function(e) {
      checksum[cloud_tags.indexOf(e.escapeHTML())] = true;
    });

    $A(this.tag_elements).each(function(e, i){
      e.className = checksum[i] ? 'selected' : '';
    });
  }
});


var GroupFilter = Class.create({
  initialize: function() {
    var checked_groups = [];

    try {
      checked_groups = $A(Cookie.get('FilterGroups', '').split(',')).select(function(s) {
        return s.substr(0, 13) == 'filter-group-';
      });
    } catch(e) {
    }

    $A(checked_groups).each(function(id){
      var element = $(id);
      if (element)
        element.removeClassName('selected');
    });
  },

  set_cookie_groups: function() {
    var ids = $A(document.getElementsByClassName('filter-group'))
                .select(function(e){ return !$(e).hasClassName('selected'); })
                .map(function(e){ return e.id; });
    Cookie.set('FilterGroups', ids.join(','));
  },

  /* クリックしたグループをtoggle */
  toggle: function(id) {
    $('filter-group-' + id).toggleClassName('selected');
    BookmarkList.toggle_group();
    this.set_cookie_groups();
  },

  select_all: function() {
    $A($$('.filter-group')).each(function(node){ Element.addClassName(node,'selected'); });
    BookmarkList.toggle_group();
    this.set_cookie_groups();
  },

  select_none: function() {
    $A($$('.filter-group')).each(function(node){ Element.removeClassName(node,'selected'); });
    BookmarkList.toggle_group();
    this.set_cookie_groups();
  }
});

/**
 * 投稿フィルタ
 */
var TagFilter = Class.create(TagCloud, {
  initialize: function() {
    this.tags = [];
    this.suggests = [];
    this.cache = [];
    TagCloud.prototype.initialize.apply(this, arguments);
  },

  request_callback: function(resobj) {
    // タグ名のみからなるリストとしてキャッシュ
    this.cache = $A(resobj).collect(function(e){ return e[0].escapeHTML(); });
    // 生成されたタグリストのDOM要素をキャッシュ
    this.tag_elements = $('tagcloud-list').getElementsByTagName('li');

    this.set_className();
  },

  // タグリストのHTML生成
  render_list_tags: function(tags) {
    var html = [];
    for (var i = 0, len = tags.length; i < len; i++) {
      var tag = tags[i][0], count = tags[i][1], ratio = tags[i][2];

      html.push(
        '<li>',
          '<a',
          ' href="javascript:void(0)"',
          ' onclick="TagFilter.click(', i, ', event || window.event)"',
          ' style="font-size:', (75+ratio), '%;"',
          ' class="tag level_', Math.floor(ratio/20), '"',
          '>',
          tag.escapeHTML(),
          '</a>',
          '<span class="count">', count, '</span>',
        '</li>',
        (Prototype.Browser.IE ? '　' : ' ')
      );
    }
    return html.join('');
  },

  set_className: function() {
    var cache = this.cache;
    var tags = this.tags;
    var suggests = this.suggests;

    $A(this.tag_elements).each(function(e, i){
      if (tags.indexOf(cache[i]) != -1)
        e.className = 'selected';
      else if (suggests.indexOf(cache[i]) != -1)
        e.className = 'suggest';
      else
        e.className = '';
    });
  },

  reset: function() {
    $A(this.tag_elements).each(function(e){ e.className = ''; });
    this.tags = [];
    this.suggests = [];
  },

  get_suggests: function() {
    new Ajax.Request(
      LOCAL_API_BASE + 'list_tags',
      {
        method: 'GET',
        parameters: {
          period: 0,
          sort: this.sort,
          tagged: this.tags.join('+')
        },
        onSuccess: function(res) {
          this.suggests = $A(res.responseText.evalJSON()).map(function(e){ return e[0].escapeHTML(); });
          this.set_className();
          var msg = '';
          if (this.suggests.length) {
            msg = '関連タグ：' + $A(this.suggests).map(function(e){
              return '<a style="color:#922;text-decoration:underline;" href="javascript:Tags.click(\'' + e + '\')">' + e + '</a>';
            }).join(' ');
          }
          $('related-tags').update(msg);
        }.bind(this),
        onFailure: Prototype.emptyFunction
      }
    );
  },

  // タグリストをクリックしたときの処理
  click: function(index, event) {
    var altkey = false;

    if(event) {
      Event.stop(event);
      altkey = event.ctrlKey;
    }

    var cache = this.cache;
    var tags = this.tags;
    if (tags.length == 1 && tags[0] == cache[index]) {
      this.filter_reset();
      return;

    } else if (altkey) {
      if (tags.indexOf(cache[index]) != -1) {
        tags.splice(tags.indexOf(cache[index]), 1);
      } else {
        tags.push(cache[index]);
      }
    } else {
      tags = [cache[index]];
    }

    this.update_hash(tags);
  },

  update_hash: function(tags) {
    tags = tags || [];
    if (tags.length == 0)
      location.href = BASE_URL + '#recent';
    else
      //location.href = BASE_URL + '#recent/' + tags.join('+');
      location.href = BASE_URL + '#recent/' + tags.map(function(t){return encodeURIComponent(t);}).join('+');
  },

  filter_reset: function() { this.update_hash([]); },

  /**
   * BookmarkList.watch_urlからのみ呼ばれるようにする
   * - this.update_hash -> (URL更新) -> BookmarkList.watch_url -> this.filter_tags
   */
  filter_tags: function(tags, no_update_url) {
    if (tags.length == 0) {
      this.reset();
      $A($$('.filter-reset')).invoke('hide');
      $('now-filtering').update('');
      $('related-tags').update('');
      return;
    }

    this.reset();
    this.tags = tags;
    var cache = this.cache;
    var tag_elements = this.tag_elements;
    $A(this.tags).each(function(e, i){
      var index = cache.indexOf(e);
      if (index != -1)
        Element.addClassName(tag_elements[index], 'selected');
    });

    this.get_suggests();
    $A($$('.filter-reset')).invoke('show');
    var msg = _('選択中のタグ：') + this.tags.join(', ');
    $('now-filtering').update(msg);
  }
});


var TagAutocomplete = Class.create({
  initialize: function(element) {
    element = $(element);
    if (!element || element.getAttribute('_autocomplete_') == 'true')
      return;

    element.setAttribute('_autocomplete_', 'true');
    this.setup(element);
  },

  setup: function(element) {
    this.el = {};
    this.el.target = element;
    this.el.container = new Element('div').addClassName('search-suggest').hide();
    document.body.appendChild(this.el.container);

    new Ajax.Autocompleter(
      this.el.target,
      this.el.container,
      '/api/self/suggest_tags',
      {
        tokens: [' ', '　'],
        paramName: 'prefix',
        updateElement: function(selectedElement) {
          this.oldElementValue = this.element.value;
          this.element.focus();
        }
      }
    );
  }
});


var QuerySuggest = Class.create({
  initialize: function(element) {
    element = $(element);
    this.setup(element);
  },

  setup: function(element) {
    this.el = {};
    this.el.target = element;
    this.el.container = new Element('div').addClassName('search-suggest').hide();
    document.body.appendChild(this.el.container);

    new Ajax.Autocompleter(
      this.el.target,
      this.el.container,
      LOCAL_API_BASE + 'suggest_query',
      {
        method: 'get',
        tokens: [' ', '　'],
        paramName: 'partial',
        updateElement: function(selectedElement) {
          this.oldElementValue = this.element.value;
          this.element.focus();
        },
        do_search: function(){ Search.search(); }
      }
    );
  }
});


var Cookie = Class.create({
  initialize: function() {
    this.lifelong = 365;
  },

  get: function(name, def_value) {
    var result = def_value || '';
    var params = document.cookie.split('; ');
    for (var i = 0, len = params.length; i < len; i++) {
      var crumb = params[i].split('=');
      if (crumb[0] == name && !!crumb[1]) {
        result = crumb[1];
        break;
      }
    }
    return result;
  },

  set: function(name, value) {
    var date = new Date(new Date().getTime() + (this.lifelong * 24*60*60*1000));
    document.cookie = name+'='+value+'; expires='+date.toGMTString()+'; path=/';
  },

  clear: function(name) {
    document.cookie = name + '=;expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/';
  }
});
Cookie = new Cookie();

/* Selectable list */
var SelectableList = Class.create({
  initialize: function(options) {
    this._options = new Hash({
      html_id: '',
      multiple: false,
      class_name_for_selected: 'selected'
    }).merge(options);
    this._element = $(this._options.get('html_id'));
    this._verify_init_options();
    this._selected = $A([]);
    this._attach();
  },
  selected: function() {
    return this._selected;
  },
  add: function(elements) {
    if(elements.constructor != Array) elements = $A(elements);
    elements.each(function(element) {
      this._element.appendChild(element);
    });
  },
  remove: function(target_element) {
    this._element.select('li').each(function(element) {
      if(target_element == element) element.remove();
    });
  },
  unselect: function() {
    var class_name = this._options.get('class_name_for_selected');
    this._element.select('li.selected').each(function(ele) {
      ele.removeClassName(class_name);
    });
    this._selected.length = 0;
  },
  _verify_init_options: function() {
    var element = this._element;
    if(!element) throw Error('Element[@id="' + this._options.get('html_id') + '"] does not exists.');
    if(element.tagName.toLowerCase() != 'ul') throw Error('SelectableList allows only UL element.');
  },
  _attach: function() {
    var class_name = this._options.get('class_name_for_selected');
    var is_multiple = this._options.get('multiple');
    $(this._element).observe('mousedown', (function(e) {
      var element = e.target;
      var selected = this._selected;
      while(['ul', 'li'].indexOf(element.tagName.toLowerCase()) == -1) {
        element = $(element.parentNode);
      }
      if(element.tagName.toLowerCase() == 'ul') return;
      element = $(element);
      if(element.hasClassName(class_name)) {
        element.removeClassName(class_name);
      }
      if(is_multiple && e.shiftKey) {
        // nop
      } else {
        selected.each(function(ele){
          ele.removeClassName(class_name);
        });
        selected.length = 0;
      }
      selected.push($(element).addClassName(class_name));
      e.stop();
    }).bind(this));
  }
});

/* ========== /post 用スクリプト ここから ==================== */
var Post = Class.create({
  initialize: function() {
    var checked_groups = Cookie.get('PostGroups' + MY_USER_ID, '[]').evalJSON();
    $A(checked_groups).each(function(id){
      var element = $(id);
      if (element) {
        element.checked = true;
        $(element.parentNode).addClassName('post-group-checked');
      }
    });

    new TagAutocomplete('post-form-tags');
    GroupSet = new GroupSet();
  },

  set_cookie_groups: function() {
    var ids = $A(document.getElementsByClassName('post-form-group'))
                .select(function(e){ return e.checked; })
                .map(function(e){ return e.id; });
    Cookie.set('PostGroups' + MY_USER_ID, ids.toJSON());
  },

  group_change: function(element) {
    var parent = $(element.parentNode);
    if (element.checked)
      parent.addClassName('post-group-checked');
    else
      parent.removeClassName('post-group-checked');

    this.set_cookie_groups();
  }
});

var GroupSet = Class.create({
  initialize: function() {
    this.reset();
    this.groupsets = Cookie.get('GroupSets' + MY_USER_ID, '[]').evalJSON();
    for (var i = 0, len = this.groupsets.length; i < len; i++)
      this.create(decodeURIComponent(this.groupsets[i][0]), this.groupsets[i][1]);
  },

  reset: function() {
    $('groupset_create').update([
      '<a href="javascript:GroupSet.naming()" title="',
      _('よく使うグループをまとめて名前をつけることができます'),
      '">', '→ ', _('グループをまとめる'), '</a>'
    ].join(''));
  },

  naming: function() {
    var groups = $A(document.getElementsByClassName('post-form-group')).select(function(e){ return e.checked; });
    var names = groups.map(function(e){ return e.getAttribute('_groupname'); });
    var ids = groups.map(function(e){ return parseInt(e.id.split('_').last()); });

    if (ids.length == 0) return;

    $('groupset_create').update(new Template(
      _('#{groups}をまとめて、 #{groupset} を作成します')
    ).evaluate({
      groups: names.join(', '),
      groupset: [
        '<input type="text" style="width:75px;"',
        ' onkeydown="if(event.keyCode==13)GroupSet.regist(this,[', ids.join(','), '])"',
        ' onkeypress="enterCancel(event||window.event)"',
        ' onblur="GroupSet.reset()"',
        '>'
      ].join('')
    }));

    $$('#groupset_create input')[0].focus();
  },

  regist: function(element, ids) {
    if (!element.value) {
      GroupSet.reset();
      return;
    }

    this.create(element.value, ids);
    this.reset();
    this.groupsets.push([encodeURIComponent(element.value), ids]);
    Cookie.set('GroupSets' + MY_USER_ID, this.groupsets.toJSON());
  },

  create: function(name, ids) {
    $('groupset_list').insert([
      '<div class="groupset">',
      '<a',
      ' class="groupset-name"',
      ' href="javascript:GroupSet.click([', ids.join(','), '])"',
      ' onmouseover="GroupSet.highlight([', ids.join(','), '])"',
      ' onmouseout="GroupSet.highlight([])"',
      '>',
      name,
      '</a>',
      '<a class="groupset-remove" href="javascript:void(0)" onclick="GroupSet.groupsets_remove(this.parentNode)"></a>',
      '</div>'
    ].join(''));
  },

  groupsets_remove: function(element) {
    var elements = $$('#groupset_list .groupset');
    for (var i = 0, len = elements.length; i < len; i++) {
      if (elements[i] == element) {
        element.remove();
        this.groupsets.splice(i, 1);
        break;
      }
    }
    Cookie.set('GroupSets' + MY_USER_ID, this.groupsets.toJSON());
  },

  click: function(ids) {
    $A(document.getElementsByClassName('post-form-group')).each(function(e){
      if (ids.indexOf(parseInt(e.id.split('_').last())) != -1) {
        e.checked = true;
        $(e.parentNode).addClassName('post-group-checked');
      } else {
        e.checked = false;
        $(e.parentNode).removeClassName('post-group-checked');
      }
    });
    Post.set_cookie_groups();
  },

  highlight: function(ids) {
    $A(document.getElementsByClassName('post-form-group')).each(function(e){
      if (ids.indexOf(parseInt(e.id.split('_').last())) != -1)
        $(e.parentNode).addClassName('post-group-highlight');
      else
        $(e.parentNode).removeClassName('post-group-highlight');
    });
  }
});

/**
 * Cancel post when you press return-key on textbox.
 * usage: <input type="text" onkeypress="enterCancel(event||window.event)">
 */
function enterCancel(evt){
  if (evt.keyCode == 13) {
    if (evt.preventDefault) {
      evt.preventDefault();
    } else {
      evt.returnValue = false;
    }
  }
}
/* ========== /post 用スクリプト ここまで ==================== */

/* ========== /invite 用スクリプト ここから ==================== */
var Invite = Class.create({
  ACCOUNT_SEPARATOR: ',',
  initialize: function() {},
  before_post: function() {
    var accounts = [];
    $$('#invite-list li').each(function(e){ accounts.push(e.getAttribute('_account')); });
    $('to-accounts').value = accounts.join(this.ACCOUNT_SEPARATOR);
  },
  addMember: function(user_info) {
    // user_info := [account:String, nickname:String]
    // If the account is already inputed, then just ignore.
    var account = decodeURIComponent(user_info[0]);
    var nickname = user_info[1].escapeHTML();
    var portrait = user_info[2];

    if ($$('#invite-list li').find(function(e){ return account == e.getAttribute('_account'); }))
      return;

    // add into list
    var el_user = new Element('li', { '_account':account })
      .update('<img src="' + portrait + '" alt="" /> ' + nickname);
    $('invite-list').insert(el_user);
  }
});
var TextareaWithInstruction = Class.create({
  initialize: function(element) {
    this.element = $(element);
    this.instruction = element.innerHTML;

    element.observe('focus', this.on_focus.bindAsEventListener(this));
    element.observe('blur', this.on_blur.bindAsEventListener(this));
  },

  on_focus: function(event) {
    if(this.element.hasClassName('instruction')) {
      this.element.value = '';
      this.element.removeClassName('instruction');
    }
  },

  on_blur: function(event) {
    if(!this.element.value) {
      this.element.value = this.instruction;
      this.element.addClassName('instruction');
    }
  }
});
TextareaWithInstruction.make = (function(element) {
  element = $(element);
  if(!element)
    return null;

  if(!element.hasClassName('instruction') || !element.innerHTML)
    return null;

  return new TextareaWithInstruction(element);
}).bind(TextareaWithInstruction);

/* ========== /invite 用スクリプト ここまで ==================== */

var Search = Class.create({
  initialize: function() {
    new QuerySuggest('search-query');
    $('search-query').observe('focus', function(e){ e.target.select(); });

    new Ajax.Request('/api/self/get_custom_filter', {
      method: 'GET',
      onSuccess: function(res) {
        var mysearch = res.responseText.evalJSON();
        for (var i = 0, len = mysearch.length; i < len; i++) {
          var query = mysearch[i];
          $('filter-mode-container').insert(this.build_mysearch(query));
          if (BookmarkList.query == query) {
            $('filter-search', 'btn-add-mysearch').invoke('hide');
            $('filter-mysearch-' + encodeURIComponent(query)).addClassName('selected');
          }
        }
      }.bind(this)
    });
  },

  search: function(query) {
    if (!query) {
      var el = $('search-query');
      if (!el)
        return;
      query = el.value;
    }
    location.href = BASE_URL + '#search/' + encodeURIComponent(query);
  },

  sync: function(query) {
    this.parse_query(query);

    if (query) {
      $('search-query').value = query;

      var el_mysearch = $('filter-mysearch-' + encodeURIComponent(query));
      if (el_mysearch) {
        $('btn-add-mysearch').hide();
        return;
      }

      this.update_filter_search(query);

    } else {
      $('btn-add-mysearch').hide();
    }
  },

  update_filter_search: function(query) {
    var el_filter = $$('#filter-search a')[0];
    el_filter.update('<span></span>' + query);
    el_filter.setAttribute('href', '#search/' + encodeURIComponent(query));

    $('btn-add-mysearch').show();
    $('filter-search').show();
  },

  build_mysearch: function(query) {
    var en_query = encodeURIComponent(query);

    var btn_remove_mysearch = new Element('a', { href:'javascript:void(0)', title:_('このフィルタを削除する') })
      .addClassName('btn-remove-mysearch')
      .observe('click', this.remove_mysearch.bind(this, query))
      .update('<span style="display:none;">' + _('削除') + '</span>');

    var filter_mysearch = new Element('li', { id:'filter-mysearch-'+en_query })
      .addClassName('filter-mysearch')
      .addClassName('filter-mode')
      .update(['<a class="filter-clickarea" href="#search/',en_query,'"><span></span>',query,'</a>'].join(''))
      .insert(btn_remove_mysearch);
    return filter_mysearch;
  },

  add_mysearch: function() {
    var query = BookmarkList.query;
    $('filter-mode-container').insert(this.build_mysearch(query));
    $('filter-search', 'btn-add-mysearch').invoke('hide');
    $('filter-mysearch-'+encodeURIComponent(query)).addClassName('selected');

    new Ajax.Request('/api/self/add_custom_filter', {
      method: 'POST',
      parameters: { q:query }
    });
  },

  remove_mysearch: function(query) {
    var el_remove = $('filter-mysearch-'+encodeURIComponent(query));
    if (el_remove.hasClassName('selected')) {
      $('filter-search').addClassName('selected');
      this.update_filter_search(query);
    }
    el_remove.remove();

    new Ajax.Request('/api/self/remove_custom_filter', {
      method: 'POST',
      parameters: { q:query }
    });
  },

  query: { id:[], group:[], tag:[], free:[] },

  parse_query: function(query) {
    var temp = { id:[], group:[], tag:[], free:[] };

    var match = query.match(/g:"[^"]+"/g);
    if (match) {
      for (var i = 0, len = match.length; i < len; i++) {
        query = query.replace(match[i],'');
        temp.group.push(match[i].match(/g:"([^"]+)"/)[1]);
      }
    }

    query.replace(/　/g, ' ')
      .split(' ')
      .findAll(function(e){ return !!e; })
      .each(function(q){
        if (q.indexOf('id:') == 0) {
          temp.id.push(q.substring(3));
        } else if (q.indexOf('g:') == 0) {
          temp.group.push(q.substring(2));
        } else if (q.indexOf('t:') == 0) {
          temp.tag.push(q.substring(2));
        } else {
          temp.free.push(q);
        }
      });

    var id = temp.id.join('又は');
    var group = temp.group.join('又は');
    var tag = temp.tag.join(', ');
    var free = temp.free.join(', ');

    if (id || group || tag || free) {
      $('search-explain').update([
        id,
        (id ? 'が' : ''),
        group,
        (group ? 'に' : ''),
        (id || group ? '投稿した' : ''),
        ((id||group) && (tag||free) ? '記事の中で' : ''),
        tag,
        (tag ? 'というタグ' : ''),
        (tag && free ? 'と' : ''),
        free,
        (free ? 'という単語' : ''),
        (tag || free ? 'を含む' : ''),
        '記事'
      ].join(''));

    } else {
      $('search-explain').update('');
    }

    this.query = temp;
  },

  cancel: function() {
    location.href = BASE_URL + '#recent';
    $('filter-search').hide();
  }
});



var GroupColor = Class.create({
  initialize: function() {},

  build: function() {
    this.el = {};
    this.el.selector = new Element('div').setStyle({
      position: 'absolute',
      zIndex: '10',
      backgroundColor: '#eef',
      border: '4px solid #66c',
      width: '120px',
      right: '0',
      display: 'none'
    });
    this.el.selector.observe('mousedown', Event.stop);
    document.observe('mousedown', function(){ this.el.selector.hide(); }.bind(this));

    var html = ['<table cellspacing="0" cellpadding="0" style="width:100%;"><tbody>'];

    for (var i = 1, len = GROUP_COLOR_MAP_LENGTH; i < len; i++) {
      if (i%6==1)
        html.push('<tr>');

      html.push(
        '<td>',
        '<a href="javascript:GroupColor.set(', i, ')" class="group-color group-color-', i,'">a</a>',
        '</td>'
      );

      if (i%6==0)
        html.push('</tr>');
    }

    var mod = (GROUP_COLOR_MAP_LENGTH - 1) % 6;
    if (mod)
      html.push('<td colspan="', (6-mod), '"></td></tr>');

    html.push(
      '<tr><td colspan="6">',
      '<a class="group-color group-color-default" href="javascript:GroupColor.set(0)">',
      _('デフォルトに戻す'),
      '</a>',
      '</td></tr>'
    );

    html.push('</tbody></table>');
    this.el.selector.update(html.join(''));
    $('filter-group-container').insert(this.el.selector);
  },

  edit: function(id) {
    if (!this.el)
      this.build();
    else if (this.el.selector.visible() && this.id == id) {
      this.el.selector.hide();
      return;
    }

    this.id = id;
    this.color = window.group_setting ? (group_setting[this.id].color || 0) : 0;

    var element = $('filter-group-' + id);
    this.el.selector.setStyle({
      top: element.offsetTop + element.offsetHeight + 'px',
      display: ''
    });
  },

  set: function(index) {
    this.el.selector.hide();
    group_setting[this.id].color = index;

    //var groups = document.getElementsByClassName('group-wrapper-' + this.id);
    var group = Object.extend(json.groups[this.id], window.group_setting[this.id] || {});
    $$('.group-' + this.id).each(function(node) { node.replace(render_group(group)); });

    new Ajax.Request(
      '/api/self/set_group_color',
      {
        method: 'POST',
        parameters: {
          group_id: this.id,
          color: index
        },
        onSuccess: {},
        onFailure: {}
      }
    );
  }
});
GroupColor = new GroupColor();


/* ========== dashboard 用スクリプト ここから ==================== */
/**
 * メッセージを読む
 */
function remove_message(event, message_id) {
  return send_message_request(
    event, message_id, '/api/self/remove_message',
    { message_id: message_id },
    function(json, msgNode) {
      msgNode.remove();
    });
}

/**
 *  招待状への処理
 */
function accept_invite(event, invite_id, accept) {
  return send_message_request(
    event, invite_id, '/api/self/accept_invite',
    { invite_id: invite_id, accept: accept },
    function(json, msgNode) {
      var node = msgNode.getElementsByClassName('invite-accept')[0];
      if(accept)
        node.update(_('グループに参加しました'));
      else
        node.update(_('参加を辞退しました'));
    });
}

function send_message_request(event, message_id, req_url, parameters, on_success) {
  event = Event.extend(event || window.event);
  var msgNode = $('message-' + message_id);

  function _show_error(msg) {
    msgNode.getElementsByClassName('error').invoke('remove');
    var footer = msgNode.getElementsByClassName('message-footer')[0];
    new Insertion.Top(footer, ['<span class="error">', msg.escapeHTML(), '</span>'].join(''));
  }

  new Ajax.Request(req_url, {
    method: 'GET',
    parameters: parameters,
    onSuccess: function(trans) {
      var json = trans.responseText.evalJSON();

      if(json.reason)
        _show_error(json.reason);
      else
        on_success(json, msgNode);
    },
    onFailure: function(trans) {
      _show_error(trans.responseText);
    }
  });

  return false;
}

/* /<user>/dashboard/settings/account */
function remove_openID(sid, openID, element) {
  new Ajax.Request('/api/self/remove_openID', {
    method: 'GET',
    parameters: {
      sid: sid,
      openID: openID
    },
    onSuccess: function(trans) {
      // remove this element
      if(element && element.tagName) {
        while(element && element.tagName != 'TR')
          element = element.parentNode;
        if(element)
          $(element).remove();
      }
    },
    onFailure: function(trans) {
//      var node = self.get_confirm_cell();
//      node.update(['<span class="error">', trans.responseText, '</span>'].join(''));
    }
  });
}

function openid_go_with(element, openID) {
    while(element && element.tagName != 'FORM')
      element = element.parentNode;

    var form = $(document.forms['add_openid']);
    form.getInputs('text', 'openid')[0].value = openID;
    form.submit();
}

/* ========== dashboard 用スクリプト ここまで ==================== */

/* ========== /group/* 用スクリプト ここから ==================== */
function open_group_dialog(request, options, base_url) {
  options = options || {};
  base_url = base_url || GROUP_BASE_URL;

  request = base_url + 'dialog/' + request;
  DialogManager.show(
    new Dialog(request, {
      on_close: function() {
        if(options.reload_when_close) {
          window.location.reload();
        }
    }})
  );
  return false;
}

/* ./settings/members */
var GroupMemberControler = Class.create({
  ROLE_BY_NAME: {
    'member':  _('メンバー'),
    'manager': _('管理者'),
    'editor':  _('編集者')
  },
  initialize: function(element) {
    this.element = $(element);
    this.user_id = this.element.id.substr(5);
  },

  get_my_role: function() {
    return this.element.classNames().find(function(cls) {
      return cls.substring(0, 5) == 'role_';
    }).substr(5);
  },

  is_me: function() {
    return this.element.hasClassName('is_me');
  },

  // change role
  change_role: function(to_role) {
    var cur_role = this.get_my_role();
    if(cur_role == to_role)
      return false;

    var change_func = this._change_role.bind(this, to_role);

    if(this.is_me() && cur_role == 'manager') {
      this.show_confirm(
        _('自分を管理者から降格しますか？　この操作は取り消せません。'),
        change_func);
    } else {
      change_func();
    }
  },

  _change_role: function(to_role) {
    this.new_api_request('api/change_role', {
          user_id: this.user_id,
          new_role: to_role },
        function() { this.on_role_changed(to_role); }.bind(this)
    );
  },

  on_role_changed: function(new_role) {
    if(this.is_me()) {
      // 自分の役割が変更されたらもうSettingsはいじれないはず
      location.href = GROUP_BASE_URL;
      return;
    }
    var currolecls = this.element.classNames().find(function(cls) {
      return cls.substring(0, 5) == 'role_';
    });

    this.element.removeClassName(currolecls);
    this.element.addClassName('role_' + new_role);

    this.element.select('.member-role-cell')[0].update(this.ROLE_BY_NAME[new_role]);
  },

  // kick user
  kick_user: function(event) {
    this.show_confirm(
      _('このユーザーを退会させますか？　この操作は取り消せません。'),
      this._kick_user.bind(this));

    return false;
  },

  _kick_user: function() {
    var self = this;
    this.new_api_request('api/kick_user',
      { user_id: this.user_id },
      function() { self.on_user_kicked(); }
    );
  },

  on_user_kicked: function() {
    this.element.remove();
  },

  // accept_join
  accept_join: function(accept) {
    this.new_api_request('api/accept_join', {
          user_id: this.user_id,
          accept: accept ? '1': '' },
        function(trans) { this.on_accept_join(trans.responseText.evalJSON()); }.bind(this)
    );
  },

  on_accept_join: function(result) {
    //this.element.remove();
    var cells = this.element.getElementsByTagName('TD');
    var cell = cells[cells.length - 1];

    cell.update(result.message/*.escapeHTML()*/);
  },

  //
  new_api_request: function(api, parameters, on_success) {
    var self = this;
    new Ajax.Request(GROUP_BASE_URL + api, {
      method: 'GET',
      parameters: parameters,
      onSuccess: on_success,
      onFailure: function(trans) {
        var node = self.get_confirm_cell();
        node.update(['<span class="error">', trans.responseText, '</span>'].join(''));
      }
    });
  },

  // confirm
  get_confirm_cell: function() {
    return this.element.next().getElementsByClassName('confirm')[0];
  },

  show_confirm: function(message, ok_func) {
    var self = this;
    var confirm_cell = this.get_confirm_cell();

    var html = [
      '<span>', message, '</span>',
      '<span class="confirm_yesno">',
        '<a class="confirm_yes">', _('はい'), '</a>', ' / ',
        '<a class="confirm_no">', _('いいえ'), '</a>',
      '</span>'
    ];

    confirm_cell.update(html.join(''));
    $(confirm_cell.parentNode).show();
    setTimeout(function() {
      var node = confirm_cell.getElementsByClassName('confirm_yes')[0];
      node.observe('click', (function() {
        ok_func();
        self.close_confirm();
      }).bindAsEventListener());
      node = confirm_cell.getElementsByClassName('confirm_no')[0];
      node.observe('click', self.close_confirm.bind(self));
    }, 0);
  },

  close_confirm: function(event) {
    if(!this.element)
      return;
    var confirm_cell = this.get_confirm_cell();
    if(!confirm_cell)
      return;
    confirm_cell.innerHTML = '';
    $(confirm_cell.parentNode).hide();
  }
});

GroupMemberControler.from_event = function(event) {
  var target_node = Event.element(event);
  var parent_row = target_node.parentNode.parentNode;
  if(parent_row.tagName!='TR')
    return null;
  return new GroupMemberControler(parent_row);
};

function change_role(event) {
  var controler = GroupMemberControler.from_event(event);
  event = Event.extend(event || window.event);

  var to_role;
  try {
    to_role = Event.element(event).classNames().find(function(cls) {
      return cls.substring(0, 3) == 'to_';
    }).substr(3);
  } catch(e) {
    return false;
  }

  controler.change_role(to_role);
  return false;
}

function kick_user(event) {
  GroupMemberControler.from_event(event).kick_user();
  return false;
}

function accept_join(event, accept) {
  GroupMemberControler.from_event(event).accept_join(accept);
}

POLICY_PRESETS = {
  'public-group': [true, true, true, true, 1],
  'bookmark-magazine': [false, false, true, true, 2],
  'with-friends': [true, true, false, true, 3],
  'with-co-workers': [true, true, false, false, 4],
  'private': [true, true, false, false, 5]
};
GroupSettingHelper = Class.create({
  initialize: function(field) {
    this.field = $(field);

    observe_if_exists('group-policy-preset', 'click', this.on_preset_click.bindAsEventListener(this));

    this.field.select('input[type="radio"][name="public"]').invoke(
      'observe',
      'click', this.update_policy_public.bindAsEventListener(this)
    );
    this.update_policy_public();
  },

  on_preset_click: function(event) {
    var cell = event.target;
    while(cell && cell.tagName != 'TD')
      cell = cell.parentNode;
    if(!cell || cell.className.substr(0, 7) != 'preset-')
      return;

    //
    new Effect.Highlight(cell, { restorecolor: '#fff' });

    //
    var policy = POLICY_PRESETS[cell.className.substr(7)];
    $A($('group-policy-custom').getElementsByTagName('tr')).each(function(row_node, idx) {
      if(idx < 4) {
        $('group-policy-' + idx + '-' + (policy[idx] ? 'true' : 'false')).checked = true;
      }
    });
    $('group-icon-' + policy[4]).checked = true;
    this.update_policy_public();
  },

  update_policy_public: function() {
    var buttons = this.field.select('input[type="radio"][name="autojoin"]');
    var labels = buttons.map(function(element) {
      while(element && element.tagName!='LABEL')
        element = element.next();
      return element;
    });

    if($('group-policy-3-true').checked) {
      // enable all
      buttons.invoke('enable');
      labels.invoke('removeClassName', 'disabled');
    } else {
      // disable all and click need authorized
      buttons.invoke('disable');
      labels.invoke('addClassName', 'disabled');

      buttons[1].checked = true;
    }
  }
});

/* ========== /group/* 用スクリプト ここまで ==================== */

/* ========== /report 用スクリプト ここから ==================== */
function reedit_report_form() {
  try {
  var form = $('form-report');
  form.getInputs('hidden', 'sid')[0].remove();
  form.getInputs('hidden', 'report')[0].remove();
  $('form-report-reedit').remove();

  var node = $('user-report-body');
  //node.update(['<textarea id="user-report-form" name="report">', node.innerHTML, '</textarea>'].join(''));
  new Insertion.After(node, ['<textarea id="user-report-form" name="report">', node.innerHTML, '</textarea>'].join(''));
  node.remove();
  } catch(e) {alert(e);}
}
/* ========== /report 用スクリプト ここまで ==================== */

/* ========== /signup 用スクリプト ここから ==================== */
function signup_test_account() {
  var from = $('input_account');
  var to = $('account-test');

  to.update(from.value ? from.value : _('[ユーザー名]'));
}
/* ========== /signup 用スクリプト ここまで ==================== */

/* ========== validation 用スクリプト ここから ==================== */

FormValidator = Class.create({
  KIND_NEW_ACCOUNT: 'new_account',
  KIND_CHANGED_ACCOUNT: 'changed_account',

  initialize: function() {
    this.forms = $A([]);
    this.timer_id = null;
  },

  add_form: function(node, kind_or_validator) {
try{
    var index = this.forms.length;
    node = $(node);

    // eventを監視
    var observer = this.on_form_updated.bindAsEventListener(this);
    node.observe('blur', observer);
    //node.observe('change', observer);

    // 表示用エレメントを作成
    var status = document.createElement('span');
    status.className = 'validation_status';
    status.innerHTML = '<span class="invalid"></span><span class="message"></span>';
    new Insertion.After(node, status);

    var node_info = {
      node: node,
      index: index,
      status: status,
      prev_value: null,
      need_update: false
    };
    if(Object.isFunction(kind_or_validator))
      node_info.validator = kind_or_validator;
    else
      node_info.kind = kind_or_validator;
    this.forms.push(node_info);
    this.need_validate(index);
} catch(e) { alert(e); }
  },

  set_node_status: function(index, status, message) {
    var span = this.forms[index].status;
    var nodes = span.getElementsByTagName('span');
    nodes[0].className = status;
    nodes[1].innerHTML = message;
  },

  need_validate: function(index) {
    var node_info = this.forms[index];
    if(node_info.prev_value == node_info.node.value ||
       node_info.need_update)
      return;
    node_info.need_update = true;

    // set status to 'loading'
    this.set_node_status(index, 'loading', _('チェック中...'));

    if(node_info.validator) {
      // validate immediately
      var result = node_info.validator();
      if(result)
        this.set_node_status(index, 'valid', 'ok!');
      else
        this.set_node_status(index, 'invalid', '');
      node_info.need_update = false;
    } else {
      // request validate
      if(this.timer_id) {
        clearTimeout(this.timer_id);
        this.timer_id = null;
      }
      this.timer_id = setTimeout(this.validate_forms.bind(this), 10);
    }
  },

  on_form_updated: function(event) {
try{
    var node_info = this.forms.find(function(val) {
      return val.node == event.target;
    });

    if(!node_info)
      return;

    this.need_validate(node_info.index);
} catch(e) { alert(e); }
  },

  validate_forms: function() {
    this.timer_id = null;

    // build request
    var parameters = {};
    this.forms.each(function(info, index) {
      if(info.need_update) {
        parameters['' + index] = info.kind + ':' + info.node.value;
        info.prev_value = info.node.value;
        info.need_update = false;
      }
    }.bind(this));

    new Ajax.Request('/api/misc/validate', {
          method: 'GET',
          parameters: parameters,
          onSuccess: this.on_success.bind(this)
          /*TODO onFailure: function(trans) {
            var node = self.get_confirm_cell();
            node.update(['<span class="error">', trans.responseText, '</span>'].join(''));*/
    });
  },

  on_success: function(transport) {
    var result = transport.responseText.evalJSON();

    $A(result).each(function(info) {
      this.set_node_status(info.index, info.status, info.message);
    }.bind(this));
  },

  /// classmethod
  validate_email: function(el) {
    el = $(el);
    return function() {
      return el.value.match(/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+([\.][a-z0-9-]+)+$/);
    };
  },

  validate_password: function(a) {
    a = $(a);
    return function() {
      return a.value.match(/^[a-z0-9_+`~!@#$%^&*()=\|;\':\",.<>\/?-]{6,32}/);
    };
  },

  validate_repassword: function(a, b) {
    a = $(a);
    b = $(b);
    return function() {
      return a.value && a.value == b.value;
    };
  },

  validate_nickname: function(a) {
    a = $(a);
    return function() {
      return 0 < a.value.length && a.value.length < 16;
    };
  }
});
FormValidator = new FormValidator();

function validate_account() {
  var account = $('input_account').value;
  var nodes = $('validate_account_message').getElementsByTagName('span');
  var set_status = function(status, msg) {
    var img = {
      'loading': '/static/imgs/ajax-loader.gif',
      'invalid': '/static/imgs/icon_message_problem_yes.png',
      'valid': '/static/imgs/icon_message_problem_no.png'
    };
    nodes[0].innerHTML = '<img src="' + img[status] + '" />';
    nodes[1].innerHTML = msg;
  };

  set_status('loading', 'チェック中...');

  if (account == '' || account.length > 16) {
    set_status('invalid', _('アカウントは1〜16字で入力してください'));
  } else if (/[^0-9a-zA-z\.\-\_]+/.test(account)) {
    set_status('invalid', _('使用できない記号が含まれています'));
  } else if (/^\.|^\-|^\_/.test(account)) {
    set_status('invalid', _('先頭の文字に記号は使えません'));
  } else {
    new Ajax.Request('/api/misc/validate', {
      method: 'GET',
      parameters: { '0': 'new_account:' + account },
      onSuccess: function(transport){
        var res = transport.responseText.evalJSON();
        set_status(res[0].status, res[0].message);
      }.bind(this)
    });
  }
}


/* ========== validation 用スクリプト ここまで ==================== */


/**
 * InitManagerクラス
 * - dom:loaded後に実行される処理をページ別に管理します
 * - どのページでも実行する処理 -> this.commonへ
 */
var InitManager = Class.create({
  initialize: function() {
    var global_map_ = {
      '/': this.init_root,
      '/post': this.init_post,
      '/invite': this.init_invite,
      '/signup': this.init_signup,
      '/signin': this.init_signin
    };
    if(global_map_[location.pathname])
      global_map_[location.pathname]();

    var global_map_with_regexp = [[/\/group\/g\d+\/invite\?from=create/, this.init_invite]];
    $A(global_map_with_regexp).each(function(v) {
      if(v[0].test(location.href)) v[1].apply();
    });

    if(MY_DASHBOARD && location.pathname.substring(0, MY_DASHBOARD.length) == MY_DASHBOARD) {
      var local_addr = location.pathname.substring(MY_DASHBOARD.length);
      var local_map = {
        'settings/account': this.init_local_settings_account,
        'settings/news': this.init_local_settings_news
      };

      if(local_map[local_addr])
        local_map[local_addr]();
    }

    this.common();
  },

  init_root: function() {
  },

  init_post: function() {
    Post = new Post();
  },

  init_invite: function() {
    Invite = new Invite();
    // This method render members who take part in specfied group.
    var MEMBERS_LI_ID_PREFIX = 'members_';
    var MEMBERS_ICON_PREFIX = 'icon_';
    var MEMBER_NICKNAME_PREFIX = 'nickname_';
    var render_members = function(members_or_html) {
      var html = members_or_html;
      if(members_or_html.constructor == Array) {
        html = $A(members_or_html).inject([], function(html, val) {
          if(val[0] == MY_ACCOUNT)
            return html;
          var id = MEMBERS_LI_ID_PREFIX + val[0];
          var li = '<li id="' + id + '">' +
                   '<img id="' + MEMBERS_ICON_PREFIX + val[0] + '" src="' + val[2] + '" class="user-icon"/> ' +
                   '<span id="' + MEMBER_NICKNAME_PREFIX + val[0] +
                   '" class="nickname">' + val[1].escapeHTML() + '</span>' +
                   '</li>';
          html.push(li);
          return html;
        }).join('');
      }
      $('members-list').update(html);
      return html;
    };

    // invite-email
    TextareaWithInstruction.make($('invite-email'));

    // selectable list
    var members_list = new SelectableList({
      html_id: 'members-list',
      multiple: true
    });
    var invite_list = new SelectableList({
      html_id: 'invite-list',
      multiple: true
    });

    // disable the group currently selected
    $$('input[name="group_id"]').each((function(element) {
      var selected;
      return function(element) {
        element.observe('click', function(e) {
          if (selected)
            selected.removeClassName('disabled');

          var group_id = (e.currentTarget || this).value;
          var group_el = $('group_id_' + group_id);
          if (group_el)
            selected = group_el.addClassName('disabled');
        });
      };
    })());

    // attach event hadlers
    $('select-group-form').observe('click', function(e) {
      $('invite-form-email').hide();
      $('invite-form-group').show();
    });
    $('select-email-form').observe('click', function(e) {
      $('invite-form-group').hide();
      $('invite-form-email').show();
    });
    $('add-to-invite').observe('mousedown', function(e) {
      members_list.selected().each(function(li) {
        var account = li.id.gsub(new RegExp('^' + MEMBERS_LI_ID_PREFIX), '');
        var nickname = $(MEMBER_NICKNAME_PREFIX + account).innerHTML;
        var portrait = $(MEMBERS_ICON_PREFIX + account).src;
        Invite.addMember([account, nickname, portrait]);
      });
      members_list.unselect();
      e.stop();
    });
    $('del-from-invite').observe('mousedown', function(e) {
      invite_list.selected().each(function(ele){ invite_list.remove(ele); });
      e.stop();
    });

    var selected;
    $$('#my-groups > li').each(function(element) {
      element.observe('click', (function() {
        // local cache
        var SELECTED_CLASS_NAME = 'selected';
        var cache = {};

        return function(e) {
          // if element has 'disabled' class name then just ingore.
          if((e.currentTarget || this).hasClassName('disabled')) { // FIXME: Keep up DRY
            return;
          }
          var group_id = (e.currentTarget || this).id.match(/group_id_(\d+)/)[1];
          var api_path = '/group/g' + group_id + '/enum_members';

          // unselect
          if(selected) selected.removeClassName(SELECTED_CLASS_NAME);
          selected = element.addClassName(SELECTED_CLASS_NAME);

          if(cache[group_id]) {
            // from cache
            render_members(cache[group_id]);
          } else {
            new Ajax.Request(api_path, {
              method: 'get',
              onSuccess: function(transport) {
                var members = transport.responseText.evalJSON();
                cache[group_id] = render_members(members);
              }
            });
          }
        };
      })());
    });
  },

  init_signup: function() {
    var el = $('input_account');
    if(el) {
      el.observe('change', signup_test_account);
      el.observe('keyup', signup_test_account);
    }
  },

  init_signin: function() {
    $('signin-email').focus();
  },

  // ~/settings/account
  init_local_settings_account: function() {
    FormValidator.add_form('home-setting-email-input', FormValidator.validate_email('home-setting-email-input'));
    FormValidator.add_form('home-setting-nickname-input', FormValidator.validate_nickname('home-setting-nickname-input'));
    FormValidator.add_form('home-setting-new-password', FormValidator.validate_password('home-setting-new-password'));
    FormValidator.add_form('home-setting-new-repassword',
      FormValidator.validate_repassword('home-setting-new-password', 'home-setting-new-repassword')
    );
  },

  // ~/settings/news
  init_local_settings_news: function() {
    FormValidator.add_form('input-to-address', FormValidator.validate_email('input-to-address'));
  },

  common: function() {
    var node;

    node = $('filter-group-container');
    if(node)
      GroupFilter = new GroupFilter();

    node = $('search-query');
    if(node)
      Search = new Search();

    node = $('bookmark-list');
    if(node && !window.NO_BOOKMARKLIST)
      BookmarkList = new BookmarkList(node);

    node= $('tag-filter');
    if(node)
      TagFilter = new TagFilter(node);

    node = $('tag-suggest');
    if(node)
      TagSuggest = new TagSuggest(node, { request_url:MY_DASHBOARD + 'list_tags' });

    node = $('search-group-input');
    if(node)
      new TagAutocomplete(node);

    node = $('group-settings-field');
    if(node)
      new GroupSettingHelper(node);

    ajax_now_loading();
  }
});
document.observe('dom:loaded', change_utc_date);
document.observe('dom:loaded', function(){ new InitManager(); });

function ajax_now_loading() {
  var is_ie6 = (Prototype.Browser.IE) && (typeof document.body.style.maxHeight == "undefined");

  var nowloading = new Element('div', { id:'nowloading' })
    .setStyle({ position:(is_ie6 ? 'absolute' : 'fixed') })
    .hide();
  $(document.body).insert(nowloading);

  Ajax.Responders.register({
    onCreate: function() {
      if (is_ie6)
        nowloading.setStyle({ left:'2px', top:(document.body.parentNode.scrollTop+2+'px') });
      if (Ajax.activeRequestCount > 0)
        Effect.Appear(nowloading, { duration:0.01, queue:'end' });
    },
    onComplete: function() {
      if (Ajax.activeRequestCount == 0)
        Effect.Fade(nowloading, { duration:0.4, queue:'end' });
    }
  });
}

/**
 * <span class="date-utc 2008-9-29-9-5-46">09/29 18:05</span>みたいなのを、現地時刻に変換する
 */
function change_utc_date() {
  var now = new Date().getTime();
  $$('span.date-utc').each(function(el) {
    var tt = ('[' + el.className.substr(9).replace(/-/g, ',') + ']').evalJSON();
    var date = new Date(Date.UTC(tt[0], tt[1] - 1, tt[2], tt[3], tt[4], tt[5]));
    var delta = now - date.getTime();

    if (delta < 1000*60*60*24*365) {
      el.replace(new Template(_('#{month}/#{date} #{hours}:#{minutes}')).evaluate({
        month: (date.getMonth()+1).digit(2),
        date: date.getDate().digit(2),
        hours: date.getHours().digit(2),
        minutes: date.getMinutes().digit(2)
      }));
    } else {
      el.replace(new Template(_('#{year}/#{month}/#{date} #{hours}:#{minutes}')).evaluate({
        year: date.getFullYear(),
        month: (date.getMonth()+1).digit(2),
        date: date.getDate().digit(2),
        hours: date.getHours().digit(2),
        minutes: date.getMinutes().digit(2)
      }));
    }
  });
}

var Widget = Class.create({
  initialize: function() {},

  toggle_filter: function() {
    var checked_groups = $$('input[name="widget_filter"]')
      .select(function(e){ return e.checked; })
      .map(function(e){ return "checked_group_ids=" + e.value; });

    new Ajax.Request(
      '/api/self/set_widget_filter',
      {
        method: 'post',
        parameters: checked_groups.join("&"),
        onSuccess: function(res) {
            ($('widget-preview').contentDocument || $('widget-preview').contentWindow.document).location.reload();
        },
        onFailure: function(res) {}
      }
    );
  },

  user_uri_update: function() {
    new Ajax.Request(
      '/api/self/user_uri_update',
      {
        method: 'post',
        onSuccess: function(res) {
          var resobj = res.responseText.evalJSON();

           $('widget-preview').src = $('widget-preview').src.replace(resobj.old_hash, resobj.new_hash);

          var feed_url = $('feed_url');
          feed_url.innerHTML = feed_url.innerHTML.replace(resobj.old_hash, resobj.new_hash);
          feed_url.href = feed_url.href.replace(resobj.old_hash, resobj.new_hash);

          var widget_url = $('widget_url');
          widget_url.value = widget_url.value.replace(resobj.old_hash, resobj.new_hash);

          var igoogle_url = $('igoogle_url');
          igoogle_url.innerHTML = igoogle_url.innerHTML.replace(resobj.old_hash, resobj.new_hash);
          igoogle_url.href = igoogle_url.href.replace(resobj.old_hash, resobj.new_hash);

          [feed_url, widget_url, igoogle_url].each(function(el){
            new Effect.Highlight(el, { restorecolor: '#fff'});
          });
        },
        onFailure: function(res) {}
      }
    );
  },

  group_uri_update: function(group_id) {
    new Ajax.Request(
      '/api/self/group_uri_update',
      {
        method: 'post',
        parameters: { group_id:group_id },
        onSuccess: function(res) {
          var resobj = res.responseText.evalJSON();

          $('widget-preview-of-group').src = $('widget-preview-of-group').src.replace(resobj.old_hash, resobj.new_hash);

          var feed_url = $('feed_url');
          feed_url.innerHTML = feed_url.innerHTML.replace(resobj.old_hash, resobj.new_hash);
          feed_url.href = feed_url.href.replace(resobj.old_hash, resobj.new_hash);

          var widget_url = $('widget_url');
          widget_url.value = widget_url.value.replace(resobj.old_hash, resobj.new_hash);

          var igoogle_url = $('igoogle_url');
          igoogle_url.innerHTML = igoogle_url.innerHTML.replace(resobj.old_hash, resobj.new_hash);
          igoogle_url.href = igoogle_url.href.replace(resobj.old_hash, resobj.new_hash);

          [feed_url, widget_url, igoogle_url].each(function(el){
            new Effect.Highlight(el, { restorecolor: '#fff'});
          });
        },
        onFailure: function(res) {}
      }
    );
  }
});
Widget = new Widget();
