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

// ログのリストが書かれたXMLのファイルパスを記入してください
var logXmlUrl = "./xml/loglist.xml";

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

// 結果表示エリアの幅のサイズを指定してください
var resultAreaWidth = 784;

// 検索フォームの幅のサイズを記述してください
var searchPanelWidth = 774;

// ログを表示するコンボボックスのサイズを記述してください
var extComboWidth = 150;

// 日記間のスパン(間隔)をPIXEL単位で記述してください
var entrySpan = 3;

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

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

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

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

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

// 一画面あたりの表示日記数です
var showLength = 5;

// 検索結果をメモリ上に保持する変数です
var loadedEntries;

/**
 * Extへのイベント登録です。すべてのDOMが利用可能になった時点で実行されます。
 */
$(document).ready(function(){
    generateForm();
    
    // テキストボックスをExt js化し、空欄入力を拒否します
    var searchTextBox = new Ext.form.TextField({
        applyTo: "searchWord",
        allowBlank: false
    });
    
    // XMLのデータをロードします
    logData = logXMLLoader();
});

/**
 * 日記の描画を行います。この部分を編集することでデザインを変更可能です。
 * @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
    });
}

/**
 * 検索フォーム及び結果表示フォームを生成するメソッドです。
 */
function generateForm(){
    var formBuffer = "<table align='center'><tbody><tr><td class='searchform' style='width: " + searchPanelWidth + "px'>" +
    "<form name='searchForm' onsubmit='searchDiary(); return false'>▼ 検索語句を入力してください (語句を半角で区切るとAND、|で区切るとORで検索します)<br/>" +
    "<input type='text' id='searchWord' style='width: " +
    (searchPanelWidth * 0.7) +
    "px;'>　<input type='submit' name='execSearch' value='検索'><br/>" +
    "<input type='checkbox' id='regexpOptionI' checked='checked'/><label for='regexpOptionI'>大文字、小文字を区別しない</label><br/>" +
    "<input type='checkbox' id='isAsyncOn'/><label for='isAsyncOn'>非同期通信モードで検索を行う</label><br/>" +
    "<span style='font-weight: bold;'>[ 注意 ]</span>非同期通信モードをオンにすると速度は上昇しますが、検索の順序が保障されません。<br/><br/>" +
    "▼ 検索対象ログ選択<br/><div id='logSelecter'/></div><input type='checkbox' id='allSearchCheck' checked='checked'/><label for='allSearchCheck'>すべてのログに対して検索を行う</label>" +
    "<br/><a href='" +
    blogUrl +
    "'>トップページへ戻る</a><br/></form></td></tr></tbody></table>"
    document.getElementById("genForm").innerHTML = formBuffer;
    
    var resultAreaBuffer = "<table align='center'><tbody><tr><td class='resultarea' style='width: " + resultAreaWidth + "px;'>" +
    "<div id='resultWriteArea'><b>検索結果</b></div></td></tr><tr><td class='resultarea' style='width: " +
    resultAreaWidth +
    "px;'>" +
    "<table align='center'><tbody><tr><td><div id='writeArea'></div></td></tr></tbody></table></td></tr></tbody></table>"
    
    document.getElementById("resultArea").innerHTML = resultAreaBuffer;
}

/**
 * 記事クラス
 * @param {Object} obj entry 要素の DOM オブジェクト
 */
function Entry(obj){
    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);
}

/**
 * 記事内が単語群を全て含んでいるか
 * @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(){
    document.getElementById("writeArea").innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';
    document.getElementById("drawItem").innerHTML = '<br/><img src="./js/ext/resources/images/default/shared/blue-loading.gif"><br/>長時間画面が切り替わらない場合はページをリロードしてください。<br/><br/>';
    
    // ロード表示用のパネルを生成
    generatePanel("Now Loading .....", "drawItem", "drawPanel", false);
}

/**
 * 日記データのエラー時の処理を行います
 */
function showError(){
    document.getElementById("writeArea").innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';
    document.getElementById("drawItem").innerHTML = '<br/>日記ファイルのロードに失敗しました！<br/><br/>';
    
    // エラー内容をパネルに描画
    generatePanel("Error!", "drawItem", "drawPanel", false);
    
    Ext.Msg.alert("Error!", "日記ファイルが読み込めません！");
}

/**
 * 日記データのエラー時の処理を行います
 */
function notFoundError(){
    document.getElementById("writeArea").innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';
    document.getElementById("drawItem").innerHTML = '<br/>検索条件に一致する日記は見つかりませんでした。<br/><br/>';
    
    // エラー内容をパネルに描画
    generatePanel("Not Found!", "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;');
    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/^[ ]+/mg, "&nbsp;").replace(/^[\t]+/mg, "");
}

/**
 * 日記本文に日付を付加します
 * @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;
}

/**
 * セマフォ制御用のオブジェクトです
 */
function Semaphore(){
    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(){
    // レコード構造を定義します
    var PathRecord = new Ext.data.Record.create([{
        name: "display",
        type: "string"
    }, {
        name: "path",
        type: "string"
    }]);
    
    // ログ用のXMLを読み込みます
    var logXMLData = new Ext.data.Store();
    jQuery.ajax({
        url: logXmlUrl,
        method: "GET",
        error: showError,
        success: function(xmlData){
            var separateTag = xmlData.getElementsByTagName("file");
            logData = new Array(separateTag.length);
            
            // 読み込んだ要素をStoreに格納して表示
            for (var i = 0; i < separateTag.length; i++) {
                // "path"ノードの値を格納
                logData[i] = separateTag[i].getElementsByTagName("path")[0].firstChild.nodeValue;
                // レコードに登録する
                var record = new PathRecord({
                    path: separateTag[i].getElementsByTagName("path")[0].firstChild.nodeValue,
                    display: separateTag[i].getElementsByTagName("display")[0].firstChild.nodeValue
                });
                logXMLData.add(record);
            }
            
            // コンボボックス要素を生成
            document.getElementById("logSelecter").innerHTML = "<input type='text' id='logbox' style='width: " + extComboWidth + "px'/>";
            
            // コンボボックスを生成
            comboBox = new Ext.form.ComboBox({
                store: logXMLData,
                applyTo: "logbox",
                displayField: "display",
                valueField: "path",
                mode: "local",
                triggerAction: "all",
                emptyText: "ログを選択してください..."
            });
        }
    });
}

/**
 * 検索単語を取得します
 */
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('');
}

/**
 * 検索結果を分割して表示します(2回目以降呼び出し)
 * @param {int} showLength 一回の画面に表示する記事数
 * @param {int} startIndex 表示を開始する日記のインデックス
 */
function showEntriesRange(showLength, startIndex){
    // メモリ上から日記データをロード
    var entries = loadedEntries;
    
    // 表示インデックスが範囲外の場合はエラーパネルを表示して終了
    if (startIndex < 0 || entries.length <= startIndex) {
        showError();
        return;
    }
    
    var stringBuffer = [];
    
    // リミッターを設定する
    var loopLimit = (showLength + startIndex > entries.length) ? entries.length : showLength + startIndex;
    var indexShowEntries = loopLimit + 1;
    
    for (var i = startIndex; i < loopLimit; i++) {
        stringBuffer.push('<div id="drawPanel');
        stringBuffer.push(i);
        stringBuffer.push('"><div id="drawItem');
        stringBuffer.push(i);
        stringBuffer.push('" class="code"><\/div><\/div><div style="line-height: ' + entrySpan + 'px;"><br/></div>');
    }
    document.getElementById("writeArea").innerHTML = stringBuffer.join('');
    
    stringBuffer.length = 0;
    for (i = startIndex; i < loopLimit; 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);
    }
    
    // メニュー表示用バッファ
    var menuBuffer = [];
    menuBuffer.push("<table width='100%' class='pager'><tbody><tr>");
    // 左パネルの表示制御
    if (startIndex - showLength >= 0) {
        menuBuffer.push("\<td align='left'><a href='' onclick='showEntriesRange(" +
        showLength +
        ", " +
        (startIndex - showLength) +
        "); return false;'>\<\<\< 前の" +
        showLength +
        "件を表示</a\></td>");
    }
    else {
        menuBuffer.push("\<td align='left'>\<\<\< 前の" +
        showLength +
        "件を表示</a\></td>");
    }
    
    // 中央のパネルの表示制御
    menuBuffer.push("<td align='center'>[ ");
    var menuNumbers = Math.ceil(entries.length / showLength);
    for (i = 0; i < menuNumbers; i++) {
        if (startIndex / showLength == i) {
            menuBuffer.push(i + " ");
        }
        else {
            menuBuffer.push("<a href='' onclick='showEntriesRange(" +
            showLength +
            ", " +
            (i * showLength) +
            "); return false;'>");
            menuBuffer.push(i);
            menuBuffer.push("</a> ");
        }
    }
    menuBuffer.push("]</td>");
    
    // 右パネルの表示制御
    if (entries.length > startIndex + showLength) {
        menuBuffer.push("\<td align='right'><a href='' onclick='showEntriesRange(" +
        showLength +
        ", " +
        (startIndex + showLength) +
        "); return false;'>次の" +
        showLength +
        "件を表示 \>\>\></a\></td>");
    }
    else {
        menuBuffer.push("\<td align='right'>次の" +
        showLength +
        "件を表示 \>\>\></a\></td>");
    }
    menuBuffer.push("</tr></tbody></table>");
    
    // 検索結果を表示します
    document.getElementById("resultWriteArea").innerHTML = "\<b\>検索結果\</b\>\<br/\>" +
    entries.length +
    "件の記事が該当しました /  " +
    (startIndex + 1) +
    "～" +
    loopLimit +
    "件目までを表示中<br/>" +
    menuBuffer.join("");
}

/**
 * 検索時の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) {
        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
        });
        
        loadedEntries = entries;
        
        // 表示ロジック呼び出し
        showEntriesRange(showLength, 0);
    }
}

/**
 * 「探索」ボタンを押されたときに呼び出されるメソッドです
 */
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);
    }
}
