package jp.ac.dendai.cdl.mori.wikie.util;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.ac.dendai.cdl.mori.wikie.WikIE;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;

/**
 * Wikipediaのエントリのタイトルの正規化に関するクラス
 * @author mori
 *
 */
public class WNormalizer {
    /**
     * ベースURL
     */
    private String baseURL;
    /**
     * 名前空間文字列がkey, 番号がvalueのMap
     */
    private Map<String, Integer> nsNumberMap = null;
    
    private Map<Integer, String> originalNsMap = null;
    
    /**
     * 言語略記のSet
     */
    private Set<String> langSet = null;
    /**
     * プロジェクトプレフィックスのSet
     */
    private Set<String> projectSet = null;
    /**
     * Wikipediaのタイトルに使えない文字．
     */
    public static final char[] INCORRECT_CHARS = {'#', '<', '>', '[', ']', '|', '{', '}'};

    public static final char[] NON_PRINTING_CHARS = {'\u0000', '\u200e', '\u200f', '\u2028', '\u2029'};

    public WNormalizer(Map<String, Integer> nsNumberMap, Set<String> langSet, Set<String> projectSet) {
        this.nsNumberMap = nsNumberMap;
        this.langSet = langSet;
        this.projectSet = projectSet;
    }

    /**
     * 処理中のWikipediaのベースURLを取得する。
     * @return ベースURL.<br>
     *          日本語版の場合http://ja.wikipedia.org/wiki/
     */
    public String getBaseURL() {
        return baseURL;
    }

    /**
     * Wikipediaのタイトルルールを適用した文字列を返す
     * @param str
     * @return WikipediaのWikipediaのタイトルルールを適用した文字列.
     *          <ul>
     *              <li>両端のスペースは削除</li>
     *              <li>アンダースコアはスペースに変換</li>
     *              <li>2個以上連続したスペースは1個に変換</li>
     *          </ul>
     */
    public static String wTitleRule(String str) {
        str = str.trim();
        str = str.replaceAll("_", " ");
        str = str.replaceAll(" {2,}", " ");
        while (StringUtils.isNotBlank(str) && str.charAt(0) == ':') {
            str = str.replaceFirst(":", "");
        }
        return str.trim();
    }

    /**
     * 制御文字を削除した文字列を返す
     * @param text
     * @return
     */
    public static String deleteNonPrintingChars(String text) {
        //制御文字を除去する。(MediaWikiにもある処理）
        for (char c : NON_PRINTING_CHARS) {
            text = text.replaceAll(Character.toString(c), "");
        }
        return text;
    }

    /**
     * 文字列中にWikipediaのタイトルに使えない文字がないか調べる。
     * タイトルとして使えない文字列は、<br>
     * # < > [ ] | { }
     * @param title 調べたい文字列
     * @return 不正な文字がなければtrue, あればfalse
     */
    public static boolean isCorrectTitle(String title) {
        for (char c : INCORRECT_CHARS) {
            if (title.indexOf(c) != -1) {
                return false;
            }
        }
        return true;
    }

    /**
     * URLエンコードとHTMLエスケープを処理した文字列を返す
     * @param str
     * @return
     */
    public static String decode(String str) {
        URLCodec codec = new URLCodec(WikIE.UTF8);
        Pattern pattern = Pattern.compile("((%[a-zA-Z0-9]+)+)");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            String encoded = matcher.group(1);
            try {
                String decoded = codec.decode(encoded);
                if (decoded.length() < encoded.length()) {
                    str = str.replaceAll(encoded, Matcher.quoteReplacement(decoded));
                }
            } catch (DecoderException e) {
            }
        }
        while (!str.equals(StringEscapeUtils.unescapeHtml(str))) {
            str = StringEscapeUtils.unescapeHtml(str);
        }

        return str;
    }

    /**
     * セクションリンクをデコードした文字列を返す
     * @param str
     * @return
     */
    public static String decodeSectionLink(String str) {
        return decode(str.replaceAll("\\.", "%"));
    }

    /**
     * タイトル文字列を正規化してWEntryで返す
     * @param target タイトル文字列
     * @return 正規化したタイトル文字列を設定したWEntry
     */
    public WTitle normalize(String target) {
        target = decode(wTitleRule(target));
        if (StringUtils.isBlank(target)) {
            return new WTitle();
        }
        String[] part = StringUtils.splitPreserveAllTokens(target, ":");
        int nameIndex = getNameStartIndex(part);
        int nsIndex = getNSStartIndex(part, nameIndex);
        int lim = Math.max(nsIndex, nameIndex);
        int langIndex = getLangStartIndex(part, lim);
        lim = Math.max(langIndex, lim);
        int prIndex = getProjectStartIndex(part, lim);
        return createWEntry(part, prIndex, langIndex, nsIndex, nameIndex);
    }

    /**
     * WEntryオブジェクトを生成する。
     * 各プレフィックスは小文字、記事名は先頭が大文字でそれ以降は小文字に統一される。
     * @param part エントリタイトルをコロン(:)でsplitした配列。
     * @param name 名前部分の始まりのインデックス。
     * @param ns　名前空間部分の始まりのインデックス。
     * @param lang　言語部分の始まりのインデックス。
     * @param pr　プロジェクト部分の始まりのインデックス。
     * @return　生成したWEntryオブジェクト
     */
    public WTitle createWEntry(String[] part, int pr, int lang, int ns, int name) {
        StringBuffer nameStr = new StringBuffer();
        for (int i = name ; i < part.length; i++) {
            nameStr.append(part[i] + ":");
        }
        nameStr.deleteCharAt(nameStr.length() -1);
        String n = StringUtils.capitalize(nameStr.toString().trim());
        String nsStr = new String();
        int nsNumber = 0;
        if (ns != -1) {
            nsStr = part[ns].toLowerCase().trim();
            nsNumber = nsNumberMap.get(nsStr);
            if (originalNsMap != null) {
                nsStr = originalNsMap.get(nsNumber);
            }
        }
        String langStr = new String();
        if (lang != -1) {
            langStr = part[lang].toLowerCase().trim();
        }
        String prStr = new String();
        if (pr != -1) {
            prStr = part[pr].toLowerCase().trim();
        }
        return new WTitle(prStr, langStr, nsStr, n, nsNumber);
    }

    /**
     * タイトルの記事名部分の始まりのインデックスを取得する。
     * @param part エントリタイトルをコロン(:)でsplitした配列。
     * @return
     */
    public int getNameStartIndex(String[] part) {
        int pr = -1;
        int lang = -1;
        int ns = -1;
        for (int i = 0; i < part.length; i++) {
            String p = part[i].toLowerCase().trim();
            if (projectSet.contains(p) && pr == -1 && lang == -1 && ns == -1) {
                pr = i;
            }
            else if (langSet.contains(p) && lang == -1 && ns == -1 ) {
                lang = i;
            }
            else if (nsNumberMap.containsKey(p) && ns == -1) {
                ns = i;
            }
            else if (!langSet.contains(p) && !nsNumberMap.containsKey(p) && !projectSet.contains(p) ||
            ns != -1 || lang != -1 || pr != -1) {
                return i;
            }
        }
        return part.length-1;
    }

    /**
     * タイトルの名前空間分の始まりのインデックスを取得する。
     * @param part　エントリタイトルをコロン(:)でsplitした配列。
     * @param lim　終了インデックス。これ以下は探さない。
     * @return
     */
    public int getNSStartIndex(String[] part, int lim) {
        if (lim == -1) {
            lim = part.length;
        }
        for (int i = 0; i < lim; i++) {
            String p = part[i].toLowerCase().trim();
            if (nsNumberMap.containsKey(p)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * タイトルの言語部分の始まりのインデックスを取得する。
     * @param part　エントリタイトルをコロン(:)でsplitした配列。
     * @param lim　終了インデックス。これ以下は探さない。
     * @return
     */
    public int getLangStartIndex(String[] part, int lim) {
        if (lim == -1) {
            lim = part.length;
        }
        for (int i = 0; i < lim; i++) {
            String p = part[i].toLowerCase().trim();
            if (langSet.contains(p)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * タイトルのプロジェクト部分の始まりのインデックスを取得する。
     * @param part　エントリタイトルをコロン(:)でsplitした配列。
     * @param lim　終了インデックス。これ以下は探さない。
     * @return
     */
    public int getProjectStartIndex(String[] part, int lim) {
        if (lim == -1) {
            lim = part.length;
        }
        for (int i = 0; i < lim; i++) {
            String p = part[i].toLowerCase().trim();
            if (projectSet.contains(p)) {
                return i;
            }
        }
        return -1;
    }

    public int getNamespaceNumber(String str) {
        return nsNumberMap.get(str);
    }

    /**
     * 文字列が言語プレフィックスか判定する
     * @param str
     * @return 言語プレフィックスならtrue, そうでなければfalse
     */
    public boolean isLangPrefix(String str) {
        return langSet.contains(str);
    }

    /**
     * 文字列が名前空間プレフィックスか判定する
     * @param str
     * @return 名前空間プレフィックスならtrue, そうでなければfalse
     */
    public boolean isNamespacePrefix(String str) {
        return nsNumberMap.containsKey(str);
    }

    /**
     * 文字列がプロジェクトプレフィックスか判定する
     * @param str
     * @return プロジェクトプレフィックスならtrue, そうでなければfalse
     */
    public boolean isProjectPrefix(String str) {
        return projectSet.contains(str);
    }

    public void setOrignalNsMap(Map<Integer, String> originalNsMap) {
        this.originalNsMap = new HashMap<Integer, String>(originalNsMap);
    }
}
