/**
 * FeedBlog SearchScript
 *
 * @copyright 2009 FeedBlog Project (http://sourceforge.jp/projects/feedblog/)
 * @author Kureha Hisame (http://www.lunardial.com/) & Yui Naruse (http://airemix.com/)
 * @since 2009/01/08
 * @version 1.5.1.0
 */
// ブログ本体のHTMLファイルの名前を記入してください
var blogUrl = "./index.html"

// Ext jsパネルのサイズを記述してください
var extPanelWidth = 500;

/**
 * XMLファイルから読み込んだファイルのバリデートモードを選択します。
 * 0 = 改行コード部分に<br/>を挿入
 * 1 = 改行コード部分に<br/>を挿入しない
 */
var validateMode = "0";

// 現在の検索語のキャッシュ
var currentSearchWords;

// fetchEntries 用のセマフォ
var fetchEntriesSemaphore = new Semaphore();

// ログのファイルリストを格納するグローバル変数です
var logData;

// コンボボックスのオブジェクトを格納するグローバル変数です
var comboBox;

/**
 * Extへのイベント登録です。すべてのDOMが利用可能になった時点で実行されます。
 */
Ext.onReady(function(){
    // XMLのデータをロードします
    logData = logXMLLoader();

    // テキストボックスをExt js化し、空欄入力を拒否します
    var searchTextBox = new Ext.form.TextField({
        applyTo: "searchWord",
        allowBlank: false
    });

    new Ext.form.Checkbox({
        applyTo: "allSearchCheck",
        boxLabel: "すべてのログに対して検索を行う",
        checked: true
    });

    new Ext.form.Checkbox({
        applyTo: "isAsyncOn",
        boxLabel: "非同期通信モードで検索を行う",
        checked: false
    });

    new Ext.form.Checkbox({
        applyTo: "regexpOptionI",
        boxLabel: "大文字、小文字を区別しない",
        checked: true
    });

});

/**
 * 日記の描画を行います。この部分を編集することでデザインを変更可能です。
 * @param {String} title パネルのタイトル部分に表示する文字列
 * @param {String} drawitem パネルの本文を格納したDIV要素のid
 * @param {String} renderto 「タイトル・更新日時・本文」の1日分の日記データを焼き付けるDIV要素のid
 * @param {String} closed (Ext jsパネルオプション)日記をクローズ状態で生成するか否か
 */
function generatePanel(title, drawitem, renderto, closed){
    // Ext jsパネルを生成する
    new Ext.Panel({
        contentEl: drawitem,
//        width: extPanelWidth,
        title: title,
        hideCollapseTool: false,
        titleCollapse: true,
        collapsible: true,
        collapsed: closed,
        renderTo: renderto
    });
}

/**
 * 記事クラス
 * @param {Object} obj entry 要素の DOM オブジェクトか、ハッシュ
 */
function Entry(obj) {
//    if (obj.getElementsByTagName) {
    if (typeof obj.getElementsByTagName != 'undefined') {
        this.title = $("title:first", obj).text();
        if (this.title == "") requiredElementError(obj, "title");
        this.title = "<span>" + validateText(this.title) + "</span>";
        this.content = $("content:first", obj).text();
        this.content = "<span>" + validateText(this.content) + "</span>";
        this.id = $("id:first", obj).text();
        if (this.id == "") requiredElementError(obj, "id");
        this.date = $("updated:first", obj).text();
        if (this.date == "") requiredElementError(obj, "updated");
        this.date = validateData(this.date);
    }
    else {
        this.title = hash['title'];
        this.content = hash['content'];
        this.id = hash['id'];
        this.date = hash['date'];
    }
}

/**
 * 記事内が単語群を全て含んでいるか
 * @param {Array} keywords 単語群
 * @param {String} regexpType 正規表現の検索モードを示す文字列
 * @return {boolean} bool 全て含んでいれば true、さもなくば false
 */
Entry.prototype.hasKeywords = function (keywords, regexpType) {
    // 正規表現が一致するかという判定"のみ"を行います
    for (var i = 0; i < keywords.length; i++) {
        // 正規表現チェック用のオブジェクトを用意します(OR条件は一時的に条件を置換)
        var reg = new RegExp('(?:' + keywords[i] + ')(?![^<>]*>)', regexpType);
        // 一致しなかったらその時点で脱出
        if (!reg.test(this.content) && !reg.test(this.title)) return false;
    }
    return true;
}

/**
 * 呼び出すとDIV:id名:writeArea上のHTMLを削除し、ロードエフェクトを表示します
 */
function loadingEffect(){
    var writeArea = Ext.getDom("writeArea");
    writeArea.innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';

    var drawItem = Ext.getDom("drawItem");
    drawItem.innerHTML = '<br/><img src="./library/ext/resources/images/default/shared/blue-loading.gif"><br/>長時間画面が切り替わらない場合はページをリロードしてください。<br/><br/>';

    // ロード表示用のパネルを生成
    generatePanel("Now Loading .....", "drawItem", "drawPanel", false);
}

/**
 * 日記データのエラー時の処理を行います
 */
function showError(){
    var writeArea = Ext.getDom("writeArea");
    writeArea.innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';

    var drawItem = Ext.getDom("drawItem");
    drawItem.innerHTML = '<br/>日記ファイルのロードに失敗しました！<br/><br/>';

    // エラー内容をパネルに描画
    generatePanel("Error!", "drawItem", "drawPanel", false);

    Ext.Msg.alert("Error!", "日記ファイルが読み込めません！");
}

/**
 * 日記データのエラー時の処理を行います
 */
function notFoundError(){
    var writeArea = Ext.getDom("writeArea");
    writeArea.innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';

    var drawItem = Ext.getDom("drawItem");
    drawItem.innerHTML = '<br/>検索条件に一致する日記は見つかりませんでした。<br/><br/>';

    // エラー内容をパネルに描画
    generatePanel("Error!", "drawItem", "drawPanel", false);
}

/**
 * 日記データのエラー時の処理を行います
 */
function requiredElementError(parent, name) {
    Ext.Msg.alert("Error!", parent.ownerDocument.URL + ": 必須な要素 " +
                  name + " が存在しないか空な " + parent.tagName + " 要素が存在します");
}

/**
 * 日付のHTML表示用バリデーション処理を行います
 * @param {String} data RFC3339形式のdate-time文字列
 */
function validateData(data){
    data = data.replace(/T/g, " ");

    // 秒数の小数点以下の部分はカットする
    data = data.substring(0, 19);

    return data;
}

/**
 * 日記本文のバリデーション処理を行います
 * @param {String} contents 日記の本文が格納されている文字列
 */
function validateText(contents){
    // <br/>タグを挿入する
    if (validateMode == 0) {
        contents = contents.replace(/[\n\r]|\r\n/g, "<br />");
    }

    return contents;
}

/**
 * XML用に要素をエスケープします
 * @param {String} str エスケープを行いたい文字列
 */
function xmlAttrContentEscape(str) {
    return str
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;');
}

/**
 * 日記本文に日付を付加します
 * @param {String} contents 日記の本文が格納されている文字列
 * @param {String} id 日記の初公開日を示す日付文字列
 */
function contentsWithid(contents, id){
    // リンク用文末作成
    var hashTag = '<br/><div style="text-align: right;"><a href="' +
        xmlAttrContentEscape(blogUrl) + '#' + xmlAttrContentEscape(id) +
        '" target="_blank">- この日の記事にリンクする -<\/a><\/span>';
    return contents + hashTag;
}

/**
 * 強調タグを追加します
 * @param {String} word 強調したい語句
 */
function emphasizeWord(word){
    return '<span style="background-color: red;">' + word + '</span>';
}

/**
 * 長い順に並べるための比較関数です
 * @param {String} a 比較対象(1)
 * @param {String} b 比較対象(2)
 */
function compareLengthDecrease(a, b){
    a = a.length;
    b = b.length;
    return a > b ? -1 : a < b ? 1 : 0;
}

/**
 * セマフォ制御用の関数です
 * @param {Object} hash
 */
function Semaphore(hash){
    this.id = null;
    this.count = 0;
    this.buf = [];
    this.xhrs = [];
}

/**
 * セマフォ初期化用の関数です
 */
Semaphore.prototype.init = function(){
    while (this.xhrs.length > 0) {
        this.xhrs.shift().abort();
    }
    this.id = Math.random();
    this.count = 0;
    this.buf = [];
}

/**
 * ログファイル選択用のコンボボックスをid名:logSelecterに生成します
 */
function logXMLLoader(){
    // XMLファイルのロードロジック生成
    var logXMLData = new Ext.data.Store({
        url: "./xml/loglist.xml",
        reader: new Ext.data.XmlReader({
            record: "file"
        }, [{
            name: "display"
        }, {
            name: "path"
        }])
    });

    // XMLを実際にロード
    logXMLData.load();

    // コンボボックスを生成
    comboBox = new Ext.form.ComboBox({
        store: logXMLData,
        applyTo: "logSelecter",
        displayField: "display",
        valueField: "path",
        triggerAction: "all",
        emptyText: "ログを選択してください..."
    });

    // コンボボックスのイベント登録
    comboBox.on("change", function(){
    });

    // ログXMLファイルを読み込む
    var loader = new jQuery.ajax({
        url: "./xml/loglist.xml",
        method: "POST",
        error: showError,
        success: function(xmlData){
            // ファイルパスの要素のみを抽出する
            var separateTag = xmlData.getElementsByTagName("file");
            logData = new Array(separateTag.length);

            // すべてのファイルパスを配列に格納する
            for (var i = 0; i < separateTag.length; i++) {
                // "path"ノードの値を格納
                logData[i] = separateTag[i].getElementsByTagName("path")[0].firstChild.nodeValue;
            }
        }
    });
}

/**
 * 検索単語を取得します
 */
function getSearchWords(){
    var searchWord = document.getElementById("searchWord").value;
    if (searchWord == "")
        return null;
    var searchWords = [];

    // 検索単語をサニタイジングします
    // HTMLのメタ文字
    searchWord = xmlAttrContentEscape(searchWord);
    // 正規表現のメタ文字
    searchWord = searchWord.replace(/([$()*+.?\[\\\]^{}])/g, '\\$1');
    // 半角スペースで配列に分割
    searchWords = searchWord.replace(/^\s+|\+$/g, '').split(/\s+/);
    // 正規表現の選択を長い順に並び替えます(AND条件)
    searchWords.sort(compareLengthDecrease);

    return searchWords.length == 0 ? null : searchWords;
}

/**
 * 文章内を特定の単語で検索し、一致した部分を強調表示タグで置き換えます
 * @param {String} searchWord 探索する単語
 * @param {String} plainText 探索を行う文章
 * @param {String} regexpType 正規表現の検索モードを示す文字列
 */
function complexEmphasize(searchWord, plainText, regexpType){
    // 正規表現の選択を長い順に並び替える
    searchWord = searchWord.split('|').sort(compareLengthDecrease).join('|');
    // タグの内側でないことを確認する正規表現を追加
    var pattern = new RegExp('(?:' + searchWord + ')(?![^<>]*>)', regexpType);

    var result = [];
    var currentIndex = -1; // 現在マッチしている部分文字列の開始位置
    var currentLastIndex = -1; // 現在マッチしている部分文字列の、現在の末尾
    var m; // 正規表現マッチの結果配列
    while (m = pattern.exec(plainText)) {
        if (m.index > currentLastIndex) {
            // 新しい部分文字列へのマッチが始まったので、そこまでの文字列をバッファに書き出す
            if (currentIndex < currentLastIndex)
                result.push(emphasizeWord(plainText.substring(currentIndex, currentLastIndex)));
            result.push(plainText.substring(currentLastIndex, m.index));
            // 開始位置の更新
            currentIndex = m.index;
        }
        // 末尾位置を更新
        currentLastIndex = pattern.lastIndex;
        // 次の正規表現マッチは今マッチした文字の次の文字から
        pattern.lastIndex = m.index + 1;
    }
    // 残った文字列を書き出す
    if (currentIndex < currentLastIndex)
        result.push(emphasizeWord(plainText.substring(currentIndex, currentLastIndex)));
    result.push(plainText.substring(currentLastIndex));

    // 結合して返す
    return result.join('');
}

/**
 * 検索結果の表示
 */
function showEntries(){
    var entries = fetchEntriesSemaphore.buf;

    // 一件もヒットしなかった場合は専用のパネルを表示して終了
    if (entries.length == 0) {
        notFoundError();
        return;
    }

    // entryを id でソート
    entries = entries.sort(function(a, b){
        a = a.id;
        b = b.id;
        return a > b ? -1 : a < b ? 1 : 0
    });

    var stringBuffer = [];
    var writeArea = Ext.getDom("writeArea");
    for (var i = 0; i < entries.length; i++) {
        stringBuffer.push('<div id="drawPanel');
        stringBuffer.push(i);
        stringBuffer.push('"><div id="drawItem');
        stringBuffer.push(i);
        stringBuffer.push('" class="code"><\/div><\/div><br/>');
    }
    writeArea.innerHTML = stringBuffer.join('');

    stringBuffer.length = 0;
    for (i = 0; i < entries.length; i++) {
        var entry = entries[i];
        document.getElementById("drawItem" + i).innerHTML = contentsWithid(entry.content, entry.id);
        generatePanel(entry.title + " / " + entry.date, "drawItem" + i, "drawPanel" + i, false);
    }

    // 検索結果を表示します
    document.getElementById("resultWriteArea").innerHTML = "\<b\>検索結果\</b\>\<br/\>" + entries.length + "件の記事が該当しました";
}

/**
 * 検索時のjQuery.ajaxのcallback関数
 */
function fetchEntries(xmlData){
    // 大文字小文字を区別するかを取得します
    var regexpOptionI = document.searchForm.regexpOptionI.checked;
    var regexpType = regexpOptionI ? "ig" : "g";

    // entry要素のみを切り出します
    var entries = xmlData.getElementsByTagName("entry");

    // entry要素の回数だけ実行します
    for (var j = 0; j < entries.length; j++) {
        var entry = new Entry(entries[j]);

        // 正規表現が一致した場合は、強調表現処理を行います
        if (entry.hasKeywords(currentSearchWords, regexpType)) {
            // 強調表現を実行します
            entry.title = complexEmphasize(currentSearchWords.join("|"), entry.title, regexpType);
            entry.content = complexEmphasize(currentSearchWords.join("|"), entry.content, regexpType);

            fetchEntriesSemaphore.buf.push(entry);
        }
    }

    // セマフォのカウンタを減少させます (Ajaxとの同期のため)
    fetchEntriesSemaphore.count--;

    // 全てのログを読み終わったら表示
    if (fetchEntriesSemaphore.count == 0) showEntries();
}

/**
 * 「探索」ボタンを押されたときに呼び出されるメソッドです
 */
function searchDiary(){
    // 検索結果フィールドをクリアします
    document.getElementById("writeArea").innerHTML = "";

    // 探索したい単語を取得します
    currentSearchWords = getSearchWords();
    if (!currentSearchWords) {
        Ext.Msg.alert("ERROR", "検索対象の単語が入力されていません");
        // 検索結果の欄をリセットします
        document.getElementById("resultWriteArea").innerHTML = "\<b\>検索結果\</b\>";
        return;
    }
	
	// ロードエフェクトを表示します
	loadingEffect();

    // 全チェックを取得します
    var allCheckedFlag = document.searchForm.allSearchCheck.checked;

    // セマフォを初期化
    fetchEntriesSemaphore.init();
    // 日記が全検索モードか否かをチェックします
    var isAsyncOn = null;
    var urls = null;
    if (allCheckedFlag == true) {
        // 全文検索時、通信のモードが非同期か否か
        isAsyncOn = document.searchForm.isAsyncOn.checked;
        // 全日記検索なので全てのログのURL
        urls = logData;
    }
    else {
        // 単独日記探索なので、選んだログのURL
        urls = [comboBox.getValue()];
    }
    fetchEntriesSemaphore.urls = urls;
    fetchEntriesSemaphore.count = urls.length;
    for (i = 0; i < urls.length; i++) {
        var xhr = new jQuery.ajax({
            url: urls[i],
            method: "POST",
            async: isAsyncOn,
            success: fetchEntries,
            error: showError
        });
        fetchEntriesSemaphore.xhrs.push(xhr);
    }
}
