/*******************************************************************************
 * Copyright (c) 2008 IGA Tosiki, NTT DATA BUSINESS BRAINS Corp.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    IGA Tosiki (NTT DATA BUSINESS BRAINS Corp.) - initial API and implementation
 *******************************************************************************/
/*
 * blanco Framework
 * Copyright (C) 2008 NTT DATA BUSINESS BRAINS CORPORATION
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */
package blanco.nlpack.generator;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;

import blanco.commons.io.Native2AsciiWriter;
import blanco.commons.util.BlancoStringUtil;
import blanco.nlpack.generator.csv.io.BlancoCsvIOException;
import blanco.nlpack.generator.csv.io.BlancoNLpackGeneratorResultDetailCsvWriter;
import blanco.nlpack.generator.csv.record.BlancoNLpackGeneratorResultDetailCsvRecord;
import blanco.nlpack.generator.dictionary.BlancoNLpackGeneratorOmitDictionary;
import blanco.nlpack.generator.dictionary.BlancoNLpackGeneratorPleiadesDictionary;
import blanco.nlpack.generator.message.BlancoNLpackGeneratorMessage;
import blanco.nlpack.generator.resourcebundle.BlancoNLpackGeneratorResourceBundle;

/**
 * プロパティファイルを、リーダ・ライタを経由して(英語から)各国語へと値を変換します。
 * 
 * @author IGA Tosiki
 */
class BlancoNLpackGeneratorPropertiesConverter {
    /**
     * メッセージオブジェクト。
     */
    private final BlancoNLpackGeneratorMessage fMsg = new BlancoNLpackGeneratorMessage();

    /**
     * プログラムで利用するリソースバンドル。
     */
    private final BlancoNLpackGeneratorResourceBundle fBundle = new BlancoNLpackGeneratorResourceBundle();

    /**
     * Pleiades辞書のインスタンス。
     */
    private BlancoNLpackGeneratorPleiadesDictionary fDictionary = new BlancoNLpackGeneratorPleiadesDictionary();

    /**
     * blancoNLpackGeneratorとしての除外辞書インスタンス。
     */
    private BlancoNLpackGeneratorOmitDictionary fOmitDictionary = new BlancoNLpackGeneratorOmitDictionary();

    /**
     * ヒットした件数。
     */
    private int fReplaceHitCount = 0;

    /**
     * ニモーニック変換をした件数。
     */
    private int fReplaceMnemonicCount = 0;

    /**
     * 除外した件数。
     */
    private int fReplaceOmitCount = 0;

    /**
     * ミスした件数。
     */
    private int fReplaceMissCount = 0;

    /**
     * 詳細ログライタ。
     */
    private BlancoNLpackGeneratorResultDetailCsvWriter fLogDetailWriter = null;

    /**
     * ヒットした件数を取得します。
     * 
     * @return ヒットした件数。
     */
    public int getReplaceHitCount() {
        return fReplaceHitCount;
    }

    /**
     * ニモーニック変換した件数を取得します。
     * 
     * @return ニモーニック変換した件数。
     */
    public int getReplaceMnemonicCount() {
        return fReplaceMnemonicCount;
    }

    /**
     * 除外した件数を取得します。
     * 
     * @return 除外した件数。
     */
    public int getReplaceOmitCount() {
        return fReplaceOmitCount;
    }

    /**
     * ミスした件数を取得します。
     * 
     * @return ミスした件数。
     */
    public int getReplaceMissCount() {
        return fReplaceMissCount;
    }

    public void setLogDetailWriter(
            final BlancoNLpackGeneratorResultDetailCsvWriter arg) {
        fLogDetailWriter = arg;
    }

    /**
     * プロパティファイルをリーダで読み込み、変換可能な文字は英語→各国語変換を行ってライタに出力します。
     * 
     * @param bundleURI
     *            バンドルURI。
     * @param props
     *            プロパティ。
     * @param reader
     *            リーダ。
     * @param writer
     *            ライタ。
     * @return 置換したかどうか。
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    public boolean process(final String bundleURI, final Properties props,
            final BufferedReader reader, final BufferedWriter writer)
            throws IOException {
        // 呼び出されるごとにカウンタをリセットします。
        fReplaceHitCount = 0;
        fReplaceMnemonicCount = 0;
        fReplaceOmitCount = 0;
        fReplaceMissCount = 0;

        if (BlancoStringUtil.null2Blank(fBundle.getPropertiesHeader()).length() > 0) {
            // 人工的にヘッダーを付与します。
            writer.write(fBundle.getPropertiesHeader());
            writer.newLine();
            writer.newLine();
        }

        boolean isReplaced = false;
        for (;;) {
            outerLoop: {
                final String line = reader.readLine();
                if (line == null) {
                    break;
                }

                if (line.trim().startsWith("#") || line.trim().startsWith("!")
                        || line.trim().length() == 0) {
                    writer.write(line);
                    writer.newLine();
                    continue;
                }

                final StringBuffer strbufKey = new StringBuffer();

                StringReader readerLine = new StringReader(line);
                for (;;) {
                    final int iRead = readerLine.read();
                    if (iRead < 0) {
                        writer.newLine();
                        break outerLoop;
                    }
                    // 直接書込。
                    writer.write(iRead);
                    if (iRead == '=') {
                        // 左辺値終了
                        break;
                    }
                    strbufKey.append((char) iRead);
                }

                // ここからは右辺値

                boolean isEscapeStarted = false;
                final StringWriter writerLine = new StringWriter();
                for (boolean isValueStarted = false;;) {
                    final int iRead = readerLine.read();
                    if (iRead < 0) {
                        if (isEscapeStarted) {
                            // エスケープ中なのに行末に来てしまいました。
                            final String line2 = reader.readLine();
                            if (line2 == null) {
                                break;
                            }
                            isEscapeStarted = false;
                            readerLine = new StringReader(line2);
                            continue;
                        }
                        break;
                    }
                    if (isValueStarted == false && iRead == ' ') {
                        continue;
                    }
                    isValueStarted = true;

                    if (iRead == '\\') {
                        if (isEscapeStarted == false) {
                            isEscapeStarted = true;
                        } else {
                            writerLine.write("\\\\");
                            isEscapeStarted = false;
                        }
                    } else {
                        if (isEscapeStarted) {
                            isEscapeStarted = false;
                            writerLine.write('\\');
                        }
                        // バッファ書込。
                        writerLine.write(iRead);
                    }
                }

                readerLine.close();
                writerLine.close();

                // 取得されたキーをもとに辞書をひきます。

                // keyに \ が入ることの考慮付きで文字列を取得します。
                final String strKey = getKeyWithEscape(strbufKey.toString()
                        .trim());

                final String inputValue = (String) props.get(strKey);
                if (inputValue == null) {
                    throw new IllegalArgumentException(fMsg.getMbebc002(
                            bundleURI, strKey));
                }

                final BlancoNLpackGeneratorResultDetailCsvRecord record = new BlancoNLpackGeneratorResultDetailCsvRecord();

                if (lookup(bundleURI, strKey, inputValue, writer, record)) {
                    isReplaced = true;
                }

                writer.newLine();

                try {
                    fLogDetailWriter.writeRecord(record);
                } catch (BlancoCsvIOException e) {
                    // TOOD 例外メッセージ採番。
                    throw new IOException(e.toString());
                }
            }
        }
        return isReplaced;
    }

    /**
     * 辞書をひきます。
     * 
     * @param bundleURI
     * @param strKey
     * @param inputValue
     * @param writer
     * @param record
     * @return
     * @throws IOException
     */
    private boolean lookup(final String bundleURI, final String strKey,
            final String inputValue, final BufferedWriter writer,
            final BlancoNLpackGeneratorResultDetailCsvRecord record)
            throws IOException {
        final String bundleURIwithKey = bundleURI + "?key=" + strKey;

        record.setBundleUri(bundleURIwithKey);
        record.setKey(strKey);
        record.setInput(inputValue);

        final String outputValue = fDictionary.lookup(inputValue);
        if (outputValue == null) {
            throw new IllegalArgumentException(fMsg.getMbebc003(bundleURI,
                    strKey));
        }

        if (fOmitDictionary.checkOmitURI(bundleURI)
                || fOmitDictionary.checkOmitURI(bundleURIwithKey)) {
            // URIベースのomit判定で除外対象とわかった場合。

            // 入力されたvalueをそのまま出力。
            writer
                    .write(Native2AsciiWriter
                            .encodeNative2AsciiValue(inputValue));
            fReplaceOmitCount++;
            record.setAction("omit.u");
            return false;
        }

        if (fOmitDictionary.checkOmitPhrase(inputValue)) {
            // 文字列だけを見た場合にフレーズ除外対象とわかった場合。

            // 入力されたvalueをそのまま出力。
            writer
                    .write(Native2AsciiWriter
                            .encodeNative2AsciiValue(inputValue));
            fReplaceOmitCount++;
            record.setAction("omit.p");
            return false;
        }

        if (fOmitDictionary.checkOmitString(inputValue)) {
            // 文字列だけを見た場合に除外対象とわかった場合。

            // 入力されたvalueをそのまま出力。
            writer
                    .write(Native2AsciiWriter
                            .encodeNative2AsciiValue(inputValue));
            fReplaceOmitCount++;
            record.setAction("omit.s");
            return false;
        }

        if (fDictionary.getFoundStatus() == BlancoNLpackGeneratorPleiadesDictionary.STATUS_NOT_FOUND) {
            // 辞書に見当たらなかった場合。

            // 入力されたvalueをそのまま出力。
            writer
                    .write(Native2AsciiWriter
                            .encodeNative2AsciiValue(inputValue));
            fReplaceMissCount++;
            record.setAction("miss");
            return false;
        }

        if (fDictionary.getFoundStatus() == BlancoNLpackGeneratorPleiadesDictionary.STATUS_MNEMONIC_CHANGED) {
            // ニモーニック変換のみ実施された場合。

            if (fOmitDictionary.checkOmitMnemonicOnlyURI(bundleURI)
                    || fOmitDictionary
                            .checkOmitMnemonicOnlyURI(bundleURIwithKey)) {
                // ニモーニック変換のみの除外ファイルに合致する場合。

                // これは変換してはダメ。
                // 入力されたvalueをそのまま出力。
                writer.write(Native2AsciiWriter
                        .encodeNative2AsciiValue(inputValue));
                fReplaceOmitCount++;
                record.setAction("omit.m");
                return false;
            }

            // 出力値をセット！
            writer.write(Native2AsciiWriter
                    .encodeNative2AsciiValue(outputValue));
            fReplaceMnemonicCount++;
            // 置換をマーク。
            record.setAction("mnemon");
            record.setOutput(outputValue);
            return true;
        }

        if (fDictionary.getFoundStatus() == BlancoNLpackGeneratorPleiadesDictionary.STATUS_FOUND) {
            // 辞書からの引き当てに成功した場合。

            // 出力値をセット！
            writer.write(Native2AsciiWriter
                    .encodeNative2AsciiValue(outputValue));
            fReplaceHitCount++;
            // 置換をマーク。
            record.setAction("hit");
            record.setOutput(outputValue);
            return true;
        }

        throw new IllegalArgumentException("想定されない箇所に入ってきました。");
    }

    /**
     * エスケープ文字を考慮しながらキー値を取得します。
     * 
     * @param input
     * @return
     * @throws IOException
     */
    private String getKeyWithEscape(final String input) throws IOException {
        final Properties prop = new Properties();
        prop.load(new ByteArrayInputStream(new String(input + "=dummy")
                .getBytes("UTF-8")));
        final Set set = prop.keySet();
        final Iterator ite = set.iterator();
        if (ite.hasNext() == false) {
            throw new IllegalArgumentException("あああ");
        }
        return (String) ite.next();
    }
}
