/*******************************************************************************
 * 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.util;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.Iterator;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

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.message.BlancoNLpackGeneratorMessage;
import blanco.nlpack.generator.resourcebundle.BlancoNLpackGeneratorResourceBundle;
import blanco.nlpack.generator.task.valueobject.BlancoNLpackGeneratorParsePropertiesProcessInput;
import blanco.properties.BlancoPropertiesUtil;
import blanco.properties.manifest.BlancoPropertiesManifestReader;
import blanco.properties.manifest.valueobject.BlancoPropertiesManifest;

/**
 * プロパティファイルをパースするユーティリティ。
 * 
 * @author IGA Tosiki
 */
public class BlancoNLpackGeneratorParseProperties {
    /**
     * メッセージオブジェクト。
     */
    private final BlancoNLpackGeneratorMessage fMsg = new BlancoNLpackGeneratorMessage();

    private final BlancoNLpackGeneratorResourceBundle fBundle = new BlancoNLpackGeneratorResourceBundle();

    private BlancoNLpackGeneratorParsePropertiesProcessInput fInput;

    private BlancoNLpackGeneratorResultDetailCsvWriter fWriter;

    /**
     * Javaランタイムが知っているロケール一覧。
     */
    private String[] fKnownLocale = null;

    /**
     * 指定されたディレクトリのプラグインについて各国語化処理を実行します。
     * 
     * @param argInput
     *            処理への入力パラメータ。
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    public void process(
            final BlancoNLpackGeneratorParsePropertiesProcessInput input)
            throws IOException {
        fInput = input;

        if (new File(fInput.getSourcedir()).exists() == false) {
            throw new IllegalArgumentException(fMsg.getMbebi001(fInput
                    .getSourcedir()));
        }

        fWriter = new BlancoNLpackGeneratorResultDetailCsvWriter(
                new BufferedWriter(new FileWriter(fInput.getTargetfile(), true)));
        fWriter.writeTitle();

        final File[] files = new File(fInput.getSourcedir()).listFiles();
        if (files != null) {
            for (int index = 0; index < files.length; index++) {
                if (files[index].isDirectory()) {
                    // 展開されたプラグインディレクトリを処理。
                    processPluginExpandedDir(files[index].getName());
                } else {
                    if (files[index].getName().endsWith(".jar")) {
                        // jarファイル形式のプラグインを処理。
                        processPluginJarFile(files[index].getName());
                    }
                }
            }
        }

        fWriter.close();
        fWriter = null;
    }

    /**
     * プラグインのjarファイルを処理します。
     * 
     * TODO 入力を String から File に変更することを検討したい。
     * 
     * @param targetPluginFullPlusJar
     * @throws IOException
     */
    private void processPluginJarFile(final String targetPluginFullPlusJar)
            throws IOException {
        final BlancoPropertiesManifest manifest = new BlancoPropertiesManifestReader()
                .readPluginJar(new File(fInput.getSourcedir() + "/"
                        + targetPluginFullPlusJar));

        String actualTargetPluginFull = targetPluginFullPlusJar;
        if (actualTargetPluginFull.endsWith(".jar")) {
            // .jar の指定箇所を除去します。
            actualTargetPluginFull = actualTargetPluginFull.substring(0,
                    actualTargetPluginFull.length() - ".jar".length());
        } else {
            throw new IllegalArgumentException(fMsg
                    .getMbebi003(targetPluginFullPlusJar));
        }

        final InputStream inStream = new BufferedInputStream(
                new FileInputStream(fInput.getSourcedir() + "/"
                        + actualTargetPluginFull + ".jar"));
        processJarFileInternal(manifest, inStream);
        inStream.close();
    }

    /**
     * プラグインの展開後ディレクトリを処理します。
     * 
     * @param targetPluginFullPlus
     * @throws IOException
     */
    private void processPluginExpandedDir(final String targetPluginFullPlus)
            throws IOException {
        final BlancoPropertiesManifest manifest = new BlancoPropertiesManifestReader()
                .readPluginDirectory(new File(fInput.getSourcedir() + "/"
                        + targetPluginFullPlus));

        final File[] files = new File(fInput.getSourcedir() + "/"
                + targetPluginFullPlus).listFiles();
        if (files != null) {
            for (int index = 0; index < files.length; index++) {
                if (files[index].isDirectory()) {
                } else {
                    if (files[index].getName().endsWith(".jar")) {
                        final InputStream inStream = new BufferedInputStream(
                                new FileInputStream(fInput.getSourcedir() + "/"
                                        + targetPluginFullPlus + "/"
                                        + files[index].getName()));
                        if (processJarFileInternal(manifest, inStream)) {
                            // 置換されました。
                        }
                        inStream.close();
                    } else if (checkFilenameForTarget(files[index].getName())) {
                        final InputStream inStream = new BufferedInputStream(
                                new FileInputStream(files[index]));

                        // 報告用ベースディレクトリ:begin
                        final String wrkBase = new File(fInput.getSourcedir()
                                + "/" + targetPluginFullPlus)
                                .getCanonicalPath();
                        final String strPropertyFilePath = files[index]
                                .getCanonicalPath().substring(
                                        wrkBase.length() + 1);
                        // 報告用ベースディレクトリ:end

                        if (processPropertiesFile(manifest,
                                BlancoPropertiesUtil.getBabelURI(manifest,
                                        strPropertyFilePath), inStream)) {
                            // 置換されました。
                        }

                        inStream.close();
                    }
                }
            }
        }
    }

    /**
     * jarファイル処理のための内部メソッドです。
     * 
     * @param manifest
     *            マニフェスト。
     * @param inStreamJarTarget
     *            jarファイルの入力ストリーム。
     * @return 処理したかどうか。
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    private boolean processJarFileInternal(
            final BlancoPropertiesManifest manifest,
            final InputStream inStreamJarTarget) throws IOException {
        boolean isReplaced = false;
        final JarInputStream jarInStream = new JarInputStream(
                new BufferedInputStream(inStreamJarTarget));
        for (;;) {
            final JarEntry entry = jarInStream.getNextJarEntry();
            if (entry == null) {
                break;
            }

            if (entry.isDirectory()) {
            } else {
                if (entry.getName().endsWith(".jar")) {
                    if (processJarFileInternal(manifest, jarInStream)) {
                        isReplaced = true;
                    }
                } else {
                    if (checkFilenameForTarget(entry.getName()) == false) {
                        continue;
                    }

                    if (processPropertiesFile(manifest, BlancoPropertiesUtil
                            .getBabelURI(manifest, entry.getName()),
                            jarInStream)) {
                        isReplaced = true;
                    }
                }
            }
            jarInStream.closeEntry();
        }
        return isReplaced;

        // 申し送り。入力ストリーム(jarInStream)は、敢えてクローズしません！これは Java
        // APIの特性を考慮したうえで必要な措置です。
    }

    /**
     * プロパティファイルを処理します。
     * 
     * @param bundleURI
     * @param jarInStream
     * @param fileTarget
     * @return 処理したかどうか。
     * @throws IOException
     */
    private boolean processPropertiesFile(
            final BlancoPropertiesManifest manifest, final String bundleURI,
            final InputStream jarInStream) throws IOException {
        // バイトとして読み込み
        final byte[] bytesProperties = BlancoPropertiesUtil
                .readBytes(jarInStream);

        final Properties props = new Properties();
        props.load(new ByteArrayInputStream(bytesProperties));

        final BufferedReader reader = new BufferedReader(new InputStreamReader(
                new ByteArrayInputStream(bytesProperties)));

        final boolean isReplaced = parseProperties(bundleURI, props, reader);

        // final BlancoNLpackGeneratorResultCsvRecord record = new
        // BlancoNLpackGeneratorResultCsvRecord();
        // record.setBundleSymbolicName(manifest.getBundleSymbolicName());
        // record.setBundleUri(bundleURI);

        return isReplaced;
    }

    /**
     * 与えられたファイルが処理すべきプロパティファイルであるかどうかを判定します。
     * 
     * @param argFilename
     * @return
     */
    private boolean checkFilenameForTarget(final String argFilename) {
        if (argFilename.endsWith(".properties") == false) {
            // ファイル名が .propertiesで終わっていないものは処理対象外です。
            return false;
        }

        if (fKnownLocale == null) {
            fKnownLocale = BlancoPropertiesUtil.getAvailableLocaleStrings();
        }

        if (argFilename.endsWith("_" + fBundle.getTargetLocale()
                + ".properties")) {
            // 現在のターゲットロケールなら処理対象です。
            return true;
        }

        for (int index = 0; index < fKnownLocale.length; index++) {
            if (argFilename.endsWith("_" + fKnownLocale[index] + ".properties")) {
                // ファイル名が _(ロケール).propertiesで終わっているものは処理対象外です。
                return false;
            }
        }

        return true;
    }

    private boolean parseProperties(final String bundleURI,
            final Properties props, final BufferedReader reader)
            throws IOException {
        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) {
                    continue;
                }

                final StringBuffer strbufKey = new StringBuffer();

                StringReader readerLine = new StringReader(line);
                for (;;) {
                    final int iRead = readerLine.read();
                    if (iRead < 0) {
                        break outerLoop;
                    }
                    if (iRead == '\\') {
                        // エスケープ開始。
                        final int iRead2 = readerLine.read();
                        if (iRead2 < 0) {
                            // これは発生しては困ります。
                            throw new IllegalArgumentException(
                                    "通過してはならない箇所を通過。エスケープ中に行末に到達。");
                        }
                        switch ((char) iRead2) {
                        case 'n':
                            strbufKey.append('\n');
                            break;
                        default:
                            strbufKey.append((char) iRead2);
                            break;
                        }
                    } else {

                        // 直接書込。
                        if (iRead == '=') {
                            // 左辺値終了
                            break;
                        }
                        strbufKey.append((char) iRead);
                    }
                }

                // ここからは右辺値

                boolean isEscapeStarted = false;
                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 {
                            isEscapeStarted = false;
                        }
                    } else {
                        if (isEscapeStarted) {
                            isEscapeStarted = false;
                        }
                    }
                }

                readerLine.close();

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

                final String strKey = strbufKey.toString().trim();
                final String inputValue = (String) props.get(strKey);
                if (inputValue == null) {
                    System.out.println("処理中のプロパティファイル(Javaによる処理結果)");
                    final Iterator ite = props.entrySet().iterator();
                    for (; ite.hasNext();) {
                        final Object obj = ite.next();
                        System.out.println("  " + obj.toString());
                    }

                    throw new IllegalArgumentException(fMsg.getMbebc002(
                            bundleURI, strKey));
                }

                final BlancoNLpackGeneratorResultDetailCsvRecord record = new BlancoNLpackGeneratorResultDetailCsvRecord();
                record.setBundleUri(bundleURI + "?key=" + strKey);
                record.setKey(strKey);
                record.setInput(inputValue);

                try {
                    fWriter.writeRecord(record);
                } catch (BlancoCsvIOException e) {
                    throw new IOException("CSV出力で例外が発生: " + e.toString());
                }
            }
        }
        return isReplaced;
    }
}
