/**
 * ProgressManagerクラス
 *
 */
var ProgressManager = Class.create();

ProgressManager.instance = null;

/**
 * Singleton
 */
ProgressManager.getInstance = function() {
  var instance = ProgressManager.instance;

  if (instance == null) {
    instance = ProgressManager.instance = new ProgressManager();
  }

  return instance;
}

ProgressManager.prototype = {
  calendarStartDate: null,
  traders: {},
  tags: {},
  linkers: [],

  /**
   * initialize
   *
   */
  initialize: function() {
  },

  /**
   * load
   *
   */
  load: function(ownerId) {

    /*
     * 指定されたURLにリクエストを送信し,
     * レスポンスをProgressManagerのプロパティに設定する。
     */
    new Ajax.Request('/progress/read/' + ownerId, {
      onSuccess: function(req, obj) {

        /*
         * スクリプトとして実行する。
         */
        eval('var res = ' + req.responseText + ';');

        /*
         * 業者, タグ, リンカをクリアする。
         */
        this.traders = {};
        this.tags = {};
        this.clearLinker();
        this.linkers = [];

        /*
         * 業者, タグ, リンカに受け取った内容を追加する。
         */
        res.traders.each(function(trader) {
          this.addTrader(new Trader(trader));
        }.bind(this));

        res.tags.each(function(tag) {
          this.addTag(new Tag(tag));
        }.bind(this));

        res.linkers.each(function(linker) {
          this.addLinker(linker.from, linker.to);
        }.bind(this));

        /*
         * 画面に反映させる。
         */
        this.enumerateTrader();
        this.deployTag();
        this.drawLinker();

      }.bind(this)
    });

  },

  /**
   * save
   *
   */
  save: function(ownerId) {

    /*
     * リクエストパラメータを構築して, リクエストを送信する。
     */
    var params = {
      traders: this.getTraderList().invoke('toJson'),
      tags: this.getTagList().invoke('toJson'),
      linkers: this.linkers
    };

    new Ajax.Request('/progress/write/' + ownerId, {
      parameters: Hash.toQueryStringForRails(params),

      onSuccess: function(req, obj) {
        this.load(ownerId);
      }.bind(this)
    });

  },

  /**
   * enumerateTrader
   *  登録業者一覧を業者リスト要素に配置する。
   *
   */
  enumerateTrader: function() {

    /*
     * 処理に必要な要素を取得する。
     */
    var traderList = $('trader_list');
    var tagDeployer = $('tag_deployer');

    /*
     * 業者リスト要素に配置してあるタグをすべて削除する。
     */
    var childs = traderList.childNodes;
    var length = childs.length;

    for (var i = length - 1; i >= 0; i--) {
      traderList.removeChild(childs[i]);
    }

    /*
     * 業者リスト要素およびタグ配置要素の高さを,
     * 業者のリストの件数に合わせる。
     */
    var traders = this.getTraderList();
    var heightString = traders.length * 40 + 'px';

    traderList.style.height = heightString;
    tagDeployer.style.height = heightString;

    /*
     * 業者のリストを並び替え, リストを生成する。
     */
    var df = document.createDocumentFragment();
    var sort = function(trader) {
      return trader.getOrder();
    }

    traders.sortBy(sort).each(function(trader) {
      df.appendChild(trader.getElement());
    });

    traderList.appendChild(df);

    /*
     * 業者リスト要素内にある要素の並び替えを有効にする。
     */
    var sortableOption = {
      tag: 'li',
      overlap: 'vertical',

      onUpdate: function(list) {
        var childs = list.childNodes;
        var length = childs.length;
        var offset = Trader.TRADER_ID_PREFIX.length;
        var order = 1;

        for (var i = 0; i < length; i++) {
          this.traders[childs[i].id.substring(offset)].setOrder(order++);
        }

        this.clearLinker();
        this.deployTag(false);
        this.drawLinker();
      }.bind(this)
    };

    Sortable.create(traderList, sortableOption);

  },

  /**
   * deployTag
   *
   */
  deployTag: function(resetCalendar) {

    /*
     * 処理に必要な要素を取得する。
     */
    var tagDeployer = $('tag_deployer');

    /*
     * タグ配置要素に配置してあるタグをすべて削除する。
     */
    tagDeployer.getElementsByClassName('tag').each(function(tag) {
      tagDeployer.removeChild(tag);
    });

    /*
     * タグの追加件数が0件の場合は, 処理を中断する。
     */
    var tags = this.getTagList();

    if (tags.length == 0) {
      return;
    }

    /*
     * カレンダーをセットする。
     */
    if (typeof resetCalendar == 'undefined' || resetCalendar) {

      /*
       * タグのリストの中からもっとも過去の開始日と,
       * もっとも未来の終了日を取得する。
       */
      var firstTag = tags.shift();
      var minDate = firstTag.getStartDate();
      var maxDate = firstTag.getEndDate();

      tags.each(function(tag) {
        var startDate = tag.getStartDate();
        var endDate = tag.getEndDate();

        if (minDate.before(startDate)) {
          minDate = startDate;
        }

        if (maxDate.after(endDate)) {
          maxDate = endDate;
        }
      });

      tags.push(firstTag);

      /*
       * 取得した開始日と終了日を元にカレンダーをセットする。
       */
      this.setCalendar(minDate, maxDate);

    }

    /*
     * タグをタグの開始日と期間および業者に応じて,
     * タグ配置要素に配置していく。
     */
    tags.each(function(tag) {

      /*
       * タグ要素およびタグ名表示要素を取得する。
       */
      var tagElement = tag.getElement();
      var handlerElement = tagElement.getElementsByClassName('handler')[0];

      /*
       * タグを配置する座標および幅を求める。
       */
      var left = tag.getStartDate().between(this.calendarStartDate) * 40 + 4;
      var top = (this.traders[tag.getTraderId()].order - 1) * 40 + 4;
      var width = tag.getPeriod() * 40 - 9;

      /*
       * タグ要素およびタグ名表示要素のスタイルを設定する。
       */
      Element.setStyle(tagElement, {
        position: 'absolute', // ここで絶対配置を指定しないとIEでダメ
        left: left + 'px',
        top: top + 'px',
        width: width + 'px'
      });
      handlerElement.style.width = (width - 18) + 'px';

      /*
       * タグをタグ配置要素に追加する。
       */
      tagDeployer.appendChild(tagElement);

    }.bind(this));
  },

  /**
   * getLinkedTagAndLinker
   *
   */
  getLinkedTagAndLinker: function(tag) {

    /*
     * 与えられたタグと,
     * それにリンクしているタグおよびその間のリンカを取得し, 返す。
     */
    var tagId = tag.getTagId();
    var tags = this.tags;
    var linkedTags = [tag];
    var linkers = [];

    this.linkers.each(function(linker) {
      var from = linker.from;
      var to = linker.to;
      var linked = true;

      /*
       * リンカの始端または終端に, 与えられたタグが含まれる場合は,
       * その片方をリンクしているタグとみなして, 配列に追加する。
       * 含まれない場合は, リンクしていないタグとし, フラグを折る。
       */
      if (from == tagId) {
        linkedTags.push(tags[to]);
      } else if (to == tagId) {
        linkedTags.push(tags[from]);
      } else {
        linked = false;
      }

      /*
       * タグがリンクされていた場合は,
       * そのリンカの要素を取得し, 配列に追加する。
       */
      if (linked) {
        var linker = Linker.getElement(from, to);

        if (linker != null) {
          linkers.push(linker);
        }
      }

    });

    return {tags: linkedTags, linkers: linkers};
  },

  /**
   * activateTagAndLinker
   *
   */
  activateTagAndLinker: function(tag) {

    /*
     * 与えられたタグにリンクしているタグと,
     * その間のリンカを取得し, アクティヴにする。
     */
    var linkedTagAndLinker = this.getLinkedTagAndLinker(tag);

    linkedTagAndLinker.tags.each(function(tag) {
      tag.activate();
    });

    linkedTagAndLinker.linkers.each(function(linker) {
      linker.addClassName('active_linker');
    });

    TagDisplay.getInstance().showPlain(tag);

  },

  /**
   * inactivateTagAndLinker
   *
   */
  inactivateTagAndLinker: function(tag) {

    /*
     * 与えられたタグにリンクしているタグと,
     * その間のリンカを取得し, 非アクティヴにする。
     */
    var linkedTagAndLinker = this.getLinkedTagAndLinker(tag);

    linkedTagAndLinker.tags.each(function(tag) {
      tag.inactivate();
    });

    linkedTagAndLinker.linkers.each(function(linker) {
      linker.removeClassName('active_linker');
    });

    TagDisplay.getInstance().hidePlain();

  },

  /**
   * drawLinker
   *
   */
  drawLinker: function(tag) {

    /*
     * 描画する対象となるリンカを選択する。
     * ただし引数でタグが与えられている場合は,
     * そのタグに繋がっているリンカのみを描画対象とする。
     */
    var linkers = null;

    if (tag) {
      var tagId = tag.getTagId();

      linkers = this.linkers.select(function(linker) {
        return linker.from == tagId || linker.to == tagId;
      });
    } else {
      linkers = this.linkers;
    }

    /*
     * リンカを生成して, タグ配置に追加する。
     */
    var df = document.createDocumentFragment();
    var tags = this.tags;

    linkers.each(function(linker) {
      var fromTag = tags[linker.from];
      var toTag = tags[linker.to];
      var linkerElement = Linker.create(fromTag, toTag);

      if (linkerElement != null) {
        df.appendChild(linkerElement);
      }
    });

    $('tag_deployer').appendChild(df);

  },

  /**
   * clearLinker
   *
   */
  clearLinker: function(tag) {

    /*
     * リンカを削除するためのイテレータを設定する。
     * ただし引数でタグが与えられている場合は,
     * そのタグに繋がっているリンカのみを削除するイテレータとする。
     */
    var tagDeployer = $('tag_deployer');
    var iterator = null;

    if (tag) {
      var tagId = tag.getTagId();
      var linkerIdSeparator = Linker.LINKER_ID_SEPARATOR;
      var regexpString = '^'
                       + Linker.LINKER_ID_PREFIX
                       + '(?:'
                       + tagId
                       + linkerIdSeparator
                       + '.+|.+'
                       + linkerIdSeparator
                       + tagId
                       + ')$';
      var regexp = new RegExp(regexpString);

      iterator = function(linker) {
        if (regexp.test(linker.id)) {
          tagDeployer.removeChild(linker);
        }
      }
    } else {
      iterator = function(linker) {
        tagDeployer.removeChild(linker);
      }
    }

    /*
     * タグ配置要素にあるリンカをすべて取得し,
     * イテレータを適用する。
     */
    tagDeployer.getElementsByClassName('linker').each(iterator);

  },

  /**
   * setCalendar
   *  与えられた開始日を含む週と, 終了日を含む週で, カレンダーを生成する。
   *  ただし週は月曜開始となる。
   *
   * @param startDate Date 開始日
   * @param endDate Date 終了日
   *
   */
  setCalendar: function(startDate, endDate) {

    /*
     * 処理に必要な要素を取得する。
     */
    var calendar = $('calendar');
    var tagDeployer = $('tag_deployer');

    /*
     * カレンダー要素内にある全要素を削除する。
     */
    var childs = calendar.childNodes;

    for (var i = childs.length - 1; i >= 0; i--) {
      calendar.removeChild(childs[i]);
    }

    /*
     * 開始日を含む週の月曜日と, 終了日を含む週の日曜日の日付を求める。
     */
    startDate = startDate.shift(-startDate.getDayFromMonday()).midnight();
    endDate = endDate.shift(6 - endDate.getDayFromMonday()).midnight();

    /*
     * タグを配置するときの基点とさせるため,
     * カレンダーの開始日を保持する。
     */
    this.calendarStartDate = startDate;

    /*
     * 週を生成する。
     */
    var df = document.createDocumentFragment();
    var endTime = endDate.getTime();
    var weekLength = 7;
    var weekCount = 0;

    while (startDate.getTime() < endTime) {

      /*
       * 週の見出しを生成する。
       */
      var weekHeader = Builder.node('div');

      weekHeader.innerHTML = startDate.getFullYear()
                           + ' - '
                           + (startDate.getMonth() + 1)
                           + ' - '
                           + startDate.getDate();

      /*
       * 週を構成する日のリストを生成する。
       */
      var weekDays = Builder.node('ul');

      weekLength.times(function() {

        /*
         * 曜日に応じて週を構成する日のクラスを設定する。
         */
        var className;

        switch (startDate.getDay()) {
          case 6:
            className = 'saturday';
            break;
          case 0:
            className = 'sunday';
            break;
          default:
            className = 'weekday';
            break;
        }

        /*
         * 週を構成する日の生成し, リストの要素に追加する。
         */
        var day = Builder.node('li', {className: className});

        day.innerHTML = startDate.getDate();
        weekDays.appendChild(day);

        /*
         * 日を進める。
         */
        startDate = startDate.tomorrow();

      });

      /*
       * 生成した週の見出しとその週を構成する日のリストを包括する要素を生成し,
       * ドキュメント・フラグメントに追加する。
       */
      df.appendChild(Builder.node('div', {className: 'week'}, [weekHeader, weekDays]));

      /*
       * 週を生成した回数を数える。
       */
      weekCount++;

    }

    /*
     * 生成した週の分に応じて, カレンダー要素とタグ配置要素の幅を調整する。
     */
    calendar.style.width = tagDeployer.style.width = (weekCount * 280) + 'px';

    /*
     * 生成した要素をカレンダー要素に追加する。
     */
    calendar.appendChild(df);

  },

  /**
   * getTraderList
   *
   */
  getTraderList: function() {
    var originalTraders = this.traders;
    var traders = [];

    for (var traderId in originalTraders) {
      traders.push(originalTraders[traderId]);
    }

    return traders;
  },

  /**
   * getTrader
   *
   */
  getTrader: function(traderId) {
    var trader = this.traders[traderId];

    return (typeof trader == 'undefined') ? null : trader;
  },

  /**
   * addTrader
   *
   */
  addTrader: function(trader) {
    this.traders[trader.getTraderId()] = trader;
  },

  /**
   * removeTrader
   *
   */
  removeTrader: function(trader) {
    /*
     * 実装がめんどう
     * ・業者の削除
     * ・オーダの振りなおし
     * ・業者の削除に伴うタグの削除
     * ・タグの削除に伴うリンカの削除(removeTagを使えばいいけど)
     * ・タグの再配置
     */
  },

  /**
   * getTagList
   */
  getTagList: function() {
    var originalTags = this.tags;
    var tags = [];

    for (var tagId in originalTags) {
      tags.push(originalTags[tagId]);
    }

    return tags;
  },

  /**
   * getTag
   *  与えられたタグIDに基づくタグを, 登録タグ一覧より取得する。
   *  もしタグIDに基づくタグが, 登録タグ一覧に存在しなかった場合は,
   *  Nullを返す。
   *
   * @param tagId String タグID
   * @return Tag タグIDに基づくタグ
   *
   */
  getTag: function(tagId) {
    var tag = this.tags[tagId];

    return (typeof tag == 'undefined') ? null : tag;
  },

  /**
   * addTag
   *  与えられたタグを, 登録タグ一覧に追加する。
   *
   * @param tag Tag 追加するタグ
   *
   */
  addTag: function(tag) {
    this.tags[tag.getTagId()] = tag;
  },

  /**
   * removeTag
   *  与えられたタグを, 登録タグ一覧から削除する。
   *  またそのタグに関わるリンカも削除する。
   *
   * @param tag Tag 除外するタグ
   *
   */
  removeTag: function(tag) {

    /*
     * 除外するタグに関わるリンカを, タグ配置要素上から取り除く,
     */
    this.clearLinker(tag);

    /*
     * 除外するタグをタグのリストから削除し,
     * タグの再配置を行う。
     */
    var tagId = tag.getTagId();

    delete this.tags[tagId];
    this.deployTag(false);

    /*
     * 除外するタグに関わるリンカをリンカのリストから削除する。
     */
    this.linkers = this.linkers.reject(function(linker) {
      return linker.from == tagId || linker.to == tagId;
    });

    /*
     * タグメニューを非表示にする。
     */
    TagMenu.getInstance().hide();

  },

  /**
   * getLinkerList
   *
   */
  getLinkerList: function() {
    return this.linkers;
  },

  /**
   * addLinker
   *  与えられた始端タグIDと終端タグIDのリンカを追加する。
   *
   * @param fromTagId String 始端タグID
   * @param toTagId String 終端タグID
   *
   */
  addLinker: function(fromTagId, toTagId) {

    /*
     * 始端タグIDと終端タグIDが異なり,
     * この組み合わせのリンカがいまだ存在しなければ, リンカの追加を行う。
     */
    if (fromTagId != toTagId && Linker.getElement(fromTagId, toTagId) == null) {
      this.linkers.push({from: fromTagId, to: toTagId});
    }

  },

  /**
   * removeLinker
   *  与えられた始端タグIDと終端タグIDのリンカを削除する。
   *
   * @param fromTagId String 始端タグID
   * @param toTagId String 終端タグID
   *
   */
  removeLinker: function(fromTagId, toTagId) {

    /*
     * タグ配置要素から指定されたリンカを削除する。
     * ただしリンカが取得できなかった場合は,
     * リンカの削除を行わず, 処理を終了する。
     */
    var linker = Linker.getElement(fromTagId, toTagId);

    if (linker == null) {
      return;
    }

    Element.remove(linker);

    /*
     * 指定されたリンカに一致しないリンカを選択する。
     */
    this.linkers = this.linkers.reject(function(linker) {
      var from = linker.from;
      var to = linker.to;

      return from == fromTagId && to == toTagId || from == toTagId && to == fromTagId;
    });

  },

  /**
   * getCalendarStartDate
   *
   */
  getCalendarStartDate: function() {
    return this.calendarStartDate;
  }

};

